LabHub

CKS — 쿠버네티스 보안 전문가 · 런타임 보안과 감사 · 이론

남기지 않은 로그는 없었던 일이 된다

LabHub 에서 이어서 보기

한 줄 요약

감사 정책은 "무엇을 기록할까"가 아니라 "무엇을 버릴까"를 정하는 문서입니다. 규칙은 위에서부터
순서대로 평가되고 처음 매칭된 하나만 적용되므로, 순서를 잘못 쓰면 조용히 아무것도 안 남습니다.

왜 이게 필요했나

침해 대응의 첫 질문은 늘 같습니다. 언제, 누가, 무엇을 했는가. 쿠버네티스에서 이 질문에 답할 수
있는 유일한 자료가 API 서버의 감사 로그입니다. 그런데 전부 기록하면 저장 비용이 폭발하고,
대충 기록하면 정작 필요한 순간에 아무것도 없습니다. 그래서 정책이 필요합니다.

레벨은 네 단계입니다.

| 레벨 | 기록 범위 |
| --- | --- |
| None | 기록하지 않음 |
| Metadata | 요청자, 시각, 동사, 리소스 등 메타데이터만 |
| Request | 메타데이터 + 요청 본문 |
| RequestResponse | 메타데이터 + 요청 본문 + 응답 본문 |

여기서 CKS 가 반복해서 묻는 함정이 하나 있습니다. Secret 에 대해 RequestResponse 를 걸면
감사 로그 파일 안에 시크릿 값이 그대로 들어갑니다. 감사 로그는 대개 애플리케이션보다 접근
권한이 넓은 곳으로 수집되므로, 시크릿을 지키려던 조치가 시크릿을 퍼뜨리는 결과가 됩니다.
그래서 Secret 과 ConfigMap 은 Metadata 까지만 기록하는 것이 정석입니다.

어떻게 동작하나

정책 파일은 audit.k8s.io/v1Policy 리소스이고, rules 배열을 위에서부터 평가해
처음 매칭된 규칙 하나를 적용합니다. 그래서 순서가 곧 의미입니다. 예를 들어 kube-system 의
Secret get 요청을 제외하고 싶다면 그 level: None 규칙이 Secret 관련 규칙보다 위에 있어야
합니다. 아래에 두면 위쪽 규칙이 먼저 매칭되어 제외가 전혀 동작하지 않습니다.

omitStages 도 함께 씁니다. 하나의 요청은 RequestReceived, ResponseStarted,
ResponseComplete, Panic 네 단계에서 이벤트를 만들 수 있는데, RequestReceived 는 거의
언제나 잡음입니다. 최상위 omitStages 에 이 단계를 넣으면 로그 양이 눈에 띄게 줄어듭니다.

정책 파일을 만든 뒤에는 API 서버에 연결해야 합니다. --audit-policy-file 로 정책을,
--audit-log-path 로 출력 위치를 지정하고, --audit-log-maxage(보관 일수),
--audit-log-maxbackup(파일 개수), --audit-log-maxsize(MB) 로 회전 정책을 정합니다.
이 세 개를 빼면 디스크가 차고, 디스크가 차면 API 서버가 멈춥니다.

RBAC 쪽 위험 동사는 특별 취급합니다. escalate·bind·impersonate 는 권한 상승 시도 그
자체이므로 RequestResponse 로 남겨 무엇을 어떻게 바꾸려 했는지까지 기록합니다.
파드 변경은 Request 정도가 균형점입니다. 매니페스트를 남기면 침해 후 무엇이 배포됐는지
재구성할 수 있고, 응답 본문까지는 대개 필요 없습니다.

현장에서 만나는 모습

감사 로그로 답할 수 없는 영역이 있고, 그 자리를 런타임 탐지가 채웁니다. Falco 는 커널 시스템
콜을 관찰해 규칙에 걸리는 행동을 알립니다. 기본 제공 규칙 중 가장 유명한 것이 컨테이너 안에서
셸이 뜨는 순간을 잡는 것으로, 조건은 evt.type=execve 에 컨테이너 여부와 프로세스 이름이
셸 목록에 있는지를 겹친 형태입니다. API 를 통하지 않은 행동은 감사 로그에 아예 나타나지
않으므로, 두 층은 대체재가 아니라 서로 다른 질문에 답하는 도구입니다.

노드에서 실제로 무엇이 돌고 있는지는 crictl 로 확인합니다. 다만 경계를 지켜야 합니다.
crictl 은 API 서버를 우회하므로 진단에는 강력하지만 상태를 바꾸는 데 쓰면 위험합니다. kubelet 은
자기가 만든 컨테이너와 샌드박스를 계속 감시하고 있어서, 사람이 crictl rm 으로 지우면 kubelet
의 인식과 실제 상태가 어긋나 이상한 중간 상태에 머뭅니다. 규칙을 단순하게 정하면 이렇습니다.
쿠버네티스가 무엇을 하려 하는지 알고 싶으면 kubectl, 런타임이 실제로 무엇을 했는지 알고 싶으면
crictl. 두 관점이 어긋나는 지점이 곧 문제의 위치입니다.

진단이 통째로 무의미해지는 사례도 있습니다. crictl 이 엉뚱한 소켓에 붙어 있으면 텅 빈 목록을
보여 주고, 그러면 "컨테이너가 하나도 없다"는 잘못된 결론에 도달합니다. crictl info 로 지금
붙은 대상을 먼저 확인하고 /etc/crictl.yamlruntime-endpoint 가 kubelet 의
--container-runtime-endpoint 와 같은지 대조하는 것이 첫 단계입니다. 엔드포인트를 지정하지
않으면 crictl 이 알려진 소켓 후보를 차례로 시도하느라 명령 하나가 십수 초씩 걸리기도 합니다.

침해된 파드를 발견했을 때의 순서도 정해져 있습니다. 즉시 삭제가 아니라 증거 보존이 먼저입니다.
로그와 상태를 수집하고, NetworkPolicy 로 그 파드의 통신을 끊어 격리하고, 침해 경로와 범위를
파악한 뒤, 취약점을 고친 새 이미지로 교체합니다. 먼저 지우면 포렌식 증거가 사라집니다.

다음 실습에서 할 것

audit.k8s.io/v1 정책 파일을 규칙 하나씩 쌓아 올립니다. 최하단의 포괄 규칙부터 시작해
omitStages 로 잡음을 줄이고, Secret 은 Metadata 까지만, kube-system 의 Secret get 은 아예
None 으로, RBAC 위험 동사는 RequestResponse 로, 파드 변경은 Request 로 두는 순서를
직접 만듭니다. 마지막으로 API 서버에 붙일 감사 로그 백엔드 플래그를 문서화합니다.