Docker 보안 기초 — 루트리스 모드와 권한 최소화
컨테이너 안에서 root로 실행되는 프로세스가 있습니다. 컨테이너니까 안전하다고 생각할 수 있지만, 컨테이너 격리가 깨지면 호스트의 root 권한을 그대로 얻게 됩니다. 이 위험을 어떻게 줄일 수 있을까요?
컨테이너 보안의 기본 원칙
컨테이너는 가상 머신이 아닙니다. 호스트 커널을 공유하고, namespace와 cgroups로 격리할 뿐입니다. 따라서 방어를 여러 겹으로 쌓는 것(defense in depth)이 중요합니다.
- 비루트 사용자로 실행
- 불필요한 권한 제거
- 시스템 콜 제한
- 읽기 전용 파일 시스템
- rootless Docker 사용
USER 지시어 — 비루트 실행
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 기반
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"]
실행 시 사용자 지정
# 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
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
CAP_SYS_ADMIN — mount, namespace 조작 등 (가장 위험)
CAP_NET_ADMIN — 네트워크 설정 변경
CAP_SYS_PTRACE — 다른 프로세스 디버깅
CAP_SYS_MODULE — 커널 모듈 로드
capabilities 제어
# 모든 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
최소 권한 패턴
# 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-privileges는 setuid/setgid 비트를 통한 권한 상승을 방지합니다.
seccomp — 시스템 콜 필터링
seccomp(Secure Computing)은 컨테이너가 호출할 수 있는 시스템 콜을 제한합니다.
Docker 기본 seccomp 프로파일
Docker는 기본적으로 약 50개의 위험한 시스템 콜을 차단합니다.
# 기본 프로파일로 실행 (기본 동작)
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 프로파일 예시
{
"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",
"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)을 자동 적용합니다.
# 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)
# SELinux 상태 확인
getenforce
# SELinux 라벨로 볼륨 마운트
docker run -v /data:/data:Z myapp # 프라이빗 라벨
docker run -v /data:/data:z myapp # 공유 라벨
Rootless Docker
일반 Docker vs Rootless Docker
일반 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) │
└────────────────────────────────┘
설치
# 사전 요구사항 설치
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 미만 포트 바인딩 | O | X (slirp4netns로 우회) |
| overlay2 스토리지 | O | O (커널 5.11+) |
| --net=host | O | X |
| cgroups v1 | O | 제한적 |
| AppArmor/SELinux | O | 제한적 |
Docker Desktop
Docker Desktop(macOS, Windows)은 이미 VM 내부에서 실행되므로 rootless와는 다른 격리 수준을 제공합니다.
실전 보안 체크리스트
# 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
# 리소스 제한
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
memory: 256M
# PID 수 제한 (fork bomb 방지)
pids_limit: 100
# 네트워크
networks:
- backend
# 재시작 정책
restart: unless-stopped
추가 보안 옵션
# 프로세스 격리
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
정리
- USER 지시어로 컨테이너 프로세스를 비루트 사용자로 실행합니다. 가장 기본적이면서도 효과적인 보안 조치입니다.
- capabilities를
ALL제거 후 필요한 것만 추가하는 방식으로 권한을 최소화합니다. - seccomp은 기본 프로파일이 자동 적용되어 위험한 시스템 콜을 차단합니다.
- no-new-privileges로 프로세스의 권한 상승을 방지합니다.
- Rootless Docker는 Docker 데몬 자체를 비루트로 실행하여 가장 강력한 보안을 제공하지만, 일부 기능 제한이 있습니다.
- 이런 보안 조치들은 단일로는 완벽하지 않지만, 겹겹이 쌓으면 공격 표면을 크게 줄일 수 있습니다.
댓글 로딩 중...