Docker를 rootless로 바꿔도 제약은 남는다…전환 전 4가지를 확인하라

|작성자: QUASA 편집팀|5 분 소요
Docker를 rootless로 바꿔도 제약은 남는다…전환 전 4가지를 확인하라

Docker daemon의 root 권한을 없애려면 subordinate UID·GID 요구사항과 cgroup·포트·소켓 제약이라는 네 가지를 점검한 뒤, 시스템 daemon에서 사용자 daemon으로 접속을 옮겨야 한다. 설치 명령을 실행하는 것만으로 기존 서비스의 권한과 네트워크 설정까지 이전되지는 않는다.

기존 컨테이너의 구성과 영속 데이터를 기록하고 시스템 daemon을 멈춘 다음, 일반 계정에서 rootless daemon을 설치한다. 이후 Docker 클라이언트와 배포 도구가 새 사용자 소켓에 연결되는지 확인하고, 컨테이너를 재생성해 자원 제한·포트·파일 접근을 검증한다. 두 daemon은 데이터 경로가 분리되므로 새 목록이 비어 보여도 기존 컨테이너가 삭제됐다고 판단해서는 안 된다.

rootless가 바꾸는 보안 경계

rootless 모드에서는 Docker daemon과 컨테이너가 모두 비루트 사용자의 user namespace 안에서 실행된다. 컨테이너의 사용자 ID만 호스트의 비루트 ID로 매핑하고 daemon은 root로 실행하는 userns-remap과 구별되는 지점이다. 따라서 daemon이나 컨테이너 런타임의 취약점이 호스트의 root 권한으로 곧바로 이어질 위험을 줄일 수 있다.

권한 축소가 Docker API에 대한 접근 통제를 대신하지는 않는다. Docker Engine 보안 안내는 daemon을 신뢰할 수 있는 사용자만 제어하도록 제한하고, 외부 요청을 받아 컨테이너를 만드는 API에서는 입력 매개변수를 엄격히 검사하도록 권고한다. rootless 계정이 접근할 수 있는 파일과 그 계정의 Docker 소켓도 계속 보호해야 한다.

첫째, subordinate UID·GID와 파일 권한

Docker Engine rootless 컨테이너의 사용자와 그룹 ID가 호스트의 별도 subordinate 범위에 매핑되는 관계

rootless daemon을 실행할 일반 계정으로 로그인해 command -v newuidmap newgidmap을 실행한다. 두 명령은 대부분의 배포판에서 uidmap 패키지가 제공한다. 이어 grep "^$(whoami):" /etc/subuid와 grep "^$(whoami):" /etc/subgid를 각각 실행해 계정 항목을 확인한다. 두 파일 모두 해당 사용자에게 최소 65,536개의 subordinate ID가 배정돼 있어야 한다.

항목이 없거나 범위가 짧다면 계정 관리 절차에 따라 범위를 배정한 뒤 설치한다. 다른 계정에 배정된 범위와 겹치지 않는지도 확인해야 한다. 설치 도구는 전제 조건이 충족되지 않으면 도움말을 표시하지만, 운영 호스트에서는 중단 시간을 잡기 전에 필요한 계정 설정을 끝내는 편이 낫다.

기존 컨테이너가 호스트 디렉터리를 bind mount한다면 그 경로의 소유자와 읽기·쓰기 권한도 기록한다. 새 daemon은 선택한 일반 계정의 권한으로 실행되므로 이전 root 권한 daemon이 사용하던 파일을 같은 방식으로 열 수 있다고 가정할 수 없다. 권한 변경이 필요하다면 서비스가 실제로 접근하는 디렉터리와 파일을 기준으로 범위를 정한다.

둘째, cgroup 제한의 적용 여부

기존 서비스가 --memory, --cpus 또는 --pids-limit에 의존한다면 호스트의 cgroup 구성이 전환 조건이다. rootless에서 이 제한을 사용하려면 cgroup v2와 systemd가 필요하다. 전환 후 docker info의 Cgroup Driver가 none으로 나오면 해당 제한 옵션은 무시된다. systemd로 나오더라도 사용자 서비스에 위임된 controller만 사용할 수 있다.

전환 전 컨테이너별 자원 제한을 기록하고, 새 daemon에 연결한 상태에서 docker info를 확인한다. 이어 cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers로 위임된 controller를 확인한다. 일반 사용자에게는 memory와 pids만 위임된 구성이 있을 수 있으므로 CPU 제한이 필요한 서비스라면 cpu controller의 존재까지 봐야 한다.

컨테이너가 정상적으로 시작했다는 사실만으로 자원 상한이 강제된다고 볼 수는 없다. 필요한 controller가 없다면 시스템 관리자가 systemd 위임 설정을 조정한 뒤 다시 확인해야 한다. 여러 서비스를 한 호스트에서 운영한다면 이 차이가 장애 시 자원 경합으로 이어질 수 있으므로, 제한의 적용 여부를 전환 완료 조건에 넣는다.

셋째, 낮은 번호의 호스트 포트

Docker Engine rootless 서비스가 호스트의 80 포트 대신 8080 포트로 게시되는 차이

호스트의 1024 미만 포트를 게시하는 서비스에는 별도 조치가 필요하다. 기존 웹 서비스가 호스트의 80이나 443 포트를 직접 사용한다면 새 daemon에서 같은 게시 설정이 가능한지 먼저 결정한다. 외부 접속 경로를 변경할 수 있다면, 예를 들어 호스트의 8080 포트를 컨테이너 내부의 80 포트로 연결하는 8080:80 구성을 사용할 수 있다.

기존 호스트 포트를 유지해야 한다면 rootlesskit 실행 파일에 CAP_NET_BIND_SERVICE를 부여하고 사용자 daemon을 재시작하거나, net.ipv4.ip_unprivileged_port_start를 조정하는 방법이 있다. 전자는 해당 실행 파일의 권한을 바꾸고 후자는 호스트의 낮은 포트 정책을 바꾸므로 운영 정책에 맞춰 선택해야 한다. 어느 방법을 택하든 컨테이너 기동 상태만 보지 말고 외부에서 실제 게시 포트로 접속해 응답을 확인한다.

프록시, 방화벽, 헬스 체크가 기존 호스트 포트를 가리킨다면 그 설정도 함께 점검한다. 호스트 포트를 높은 번호로 바꾼 경우에는 컨테이너 내부 포트를 그대로 유지해도 외부 경로는 달라진다. 특히 프록시가 새 포트로 전달하지 않으면 애플리케이션 자체가 정상이어도 요청이 도달하지 않는다.

넷째, 소켓·context·사용자 서비스

기본 시스템 daemon의 소켓은 /var/run/docker.sock이고, rootless 소켓의 기본 경로는 $XDG_RUNTIME_DIR/docker.sock이다. $XDG_RUNTIME_DIR은 일반적으로 /run/user/사용자ID를 가리킨다. 설치 도구는 사용자 systemd 서비스와 rootless CLI context를 만들므로 docker context show와 docker info의 Server 섹션을 함께 확인한다. Security Options에 rootless가 표시돼야 새 daemon에 접속한 것이다.

CLI에서는 docker context use rootless로 접속 대상을 선택할 수 있다. 명시적인 소켓 주소를 요구하는 배포 도구에는 export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock을 설정한다. 자동화마다 context, DOCKER_HOST, 명령행의 호스트 지정 중 무엇을 사용하는지 확인해야 한다. 한 셸의 context를 바꿔도 별도 서비스나 CI 작업의 연결 주소까지 자동으로 바뀌지는 않는다.

서비스 파일과 백업 스크립트에서 /var/run/docker.sock을 직접 참조하는 부분도 찾는다. 부팅 시 사용자 daemon이 필요하다면 일반 계정에서 systemctl --user enable docker를 실행하고, 관리자가 sudo loginctl enable-linger 사용자이름을 설정한다. 상태 확인에는 systemctl --user status docker를 사용한다. 시스템 서비스의 docker.service와 사용자 서비스의 docker.service는 이름이 같아도 관리 대상이 다르다.

기존 daemon 중지부터 복구까지

전환 전 기존 daemon에 연결된 상태에서 docker ps -a, docker image ls, docker volume ls, docker network ls로 리소스를 기록한다. Compose 파일, 환경 변수, bind mount 경로와 영속 데이터의 백업·복구 방법도 확보한다. rootless daemon의 기본 데이터 디렉터리는 ~/.local/share/docker로, 기존 시스템 daemon의 저장소와 분리된다. 따라서 이미지와 컨테이너를 새 daemon에서 사용할 수 있도록 재구성할 계획이 필요하다.

  1. 서비스 중단 시간을 정해 기존 컨테이너를 정상 종료하고 데이터 백업을 확인한다. sudo systemctl disable --now docker.service docker.socket으로 시스템 daemon과 소켓 활성화를 멈춘 뒤 systemctl is-active docker.service docker.socket으로 상태를 확인한다. 기존 소켓 파일이 남아 설치를 방해한다면 daemon 중지를 확인한 후 제거한다.
  2. 일반 계정에서 dockerd-rootless-setuptool.sh install을 실행한다. 명령이 없다면 rootless extras 패키지의 설치 여부를 확인한다. 설치 출력에서 사용자 서비스와 rootless context 생성 여부를 보고 systemctl --user status docker로 daemon 상태를 확인한다.
  3. docker context show와 docker info를 실행해 접속 대상을 검증한다. rootless context를 사용하고 Server 섹션의 Security Options에 rootless가 나타나는지 본다. DOCKER_HOST를 쓰는 셸이나 자동화는 값이 사용자 소켓을 가리키는지도 별도로 확인한다.
  4. 기록한 구성으로 컨테이너를 새 daemon에서 재생성한다. 영속 데이터는 백업 계획과 파일 권한을 확인한 뒤 연결한다. 호스트 포트 접속, bind mount 읽기·쓰기, 필요한 cgroup 제한을 서비스별로 확인하고 트래픽을 전환한다.

되돌려야 한다면 먼저 새 서비스의 쓰기 작업을 멈춰 두 daemon의 데이터가 갈라지지 않게 한다. 일반 계정에서 systemctl --user disable --now docker를 실행하고, 해당 셸의 DOCKER_HOST와 DOCKER_CONTEXT를 해제한 뒤 docker context use default로 돌아간다. 이어 sudo systemctl enable --now docker.service docker.socket으로 기존 daemon을 시작하고 docker info와 기존 컨테이너 목록을 확인한다. 전환 중 새로 기록된 데이터는 서비스별 복구 절차에 따라 처리하고, 두 daemon의 데이터 디렉터리를 서로 덮어쓰지 않는다.

함께 읽기:

공유:

뉴스레터 구독

최신 Web3, AI, 암호화폐 뉴스를 이메일로 받아보세요.

0