LLM WikiAccess-protected knowledge portal
← 스터디 홈
4편 · 약 21분

보안 강화: rootless·seccomp·AppArmor

컨테이너는 기본적으로 얼마나 안전한가

"컨테이너는 격리돼 있으니 안전하다"는 믿음은 절반만 맞다. 기본 설정의 Docker는 컨테이너 프로세스가 host root와 동일한 UID 0으로 실행된다. 컨테이너 탈출 취약점이 발생하면 공격자는 호스트에서 root 권한을 얻는다.

VM과 달리 컨테이너는 커널을 공유한다. 따라서 컨테이너 보안은 커널 공격 표면을 최소화하는 것이 핵심이다. Docker는 이를 위해 여러 레이어의 방어 기제를 제공한다.

공격자의 시각: 각 레이어를 뚫어야 호스트 root 획득
레이어 1 — 프로세스 격리 Rootless Mode — 데몬 자체가 비-root 사용자로 실행 (user namespace) --user 옵션 — 컨테이너 프로세스를 비-root UID로 실행
↓ 뚫으면
레이어 2 — 권한 최소화 --cap-drop ALL + --cap-add (필요한 것만) — Linux Capability 제한 --security-opt no-new-privileges — 권한 상승 차단
↓ 뚫으면
레이어 3 — 시스템 콜 필터링 Seccomp — 300+ 시스템 콜 중 위험한 44개 차단 (기본 프로파일)
↓ 뚫으면
레이어 4 — MAC (강제 접근 제어) AppArmor / SELinux — 파일·네트워크 접근을 프로파일로 제한
↓ 모두 뚫어야 ⚠️ 호스트 Linux Kernel (cgroup / namespace)
Docker 컨테이너 보안 레이어 구조

Rootless Mode: 데몬 자체를 비-root로

기본 모드의 문제점

일반 Docker 설치에서 dockerd는 root로 실행된다. /var/run/docker.sock 에 쓸 수 있는 사용자는 사실상 호스트 root와 동등한 권한을 가진다.

# docker.sock에 접근할 수 있으면 호스트 root가 된다
docker run -v /:/host --privileged alpine chroot /host

Rootless Mode 내부 동작

Rootless Mode는 dockerd와 컨테이너 전체를 사용자 네임스페이스(user namespace) 안에서 실행한다. 핵심 메커니즘은 다음과 같다.

UID/GID 매핑: /etc/subuid/etc/subgid에 정의된 종속 UID 범위를 사용한다. 컨테이너 내부에서 root(UID 0)로 보이는 프로세스가 호스트에서는 일반 사용자 UID(예: 100000)에 매핑된다.

# /etc/subuid 예시
alice:100000:65536
# alice 계정의 컨테이너 root → 호스트 UID 100000

네트워크: 일반 사용자는 네트워크 네임스페이스를 직접 구성할 수 없어서 slirp4netns 또는 pasta 가 userspace에서 TCP/IP 스택을 에뮬레이트한다. NAT 없이 완전한 격리를 제공하지만, 커널 네트워크 스택보다 약간 느리다.

newuidmap / newgidmap: 단, 여러 UID를 user namespace에 매핑하려면 setuid 비트가 설정된 이 두 바이너리가 필요하다. Rootless라도 이 두 파일은 예외적으로 setuid로 실행된다.

Rootless 설치 및 활성화

# 설치 (Ubuntu/Debian)
sudo apt-get install -y uidmap dbus-user-session
dockerd-rootless-setuptool.sh install

# 환경 변수 설정 (~/.bashrc에 추가)
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

# 자동 시작 (systemd --user)
systemctl --user enable docker
sudo loginctl enable-linger $(whoami)

Rootless의 제약사항

제약이유
특권 포트(< 1024) 바인딩 불가일반 사용자 권한
--privileged 사용 불가user namespace 내에서 의미 없음
AppArmor/Overlay2 일부 제한커널 버전에 따라 다름
성능 (slirp4netns 네트워크 오버헤드)userspace 네트워크 스택

Rootless는 개발 환경과 멀티 테넌트 CI 환경에 특히 적합하다.


Linux Capabilities: root를 쪼개다

UNIX 전통에서 프로세스는 "root이거나 아니거나" 두 가지였다. Linux 2.2부터 Capabilities가 root 권한을 38개(Linux 6.x 기준 42개)의 독립적인 단위로 분리한다.

Docker 기본 Capability 세트

Docker는 기본적으로 컨테이너에 약 14개의 Capability를 부여한다. 나머지는 모두 제거된 상태다.

기본 부여 Capability의미
CAP_CHOWN파일 소유자 변경
CAP_NET_BIND_SERVICE특권 포트(< 1024) 바인딩
CAP_NET_RAW원시 소켓 생성 (ping 등)
CAP_SETUID / CAP_SETGIDUID/GID 변경
CAP_MKNOD디바이스 파일 생성
...

최소 권한 원칙 적용

# 모든 Capability 제거 후 필요한 것만 추가
docker run \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --cap-add CHOWN \
  --security-opt no-new-privileges \
  nginx:alpine

# 위험한 Capability (절대 부여하지 말 것)
# CAP_SYS_ADMIN  — 호스트 마운트, 네임스페이스 조작 등 거의 모든 것
# CAP_SYS_PTRACE — 다른 프로세스 메모리 접근
# CAP_SYS_MODULE — 커널 모듈 로드
# CAP_NET_ADMIN  — 네트워크 인터페이스, 라우팅 테이블 변경

--security-opt no-new-privileges는 컨테이너 프로세스가 execve()를 통해 더 높은 권한을 얻는 것을 금지한다. setuid 바이너리(예: sudo, passwd)가 컨테이너 안에 있어도 작동하지 않는다.


Seccomp: 시스템 콜 방화벽

Seccomp이란

Seccomp(Secure Computing Mode) 는 Linux 커널이 제공하는 시스템 콜 필터링 기능이다. BPF 프로그램으로 구현되어 각 시스템 콜 호출 시 허용 여부를 결정한다. 컨테이너 탈출 공격의 대부분은 커널 취약점을 시스템 콜로 트리거하므로, seccomp은 공격 표면을 대폭 줄인다.

Docker 기본 프로파일

Docker는 기본적으로 약 44개의 시스템 콜을 차단한다. 차단되는 주요 콜은 다음과 같다.

차단된 시스템 콜이유
ptrace프로세스 메모리 조작, 디버거 연결
kexec_load새 커널 부팅
mount임의 마운트
pivot_root루트 파일시스템 전환
reboot시스템 재부팅
clone (일부 플래그)새 네임스페이스 생성
# 기본 seccomp 프로파일 확인 (JSON 형식)
# https://github.com/moby/moby/blob/master/profiles/seccomp/default.json

# seccomp 비활성화 (절대 프로덕션에서 사용하지 말 것)
docker run --security-opt seccomp=unconfined myapp

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

커스텀 Seccomp 프로파일 작성

특수한 애플리케이션(예: eBPF 사용 앱, 고급 네트워킹 도구)은 기본 프로파일로는 동작하지 않을 수 있다. 이때 커스텀 프로파일을 작성한다.

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    {
      "names": ["read", "write", "open", "close", "stat", "fstat",
                "mmap", "mprotect", "brk", "exit_group", "futex",
                "clone", "execve", "wait4", "socket", "connect",
                "accept", "sendto", "recvfrom", "epoll_wait"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

defaultAction: SCMP_ACT_ERRNO는 명시되지 않은 모든 시스템 콜을 거부한다. 이 방식은 매우 강력하지만 애플리케이션마다 허용 목록을 세밀하게 조정해야 한다.

실전 팁: strace -f -c myapp 으로 실제 사용하는 시스템 콜 목록을 먼저 파악한 뒤 프로파일을 작성한다.


AppArmor: 파일·네트워크 접근 제어

Seccomp vs AppArmor

두 기술은 다른 계층에서 작동한다.

항목SeccompAppArmor
제어 대상시스템 콜 종류파일·네트워크·리소스 접근 경로
정의 방식syscall 번호 + BPF 필터텍스트 프로파일 (경로 패턴)
우선 순위시스템 콜 진입 전시스템 콜 내부에서
기본 동작44개 차단docker-default 프로파일

Docker 기본 AppArmor 프로파일

Docker는 컨테이너마다 자동으로 docker-default AppArmor 프로파일을 적용한다 (비활성화하지 않는 한).

# 현재 컨테이너에 적용된 프로파일 확인
docker inspect --format='{{.HostConfig.SecurityOpt}}' mycontainer

# AppArmor 상태 확인
aa-status

커스텀 AppArmor 프로파일

#include <tunables/global>

profile docker-nginx flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>

  # 필요한 파일 경로만 허용
  /etc/nginx/** r,
  /var/log/nginx/** w,
  /usr/share/nginx/html/** r,
  /var/run/nginx.pid rw,
  /tmp/** rw,

  # 네트워크 허용
  network inet tcp,
  network inet udp,

  # 다른 모든 파일 접근 거부 (암묵적)
}
# 프로파일 로드
apparmor_parser -r -W /etc/apparmor.d/docker-nginx

# 프로파일 적용
docker run --security-opt apparmor=docker-nginx nginx:alpine

추가 강화 기법

읽기 전용 루트 파일시스템

# 루트 FS를 읽기 전용으로, 쓰기 필요 경로만 tmpfs
docker run \
  --read-only \
  --tmpfs /tmp:rw,size=64m \
  --tmpfs /run:rw,noexec,nosuid \
  myapp

파일시스템 변조(웹 쉘 업로드, 바이너리 교체)를 원천 차단한다.

리소스 제한

# CPU와 메모리 제한으로 DoS 방어
docker run \
  --cpus="0.5" \
  --memory="256m" \
  --memory-swap="256m" \
  --pids-limit=100 \
  myapp

--pids-limit은 fork 폭탄 공격을 방어한다.

비-root 사용자로 실행

Dockerfile 수준에서 비-root 사용자를 설정하는 것이 가장 기본적인 방어다.

FROM node:20-alpine

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

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

USER appuser
CMD ["node", "server.js"]
# 실행 시 오버라이드도 가능
docker run --user 1001:1001 myapp

보안 강화 체크리스트

Dockerfile 수준 ✅ USER 비-root 지정 ✅ 최소 베이스 이미지 (alpine, distroless) ✅ 불필요한 패키지 제거 ✅ 비밀키·자격증명 COPY 금지
런타임 수준 ✅ --cap-drop ALL + 필요한 것만 --cap-add ✅ --security-opt no-new-privileges ✅ --read-only + tmpfs (필요한 경로만) ✅ --pids-limit 설정 ✅ seccomp 커스텀 프로파일 (민감 서비스) ✅ AppArmor/SELinux 프로파일 적용
절대 금지 ❌ --privileged 프로덕션 사용 ❌ CAP_SYS_ADMIN 부여 ❌ docker.sock 컨테이너에 마운트 ❌ seccomp=unconfined 프로덕션 사용
컨테이너 보안 강화 실전 체크리스트

Docker Compose에서 보안 옵션 적용

services:
  api:
    image: myapi:latest
    user: "1001:1001"
    read_only: true
    tmpfs:
      - /tmp:size=32m
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    security_opt:
      - no-new-privileges:true
      - seccomp:./seccomp-profile.json
      - apparmor:docker-myapi
    pids_limit: 50
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 256M

심층 방어의 핵심: 레이어 중첩

단일 기술만으로는 충분하지 않다. Rootless + Capability 최소화 + Seccomp + AppArmor + 읽기 전용 FS를 중첩하면, 공격자는 여러 독립적인 방어 레이어를 모두 우회해야 한다. 각 레이어가 완벽하지 않아도 중첩 자체가 공격 비용을 크게 높인다.

Open question: 2025년 Qualys는 Ubuntu의 unprivileged namespace 제한을 우회하는 세 가지 방법을 공개했다. Rootless 모드도 BuildKit을 컨테이너 안에서 실행할 때는 seccomp=unconfinedapparmor=unconfined가 필요해 보안 수준이 낮아질 수 있다. 이 제약의 해소 방안은 커뮤니티에서 활발히 논의 중이다.

References