컨테이너 안에서 root로 실행되는 프로세스가 있습니다. 컨테이너니까 안전하다고 생각할 수 있지만, 컨테이너 격리가 깨지면 호스트의 root 권한을 그대로 얻게 됩니다. 이 위험을 어떻게 줄일 수 있을까요?

컨테이너 보안의 기본 원칙

컨테이너는 가상 머신이 아닙니다. 호스트 커널을 공유하고, namespace와 cgroups로 격리할 뿐입니다. 따라서 **방어를 여러 겹으로 쌓는 것 **(defense in depth)이 중요합니다.

  1. 비루트 사용자로 실행
  2. 불필요한 권한 제거
  3. 시스템 콜 제한
  4. 읽기 전용 파일 시스템
  5. rootless Docker 사용

USER 지시어 — 비루트 실행

Dockerfile에서 비루트 사용자 설정

DOCKERFILE
FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

COPY . .

# 비루트 사용자 생성
RUN addgroup -g 1001 appgroup \
    && adduser -u 1001 -G appgroup -s /bin/sh -D appuser

# 파일 소유권 변경
RUN chown -R appuser:appgroup /app

# 이후 모든 명령어는 appuser로 실행
USER appuser

EXPOSE 3000
CMD ["node", "server.js"]

Debian/Ubuntu 기반

DOCKERFILE
FROM python:3.12-slim

RUN groupadd -r appgroup && useradd -r -g appgroup -m appuser

WORKDIR /app
COPY --chown=appuser:appgroup . .

USER appuser
CMD ["python", "main.py"]

실행 시 사용자 지정

BASH
# Dockerfile에 USER가 없어도 실행 시 지정 가능
docker run --user 1001:1001 nginx

# 또는 사용자 이름으로
docker run --user nobody:nogroup nginx

주의사항

  • 1024 미만의 포트(80, 443 등)는 root만 바인딩할 수 있습니다. 비루트 사용자는 8080 같은 높은 포트를 사용해야 합니다.
  • 일부 이미지는 root로 실행을 전제합니다. 비루트로 전환 시 파일 퍼미션 문제가 생길 수 있습니다.
  • 볼륨 마운트 시 호스트와 컨테이너의 UID/GID가 일치해야 합니다.

Linux Capabilities — 세분화된 권한 제어

전통적인 Linux에서 root는 모든 권한을 가집니다. Capabilities는 이 권한을 약 40개로 세분화합니다.

Docker가 기본으로 허용하는 capabilities

PLAINTEXT
CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FSETID, CAP_FOWNER,
CAP_MKNOD, CAP_NET_RAW, CAP_SETGID, CAP_SETUID,
CAP_SETFCAP, CAP_SETPCAP, CAP_NET_BIND_SERVICE,
CAP_SYS_CHROOT, CAP_KILL, CAP_AUDIT_WRITE

Docker가 기본으로 차단하는 capabilities

PLAINTEXT
CAP_SYS_ADMIN    — mount, namespace 조작 등 (가장 위험)
CAP_NET_ADMIN    — 네트워크 설정 변경
CAP_SYS_PTRACE   — 다른 프로세스 디버깅
CAP_SYS_MODULE   — 커널 모듈 로드

capabilities 제어

BASH
# 모든 capabilities 제거
docker run --cap-drop ALL myapp

# 필요한 것만 추가
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp

# 특정 capability 제거
docker run --cap-drop NET_RAW myapp

최소 권한 패턴

YAML
# compose.yaml
services:
  api:
    image: myapi:latest
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE  # 80, 443 포트 바인딩 필요 시
    security_opt:
      - no-new-privileges:true  # 프로세스가 추가 권한 획득 방지

no-new-privilegessetuid/setgid 비트를 통한 권한 상승을 방지합니다.

seccomp — 시스템 콜 필터링

seccomp(Secure Computing)은 컨테이너가 호출할 수 있는 시스템 콜을 제한 합니다.

Docker 기본 seccomp 프로파일

Docker는 기본적으로 약 50개의 위험한 시스템 콜을 차단합니다.

BASH
# 기본 프로파일로 실행 (기본 동작)
docker run myapp

# seccomp 비활성화 (위험! 테스트 용도로만)
docker run --security-opt seccomp=unconfined myapp

# 커스텀 seccomp 프로파일 적용
docker run --security-opt seccomp=custom-profile.json myapp

차단되는 대표적인 시스템 콜

시스템 콜차단 이유
mount호스트 파일시스템 마운트 방지
reboot호스트 재부팅 방지
clock_settime시스템 시간 변경 방지
kernel_module커널 모듈 로드 방지
unshare새 네임스페이스 생성 방지

커스텀 seccomp 프로파일 예시

JSON
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": ["read", "write", "open", "close", "stat", "fstat",
                "lstat", "poll", "lseek", "mmap", "mprotect",
                "munmap", "brk", "ioctl", "access", "pipe",
                "select", "sched_yield", "mremap", "msync",
                "mincore", "madvise", "dup", "dup2", "nanosleep",

나머지 설정을 이어서 정의합니다.

JSON
                "socket", "connect", "accept", "sendto", "recvfrom",
                "bind", "listen", "getsockname", "getpeername",
                "clone", "fork", "vfork", "execve", "exit",
                "wait4", "kill", "uname", "getcwd", "chdir",
                "rename", "mkdir", "rmdir", "link", "unlink",
                "chmod", "chown", "getuid", "getgid", "geteuid",
                "getegid", "getpid", "getppid"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

AppArmor와 SELinux

AppArmor (Ubuntu/Debian)

Docker는 기본 AppArmor 프로파일(docker-default)을 자동 적용합니다.

BASH
# AppArmor 상태 확인
sudo aa-status

# 기본 프로파일 확인
docker inspect mycontainer --format '{{.HostConfig.SecurityOpt}}'

# 커스텀 프로파일 적용
docker run --security-opt apparmor=my-custom-profile myapp

# AppArmor 비활성화 (테스트 용도만)
docker run --security-opt apparmor=unconfined myapp

SELinux (CentOS/RHEL/Fedora)

BASH
# SELinux 상태 확인
getenforce

# SELinux 라벨로 볼륨 마운트
docker run -v /data:/data:Z myapp     # 프라이빗 라벨
docker run -v /data:/data:z myapp     # 공유 라벨

Rootless Docker

일반 Docker vs Rootless Docker

PLAINTEXT
일반 Docker:
┌── root ────────────────────────┐
│  dockerd (root)                │
│  ├── containerd (root)         │
│  │   └── runc (root)           │
│  │       └── 컨테이너 (root)    │
│  └── docker-proxy (root)       │
└────────────────────────────────┘

Rootless Docker:
┌── 일반 사용자 (UID 1000) ──────┐
│  dockerd (UID 1000)            │
│  ├── containerd (UID 1000)     │
│  │   └── runc (UID 1000)       │
│  │       └── 컨테이너           │
│  └── docker-proxy (UID 1000)   │
└────────────────────────────────┘

설치

BASH
# 사전 요구사항 설치
sudo apt-get install -y uidmap dbus-user-session

# rootless Docker 설치
dockerd-rootless-setuptool.sh install

# 환경변수 설정
export PATH=/home/myuser/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

Rootless의 제한사항

기능일반Rootless
1024 미만 포트 바인딩OX (slirp4netns로 우회)
overlay2 스토리지OO (커널 5.11+)
--net=hostOX
cgroups v1O제한적
AppArmor/SELinuxO제한적

Docker Desktop

Docker Desktop(macOS, Windows)은 이미 VM 내부에서 실행되므로 rootless와는 다른 격리 수준을 제공합니다.

실전 보안 체크리스트

YAML
# compose.yaml — 보안 강화 예시
services:
  api:
    image: myapi:latest
    user: "1001:1001"

    # 권한 최소화
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true

    # 읽기 전용 파일 시스템
    read_only: true
    tmpfs:
      - /tmp
      - /var/run

배포 관련 설정과 리소스 제한을 이어서 정의합니다.

YAML
    # 리소스 제한
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 512M
        reservations:
          memory: 256M

    # PID 수 제한 (fork bomb 방지)
    pids_limit: 100

    # 네트워크
    networks:
      - backend

    # 재시작 정책
    restart: unless-stopped

추가 보안 옵션

BASH
# 프로세스 격리
docker run --pid=private myapp     # PID 네임스페이스 격리

# IPC 격리
docker run --ipc=private myapp     # IPC 네임스페이스 격리

# 읽기 전용 루트 파일시스템
docker run --read-only --tmpfs /tmp myapp

# PID 제한
docker run --pids-limit 100 myapp

# 메모리 스왑 비활성화
docker run --memory 512m --memory-swap 512m myapp

주의할 점

1. USER를 설정했는데 이전 RUN 명령어에서 만든 파일의 소유자가 root라 읽기 실패한다

Dockerfile에서 USER appuser를 마지막에 추가해도, 그 전에 COPYRUN으로 생성된 파일은 root 소유입니다. 애플리케이션이 설정 파일이나 로그 디렉토리에 쓰려고 하면 Permission denied가 발생합니다. COPY --chown=appuser:appuser를 사용하거나, 파일 생성 후 chown으로 소유자를 변경하세요.

2. --privileged 플래그를 사용하면 컨테이너 격리가 사실상 무효화된다

--privileged는 모든 capabilities를 부여하고, 장치 접근 권한까지 열어줍니다. 디버깅이나 "일단 동작하게" 하려고 추가한 --privileged가 프로덕션까지 남아있으면, 컨테이너 탈출 공격에 취약해집니다. 대신 필요한 capability만 --cap-add로 추가하세요.

3. Rootless Docker에서는 1024 미만 포트 바인딩과 일부 스토리지 드라이버가 동작하지 않는다

Rootless 모드에서는 비특권 사용자로 Docker 데몬이 실행되므로, 80이나 443 같은 well-known 포트에 직접 바인딩할 수 없습니다. 또한 overlay2 대신 fuse-overlayfs를 사용하게 되어 I/O 성능이 다소 떨어질 수 있습니다. 이런 제한을 이해하고 도입을 결정하세요.

정리

  • USER 지시어 로 컨테이너 프로세스를 비루트 사용자로 실행합니다. 가장 기본적이면서도 효과적인 보안 조치입니다.
  • capabilities 를 ALL 제거 후 필요한 것만 추가하는 방식으로 권한을 최소화합니다.
  • seccomp 은 기본 프로파일이 자동 적용되어 위험한 시스템 콜을 차단합니다.
  • no-new-privileges 로 프로세스의 권한 상승을 방지합니다.
  • Rootless Docker 는 Docker 데몬 자체를 비루트로 실행하여 가장 강력한 보안을 제공하지만, 일부 기능 제한이 있습니다.
  • 이런 보안 조치들은 단일로는 완벽하지 않지만, 겹겹이 쌓으면 공격 표면을 크게 줄일 수 있습니다.
댓글 로딩 중...