Skip to Content
Infra & DevOpsDocker 컨테이너를 non-root로 실행해야 하는 이유
☁️ Infra & DevOps2025년 10월 23일

Docker 컨테이너를 non-root로 실행해야 하는 이유

#docker#container-security#devops#infra

컨테이너 보안은 종종 나중으로 밀린다. 애플리케이션이 정상적으로 뜨고 배포만 되면, USER를 명시하지 않은 Dockerfile도 큰 문제 없이 지나가기 쉽다. 하지만 컨테이너가 기본적으로 root로 실행된다는 사실을 의식하고 나면, 이 설정을 그냥 두는 것이 얼마나 불편한 기본값인지 보이기 시작한다.

실제로 내가 맡았던 서비스도 처음에는 root로 실행되고 있었다. 서비스 자체는 잘 동작했고, 기능상 문제도 없었다. 하지만 사내 보안팀의 권고가 있었고, 내부 보안 점검 도구에서도 이 구성이 취약점으로 반복 보고되었다. 프로덕션 컨테이너를 non-root로 실행하도록 바꾸는 작업을 진행하면서, 이 변경이 단순한 보안 체크리스트 대응이 아니라 컨테이너 런타임의 기본 권한 모델을 다시 보는 일이라는 걸 체감했다.

root 실행이 왜 문제인가

Docker 컨테이너는 별도의 사용자를 지정하지 않으면 root로 실행된다. 컨테이너 내부의 root가 곧바로 호스트 root와 동일한 것은 아니지만, 그럼에도 non-root보다 훨씬 많은 권한을 가진다. 컨테이너 탈출 취약점이나 잘못된 런타임 설정이 있을 때 피해 범위를 키우는 쪽은 늘 root 실행이다.

특히 root 실행은 이런 점에서 불리하다.

  • 애플리케이션 프로세스가 불필요하게 넓은 권한을 가진다.
  • 파일 소유권과 퍼미션을 대충 처리해도 동작해 버린다.
  • 나중에 권한을 줄이려 하면 어떤 경로에 쓰기 권한이 필요한지 다시 다 확인해야 한다.
  • 내부 보안 점검 도구에서 반복적으로 취약점 항목으로 보고되기 쉽다.

결국 non-root 전환의 핵심은 “컨테이너가 할 수 있는 일을 줄인다”는 데 있다. 서비스가 굳이 root 권한을 필요로 하지 않는다면, 처음부터 그 권한을 주지 않는 편이 맞다.

Dockerfile에서 실제로 바뀌는 것들

non-root 전환은 USER app 한 줄 넣는 것으로 끝나지 않는다. 그 한 줄이 제대로 동작하려면, 그 전까지 Dockerfile이 암묵적으로 기대하고 있던 root 권한 의존성을 다 걷어내야 한다.

보통은 이런 순서로 정리하게 된다.

  1. 패키지 설치와 시스템 설정은 root 상태에서 끝낸다.
  2. 애플리케이션 실행에 필요한 디렉터리와 파일 소유권을 미리 맞춘다.
  3. 실행 파일과 일반 파일의 퍼미션을 최소 권한으로 조정한다.
  4. 마지막에 USER를 전환하고, 그 상태에서 앱을 실행한다.

개념적으로는 이런 Dockerfile에 가까워진다.

dockerfile
Dockerfile
FROM eclipse-temurin:21-jre RUN groupadd -r app && useradd -r -g app app WORKDIR /app COPY build/libs/service.jar /app/service.jar RUN chown -R app:app /app \ && chmod 644 /app/service.jar USER app ENTRYPOINT ["java", "-jar", "/app/service.jar"]

겉보기엔 단순하지만, 실제로는 이 단계에서 permission denied가 많이 드러난다. 예전에는 root가 다 해주던 일을 이제는 명시적으로 준비해야 하기 때문이다.

USER 위치가 중요한 이유

non-root 전환에서 가장 먼저 많이 틀리는 부분은 USER 지시어의 위치다. 너무 일찍 바꾸면 패키지 설치나 파일 복사 후 권한 조정이 실패한다. 너무 늦게 바꾸면 결국 실행 시점까지 root 권한이 남는다.

실무에서는 보통 이렇게 나눈다.

작업권장 사용자
패키지 설치root
디렉터리 생성root
파일 소유권 변경root
애플리케이션 실행non-root

USER는 “가능한 빨리”가 아니라, “필요한 root 작업이 끝난 직후”에 두는 것이 맞다.

파일 퍼미션은 왜 같이 봐야 하나

root로 실행할 때는 파일 퍼미션이 다소 거칠어도 티가 잘 안 난다. 하지만 non-root로 바꾸면, 애플리케이션이 읽어야 할 파일과 실행해야 할 스크립트, 써야 할 디렉터리가 정확히 드러난다. 이건 불편하지만 오히려 좋은 신호다. 지금까지 무엇에 과한 권한을 주고 있었는지 보이기 때문이다.

보통은 이런 기준으로 정리하면 충분하다.

  • 일반 파일: 644
  • 실행 파일: 755
  • 쓰기 필요한 디렉터리만 소유권 또는 쓰기 권한 부여

예를 들어 JAR 파일은 대개 실행 비트가 필요 없다. Java 프로세스가 읽기만 하면 되기 때문이다.

dockerfile
Dockerfile
COPY build/libs/service.jar /app/service.jar RUN chown app:app /app/service.jar \ && chmod 644 /app/service.jar

이런 식으로 퍼미션을 최소화하면, 컨테이너 안에서 프로세스가 실행 중이더라도 파일을 마음대로 덮어쓰는 위험을 줄일 수 있다.

ADD보다 명시적 다운로드를 택한 이유

외부 파일을 Docker 이미지 빌드 중에 가져와야 할 때 ADD는 편리해 보이지만, 실패 원인이 불분명한 경우가 많다. 반면 wget이나 curl은 다운로드 실패를 더 명확하게 드러내고, 이후 권한 설정까지 한 흐름으로 묶기 쉽다.

예를 들어 이런 패턴이 더 다루기 편하다.

dockerfile
Dockerfile
RUN wget -O /tmp/agent.jar https://example.com/agent.jar \ && chmod 644 /tmp/agent.jar

이 방식의 장점은 두 가지다.

  • 다운로드 실패가 빌드 실패로 바로 드러난다.
  • 파일 권한 조정과 검증 흐름을 한 곳에 모을 수 있다.

non-root 전환을 하다 보면, 결국 “빌드 단계에서 어떤 파일이 어디로 들어오고 어떤 권한을 가져야 하는가”를 더 엄격하게 보게 된다. 이 점에서도 명시적 명령이 유리했다.

non-root가 만들어내는 보안 효과

non-root 실행은 컨테이너 보안의 모든 것을 해결하지는 않는다. 이미지 취약점이 사라지는 것도 아니고, 잘못된 네트워크 정책이 자동으로 안전해지는 것도 아니다. 다만 공격자가 컨테이너 안에서 얻는 초기 권한을 줄인다는 점에서 매우 값싼 방어선이다.

컨테이너가 침해되더라도 non-root 환경에서는 이런 행동이 더 어려워진다.

  • 시스템 경로에 파일 쓰기
  • 프로세스/파일 소유권 임의 변경
  • 불필요한 capability 활용
  • 런타임 설정 실수와 결합된 과도한 권한 행사

즉 이 변경의 의미는 “절대 안전”이 아니라 “피해 반경 축소”에 있다. 심층 방어 관점에서 보면 꽤 좋은 기본기다.

적용하면서 실제로 중요했던 체크포인트

  • 애플리케이션이 실제로 쓰는 디렉터리가 어디인지 먼저 찾는다.
  • JAR, 설정 파일, 스크립트의 퍼미션을 용도별로 나눈다.
  • USER 전환 이후에도 빌드나 실행이 깨지지 않는지 확인한다.
  • root가 아니어도 되는 작업을 root로 남겨두지 않는다.
  • 나중에 다른 서비스에도 복사할 수 있게 하나의 참조 Dockerfile을 만든다.

이런 작업은 코드 변경량 자체는 작아 보이지만, 운영 품질에는 꽤 큰 차이를 만든다. 특히 여러 서비스에 같은 패턴을 반복 적용해야 하는 환경에서는, 한 번 잘 정리한 참조 구현이 이후 작업 속도를 많이 올려 준다.

마무리

컨테이너를 non-root로 실행하는 일은 화려한 아키텍처 변경은 아니다. 하지만 이런 작은 보안 기본기가 쌓여야 나중에 더 큰 리스크를 줄일 수 있다. root로 실행해도 서비스는 돌아가지만, 그게 좋은 기본값이라는 뜻은 아니다.

지금 다시 보면 이 작업의 가장 큰 가치는 보안 정책을 “맞췄다”는 데 있지 않았다. 애플리케이션이 실제로 어떤 권한을 필요로 하는지 Dockerfile 수준에서 다시 드러냈다는 점이 더 중요했다. 그런 의미에서 non-root 전환은 컨테이너 보안의 시작점에 가깝다.

Last updated on