지난 주, 새로운 마이크로서비스 아키텍처로 전환하는 프로젝트를 진행하던 중이었습니다. 모든 서비스를 Docker 컨테이너로 구성하고, NGINX 컨테이너를 통해 외부 요청을 각 서비스에 라우팅하는 구조였죠. 그런데 개발 환경에서는 문제없이 작동하던 시스템이 실제 운영 환경에 배포하는 과정에서 예상치 못한 오류를 뱉어냈습니다.
Error starting userland proxy: listen tcp 0.0.0.0:80: bind: permission denied
보안 정책상 컨테이너는 root가 아닌 일반 사용자 권한으로 실행해야 했는데, 이로 인해 80, 443 포트에 접근할 수 없었던 것입니다. 서비스 중단 없이 이 문제를 해결해야 했고, 여기서 Linux Capabilities 시스템의 강력함을 경험하게 되었습니다.
Linux Capabilities란?
전통적인 Unix/Linux 시스템에서는 권한 관리가 ‘root vs non-root’의 이분법적 구조였습니다. 그러나 이는 “모든 것이 가능하거나 아무것도 할 수 없거나”의 극단적인 상황을 만들어냈죠. Linux Capabilities는 이러한 문제를 해결하기 위해 root 권한을 더 작은 단위로 분할한 개념입니다.
각 Capability는 특정 권한을 나타내며, 프로세스에는 필요한 Capability만 정확히 부여할 수 있습니다. 이는 “최소 권한의 원칙”을 따르는 보안 관행과 완벽하게 일치합니다.
Docker에서의 포트 바인딩 문제
리눅스 시스템에서 1024 미만의 포트(잘 알려진 포트)는 보안상의 이유로 root 권한을 가진 프로세스만 사용할 수 있습니다. 웹 서버가 일반적으로 사용하는 80(HTTP)과 443(HTTPS) 포트도 여기에 해당됩니다.
Docker 컨테이너를 root가 아닌 일반 사용자로 실행할 경우, 이러한 특권 포트에 바인딩할 수 없게 됩니다. 이 문제를 해결하기 위한 몇 가지 방법이 있지만, Capabilities를 활용하는 방법이 가장 깔끔한 해결책입니다.
CAP_NET_BIND_SERVICE로 해결하기
CAP_NET_BIND_SERVICE
Capability는 root가 아닌 사용자에게도 1024 미만의 포트에 바인딩할 수 있는 권한을 부여합니다. Docker Compose에서는 다음과 같이 설정할 수 있습니다:
yamlversion: '3'
services:
webserver:
image: nginx:latest
container_name: my-web-server
ports:
- "80:80"
- "443:443"
cap_add:
- NET_BIND_SERVICE
restart: unless-stopped
volumes:
- ./nginx/conf:/etc/nginx/conf.d
- ./www:/var/www/html
- ./certbot/www:/var/www/certbot
- ./certbot/conf:/etc/letsencrypt
이 설정의 핵심은 cap_add
섹션에서 NET_BIND_SERVICE
를 추가한 부분입니다. 이를 통해 컨테이너는 비root 사용자로 실행되더라도 80, 443과 같은 특권 포트에 바인딩할 수 있게 됩니다.
추가로 비root 사용자로 명시적으로 실행하려면 다음과 같이 설정할 수 있습니다:
yamlservices:
webserver:
# ... 이전 설정 동일 ...
cap_add:
- NET_BIND_SERVICE
user: "1000:1000" # UID:GID 형식으로 지정
다양한 Capabilities 옵션 활용하기
Docker 보안을 강화하면서도 필요한 기능을 구현하기 위한 몇 가지 유용한 Capabilities 옵션을 소개합니다:
1. CAP_SYS_PTRACE
프로세스 추적 기능을 활성화하여 다른 프로세스의 메모리를 읽거나 쓸 수 있습니다.
yamlservices:
debug-tool:
image: debugging-image
cap_add:
- SYS_PTRACE
활용 사례: APM(Application Performance Monitoring) 도구를 실행할 때, 예를 들어 Datadog이나 New Relic 에이전트가 애플리케이션 성능을 모니터링하기 위해 필요할 수 있습니다.
2. CAP_SYS_ADMIN
시스템 관리 작업을 수행할 수 있는 강력한 권한을 부여합니다.
yamlservices:
admin-tool:
image: admin-image
cap_add:
- SYS_ADMIN
활용 사례: 특수 파일시스템을 마운트해야 하는 경우, 예를 들어 FUSE 기반 파일시스템이나 S3FS와 같은 도구를 사용할 때 필요합니다. 단, 매우 강력한 권한이므로 신중하게 사용해야 합니다.
3. CAP_NET_RAW
네트워크 패킷을 직접 조작할 수 있는 권한을 부여합니다.
yamlservices:
network-monitor:
image: network-tool
cap_add:
- NET_RAW
활용 사례: 네트워크 모니터링 도구나 와이어샤크와 같은 패킷 분석 도구를 컨테이너 내에서 실행할 때 필요합니다.
4. CAP_CHOWN
파일 소유권을 변경할 수 있는 권한입니다.
yamlservices:
file-manager:
image: file-tool
cap_add:
- CHOWN
활용 사례: 동적으로 생성된 파일의 소유권을 조정해야 하는 백업 도구나 데이터 처리 애플리케이션에서 필요합니다.
보안 강화: 불필요한 Capabilities 제거하기
컨테이너 보안을 더욱 강화하기 위해, 필요하지 않은 Capabilities는 명시적으로 제거하는 것이 좋습니다:
yamlservices:
secure-app:
image: my-app
cap_drop:
- ALL # 모든 capabilities 제거
cap_add:
- NET_BIND_SERVICE # 필요한 기능만 추가
이 접근 방식은 “화이트리스트” 전략으로, 기본적으로 모든 권한을 제거한 후 필요한 것만 명시적으로 추가합니다. 이는 보안 관점에서 가장 안전한 방법입니다.
실제 활용 사례
우리의 마이크로서비스 아키텍처에서는 다음과 같은 방식으로 Capabilities를 활용했습니다:
- 프론트 엔드 NGINX: 외부 요청을 처리하는 NGINX 컨테이너에는
NET_BIND_SERVICE
만 부여하여 80, 443 포트를 사용할 수 있게 했습니다. - 로깅 서비스: 시스템 로그를 수집하는 컨테이너에는
SYS_PTRACE
를 부여하여 다른 컨테이너의 로그를 수집할 수 있게 했습니다. - 백업 서비스: 데이터 백업을 담당하는 컨테이너에는
CHOWN
을 부여하여 다양한 소유권을 가진 파일들을 효율적으로 처리할 수 있게 했습니다. - 일반 애플리케이션: 대부분의 일반 서비스 컨테이너는 특별한 Capabilities 없이 실행하여 보안을 최대화했습니다.
마치며
Docker 컨테이너의 Capabilities를 적절히 관리함으로써, 보안을 강화하면서도 필요한 기능은 모두 구현할 수 있었습니다. “최소 권한의 원칙”에 따라 각 컨테이너에 꼭 필요한 권한만 부여함으로써, 시스템 전체의 보안 태세를 크게 향상시킬 수 있었습니다.
특히 CAP_NET_BIND_SERVICE를 통한 포트 바인딩 문제 해결은 root 권한 없이도 웹 서비스를 표준 포트로 제공할 수 있게 해주어 매우 유용했습니다.
Docker를 운영 환경에서 사용하신다면, Capabilities 시스템을 잘 이해하고 활용하는 것이 보안과 기능 사이의 최적의 균형을 찾는 데 큰 도움이 될 것입니다.