Docker Security 컨테이너 보안

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로 주기적으로 점검하면 프로덕션 환경에서도 안전한 컨테이너 운영이 가능합니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux