지식
Security
컨테이너 보안: Docker·Kubernetes 취약점과 방어 전략
Dockerfile 하드닝, 비루트 컨테이너, 이미지 서명·스캔, K8s RBAC, NetworkPolicy, Pod Security Admission, 컨테이너 탈출 방어까지 실전 설정을 다룹니다.
지난 글에서 WAF의 원리와 배포 전략을 살펴봤다. 이번 글은 컨테이너 환경 특유의 보안 위협과 Docker·Kubernetes 하드닝 방법을 다룬다.
컨테이너 보안이 특별한 이유
컨테이너는 VM과 달리 호스트 커널을 공유한다. 컨테이너가 탈출(escape)에 성공하면 호스트 전체를 장악할 수 있다. 또한 이미지는 레이어로 쌓이기 때문에 중간 레이어에 남긴 시크릿이 docker history로 노출된다.
주요 공격 경로:
- 이미지 취약점: 공개 레지스트리의 베이스 이미지에 포함된 CVE
- 과도한 권한:
--privileged플래그 또는 root 실행으로 컨테이너 탈출 가능 - 시크릿 노출:
ENV DB_PASS=...하드코딩 또는 빌드 레이어 잔류 - etcd 무인증 노출: K8s 클러스터 시크릿 전체 탈취
Dockerfile 하드닝
핵심 원칙 4가지:
1. 다이제스트로 베이스 이미지 고정
# 취약: latest는 언제든 변경될 수 있음
FROM python:3.12-slim
# 안전: SHA-256 다이제스트로 정확한 버전 고정
FROM python:3.12-slim@sha256:a1b2c3d4e5f6...
2. 비루트 사용자로 실행
RUN groupadd -r appgroup && useradd -r -g appgroup -u 1001 appuser
COPY --chown=appuser:appgroup . /app
WORKDIR /app
USER appuser
3. 멀티 스테이지 빌드로 공격 표면 최소화
# 빌드 스테이지 (컴파일러, 도구 포함)
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o app .
# 실행 스테이지 (최소 이미지)
FROM gcr.io/distroless/static-debian12
COPY --from=builder /src/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
Distroless 이미지에는 셸, 패키지 매니저가 없어 공격자가 명령을 실행할 수 없다.
4. 시크릿을 이미지에 넣지 않기
# 잘못된 방법: 이미지 레이어에 시크릿 남음
RUN curl -H "Authorization: Bearer $TOKEN" https://api.example.com/config
# 올바른 방법: 런타임에 환경 변수로 주입
# docker-compose.yml
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Kubernetes 보안 설정
Pod Security Context
apiVersion: v1
kind: Pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:1.2.3@sha256:abc...
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"] # 필요한 경우만
resources:
limits:
memory: "256Mi"
cpu: "500m"
NetworkPolicy: 기본 거부 정책
# 네임스페이스 내 모든 인그레스/이그레스 차단
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# 필요한 연결만 허용
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
spec:
podSelector:
matchLabels:
app: backend
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
RBAC 최소 권한
# ServiceAccount에 필요한 권한만 부여
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"] # create/delete/patch 금지
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
subjects:
- kind: ServiceAccount
name: my-app-sa
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
이미지 취약점 스캔
# Trivy로 이미지 스캔
trivy image --severity HIGH,CRITICAL \
--exit-code 1 \
myapp:latest
# CI/CD 파이프라인 통합 (GitHub Actions)
# .github/workflows/security.yml
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE_NAME }}
format: sarif
exit-code: 1
severity: CRITICAL,HIGH
# Grype로 SBOM 기반 스캔
grype myapp:latest --fail-on high
컨테이너 런타임 보안
# Falco 실시간 이상 탐지 규칙 예시
# /etc/falco/falco_rules.yaml
- rule: Container Shell
desc: 컨테이너 내부에서 셸이 실행됨
condition: >
spawned_process and
container and
proc.name in (shell_binaries)
output: >
Shell spawned in container
(user=%user.name container=%container.name
cmd=%proc.cmdline)
priority: WARNING
# 실행 중 특권 마운트 감지
- rule: Mount Sensitive Host Path
condition: >
mount and
(evt.arg.dev startswith "/dev" or
evt.arg.target in ("/etc", "/proc", "/sys"))
priority: CRITICAL
지난 글: WAF: 웹 애플리케이션 방화벽 원리, 배포 전략, 우회 방어
다음 글: 클라우드 IAM 잘못된 설정: 과도한 권한과 방어 전략
읽어주셔서 감사합니다. 😊