서버 보안 점검 4편: Portainer·Agent·Docker 관리면
Portainer는 브라우저에서 컨테이너를 관리하게 해 주지만, 그 편리함만큼 권한도 큽니다. Docker 공식 문서는 Docker 데몬을 제어할 수 있는 사용자를 신뢰된 사용자로 제한해야 한다고 강조합니다. Docker 소켓을 통해 호스트 루트 파일시스템을 컨테이너에 마운트하는 식의 강력한 작업이 가능하기 때문입니다.
이번 점검의 목표는 단순히 “Portainer 화면이 열린다”를 확인하는 것이 아니었습니다. 관리 UI와 Agent가 공용 인터넷에 불필요하게 노출되지 않는지, Docker 소켓 접근 권한이 과도하지 않은지, 설정 백업과 이미지 버전 관리가 복구 가능한 형태인지 확인했습니다.
3줄 요약
- Portainer 관리 포트는 공용 인터페이스에 직접 게시되지 않았고, 외부 점검에서도 관리용 포트의 열린 응답이 관찰되지 않았습니다.
- 원격 Agent는 공용 IP 전체가 아니라
<TAILSCALE_IP>:<AGENT_PORT>에만 바인딩되어 있었습니다.- 구성 백업과 이미지 태그 고정은 완료됐지만, Server·Agent 버전 정합성, Docker 소켓 권한 오류, 실제 복원 훈련은 추가 확인이 필요합니다.
먼저 구분할 것: 보존된 결과와 재현용 명령
이 글에는 두 종류의 정보가 있습니다.
- 실제로 보존된 결과: 당시 점검 기록에서 성공·경고·미확인 상태가 남아 있는 항목
- 재현용 명령: 같은 종류의 서버에서 현재 상태를 다시 확인하기 위한 일반 명령
원본의 실제 경로, IP, 도메인, 사용자명, 호스트명과 버전 문자열은 공개하지 않습니다. 정확한 명령 원문 전체도 보존되지 않았으므로, 재현용 명령을 당시 실행한 명령인 것처럼 서술하지 않습니다.
점검 결과 요약
| 상태 | 점검 항목 | 보존된 결과 | 판단 |
|---|---|---|---|
| 성공 | Portainer 구성 백업 | 백업 작업 완료 기록이 남아 있음 | 구성 복구 재료는 확보했지만 복원 성공을 뜻하지는 않음 |
| 성공 | 이미지 버전 고정 | Portainer 이미지가 명시적 태그로 고정됨 | 의도하지 않은 latest 변경 위험을 줄임 |
| 성공 | 관리 UI 외부 게시 제한 | 관리용 호스트 포트가 외부에 직접 게시되지 않도록 구성됨 | 공용 공격면 축소 |
| 성공 | 외부 포트 관찰 | 점검 당시 외부 스캔에서 Docker·Portainer 관리용 포트가 열린 상태로 관찰되지 않음 | 해당 경로·시점에서 직접 노출 증거 없음 |
| 성공 | Agent 바인딩 | Agent가 <TAILSCALE_IP>:<AGENT_PORT>에만 바인딩됨 |
사설 오버레이 네트워크를 통한 접근으로 제한 |
| 부분 성공 | Server·Agent 버전 | 서로 다른 버전 문자열이 관찰됨 | 곧바로 장애라고 단정할 수 없으며 지원 조합 확인 필요 |
| 실패 | Docker 소켓 조회 명령 | 점검 중 docker.sock 권한 오류가 발생함 |
해당 진단 명령은 실패했지만 외부 노출 실패를 의미하지는 않음 |
| 보류 | Portainer 복원 훈련 | 실제 복원 성공 기록이 보존되지 않음 | 백업 파일만으로 복구 가능하다고 확정할 수 없음 |
| 실패 | 확정된 보안 실패 | 보존된 기록 안에서는 없음 | 미확인 항목을 성공으로 간주하지 않음 |
외부 스캔의 filtered 또는 무응답 결과는 “모든 네트워크 경로에서 영구적으로 닫혀 있다”는 뜻이 아닙니다. 점검한 출발지, 방화벽 경로와 시점에서 열린 응답을 받지 못했다는 제한된 증거입니다. Compose 설정, 실제 리스닝 주소, 클라우드 방화벽, 다른 네트워크 구간을 함께 확인해야 합니다.
1. Portainer Server의 공용 노출 확인
실제로 보존된 결과
- Portainer의 외부 호스트 포트 게시 설정은 비활성화된 상태였습니다.
- 점검 당시 외부 스캔에서 Docker API와 Portainer 관리용 포트는 열린 상태로 관찰되지 않았습니다.
- Portainer 웹 접근은 별도의 프록시·인증 경로를 사용하도록 구성되어 있었습니다.
이 결과는 관리 UI를 Docker의 0.0.0.0:<PORT> 형태로 직접 공개하는 것보다 안전한 방향입니다. 다만 프록시 뒤에 숨겼다는 사실만으로 인증, 세션, TLS와 접근 정책이 자동으로 안전해지는 것은 아닙니다.
재현용 점검 명령
현재 실행 중인 컨테이너의 이미지와 게시 포트를 먼저 확인합니다.
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}'
Portainer 컨테이너의 실제 포트 바인딩을 JSON으로 확인합니다.
docker inspect '<PORTAINER_CONTAINER>' \
--format '{{json .HostConfig.PortBindings}}'
호스트에서 관리용 포트를 어떤 주소가 듣고 있는지도 확인합니다. 아래 숫자는 Portainer와 Docker 문서의 일반적인 기본 포트 예시이며, 이 서버의 실제 값을 뜻하지 않습니다.
sudo ss -lntp | grep -E ':(2375|2376|8000|9000|9001|9443)([[:space:]]|$)'
판정 기준은 다음과 같습니다.
0.0.0.0:<PORT>또는[::]:<PORT>: 모든 인터페이스에 바인딩될 수 있으므로 공개 방화벽까지 확인127.0.0.1:<PORT>: 로컬 프록시를 거치는 구성인지 확인<TAILSCALE_IP>:<PORT>: 해당 오버레이 인터페이스로 제한됐는지 확인- 출력 없음: 컨테이너 내부 포트만 사용하거나 서비스가 내려가 있을 수 있으므로
docker ps와 함께 판단
외부 노출은 반드시 서버 밖의 신뢰할 수 있는 점검 호스트에서도 확인합니다.
nmap -Pn -p 2375,2376,8000,9000,9001,9443 '<PUBLIC_DOMAIN>'
실제 결과를 블로그에 옮길 때는 도메인, 공용 IP, 비표준 포트와 스캔 출발지 정보를 제거합니다.
2. Portainer Agent를 사설 오버레이 IP에만 바인딩
실제로 보존된 결과
원격 호스트의 Portainer Agent는 모든 인터페이스가 아니라 <TAILSCALE_IP>:<AGENT_PORT>에만 바인딩되어 있었습니다. 공개 글에서는 실제 사설 IP, 호스트명, API 계열과 버전 문자열을 기록하지 않습니다.
Portainer 공식 문서에 따르면 Docker Standalone Agent의 기본 연결 포트는 9001이며, Server와 Agent 간 통신은 Agent 설치 시 생성된 인증서를 사용하는 HTTPS로 수행됩니다. 기본 포트라는 사실과 공용 인터넷에 열어도 된다는 판단은 전혀 다릅니다. 관리 경로는 VPN 또는 별도 관리망으로 제한하는 편이 안전합니다.
안전한 Compose 형태 예시
아래는 구조를 설명하기 위한 예시입니다. 실제 IP와 태그는 환경 변수 또는 비공개 배포 설정에서 관리합니다.
services:
portainer_agent:
image: portainer/agent:<PINNED_AGENT_VERSION>
restart: unless-stopped
ports:
- "<TAILSCALE_IP>:<AGENT_PORT>:9001"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
environment:
AGENT_SECRET: ${AGENT_SECRET}
AGENT_SECRET를 사용한다면 Portainer Server와 Agent에 같은 값을 설정해야 합니다. 값을 확인한다는 이유로 echo, docker inspect 전체 출력이나 스크린샷에 비밀값을 노출하지 않습니다. 공개 기록에는 SET 또는 MISSING만 남깁니다.
재현용 점검 명령
docker inspect '<PORTAINER_AGENT_CONTAINER>' \
--format '{{json .HostConfig.PortBindings}}'
docker port '<PORTAINER_AGENT_CONTAINER>'
sudo ss -lntp | grep -F '<TAILSCALE_IP>:<AGENT_PORT>'
Compose의 최종 해석 결과를 볼 때는 환경변수 값이 출력될 수 있으므로 원문을 그대로 게시하지 않습니다.
docker compose -f '<AGENT_COMPOSE_FILE>' config -q
config -q는 구문 유효성만 확인합니다. 바인딩된 IP가 실제로 원하는 인터페이스인지, 방화벽이 다른 인터페이스의 접근을 차단하는지는 별도 확인이 필요합니다.
3. Docker 소켓 권한 오류를 보안 관점에서 해석하기
실제로 보존된 결과
점검 과정에서 Docker Unix 소켓 접근 시 권한 오류가 발생했습니다. 해당 진단 명령 자체는 실패로 분류했습니다. 다만 이 오류는 실행 사용자의 접근 권한이 없었다는 뜻이며, Docker가 외부에 노출됐거나 반대로 안전하다는 증거는 아닙니다. 이후 권한 모델을 어떻게 확정했는지에 대한 기록은 남아 있지 않습니다.
이 오류를 없애기 위해 다음과 같이 처리하면 안 됩니다.
# 금지 예시: 누구나 Docker 데몬을 제어할 수 있게 만들 수 있음
sudo chmod 666 /var/run/docker.sock
Docker 공식 문서는 데몬 제어 권한이 사실상 호스트의 높은 권한으로 이어질 수 있음을 설명합니다. docker 그룹에 사용자를 추가하는 것도 단순 편의 설정이 아니라 강한 권한 부여입니다.
재현용 점검 명령
ls -l /var/run/docker.sock
id
getent group docker
docker context show
docker info
출력에는 실제 사용자명, 그룹 구성원, 호스트명, 레지스트리와 플러그인 정보가 포함될 수 있으므로 공개 글에는 원문을 붙이지 않습니다.
권장 판단 순서는 다음과 같습니다.
- 자동화 계정이 정말 Docker 데몬 제어권을 가져야 하는지 결정합니다.
- 필요하다면 승인된 관리자만
sudo또는 제한된 운영 절차로 접근하게 합니다. docker그룹을 사용할 경우 그 사용자를 루트 권한 보유자와 같은 수준으로 관리합니다.- Docker TCP API가 필요 없다면 활성화하지 않습니다.
- 원격 API가 꼭 필요하면 Docker 공식 문서의 SSH 또는 상호 TLS 방식을 사용하고 신뢰 네트워크로 제한합니다.
4. Server와 Agent 버전 정합성
실제로 보존된 결과
점검 당시 Portainer Server와 Agent에서 서로 다른 버전 문자열이 관찰됐습니다.
server_image=portainer/portainer-ce:<PORTAINER_SERVER_VERSION>
agent_image=portainer/agent:<PORTAINER_AGENT_VERSION>
버전이 다르다는 사실만으로 통신 실패나 취약성을 단정할 수는 없습니다. 반대로 화면이 연결된다는 이유만으로 지원되는 조합이라고 확정할 수도 없습니다. 점검 당시 사용한 릴리스 계열의 Portainer 업그레이드 문서와 릴리스 정보를 기준으로 정합성을 확인해야 합니다.
재현용 점검 명령
docker inspect '<PORTAINER_CONTAINER>' \
--format '{{.Config.Image}}'
docker inspect '<PORTAINER_AGENT_CONTAINER>' \
--format '{{.Config.Image}}'
태그가 latest라면 실행 시점에 따라 내용이 바뀔 수 있습니다. 운영 서버에서는 검토한 명시적 태그로 고정하고, 업데이트 전에 구성 백업과 애플리케이션 데이터 백업을 분리해 준비합니다.
5. Portainer 백업의 범위를 오해하지 않기
실제로 보존된 결과
- Portainer 구성 백업을 생성했습니다.
- Portainer 이미지 태그를 고정했습니다.
- 실제로 새 인스턴스에 복원해 본 결과는 보존되지 않았습니다.
Portainer 공식 문서에 따르면 Portainer의 자체 백업은 /data에 저장된 Portainer 구성을 위한 것입니다. 관리 대상 환경의 컨테이너, 이미지, 볼륨과 볼륨 안의 애플리케이션 데이터는 포함하지 않습니다. 따라서 다음 두 백업은 별개입니다.
- Portainer 사용자·환경·접근 제어·스택 정의 등 구성 백업
- Ghost, 데이터베이스, 업로드 파일과 기타 서비스 데이터 백업
구성 백업 파일을 받은 뒤에는 파일 자체의 체크섬도 별도로 기록할 수 있습니다.
cd '<PORTAINER_BACKUP_DIR>'
sha256sum '<PORTAINER_BACKUP_FILE>' > '<PORTAINER_BACKUP_FILE>.sha256'
sha256sum --check '<PORTAINER_BACKUP_FILE>.sha256'
이 명령의 OK는 파일이 기록 당시와 같은 바이트인지 확인할 뿐입니다. Portainer가 실제로 복원되고 관리 대상 환경에 다시 연결되는지는 격리된 새 인스턴스에서 복원 훈련으로 확인해야 합니다.
재점검 체크리스트
- [ ] Portainer Server의 호스트 포트가 공용 인터페이스에 직접 게시되지 않는다.
- [ ] Agent는
<TAILSCALE_IP>:<AGENT_PORT>또는 별도 관리망에만 바인딩된다. - [ ] 클라우드 방화벽과 호스트 방화벽에서 관리 포트의 출발지 범위를 제한한다.
- [ ] Docker TCP API가 불필요하게 활성화되어 있지 않다.
- [ ] Docker 소켓 권한은 승인된 관리자에게만 있다.
- [ ] Server와 Agent 이미지가 검토한 명시적 태그로 고정되어 있다.
- [ ] Server·Agent 조합이 해당 릴리스 계열에서 지원되는지 확인했다.
- [ ]
AGENT_SECRET사용 여부를 확인하되 실제 값은 로그에 남기지 않았다. - [ ] Portainer 구성 백업과 애플리케이션 데이터 백업을 별도로 보관한다.
- [ ] 격리 환경에서 Portainer 구성 복원을 실제로 검증했다.
남은 작업
이번 기록에서 확정하지 못한 핵심은 세 가지입니다.
- Docker 소켓 권한 오류의 최종 원인과 승인된 해결 방식
- Portainer Server·Agent 버전 조합의 공식 지원 여부
- 새 인스턴스에서의 Portainer 구성 복원 성공 여부
이 세 항목이 완료돼야 “Portainer 관리면 점검 완료”라고 부를 수 있습니다. 현재 결론은 공용 노출 축소는 성공, Docker 소켓 조회는 실패, 운영 권한과 복구 검증은 미완료입니다.
공식 문서
- Docker Engine security: Docker daemon attack surface
- Docker 공식 문서: Protect the Docker daemon socket
- Portainer 공식 문서: Install Portainer Agent on Docker Standalone
- Portainer 공식 FAQ: Agent 연결 보안 방식
- Portainer 공식 문서: General settings와 Portainer 백업
- Portainer 공식 FAQ: What does Portainer's backup include?
- Portainer 공식 문서: Updating on Docker Standalone