네트워크·보안
읽기 전용 NAS 관제 컨테이너와 Docker 소켓 권한 점검
읽기 전용 루트와 capability 제거를 적용한 NAS 관제 컨테이너에서 Docker 소켓의 :ro 마운트가 API 쓰기를 막지 못한 이유와 점검 순서를 정리합니다.
:ro Docker 소켓은 컨테이너 목록 조회와 API 권한을 분리하지 못했습니다
ORBIT는 iPad 거치용 NAS 실시간 관제 화면으로 운영하고 있으며, LAN 안에서만 열어 두었습니다. 시스템 지표는 /proc에서 읽고, 저장소 사용량은 읽기 전용으로 연결한 Volume 2에서 가져옵니다. Docker 컨테이너 상태도 화면에 표시해야 했기 때문에 Docker Unix 소켓을 데이터 소스로 연결했습니다.
점검에서 Docker 소켓은 읽기 전용 마운트로 표시됐지만, 대시보드는 컨테이너 목록을 정상적으로 받아오고 있었습니다. 이 동작은 마운트 옵션의 읽기 전용 표기가 Docker API의 요청 권한을 읽기 전용으로 바꾸지 않는다는 점을 보여 주었습니다.
소켓 연결이 가능한 프로세스는 Docker API에 쓰기 요청도 보낼 수 있습니다. 컨테이너 생성이나 중지처럼 상태를 바꾸는 API 요청도 같은 연결 경로를 사용합니다. Docker 데몬은 root 권한으로 동작하므로, Docker 보안 문서도 데몬을 제어할 수 있는 사용자는 신뢰해야 한다고 설명합니다.
따라서 소켓 마운트의 :ro는 Volume 2의 읽기 전용 마운트와 다른 의미로 다뤘습니다. Volume 2 마운트는 대시보드가 해당 경로에 파일을 쓰지 못하게 하지만, Docker 소켓의 표기는 Docker API 메서드를 제한하는 경계가 아니었습니다.
현재 구성에서 Docker API 쓰기를 좁히는 부분은 애플리케이션 구현입니다. 소켓 접근이 필요한 이유와, 소켓에 닿은 코드가 어떤 요청을 만들 수 있는지는 별도로 확인해야 했습니다. 이 구분을 하지 않으면 읽기 전용 컨테이너라는 상태만 보고 Docker 데몬 제어 가능성을 놓치게 됩니다.
루트 파일시스템은 읽기 전용으로 두고 /tmp만 메모리 쓰기 경로로 남겼습니다
컨테이너의 파일 쓰기 범위는 Compose 설정에서 먼저 확인했습니다. 루트 파일시스템은 읽기 전용으로 두고, 임시 파일이 필요할 수 있는 /tmp만 tmpfs로 분리했습니다. Docker 소켓과 Volume 2도 각각 컨테이너 내부 경로에 읽기 전용으로 연결했습니다.
services:
orbit-dashboard:
build: .
image: orbit-dashboard:1.0.0
restart: unless-stopped
network_mode: host
read_only: true
tmpfs:
- /tmp:size=16m
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment:
PORT: 3080
DISK_PATH: /nas-volume2
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /volume2:/nas-volume2:ro
read_only: true는 컨테이너 루트 파일시스템을 읽기 전용으로 둡니다. Docker의 실행 옵션 문서에서 설명하는 --read-only와 같은 성격의 설정입니다. 쓰기가 필요한 경로를 넓게 열지 않고 tmpfs로 따로 지정한 이유도 여기에 있습니다.
cap_drop: ALL은 Linux capability를 모두 제거하는 설정입니다. 여기에 no-new-privileges:true를 더해 setuid 같은 방식으로 새 권한을 얻지 못하게 했습니다. Docker 보안 문서도 capability를 줄이면 컨테이너 안 root 프로세스의 권한을 줄일 수 있다고 설명합니다.
다만 이 설정들은 Docker 소켓을 통해 Docker 데몬에 보낼 수 있는 API 요청과는 다른 층위입니다. 루트 파일시스템 쓰기, capability, 새 권한 획득, 볼륨 쓰기 여부는 Compose와 inspect에서 확인하고, Docker API 접근은 소켓을 쓰는 코드에서 따로 확인했습니다.
GET 제한은 대시보드 HTTP 서버와 Docker API 호출부에서 따로 확인했습니다
ORBIT의 Docker API 호출 함수는 Unix 소켓을 대상으로 요청을 만들며, 메서드는 GET으로 고정되어 있습니다. 호출하는 경로도 컨테이너 목록을 가져오는 /containers/json?all=1 한 곳입니다. 대시보드가 표시하는 컨테이너 상태가 조회 기능으로만 구현된 이유입니다.
대시보드 자체의 HTTP 서버에도 메서드 검사가 있습니다. 서버는 GET이 아닌 요청을 받으면 405를 반환합니다. 이 검사는 대시보드에 접속하는 HTTP 요청을 처리하는 부분이고, Docker 소켓에 요청을 만드는 코드의 GET 제한과는 위치와 대상이 다릅니다.
| 점검 항목 | 확인된 상태 |
|---|---|
POST /health | 405 |
GET /health | 200 |
| 컨테이너 health | healthy |
| 컨테이너 실행 사용자 | root |
| 메모리 제한 | 0, 제한 없음 |
| PID 제한 | 없음 |
| 점검 시 CPU 사용량 | 0.00% |
| 점검 시 메모리 사용량 | 52.9MiB |
HTTP 메서드 검사 결과와 Docker API 호출부의 메서드 고정은 함께 확인했습니다. 앞의 검사는 대시보드 서버가 일반 HTTP 쓰기 요청을 받지 않게 하고, 뒤의 검사는 현재 구현된 Docker 조회 기능이 GET 요청만 만들게 합니다. 어느 한쪽만 확인해서는 소켓 접근의 의미를 판단하기 어렵습니다.
Dockerfile에는 /health를 주기적으로 가져오는 HEALTHCHECK도 포함되어 있습니다. 컨테이너가 실행 중인지와 health가 응답하는지는 같은 상태가 아니므로, 상태 확인 때 두 결과를 나누어 보았습니다.
inspect 결과는 root 실행과 자원 상한 미설정을 동시에 보여 주었습니다
inspect에서는 읽기 전용 루트 파일시스템, 전체 capability 제거, 추가 capability 없음, no-new-privileges 적용, 비특권 컨테이너 상태를 확인했습니다. Compose에 적은 격리 설정이 실제 컨테이너에도 적용됐는지 판단하는 기준으로 사용했습니다.
반면 Dockerfile에는 USER 지시어가 없고 inspect의 사용자 항목도 비어 있었습니다. capability를 제거한 상태라도 프로세스의 실행 사용자가 자동으로 비root가 되는 것은 아니므로, 실행 사용자 확인은 capability 확인과 분리했습니다.
자원 사용량과 자원 제한도 분리해 읽었습니다. 표의 CPU와 메모리 수치는 점검 시점의 docker stats 값이며, 메모리나 PID에 상한이 설정됐다는 뜻은 아닙니다. 사용량이 낮은 시점의 관측값을 제한값으로 해석하지 않는 것이 필요했습니다.
ORBIT는 CPU, 메모리, Volume 2 사용량, 온도, uptime, 네트워크 전송량을 일정 간격으로 표시합니다. 이런 관제 기능에 필요한 시스템 지표는 /proc/stat, /proc/meminfo, /proc/net/dev, /proc/uptime, /proc/loadavg를 읽어 수집합니다. Docker 상태 조회와 시스템 지표 수집 경로를 같은 권한 문제로 묶지 않았습니다.
상태 확인과 설정 되돌림은 서로 다른 결과를 확인합니다
실행 상태는 컨테이너 존재 여부와 health 응답으로 먼저 나눠 확인했습니다. 서비스 문서에 남긴 두 명령은 이 순서에서 그대로 사용했습니다.
docker ps --filter name=orbit-dashboard
curl -fsS http://127.0.0.1:3080/health
첫 번째 명령은 이름으로 대상 컨테이너를 확인하고, 두 번째 명령은 로컬 health 응답이 실패하면 종료 상태로 알립니다. 둘 다 통과하더라도 실행 사용자, capability, tmpfs, 볼륨 마운트, 자원 상한까지 정상이라는 뜻은 아니므로 inspect 결과를 이어서 확인했습니다.
설정을 되돌릴 때는 Compose의 read_only, tmpfs, cap_drop, security_opt, Docker 소켓 마운트, Volume 2 마운트를 각각 원래 역할대로 맞춥니다. 루트 파일시스템 쓰기 범위와 임시 쓰기 경로, 새 권한 획득 방지, 저장소 읽기 전용 여부는 같은 설정 하나가 담당하지 않습니다.
특히 Docker 소켓 항목은 일반 파일 볼륨의 읽기 전용 여부와 분리해 판단했습니다. 컨테이너 목록 표시를 유지하려면 소켓 연결과 GET 호출 경로가 필요하지만, 그 연결 자체가 Docker 데몬 접근이라는 사실은 남습니다. 그래서 설정을 확인할 때는 rw=false 표시만 보지 않고 Docker API 요청 메서드와 호출 경로까지 함께 점검했습니다.
확인한 공식 자료
구현 과정에서 판단 기준을 교차 확인한 공식 문서입니다. 글의 사례와 결론은 운영자가 직접 겪은 작업을 바탕으로 작성했습니다.