[CKS] Mutable vs Immutable Infrastructure & Audit Log
Kubernetes 클러스터의 불변 인프라 패턴과 감사 로깅을 통해 런타임 컨테이너 보안을 강화하는 방법을 다룹니다. 읽기 전용 파일 시스템, Pod 보안 정책, Audit 로그를 통해 무단 수정을 방지하고 의심 활동을 감지합니다.
개요
아래 강의를 듣고 정리했습니다.
Mutable(가변) 인프라
변경 사항이 발생할 때 기존 서버 또는 리소스를 직접 업데이트하는 것을 의미합니다.
- Nginx 버전을 업데이트하기 위해 수동 실행하거나 Ansible같은 구성 관리 도구 및 스크립트 사용
대규모 환경의 모든 서버에 동일한 업그레이드 프로세스를 수행합니다.
인프라 교체가 아닌 서버의 소프트웨어를 업데이트하는 방식으로 Inplace Update라고 하며 가변 인프라의 대표적인 예시입니다.
가변 인프라의 위험성

업그레이드 과정은 실제 시나리오에서 여러 가지 문제를 제공할 수 있습니다.
많은 서버 중 1대만 네트워크, 디스크 공간 부족, 운영 체제 불일치 등의 이유로 종속성이 부족한 경우 업그레이드가 실패할 수 있습니다. 이로 인해 여러 서버가 다른 소프트웨어를 실행 Configuration Drift가 발생합니다.
Immutable(불변) 인프라
불변 인프라는 기존 서버의 소프트웨어를 업데이트하는 방식이 아닙니다.
새로운 서버에 업데이트된 버전을 설치하고 기존 서버는 폐기합니다. 서버가 한 번 배포되면 수명 주기 동안 수정되지않습니다. 변경 사항이 발생한 경우 소프트웨어를 새로 설치한 서버를 사용합니다.
이를 통해 서버는 표준화됩니다. 변경될 수 없는 구성에서 시작하기에 구성 변경을 최소화하며 일관성, 보안 및 확장성이 중요하고 현대의 컨테이너 및 클라우드 인프라에서 최적화됩니다.
Container와 Immutable 인프라
불변성 인프라는 컨테이너 구조에 완벽하게 적용됩니다. 컨테이너느 이미지 → 컨테이너로 변환되기에 업데이트 전 최신 버전의 이미지를 생성합니다. 업데이트된 이미지는 롤링, 블루-그린, 카나리 등 업데이트 프로세스를 통해 배포됩니다. 이를 기반으로 전환 과정에서 다운타임이 발생하지 않습니다.
- 실행 중인 컨테이너를 수정하거나 수동으로 접근하는 것은 기술적으로 가능하지만 보안 취약점, 가변성의 위험성이 커집니다.
Runtime 컨테이너 불변성을 보장하는 방법
Kubernetes Pod가 불변성 개념을 준수하는 방법을 확인합니다.
Container는 기본적으로 불변하지만 직접 접근하여 수정하는 것도 얼마든지 가능합니다. 하지만 이는 불변성을 해치는 요소이므로 런타임 중 무단 수정을 방지하는 방법으 확인합니다.
ReadOnly 파일 시스템 강제 적용
컨테이너의 불변성을 수행하는 방법 중 하나로 ReadOnly 상태를 유지하는 방법이 있습니다.
Pod definition의 securityContext를 통해 설정가능합니다.
하지만 구성에서 기능적인 문제를 일으킬 수 있으니 주의합니다. Nginx는 런타임 데이터 저장을 위한 디렉토리(/var/run/nginx)와 캐싱하기 위한 디렉토리(/var/cache/nginx) 사용합니다.
- 읽기 전용을 적용하기 전 애플리케이션이 런타임 중 루트 파일 시스템에 쓰기를 수행하는지 파악해야합니다.
apiVersion: v1
kind: Pod
metadata:
labels:
run: nginx
name: nginx
spec:
containers:
- image: nginx
name: nginx
securityContext:
readOnlyRootFilesystem: true
혹은 쓰기 권한이 필요한 디렉토리에 볼륨을 마운트하는 방식입니다.
readOnlyRootFilesystem: 불변 컨테이너 패턴에서 자주 사용되는 방식
apiVersion: v1
kind: Pod
metadata:
labels:
run: nginx
name: nginx
spec:
containers:
- image: nginx
name: nginx
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: cache-volume
mountPath: /var/cache/nginx
- name: runtime-volume
mountPath: /var/run
volumes:
- name: cache-volume
emptyDir: {}
- name: runtime-volume
emptyDir: {}
Privileged Mode를 활용한 불변 컨테이너 테스트
privileged 모드 사용시 readOnlyRootFileSystem이 켜져있기 때문에 실패합니다.
하지만 호스트의 /proc 접근을 통해 swappiness를 수정하면 호스트 커널 설정 등이 변경될 여지가 있기에 privileged 설정은 반드시 피해야합니다.
apiVersion: v1
kind: Pod
metadata:
labels:
run: nginx
name: nginx
spec:
containers:
- image: nginx
name: nginx
securityContext:
readOnlyRootFilesystem: true
privileged: true
volumeMounts:
- name: cache-volume
mountPath: /var/cache/nginx
- name: runtime-volume
mountPath: /var/run
volumes:
- name: cache-volume
emptyDir: {}
- name: runtime-volume
emptyDir: {}
컨테이너 불변성을 위한 모범 사례
| 모범 사례 | 설명 |
|---|---|
| 읽기 전용 루트 파일 시스템 | 컨테이너의 루트 파일 시스템을 읽기 전용으로 설정하여 제자리 수정을 방지하십시오. |
| 제한된 쓰기 용량 | 볼륨(예: **emptyDir**영구 볼륨)은 쓰기 권한이 필요한 디렉터리에만 마운트하십시오. |
| 특권 모드 제거 | 컨테이너가 호스트 시스템에 미치는 영향을 최소화하려면 privileged 플래그 사용을 자제하십시오. |
| 루트 없는 컨테이너 | 위험을 최소화하려면 가능한 한 루트가 아닌 사용자 권한으로 컨테이너를 실행하십시오. |
| 보안 정책 시행 | Pod 보안 정책(PSP)을 사용하여 불변성 및 기타 보안 모범 사례를 적용하십시오. |
대표적인 예시는 다음과 같습니다.
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: example
spec:
privileged: false
readOnlyRootFilesystem: true
runAsUser:
rule: RunAsNonRoot
seLinux:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
fsGroup:
rule: RunAsAny
Immutable Infa Lab
Pod 레벨, Container 레벨, 공통 환경에서 설정 가능한 securityContext 주의
| Pod 레벨 전용 | Container 레벨 전용 | 공통 (Container 우선) |
|---|---|---|
| fsGroup | allowPrivilegeEscalation | runAsUser |
| fsGroupChangePolicy | capabilities | runAsGroup |
| supplementalGroups | privileged | runAsNonRoot |
| sysctls | procMount | seLinuxOptions |
| readOnlyRootFilesystem | seccompProfile |
Audit Log
Falco가 Container 내부에서 shell 혹은 민감한 파일에 접근하는 의심스러운 활동을 검사했다면 Kubernetes Audit Log를 통해 클러스터 이벤트를 감사하고 모니터링하며 추적하는 방법을 확인합니다.
kubectl logs -f falco-6t2dd
22:57:09.163982780: Notice A shell was spawned in a container with an attached terminal (user=root user_loginuid=-1 k8s.ns=default k8s.pod=nginx container=c73d9fc1a75d shell=bash parent=runc cmdline=bash terminal=34816 container_id=c73d9fc1a75d image=nginx) k8s.ns=default k8s.pod=nginx
23:09:03.279503809: Warning Sensitive file opened for reading by non-trusted program (user=root user_loginuid=-1 program=cat command=cat /etc/shadow file=/etc/shadow parent=bash gparent=runc ggrandparent=containerd-shim gggparent=containerd-shim container_id=c73d9fc1a75d image=nginx) k8s.ns=default k8s.pod=nginx container=c73d9fc1a75d
감사 로그로 생성, 삭제된 객체, 요청자 정보, 네임스페이스, entrypoint같은 주요 세부 정보를 확인 가능합니다.
이벤트를 감사하며 비정상적인 작업, 의심스러운 작업을 감지하는 것은 매우 중요하며 kube-apiserver기반으로 확인 가능하지만 적절한 구성을 명시적으로 활성화하기 전까지는 기능이 비활성화됩니다.
- Kubernetes에서 요청 수명 주기 각 단계에서 이벤트를 생성하므로 작업을 자세히 추적할 수 있습니다.
Request Life Cycle
Audit 기능을 확인하기 전 Kubernetes 요청이 거치는 단계를 확인해보겠습니다.
kubectl run nginx --image nginx
[1. 요청 수신]
요청을 보내면 우선 kube-apiserver 로 도착합니다.
[2. 요청 기록됨]
kube-apiserver 로 요청 수신하는대로 유효성(성공/실패) 관계없이 “request received” 단계 이벤트를 기록합니다.
[3. 응답 시작]
요청이 인증, 유효성 검사 및 권한 부여를 성공적으로 통과하면 “Response Started” 이벤트를 생성합니다.
이는 시작 시점을 감사 기록으로 남기는 것으로 --watch 요청같이 장시간 연결 요청이 들어왔을 때 계속 기록하는 것이 아닌 시작 시점을 기록합니다.
[4. 응답 완료]
요청이 성공적으로 수행되면 “response complete” 이벤트를 기록합니다.
[5. 패닉 단계]
API 서버 내부의 심각한 오류가 발생한 경우 “panic”을 기록합니다.
RequestReceived → ResponseComplete
RequestReceived → ResponseStarted → ResponseComplete
RequestReceived → Panic
각 단계의 모든 이벤트를 저장하기엔 로그가 많아질 수 있습니다. namespace에서 Pod를 삭제한 기록 같이 특정 이벤트만 기록하는 것이 좋을 수 있습니다.
Audit Policy
감사 정책 예시
kube-apiserver로 감사 정책을 정의할 수있습니다.
omitStages,rules로 나눠지며omitStages옵션은 선택입니다.
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages: ["RequestReceived"]
rules:
- level: RequestResponse
namespaces: ["prod-namespace"]
verbs: ["delete"]
resources:
- group: ""
resources: ["pods"]
resourceNames: ["webapp-pod"]
omitStages는 적용된 이벤트를 제외prod-namespace에서webapp-podpod를delete하는 것만 감사- Log Level은 4단계로 구성
None: 기록 안 함Metadata: 메타데이터만 기록Request: 메타데이터 + 요청 bodyRequestResponse: 메타데이터 + 요청 body + 응답 body
kube-apiserver 감사 로깅 활성화
감사 로깅은 비활성화 되어 있습니다. 활성화하려면 감사 백엔드를 구성해야합니다.
- 로그 백엔드: 감사 이벤트를 마스터 노드 파일에 기록
- Webhook 백엔드: 감사 이벤트를 원격 서비스(ex. Falco)로 전송
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --authorization-mode=Node,RBAC
- --advertise-address=172.17.0.107
- --allow-privileged=true
- --enable-bootstrap-token-auth=true
- --audit-log-path=/var/log/k8-audit.log
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
- --audit-log-maxage=10
- --audit-log-maxbackup=5
--audit-log-path/var/log/k8-audit.log: 감사 로그를 저장할 파일 경로/var/log/k8s-audit.log
--audit-policy-file: 감사 정책 파일의 절대 경로--audit-log-maxage: 이전 감사 로그를 보존할 최대 일수--audit-log-maxbackup: 보존할 감사 로그 파일의 최대 개수--audit-log-maxsize: 감사 로그 파일의 순환 최대 크기(메가바이트)
Static Pod가 아닌 Service로 배포한 경우(참고)
ExecStart=/usr/local/bin/kube-apiserver \
--advertise-address=${INTERNAL_IP} \
--allow-privileged=true \
--apiserver-count=3 \
--authorization-mode=Node,RBAC \
--bind-address=0.0.0.0 \
--enable-swagger-ui=true \
--etcd-servers=https://127.0.0.1:2379 \
--event-ttl=1h \
--runtime-config=api/all \
--service-cluster-ip-range=10.32.0.0/24 \
--service-node-port-range=30000-32767 \
-v=2 \
--audit-log-path=/var/log/k8-audit.log \
--audit-policy-file=/etc/kubernetes/audit-policy.yaml \
--audit-log-maxage=10 \
--audit-log-maxbackup=5
감사 정책 테스트
감사 정책을 확인합니다.
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
rules:
- level: Metadata
namespaces: ["prod-namespace"]
verbs: ["delete"]
resources:
- group: ""
resources: ["pods"]
감사 기록을 확인합니다.
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"da2ad1a3-df15-4b10-a44d-79e73d7ec3c0","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/prod-namespace/pods/webapp-pod","verb":"delete","user":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]},"sourceIPs":["172.17.0.36"],"userAgent":"kubectl/v1.19.0 (linux/amd64) kubernetes/e199641","objectRef":{"resource":"pods","namespace":"prod-namespace","name":"webapp-pod","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":200},"requestReceivedTimestamp":"2021-04-12T05:15:24.182178Z","stageTimestamp":""}
Audit Log Lab
Audit Policy 정의
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
namespaces: ["prod"]
verbs: ["delete"]
resources:
- group: ""
resources: ["secrets"]API Server 수정
- --audit-policy-file=/etc/kubernetes/prod-audit.yaml
- --audit-log-path=/var/log/prod-secrets.log
- --audit-log-maxage=30API Server 관련 설정 추가
- 보통
DirectoryOrCreate형식으로 접근
- name: audit
hostPath:
path: /etc/kubernetes/prod-audit.yaml
type: File
- name: audit-log
hostPath:
path: /var/log/prod-secrets.log
type: FileOrCreate - mountPath: /etc/kubernetes/prod-audit.yaml
name: audit
readOnly: true
- mountPath: /var/log/prod-secrets.log
name: audit-log
readOnly: false