Certified Kubernetes Security Specialist (CKS) #17 Falco 행동 분석, audit logs (Runtime)
#16 Admission control: OPA/Gatekeeper, Kyverno에서는 위험한 매니페스트가 클러스터에 들어오기 전에 막는 사전 통제를 다뤘습니다. 하지만 모든 공격을 입구에서 막을 수는 없습니다. 정상적으로 배포된 Pod안에서 공격자가 셸을 띄우거나, 민감 파일을 읽거나, 권한을 올리는 행위는 admission 단계를 이미 지난 뒤에 일어납니다. 이번 글은 이미 도는 워크로드가 런타임에 하는 이상 행동을 탐지하는 마지막 도메인 Monitoring,Logging,Runtime Security를 다루겠습니다.
런타임 보안의 두 축은 분명합니다. 노드의 커널에서 일어나는 syscall을 감시하는 Falco, 그리고 누가 API 서버에 무슨 요청을 보냈는지 기록하는 audit log입니다. 앞쪽이 컨테이너 내부의 행동을 본다면, 뒤쪽은 클러스터 제어 평면으로 들어온 요청을 본다고 이해하면 됩니다. 둘 다 시험 단골이므로, 룰과 정책을 직접 작성하고 출력을 읽는 감각을 잡겠습니다.
런타임 위협 탐지란 #
지금까지의 도메인은 대부분 사전 통제였습니다. NetworkPolicy로 통신을 막고, PSA로 위험한 Pod를 거부하고, admission 웹훅으로 매니페스트를 검사했습니다. 이 통제들은 공격이 시작되기 전에 작동합니다. 하지만 다음 같은 상황을 생각해 보겠습니다.
- 정상 이미지로 배포된 컨테이너가 알려지지 않은 취약점으로 탈취됐다
- 공격자가 그 컨테이너 안에서
/bin/bash를 띄워 대화형 셸을 얻었다 - 컨테이너 안에서
/etc/shadow를 읽거나 호스트 디렉터리를 탐색한다
이 행위들은 매니페스트 수준에서는 보이지 않습니다. 입구 통제를 다 통과한 정상 Pod안에서 일어나기 때문입니다. 런타임 탐지는 이렇게 이미 도는 워크로드의 실제 행동을 관찰해 비정상을 잡아냅니다. 막는 것이 아니라 보고 알리는 것이 일차 목표라는 점이 사전 통제와 다릅니다.
Falco: syscall 기반 룰 엔진 #
Falco는 CNCF의 런타임 보안 도구로, 리눅스 커널의 **system call(syscall)**과 쿠버네티스 audit이벤트를 실시간으로 받아, 미리 정의한 룰과 맞춰 보고 위반을 경보로 내보냅니다. 컨테이너가 셸을 띄우는 순간, 민감 파일을 여는 순간, 권한을 올리는 순간을 syscall 흐름에서 포착합니다.
Falco가 syscall을 수집하는 방식은 두 가지입니다. 커널 모듈이나 eBPF probe로 직접 커널에서 이벤트를 받습니다. 어느 쪽이든 결과적으로 같은 룰 엔진이 평가하므로, 시험에서는 수집 드라이버보다 룰을 읽고 쓰는 능력이 중요합니다.
룰 구조: rule, condition, output, priority #
Falco 룰은 YAML 한 항목으로 정의됩니다. 핵심 필드는 다음과 같습니다.
| 필드 | 역할 |
|---|---|
rule | 룰 이름. 경보에 표시됨 |
desc | 룰 설명 |
condition | 어떤 이벤트를 위반으로 볼지 결정하는 표현식 |
output | 경보 한 줄에 무엇을 담을지 정의하는 템플릿 |
priority | 심각도(EMERGENCY〜DEBUG) |
tags | 분류 태그 |
가장 기본이 되는 셸 실행 탐지 룰을 보겠습니다.
- rule: Terminal shell in container
desc: A shell was used as the entrypoint/exec target in a container
condition: >
spawned_process and container
and shell_procs and proc.tty != 0
and container_entrypoint
output: >
A shell was spawned in a container
(user=%user.name container_id=%container.id
container_name=%container.name shell=%proc.name
parent=%proc.pname cmdline=%proc.cmdline)
priority: NOTICE
tags: [container, shell, mitre_execution]condition은 불리언 표현식입니다. spawned_process는 프로세스가 새로 떠진 이벤트, container는 그 이벤트가 컨테이너 안에서 일어났다는 조건입니다. 여러 조건을 and로 묶어, “컨테이너 안에서 셸 프로세스가 떠졌다"는 패턴을 잡습니다.
macro와 list로 룰을 읽기 쉽게 #
위 룰에 나온 shell_procs나 container는 미리 정의된 macro입니다. 자주 쓰는 조건 조각에 이름을 붙여 재사용합니다. list는 값의 묶음에 이름을 붙입니다.
- list: shell_binaries
items: [bash, sh, zsh, ksh, csh, ash, dash]
- macro: shell_procs
condition: proc.name in (shell_binaries)list로 셸 바이너리 이름 묶음을 정의하고, macro로 “프로세스 이름이 그 묶음에 들어있다"는 조건에 shell_procs라는 이름을 붙입니다. 이렇게 하면 룰의 condition이 짧고 읽기 쉬워집니다. 시험에서 기존 룰을 수정할 때, list에 항목 하나를 추가하는 방식으로 푸는 경우가 많습니다.
priority: 심각도 등급 #
priority는 경보의 심각도를 나타내며 위에서부터 아래로 다음 순서입니다.
EMERGENCY ALERT CRITICAL ERROR WARNING NOTICE INFORMATIONAL DEBUGFalco 설정에서 priority 임곗값을 두어, 일정 등급 이상만 출력하도록 거를 수 있습니다. 시험에서 “WARNING 이상만 보이게 하라” 같은 조정이 나올 수 있으므로 등급 순서를 외워 두는 것이 좋습니다.
기본 룰: 셸 실행, 민감 파일 접근, 권한 상승 #
Falco를 설치하면 /etc/falco/falco_rules.yaml에 검증된 기본 룰이 들어있습니다. 대표적인 탐지 항목은 다음과 같습니다.
| 기본 룰 | 잡아내는 행위 |
|---|---|
| Terminal shell in container | 컨테이너 안에서 대화형 셸 실행 |
| Read sensitive file untrusted | /etc/shadow, /etc/sudoers 등 민감 파일 읽기 |
| Write below etc | /etc 아래에 파일 쓰기 |
| Launch privileged container | privileged 컨테이너 실행 |
| Change thread namespace | 컨테이너가 호스트 네임스페이스로 탈출 시도 |
| Mkdir binary dirs | /bin, /usr/bin 등 바이너리 디렉터리 변경 |
이 기본 룰만으로도 흔한 침입 행위를 폭넓게 탐지합니다. 기본 룰 파일은 직접 수정하지 않는 것이 원칙입니다. Falco 업그레이드 시 덮어쓰기되기 때문입니다.
커스텀 룰: falco_rules.local.yaml #
룰을 추가하거나 기존 룰을 덮어쓸 때는 /etc/falco/falco_rules.local.yaml에 작성합니다. Falco는 기본 룰을 먼저 읽고 그 다음 local 파일을 읽으므로, local 파일이 나중에 적용되어 기본 룰을 안전하게 보강하거나 재정의합니다.
예를 들어 특정 디렉터리에 대한 쓰기를 탐지하는 커스텀 룰을 추가하겠습니다.
# /etc/falco/falco_rules.local.yaml
- rule: Write to app config dir
desc: Detect any write attempt under /app/config
condition: >
open_write and container
and fd.name startswith /app/config
output: >
Write under /app/config detected
(user=%user.name file=%fd.name
container=%container.name command=%proc.cmdline)
priority: WARNING
tags: [filesystem, custom]open_write는 쓰기 모드로 파일을 여는 이벤트를 잡는 매크로이고, fd.name은 대상 파일 경로입니다. startswith로 경로 접두어를 비교합니다. 룰을 추가한 뒤에는 Falco를 다시 읽혀야 합니다.
# systemd 서비스로 도는 경우 재시작
systemctl restart falco
# 룰 문법만 먼저 검사
falco -V -r /etc/falco/falco_rules.local.yaml출력 읽기: 어떤 Pod, 프로세스, syscall #
Falco의 경보 한 줄에는 output 템플릿에 넣은 필드가 채워져 나옵니다. 실제 출력은 이런 모습입니다.
14:32:07.991 Notice A shell was spawned in a container
(user=root container_id=3f2a1b container_name=nginx-app
shell=bash parent=runc cmdline=bash -i)이 한 줄에서 읽어야 할 것은 분명합니다.
- 어떤 컨테이너인가:
container_name=nginx-app,container_id=3f2a1b - 무슨 프로세스인가:
shell=bash,cmdline=bash -i로 대화형 셸임을 확인 - 누가 실행했나:
user=root - 부모 프로세스:
parent=runc
시험에서 “Falco 로그에서 셸이 떠진 Pod 이름을 찾아 파일에 적어라” 같은 작업이 나옵니다. Falco 로그는 보통 /var/log/syslog나 journalctl -u falco, 또는 설정에 따라 별도 파일로 나가므로 출력 위치부터 확인하겠습니다.
# systemd 저널에서 Falco 경보 확인
journalctl -u falco --no-pager | grep "shell was spawned"
# 셸이 떠진 컨테이너 이름만 추출하는 예
journalctl -u falco --no-pager \
| grep "Terminal shell in container" \
| grep -oP 'container_name=\K[^ )]+'자주 쓰는 필드 이름을 정리해 두겠습니다.
| 필드 | 의미 |
|---|---|
proc.name | 프로세스 이름 |
proc.cmdline | 실행 명령줄 |
proc.pname | 부모 프로세스 이름 |
user.name | 실행 사용자 |
fd.name | 접근한 파일 경로 |
container.name | 컨테이너 이름 |
container.id | 컨테이너 ID |
k8s.pod.name | Pod 이름(쿠버네티스 메타) |
evt.type | syscall 종류 |
audit log: 쿠버네티스 API 감사 #
Falco가 노드 커널의 행동을 본다면, audit log는 쿠버네티스 API 서버로 들어온 요청을 기록합니다. 누가 어떤 리소스에 어떤 동작(get/create/delete)을 요청했고, 결과가 무엇이었는지가 남습니다. “누가 이 Secret를 읽었나”, “어떤 ServiceAccount가 Pod를 만들었나” 같은 질문의 답이 여기에 있습니다.
audit policy: 무엇을 얼마나 기록할까 #
감사 로그는 audit policy가 정의합니다. 모든 요청을 다 기록하면 로그가 폭증하므로, 어떤 요청을 어느 수준으로 남길지를 정책으로 거릅니다. 정책 파일은 보통 /etc/kubernetes/audit-policy.yaml에 둡니다.
기록 수준은 네 가지 level이 있습니다.
| level | 기록 내용 |
|---|---|
None | 기록하지 않음 |
Metadata | 요청 메타데이터만(누가, 무엇을, 언제). 본문 제외 |
Request | 메타데이터 + 요청 본문 |
RequestResponse | 메타데이터 + 요청 본문 + 응답 본문 |
또한 요청이 처리되는 시점을 나타내는 stage가 있습니다.
| stage | 시점 |
|---|---|
RequestReceived | 요청을 받은 직후 |
ResponseStarted | 응답을 보내기 시작한 시점(주로 watch) |
ResponseComplete | 응답이 끝난 시점 |
Panic | 내부 패닉 발생 |
audit policy 예제 #
정책은 위에서 아래로 규칙을 평가하며, 처음 일치한 규칙의 level이 적용됩니다. 따라서 규칙 순서가 중요합니다. 다음은 시험에서 자주 변형되는 형태입니다.
# /etc/kubernetes/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
# 모든 RequestReceived stage는 기록 생략(노이즈 감소)
omitStages:
- "RequestReceived"
rules:
# Secret 본문은 RequestResponse로 남기되 응답 본문은 제외
- level: Metadata
resources:
- group: ""
resources: ["secrets", "configmaps"]
# 특정 네임스페이스의 Pod 변경은 본문까지
- level: Request
namespaces: ["prod"]
resources:
- group: ""
resources: ["pods"]
verbs: ["create", "update", "delete"]
# 읽기 전용 시스템 요청은 기록하지 않음
- level: None
users: ["system:kube-proxy"]
verbs: ["watch", "get"]
# 그 외 모든 요청은 메타데이터만
- level: Metadata위 정책은 Secret과 ConfigMap는 메타데이터만, prod 네임스페이스의 Pod변경은 본문까지, kube-proxy의 읽기는 무시, 나머지는 메타데이터만 남깁니다. 마지막 catch-all 규칙(level: Metadata)을 빠뜨리면 일치하지 않는 요청이 기록되지 않으므로 주의하겠습니다.
apiserver 플래그 설정 #
정책 파일을 만들었으면 API 서버가 그 정책을 쓰도록 플래그를 켜야 합니다. kubeadm 클러스터에서는 /etc/kubernetes/manifests/kube-apiserver.yaml을 직접 편집합니다.
| 플래그 | 역할 |
|---|---|
--audit-policy-file | 적용할 audit policy 파일 경로 |
--audit-log-path | 감사 로그를 쓸 파일 경로 |
--audit-log-maxage | 로그 파일 보관 일수 |
--audit-log-maxbackup | 보관할 로그 파일 개수 |
--audit-log-maxsize | 로그 파일 회전 크기(MB) |
kube-apiserver는 static Pod이므로, 정책 파일과 로그 디렉터리를 hostPath 볼륨으로 마운트해 주어야 컨테이너 안에서 접근할 수 있습니다. 이 마운트를 빠뜨려 apiserver가 뜨지 않는 실수가 시험에서 가장 흔합니다.
# /etc/kubernetes/manifests/kube-apiserver.yaml (발췌)
spec:
containers:
- name: kube-apiserver
command:
- kube-apiserver
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes/audit/audit.log
- --audit-log-maxage=7
- --audit-log-maxbackup=2
- --audit-log-maxsize=50
# 기존 플래그들...
volumeMounts:
- name: audit-policy
mountPath: /etc/kubernetes/audit-policy.yaml
readOnly: true
- name: audit-log
mountPath: /var/log/kubernetes/audit/
readOnly: false
volumes:
- name: audit-policy
hostPath:
path: /etc/kubernetes/audit-policy.yaml
type: File
- name: audit-log
hostPath:
path: /var/log/kubernetes/audit/
type: DirectoryOrCreate매니페스트를 저장하면 kubelet이 apiserver Pod를 자동으로 다시 띄웁니다. 다시 뜨는 데 시간이 걸리므로, crictl ps나 kubectl get pods -n kube-system로 apiserver가 정상 기동했는지 확인하겠습니다. 띄우지 못하면 로그 경로의 디렉터리 권한이나 hostPath 마운트 누락을 먼저 의심합니다.
로그 분석 #
감사 로그는 한 줄에 JSON 한 건이 들어가는 형식입니다. 한 항목을 보겠습니다.
{
"kind": "Event",
"level": "Metadata",
"stage": "ResponseComplete",
"requestURI": "/api/v1/namespaces/prod/secrets/db-cred",
"verb": "get",
"user": { "username": "dev-user" },
"objectRef": {
"resource": "secrets",
"namespace": "prod",
"name": "db-cred"
},
"responseStatus": { "code": 200 }
}이 한 건에서 “dev-user가 prod 네임스페이스의 db-cred Secret를 읽었고 성공했다"를 읽어냅니다. 시험에서는 jq로 특정 조건을 거르는 작업이 나옵니다.
# 특정 Secret에 접근한 사용자만 추출
jq 'select(.objectRef.resource=="secrets"
and .objectRef.name=="db-cred")
| .user.username' \
/var/log/kubernetes/audit/audit.log
# delete 동작만 시간순으로 보기
jq 'select(.verb=="delete")
| {time:.requestReceivedTimestamp,
user:.user.username,
res:.objectRef.resource}' \
/var/log/kubernetes/audit/audit.logjq의 select로 조건을 거르고, 객체 표기로 원하는 필드만 뽑는 패턴 하나만 손에 익혀 두면 대부분의 분석 작업을 빠르게 끝냅니다.
시험 포인트 #
런타임 도메인에서 실기로 자주 나오는 작업을 정리하겠습니다.
- Falco 룰로 이상 탐지. 기존 룰을 읽고 어떤 행위를 잡는지 파악하거나, list에 항목을 추가하는 식으로 탐지 범위를 넓히는 작업
- Falco 출력에서 정보 추출. 로그에서 셸이 떠진 컨테이너,Pod 이름이나 실행 사용자를 찾아 지정 파일에 적기. 로그 위치(
journalctl -u falco또는 설정 파일 경로)부터 확인 - 커스텀 룰 작성.
/etc/falco/falco_rules.local.yaml에 룰을 추가하고 Falco를 다시 읽혀 적용 - audit policy 작성. 요구된 리소스,동사,네임스페이스에 맞는 level과 규칙 순서를 정확히 작성. 마지막 catch-all 규칙을 빠뜨리지 않기
- apiserver 감사 활성화.
--audit-policy-file,--audit-log-path플래그 추가와 hostPath 볼륨 마운트를 함께 처리. apiserver가 다시 뜨는지 반드시 확인 - audit log 분석.
jq의select로 특정 사용자,리소스,동사 조건을 걸러 답을 찾기
가장 흔한 감점은 두 가지입니다. audit policy를 잘 써 놓고도 apiserver 매니페스트에 hostPath 마운트를 빠뜨려 apiserver가 죽는 경우, 그리고 Falco 룰을 추가한 뒤 다시 읽히지 않아 적용되지 않는 경우입니다. 정책과 룰은 반영과 기동 확인까지가 한 작업임을 잊지 않겠습니다.
정리 #
이번 글에서 잡은 것:
- 런타임 위협 탐지는 사전 통제가 놓친 행동을 본다. 정상 배포된 Pod안의 셸 실행,민감 파일 접근,권한 상승을 실시간으로 포착
- Falco는 syscall 기반 룰 엔진.
rule/condition/output/priority구조, macro와 list로 조건을 재사용, 기본 룰은 손대지 말고falco_rules.local.yaml에 커스텀 작성 - Falco 출력은 컨테이너,프로세스,사용자,syscall를 한 줄에 담는다. 로그 위치를 먼저 찾고 필드를 추출
- audit log는 API 서버 요청을 기록. level(None/Metadata/Request/RequestResponse)과 stage, 규칙 순서가 핵심이고 catch-all 규칙을 마지막에 둠
- apiserver 감사 활성화는 플래그 + hostPath 마운트 + 기동 확인이 한 묶음. 마운트 누락이 가장 흔한 실수
- 로그 분석은
jq의select패턴 하나로 대부분 해결
다음: Container immutability #
런타임에서 이상 행동을 탐지하는 법을 잡았습니다. 하지만 탐지보다 더 근본적인 대응은 애초에 컨테이너가 변경되지 못하게 만드는 것입니다.
#18 Container immutability, forensics에서는 readOnlyRootFilesystem으로 컨테이너 파일시스템을 읽기 전용으로 굳히는 법, immutable 컨테이너를 위한 보안 컨텍스트 설정, 그리고 침해가 일어난 뒤 증거를 수집하고 분석하는 forensics의 기초까지 다루며 런타임 도메인을 마무리하겠습니다.