Docker 보안 모범사례 — rootless, 이미지 스캔, 시크릿

Docker 보안이 중요한 이유

Docker 컨테이너는 호스트 OS의 커널을 공유합니다. 컨테이너가 root로 실행되고 적절한 격리가 없으면, 컨테이너 탈출(container escape)을 통해 호스트 시스템 전체가 위험에 노출될 수 있습니다. 또한 취약점이 있는 베이스 이미지, 하드코딩된 시크릿, 과도한 권한은 공격 표면을 넓힙니다.

보안의 핵심 원칙은 **최소 권한(Principle of Least Privilege)**입니다. 컨테이너에 필요한 최소한의 권한, 최소한의 파일, 최소한의 네트워크만 부여합니다.

비루트 사용자로 실행

Docker 컨테이너는 기본적으로 root로 실행됩니다. 이는 가장 흔하고 위험한 보안 문제입니다.

# 잘못된 예: root로 실행 (기본값)
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
# root 사용자로 실행됨 — 위험!
CMD ["node", "server.js"]
# 올바른 예: 비루트 사용자 생성 및 사용
FROM node:22-alpine
WORKDIR /app

# 시스템 사용자/그룹 생성 (로그인 불가, 홈 디렉토리 없음)
RUN addgroup --system appgroup && \
    adduser --system --ingroup appgroup --no-create-home appuser

COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --omit=dev
COPY --chown=appuser:appgroup . .

# 비루트 사용자로 전환
USER appuser

# 1024 이상의 포트 사용 (비루트는 1024 미만 포트 바인딩 불가)
EXPOSE 3000
CMD ["node", "server.js"]
# 실행 중인 컨테이너의 사용자 확인
docker exec my-app whoami
# appuser  ← root가 아닌 것을 확인

# 컨테이너 프로세스 확인
docker exec my-app ps aux
# PID   USER     COMMAND
#   1   appuser  node server.js  ← 비루트로 실행

Node.js 공식 이미지는 node 사용자를 내장하고 있으므로 USER node만 추가해도 됩니다. 단, COPY --chown=node:node으로 파일 소유권도 변경해야 합니다.

이미지 취약점 스캔

베이스 이미지에 포함된 OS 패키지와 라이브러리의 알려진 취약점(CVE)을 스캔합니다.

# Docker Scout (Docker Desktop 내장)
docker scout cves my-app:latest
# ✗ CRITICAL  1   CVE-2024-xxxxx  openssl  3.1.0 → 3.1.5
# ✗ HIGH      3   CVE-2024-yyyyy  libcurl  8.1.0 → 8.5.0
# ✗ MEDIUM    7   ...

# Trivy (오픈소스 스캐너)
# 설치
docker pull aquasec/trivy:latest

# 이미지 스캔
docker run --rm aquasec/trivy:latest image my-app:latest
# my-app:latest (alpine 3.19)
# ==============================
# Total: 12 (CRITICAL: 1, HIGH: 3, MEDIUM: 7, LOW: 1)

# CRITICAL/HIGH만 필터링
docker run --rm aquasec/trivy:latest image --severity CRITICAL,HIGH my-app:latest

# CI/CD에서 사용: CRITICAL 취약점이 있으면 빌드 실패
docker run --rm aquasec/trivy:latest image --exit-code 1 --severity CRITICAL my-app:latest
# GitHub Actions CI에서 이미지 스캔
# .github/workflows/docker-scan.yml
name: Docker Image Scan
on: [push]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t my-app:ci .
      - name: Run Trivy scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'my-app:ci'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

시크릿 관리

Dockerfile이나 이미지에 시크릿을 포함하면 안 됩니다. 이미지 레이어에 영구 기록되어 누구나 추출할 수 있습니다.

# 절대 금지: Dockerfile에 시크릿 하드코딩
ENV DATABASE_PASSWORD=mysecretpassword
# docker history로 누구나 확인 가능!

# 절대 금지: 빌드 시 시크릿 COPY
COPY .env /app/.env
# 이미지 레이어에 영구 기록됨!

올바른 시크릿 관리 방법입니다.

# docker-compose.yml — 환경변수 파일로 주입
services:
  api:
    build: ./api
    # 방법 1: env_file로 주입 (.env 파일은 이미지에 포함되지 않음)
    env_file:
      - .env
    # 방법 2: environment로 개별 지정
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - API_KEY=${API_KEY}
# 실행 시 환경변수 전달
docker run -d \
  --name api \
  -e DATABASE_URL="postgres://user:pass@db:5432/myapp" \
  -e API_KEY="${API_KEY}" \
  my-app:latest

# Docker Swarm 시크릿 (암호화 저장)
echo "my-secret-password" | docker secret create db_password -

# Compose에서 Docker 시크릿 사용
# docker-compose.yml
services:
  api:
    image: my-app:latest
    secrets:
      - db_password
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    external: true
# BuildKit 시크릿 (빌드 시 시크릿이 레이어에 기록되지 않음)
# Dockerfile
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

# 빌드 명령
docker build --secret id=npmrc,src=.npmrc -t my-app:latest .

이미지 보안 강화

최소한의 파일만 포함하는 안전한 이미지를 만드는 방법입니다.

# 보안 강화 Dockerfile 예제
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm ci --omit=dev

FROM node:22-alpine
WORKDIR /app

# 불필요한 패키지 제거
RUN apk --no-cache upgrade && \
    # 셸 접근 제한 (디버깅 불편하지만 보안 강화)
    rm -rf /bin/sh /bin/ash 2>/dev/null || true

# 비루트 사용자
RUN addgroup --system app && adduser --system --ingroup app app

# 빌드 결과물만 복사
COPY --from=builder --chown=app:app /app/dist ./dist
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
COPY --from=builder --chown=app:app /app/package.json ./

# 파일시스템 읽기 전용 설정
RUN chmod -R a-w /app/dist

USER app

EXPOSE 3000
# 시그널 처리를 위해 exec 형태의 CMD 사용
CMD ["node", "dist/server.js"]
# 읽기 전용 파일시스템으로 실행
docker run -d --read-only \
  --tmpfs /tmp:rw,noexec,nosuid \
  --name api \
  my-app:latest

# 커널 기능 제한 (불필요한 Linux capability 제거)
docker run -d \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --name api \
  my-app:latest

# 보안 옵션 종합 적용
docker run -d \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid \
  --cap-drop=ALL \
  --security-opt=no-new-privileges:true \
  --memory=512m \
  --cpus=1 \
  --pids-limit=100 \
  --name api \
  my-app:latest

Rootless Docker

Docker 데몬 자체를 비루트로 실행하는 모드입니다. 데몬이 root 권한 없이 동작하므로 컨테이너 탈출 시에도 호스트 root 권한을 얻을 수 없습니다.

# Rootless Docker 설치 (Ubuntu)
# 사전 요구사항
sudo apt install -y uidmap dbus-user-session

# 설치 스크립트 실행
dockerd-rootless-setuptool.sh install

# 환경변수 설정 (~/.bashrc에 추가)
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

# Rootless 모드 확인
docker info | grep -i rootless
# Security Options: rootless

# Rootless 모드에서 컨테이너 실행
docker run -d --name web -p 8080:80 nginx:alpine

Rootless 모드의 제한사항입니다.

항목제한
포트1024 미만 포트 바인딩 불가 (sysctl로 해제 가능)
네트워크overlay 네트워크 미지원
스토리지일부 스토리지 드라이버 미지원
cgroupcgroup v2 필요

보안 체크리스트

# Docker Bench Security — CIS 벤치마크 자동 검사
docker run --rm --net host --pid host \
  --userns host --cap-add audit_control \
  -v /var/lib:/var/lib:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -v /etc:/etc:ro \
  docker/docker-bench-security

# 출력 예시
# [PASS] 1.1  - Ensure Docker is up to date
# [WARN] 2.1  - Run the Docker daemon as a non-root user
# [PASS] 4.1  - Ensure a user for the container has been created
# [WARN] 4.6  - Ensure HEALTHCHECK instructions have been added

실전 팁

  • 이미지 태그 고정: FROM node:latest 대신 FROM node:22.12-alpine처럼 구체적인 버전을 지정하세요. latest는 예고 없이 변경될 수 있어 빌드 재현성과 보안 감사가 불가능합니다.
  • 멀티스테이지 빌드 필수: 빌드 도구, 소스 코드, devDependencies가 프로덕션 이미지에 포함되면 공격 표면이 넓어집니다. 실행에 필요한 파일만 최종 스테이지에 복사하세요.
  • docker.sock 마운트 주의: /var/run/docker.sock을 컨테이너에 마운트하면 해당 컨테이너가 호스트의 Docker를 완전히 제어할 수 있습니다. CI/CD 도구에서 꼭 필요한 경우에만 사용하고, 대안을 검토하세요.
  • 리소스 제한: --memory, --cpus, --pids-limit 옵션으로 컨테이너의 리소스를 제한하세요. 제한 없이 실행하면 하나의 컨테이너가 호스트 전체 리소스를 소모할 수 있습니다.
  • 정기적 이미지 업데이트: 베이스 이미지를 정기적으로 업데이트하고, CI/CD 파이프라인에 취약점 스캔을 포함하세요. 알려진 취약점이 있는 이미지를 프로덕션에 배포하지 않는 것이 핵심입니다.

이 글이 도움이 되었나요?