Docker 비루트 사용자: 컨테이너 권한 최소화
컨테이너를 루트가 아닌 사용자로 실행해야 하는 이유, Alpine·Debian 기반 비루트 사용자 생성 패턴, 파일 소유권 문제 해결, Kubernetes PodSecurity 연동까지 실전 가이드를 제공합니다.
지난 글에서 Docker 보안의 5개 레이어를 살펴봤다. 그 중 첫 번째 실천 항목인 **비루트 사용자(non-root user)**를 이번 글에서 깊이 다룬다. 단순히 Dockerfile에 USER 한 줄을 추가하는 것이지만, 파일 소유권과 실행 권한 문제를 제대로 이해하지 않으면 컨테이너가 시작조차 하지 않는 상황이 생긴다.
왜 비루트로 실행해야 하는가
Docker 컨테이너는 기본적으로 root(UID 0)로 실행된다. User Namespace가 활성화되지 않은 환경에서 컨테이너 내부의 UID 0은 호스트의 UID 0과 동일한 식별자를 공유한다. 컨테이너 격리가 뚫렸을 때 root 권한으로 호스트 파일 시스템에 접근하거나 커널 기능을 악용할 수 있다.
비루트 사용자로 실행하면 이 폭발 반경(blast radius)이 크게 줄어든다. 컨테이너가 침해되더라도 공격자는 그 UID의 권한 범위 안에 갇힌다.
사용자 생성 패턴
Alpine Linux
FROM node:20-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --chown=app:app package*.json ./
RUN npm ci --only=production
COPY --chown=app:app . .
USER app
CMD ["node", "server.js"]
Alpine은 addgroup/adduser를 사용하며 -S 플래그가 시스템 사용자(no login shell, no home dir)를 의미한다.
Debian/Ubuntu
FROM python:3.12-slim
RUN groupadd -r app && useradd -r -g app -s /sbin/nologin app
WORKDIR /app
COPY --chown=app:app requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=app:app . .
USER app
CMD ["python", "app.py"]
-r 플래그는 시스템 사용자를 만든다(UID < 1000, no password aging).
공식 이미지의 기본 제공 사용자
많은 공식 이미지는 이미 비루트 사용자를 포함한다.
# node 이미지: 'node' 사용자 (UID 1000)
FROM node:20-alpine
USER node
# nginx 이미지: 'nginx' 사용자
FROM nginx:alpine
# nginx는 master process만 root로 기동, worker는 자동으로 nginx 사용자
# postgres 이미지: 'postgres' 사용자 (UID 999)
FROM postgres:16-alpine
USER postgres
—chown 플래그의 중요성
COPY 명령어는 기본적으로 파일을 root 소유로 복사한다. USER app으로 전환한 후에는 이 파일들을 읽기만 할 수 있고 쓸 수 없다.
# 잘못된 예: root 소유로 복사 후 USER 전환
COPY . .
USER app
# → app이 파일에 쓰려 하면 Permission denied
# 올바른 예: 복사 시점에 소유권 지정
COPY --chown=app:app . .
USER app
이미 빌드된 이미지에서 소유권을 바꾸고 싶다면 RUN chown -R app:app /app을 사용하지만, 이 방법은 레이어를 하나 더 추가한다.
볼륨 마운트와 소유권 충돌
볼륨을 마운트하면 호스트 디렉터리의 소유권이 컨테이너 내부에 그대로 반영된다.
# 호스트에서 root:root 소유인 디렉터리를 마운트
docker run -v /host/data:/app/data --user 1000:1000 myimage
# → 컨테이너 내부 app(1000)이 /app/data에 쓸 수 없음
# 해결: 호스트 디렉터리 소유권을 맞추거나
sudo chown -R 1000:1000 /host/data
# 또는 named volume 사용 (Docker가 소유권 관리)
docker run -v mydata:/app/data --user 1000:1000 myimage
Named volume은 컨테이너 최초 실행 시 Dockerfile의 VOLUME 선언 기준으로 초기화되므로 소유권 문제가 없다.
런타임 오버라이드
Dockerfile에 USER가 없어도 docker run --user로 덮어쓸 수 있다.
# UID:GID 직접 지정
docker run --user 1000:1000 myimage
# 호스트 현재 사용자의 UID/GID 전달 (개발 환경에서 유용)
docker run --user "$(id -u):$(id -g)" myimage
# 현재 컨테이너 내 사용자 확인
docker exec mycontainer id
# uid=1000(app) gid=1000(app) groups=1000(app)
자주 발생하는 오류
Permission denied: /tmp
/tmp는 sticky bit가 설정된 디렉터리지만 앱이 직접 쓰는 경우 권한 문제가 생길 수 있다.
# /tmp를 앱 사용자가 쓸 수 있도록 명시적으로 권한 부여
RUN mkdir -p /tmp/app && chown -R app:app /tmp/app
USER app
또는 --tmpfs /tmp로 메모리 마운트를 사용한다.
1000번 이하 포트 바인딩
리눅스에서 1024 미만 포트를 열려면 CAP_NET_BIND_SERVICE capability가 필요하다. 비루트 사용자는 이 capability를 기본적으로 갖지 않는다.
# 잘못된 예: 비루트에서 80 포트 열기 시도
EXPOSE 80
USER app
# → bind: permission denied
# 올바른 예 1: 1024 이상 포트 사용
EXPOSE 8080
USER app
# 올바른 예 2: capability 추가
# docker run --cap-add=NET_BIND_SERVICE --user app myimage
프로덕션에서는 앱을 8080 등 고번호 포트로 실행하고 로드밸런서에서 80/443을 처리하게 하는 것이 일반적이다.
pip install / npm install 권한 오류
패키지 설치는 root 권한으로 해야 한다. USER 지시어는 패키지 설치 이후에 배치한다.
FROM python:3.12-slim
RUN groupadd -r app && useradd -r -g app app
WORKDIR /app
# root로 패키지 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 설치 완료 후 사용자 전환
COPY --chown=app:app . .
USER app
CMD ["python", "app.py"]
Compose에서 사용자 지정
services:
web:
image: myimage
user: "1000:1000" # Dockerfile USER 오버라이드
# 또는 환경변수로 동적 지정
# user: "${UID}:${GID}"
Kubernetes PodSecurity 연동
Kubernetes의 PodSecurity Admission은 Restricted 레벨에서 비루트 실행을 강제한다.
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
Dockerfile에 USER를 명시하면 runAsNonRoot: true 검사를 자동으로 통과한다.
비루트 사용자 체크리스트
USER지시어를 패키지 설치 이후, CMD 이전에 배치했는가?COPY --chown또는RUN chown으로 파일 소유권을 맞췄는가?- 앱이 바인딩하는 포트가 1024 이상인가?
- 쓰기가 필요한 디렉터리에 앱 사용자 권한이 있는가?
- 볼륨 마운트 시 호스트 디렉터리 소유권이 컨테이너 UID와 일치하는가?
지난 글: Docker 보안 개요: 컨테이너 보안의 핵심 원칙
다음 글: Docker Secrets: 시크릿 안전하게 관리하기
읽어주셔서 감사합니다. 😊