Docker 컨테이너 보안이란?
컨테이너는 호스트 커널을 공유하므로, 잘못된 설정 하나로 컨테이너 탈출(Container Escape)이나 권한 상승이 발생할 수 있습니다. 프로덕션 환경에서는 이미지 빌드 단계부터 런타임까지 체계적인 보안 전략이 필수입니다. 이 글에서는 non-root 실행, 이미지 스캔, read-only 파일시스템, Seccomp/AppArmor, Docker Secret 관리까지 실무에서 바로 적용할 수 있는 심화 보안 전략을 다룹니다.
Non-Root 컨테이너: 기본 중의 기본
Docker 컨테이너는 기본적으로 root로 실행됩니다. 이는 컨테이너 탈출 시 호스트의 root 권한을 얻을 수 있다는 의미입니다.
# ❌ 위험: root로 실행
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci --production
CMD ["node", "dist/main.js"]
# ✅ 안전: non-root 사용자로 실행
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --production
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 3000
CMD ["node", "dist/main.js"]
USER appuser로 전환한 후에는 컨테이너 내부에서도 root 권한을 사용할 수 없습니다. 이는 가장 기본적이면서 가장 효과적인 보안 조치입니다.
최소 베이스 이미지 선택
베이스 이미지가 작을수록 공격 표면(Attack Surface)이 줄어듭니다:
| 이미지 | 크기 | 패키지 수 | CVE 수 (평균) |
|---|---|---|---|
| node:20 | ~1GB | ~400+ | 50~100+ |
| node:20-slim | ~200MB | ~100 | 10~30 |
| node:20-alpine | ~130MB | ~30 | 5~15 |
| gcr.io/distroless/nodejs20 | ~50MB | 최소 | 0~5 |
# Distroless 이미지 사용 (쉘조차 없음)
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["dist/main.js"]
Distroless 이미지에는 쉘, 패키지 매니저, 디버깅 도구가 없어 공격자가 침입해도 할 수 있는 것이 극히 제한됩니다. Multi-Stage Build와 함께 사용하면 최적의 보안·크기 조합을 달성할 수 있으며, 이에 대한 자세한 내용은 Docker Multi-Stage Build 최적화를 참고하세요.
이미지 취약점 스캔: Trivy
Trivy는 컨테이너 이미지의 OS 패키지와 언어 라이브러리 취약점을 모두 스캔합니다:
# 로컬 이미지 스캔
trivy image myapp:latest
# CRITICAL, HIGH만 표시
trivy image --severity CRITICAL,HIGH myapp:latest
# CI에서 취약점 발견 시 빌드 실패
trivy image --exit-code 1 --severity CRITICAL myapp:latest
# SBOM(Software Bill of Materials) 생성
trivy image --format spdx-json -o sbom.json myapp:latest
# GitHub Actions CI 통합
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Upload scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
Read-Only 파일시스템
런타임에서 파일시스템을 읽기 전용으로 마운트하면 악성 코드 주입을 방지합니다:
# docker run
docker run --read-only
--tmpfs /tmp:rw,noexec,nosuid
--tmpfs /var/run:rw,noexec,nosuid
myapp:latest
# docker-compose.yml
services:
app:
image: myapp:latest
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=100m
- /var/run:rw,noexec,nosuid
volumes:
- app-logs:/var/log/app # 로그만 쓰기 허용
noexec 옵션은 tmpfs에서 실행 파일 실행을 차단합니다. nosuid는 setuid 비트를 무시합니다.
Capability 제한
Linux Capability는 root 권한을 세분화한 것입니다. 기본적으로 Docker는 14개의 Capability를 부여하는데, 대부분 불필요합니다:
# 모든 Capability 제거 후 필요한 것만 추가
docker run --cap-drop ALL
--cap-add NET_BIND_SERVICE # 1024 이하 포트 바인딩
myapp:latest
# docker-compose.yml
services:
app:
image: myapp:latest
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true # 권한 상승 차단
no-new-privileges는 프로세스가 setuid, setgid 등으로 권한을 상승시키는 것을 완전히 차단합니다.
Seccomp 프로파일: 시스템콜 제한
Seccomp은 컨테이너가 호출할 수 있는 Linux 시스템콜을 제한합니다:
// custom-seccomp.json
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": [
"accept4", "access", "arch_prctl", "bind", "brk",
"clone", "close", "connect", "dup2", "epoll_create1",
"epoll_ctl", "epoll_wait", "execve", "exit_group",
"fcntl", "fstat", "futex", "getdents64", "getpid",
"getsockname", "getsockopt", "ioctl", "listen",
"lseek", "mmap", "mprotect", "munmap", "openat",
"pipe2", "read", "recvfrom", "rt_sigaction",
"rt_sigprocmask", "sendto", "setsockopt", "socket",
"stat", "write"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
docker run --security-opt seccomp=custom-seccomp.json myapp:latest
Docker Secret과 환경변수 보안
비밀번호와 API 키를 환경변수로 전달하면 /proc이나 docker inspect로 노출될 수 있습니다:
# ❌ 위험: 환경변수로 비밀 전달
docker run -e DB_PASSWORD=secret123 myapp
# ✅ Docker Secret 사용 (Swarm/Compose)
# docker-compose.yml
services:
app:
image: myapp:latest
secrets:
- db_password
- api_key
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
external: true
// 애플리케이션에서 Secret 파일 읽기
import { readFileSync } from 'fs';
function getSecret(name: string): string {
const filePath = process.env[`${name}_FILE`];
if (filePath) {
return readFileSync(filePath, 'utf-8').trim();
}
return process.env[name] ?? '';
}
const dbPassword = getSecret('DB_PASSWORD');
Dockerfile 보안 베스트 프랙티스
# 1. 고정 태그 사용 (latest 금지)
FROM node:20.11.1-alpine3.19
# 2. .dockerignore로 불필요한 파일 제외
# .dockerignore
.git
.env
node_modules
*.md
docker-compose*.yml
# 3. COPY 순서 최적화 (캐시 활용)
COPY package*.json ./
RUN npm ci --production
COPY . .
# 4. 멀티스테이지로 빌드 도구 제거
FROM node:20-alpine AS build
RUN npm ci && npm run build
FROM gcr.io/distroless/nodejs20
COPY --from=build /app/dist ./dist
# 5. HEALTHCHECK 추가
HEALTHCHECK --interval=30s --timeout=3s --retries=3
CMD ["node", "-e", "require('http').get('http://localhost:3000/health')"]
# 6. 불필요한 setuid 바이너리 제거
RUN find / -perm /6000 -type f -exec chmod a-s {} ; 2>/dev/null || true
런타임 보안 체크리스트
# docker-compose.yml - 완전 보안 설정
services:
app:
image: myapp:v1.2.3 # 고정 태그
read_only: true # 읽기 전용 파일시스템
user: "1000:1000" # non-root
cap_drop: [ALL] # 모든 Capability 제거
cap_add: [NET_BIND_SERVICE] # 필요한 것만
security_opt:
- no-new-privileges:true # 권한 상승 차단
- seccomp:custom-seccomp.json # 시스템콜 제한
tmpfs:
- /tmp:rw,noexec,nosuid,size=100m
deploy:
resources:
limits:
cpus: '1.0' # CPU 제한
memory: 512M # 메모리 제한
reservations:
cpus: '0.25'
memory: 128M
networks:
- internal # 격리된 네트워크
logging:
driver: json-file
options:
max-size: "10m" # 로그 크기 제한
max-file: "3"
healthcheck:
test: ["CMD", "node", "-e",
"require('http').get('http://localhost:3000/health')"]
interval: 30s
timeout: 3s
retries: 3
이 설정은 Docker Compose 운영에서 다룬 기본 구성에 보안 레이어를 추가한 것입니다.
Docker Bench Security
Docker 환경의 보안 상태를 자동으로 점검하는 도구입니다:
# Docker Bench 실행
docker run --rm --net host --pid host
--userns host --cap-add audit_control
-v /etc:/etc:ro
-v /var/lib:/var/lib:ro
-v /var/run/docker.sock:/var/run/docker.sock:ro
docker/docker-bench-security
# 출력 예시:
# [PASS] 4.1 - Ensure that a user for the container has been created
# [WARN] 4.5 - Ensure Content trust is enabled
# [PASS] 5.2 - Ensure SELinux or AppArmor is enabled
# [WARN] 5.12 - Ensure the container's root filesystem is read-only
마무리
Docker 컨테이너 보안은 빌드 → 배포 → 런타임 전 과정에 걸쳐 적용해야 합니다. non-root 실행, Distroless 이미지, Trivy 스캔으로 기본기를 갖추고, read-only 파일시스템, Capability 제한, Seccomp 프로파일로 런타임 공격 표면을 최소화하세요. Docker Bench로 주기적으로 점검하면 프로덕션 환경에서도 안전한 컨테이너 운영이 가능합니다.