[도커 30강] 24강. 네트워크 분리와 서비스 간 접근 제어
도커를 쓰다 보면 처음에는 "컨테이너가 서로 통신되기만 하면 된다"는 관점으로 시작합니다. 하지만 서비스가 늘어나고 운영 환경으로 갈수록, 통신이 되는 것보다 어디까지 통신을 허용할지가 더 중요해집니다. 예를 들어 웹 서버, API 서버, DB, 캐시, 배치 워커가 한 호스트에 함께 올라갈 때 모든 컨테이너가 서로 직접 접근 가능한 상태라면, 한 서비스가 침해되었을 때 피해가 수평으로 빠르게 확산됩니다.
오늘 강의의 목표는 단순합니다. 네트워크를 역할별로 분리하고, 서비스 간 접근 경로를 의도적으로 설계하는 법을 익히는 것입니다. 도커의 bridge 네트워크, internal 네트워크, 서비스별 다중 네트워크 연결, publish 포트 최소화 원칙을 한 번에 정리해보겠습니다. 이 원칙은 이후 Kubernetes NetworkPolicy를 이해할 때도 그대로 이어집니다.
핵심 개념
- **네트워크 분리(Network Segmentation)**는 서비스 간 통신을 최소 단위로 나누어 공격면을 줄이는 방법입니다. "필요한 통신만 허용"이 기본입니다.
- 외부 노출(ingress)과 내부 통신(east-west)을 분리해야 합니다. 외부에서 들어오는 요청을 받는 서비스와 내부 데이터 계층(DB/Redis)은 같은 접근 정책이면 안 됩니다.
- 도커에서는 컨테이너가 같은 사용자 정의 네트워크에 있을 때만 이름 기반 DNS 통신이 가능합니다. 이 특성을 이용해 의도한 서비스만 같은 네트워크에 넣을 수 있습니다.
ports는 호스트 외부 노출,expose는 문서적 의도 표현(같은 네트워크 내부 참고)이라는 점을 구분해야 합니다. 실무에서는ports를 최소화하는 것이 핵심입니다.internal: true네트워크는 외부 인터넷으로의 직접 egress를 제한하는 데 유용합니다. DB처럼 외부 통신이 불필요한 계층에 적용하면 안전성이 올라갑니다.
기본 사용
예제 1) 단일 네트워크에서 시작해 구조 이해하기
mkdir -p ~/docker100/lesson24 && cd ~/docker100/lesson24
cat > compose.single.yaml <<'YAML'
services:
web:
image: nginx:alpine
ports:
- "8080:80"
depends_on:
- api
api:
image: python:3.12-slim
command: ["python", "-m", "http.server", "8000"]
working_dir: /app
volumes:
- ./:/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: example
YAML
docker compose -f compose.single.yaml up -d
docker compose -f compose.single.yaml ps
docker network ls | grep lesson24 || true
설명:
- 처음에는 모든 서비스가 같은 기본 네트워크에 올라갑니다.
- 이 상태에서는 서비스 간 통신이 편하지만, 분리 관점에서는 과도하게 열려 있는 구조입니다.
- 학습 포인트는 "동작은 쉽지만 통제는 약하다"는 출발점을 명확히 인식하는 것입니다.
예제 2) 프론트/백엔드 네트워크 분리 + 다중 네트워크 연결
cat > compose.segmented.yaml <<'YAML'
services:
web:
image: nginx:alpine
ports:
- "8080:80"
networks:
- frontend
depends_on:
- api
api:
image: hashicorp/http-echo:1.0
command: ["-text=api ok"]
networks:
- frontend
- backend
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: example
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
YAML
docker compose -f compose.segmented.yaml up -d
docker compose -f compose.segmented.yaml exec api sh -lc 'getent hosts db && getent hosts web || true'
docker compose -f compose.segmented.yaml exec web sh -lc 'getent hosts api && getent hosts db || true'
설명:
api는frontend와backend를 모두 연결해 게이트웨이 역할을 합니다.web은frontend만 연결하므로db를 직접 찾기 어렵습니다(설계 의도).db는backend에만 존재하므로 외부 진입점을 최소화할 수 있습니다.
예제 3) internal 네트워크로 데이터 계층 egress 제한하기
cat > compose.internal.yaml <<'YAML'
services:
api:
image: alpine:3.20
command: ["sh", "-c", "sleep infinity"]
networks:
- app_net
- data_net
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: example
networks:
- data_net
networks:
app_net:
driver: bridge
data_net:
driver: bridge
internal: true
YAML
docker compose -f compose.internal.yaml up -d
# 네트워크 상세 확인
for n in $(docker network ls --format '{{.Name}}' | grep 'lesson24'); do
echo "=== $n ==="
docker network inspect "$n" --format '{{.Name}} internal={{.Internal}} containers={{len .Containers}}'
done
설명:
internal: true는 해당 네트워크의 외부 라우팅을 제한해 데이터 계층을 더 고립시킵니다.- DB가 꼭 외부 인터넷과 통신할 이유가 없다면 internal 네트워크는 매우 유효한 기본값입니다.
- 단, 백업 에이전트/모니터링 에이전트 등 필요한 통신 경로는 사전에 설계해야 운영 장애를 막을 수 있습니다.
예제 4) 불필요한 포트 공개 제거와 검증
# 현재 포트 공개 현황 확인
docker ps --format 'table {{.Names}}\t{{.Ports}}'
# compose 파일에서 db 서비스의 ports 항목이 없는지 확인
python3 - <<'PY'
import yaml
for fn in ['compose.segmented.yaml','compose.internal.yaml']:
with open(fn) as f:
doc=yaml.safe_load(f)
db_ports=doc.get('services',{}).get('db',{}).get('ports')
print(fn, 'db ports =', db_ports)
PY
설명:
- 운영 실수의 절반은 "테스트 편의로 연 포트를 그대로 배포"하면서 시작됩니다.
- DB, Redis, 내부 관리 포트는 기본적으로
ports를 열지 않는 습관이 중요합니다. - 연결이 필요하면 앱 계층을 통해 접근시키고, 직접 접근은 bastion/운영 VPN 등 통제된 경로로만 허용하세요.
자주 하는 실수
실수 1) 모든 서비스를 하나의 네트워크에 몰아넣기
- 원인: 설정이 간단하고 초기에는 빨리 동작하기 때문입니다.
- 문제: 한 컨테이너 침해 시 다른 내부 서비스 탐색/접근이 쉬워집니다.
- 해결: 최소한
frontend/backend두 계층으로 나누고, 다중 연결은 필요한 중간 서비스(API)만 허용합니다.
실수 2) 내부 서비스까지 습관적으로 ports 공개
- 원인: 로컬 디버깅 습관이 운영 설정으로 유입됩니다.
- 문제: DB/캐시가 호스트 외부에 노출되어 공격 표면이 급격히 커집니다.
- 해결: 내부 서비스는
ports제거를 기본 정책으로 두고, 필요시 임시 터널링/관리 네트워크로 접근합니다.
실수 3) 네트워크 분리만 하고 실제 통신 검증을 생략
- 원인: Compose가 정상 기동되면 정책도 정상이라고 오해합니다.
- 문제: 의도치 않은 우회 경로가 남아 있거나, 반대로 필요한 통신이 막혀 장애가 발생합니다.
- 해결: 배포 전/후에
docker compose exec로 이름 해석, 포트 접근, 실패 케이스까지 검증하는 체크리스트를 운영합니다.
실수 4) internal 네트워크 도입 후 운영 작업 경로를 설계하지 않음
- 원인: 보안 강화 자체에만 집중하고 백업/모니터링/마이그레이션 흐름을 놓칩니다.
- 문제: 긴급 점검 시 접속 경로가 없어 대응 시간이 늘어납니다.
- 해결: 통제된 관리 경로(예: 전용 관리 컨테이너, VPN, bastion)를 함께 설계하고 문서화합니다.
실무 패턴
실무에서 네트워크 분리를 잘 적용하는 팀은 공통적으로 "구조를 먼저 정의하고 서비스는 그 구조 위에 배치"합니다. 즉 애플리케이션을 만든 뒤 네트워크를 덧붙이는 게 아니라, 네트워크 경계를 먼저 그려두고 서비스별 역할을 맞춰 넣습니다.
첫 번째 패턴은 3계층 기본 모델입니다. edge(front)는 외부 트래픽을 받는 레이어(리버스 프록시/API Gateway), app은 비즈니스 로직, data는 DB/캐시/메시지 브로커 계층으로 분리합니다. edge↔app, app↔data만 허용하고 edge↔data 직접 접근은 원칙적으로 금지합니다. 도커 컴포즈에서는 이 규칙을 네트워크 연결 자체로 강제할 수 있습니다.
두 번째 패턴은 포트 공개 최소화 규칙의 코드화입니다. 팀 규약으로 "외부 공개 가능한 서비스 목록"을 정하고, 그 외 서비스에서 ports가 등장하면 PR에서 차단합니다. 사람이 매번 기억하기 어렵기 때문에 lint 스크립트나 CI 정책으로 자동화하는 것이 안전합니다.
세 번째 패턴은 운영 디버깅 경로 분리입니다. 내부 서비스 문제를 디버깅하려고 평소에 DB 포트를 열어두는 것은 나쁜 트레이드오프입니다. 대신 관리 작업은 일시적 점프 컨테이너나 사설 네트워크 기반 도구를 통해 수행하고, 작업 종료 후 즉시 종료하는 절차를 표준화합니다.
네 번째 패턴은 **명시적 통신 계약(Service Contract)**입니다. 어떤 서비스가 어떤 포트로 누구와 통신하는지 문서화하고, 실제 Compose/Kubernetes 설정과 맞는지 주기적으로 비교합니다. 문서와 실제가 어긋나기 시작하면 보안 구멍이나 장애 원인이 누적됩니다.
다섯 번째 패턴은 관측 가능한 거부(deny) 전략입니다. 접근을 막는 것은 시작일 뿐이며, 막힌 이벤트를 로그/메트릭으로 확인할 수 있어야 운영 품질이 올라갑니다. 예를 들어 API가 DB 외 다른 내부 대상에 연결 시도하는 패턴이 보인다면 설계 위반이나 침해 신호일 수 있습니다.
결론적으로 네트워크 분리의 목적은 복잡함을 늘리는 것이 아니라, 사고가 나도 "어디까지 번질 수 있는지"를 제한하는 데 있습니다. 초기에 조금 더 설계하면, 나중에 훨씬 큰 장애와 보안 비용을 줄일 수 있습니다.
오늘의 결론
한 줄 요약: 도커 네트워크는 연결을 쉽게 만드는 도구이면서 동시에 경계를 강제하는 보안 장치이므로, 서비스 역할 기준으로 분리하고 공개 포트를 최소화해야 한다.
연습문제
- 현재 사용하는 Compose 프로젝트를 기준으로
frontend,backend네트워크를 분리하고, 어떤 서비스가 두 네트워크를 동시에 가져야 하는지 표로 정리해보세요. - 내부 서비스(DB/Redis 등)에서
ports를 제거한 뒤 애플리케이션이 정상 동작하는지 확인하고, 운영 디버깅 대체 경로를 설계해보세요. internal: true네트워크를 데이터 계층에 적용한 후, 필요한 백업/모니터링 통신이 유지되는지 검증 체크리스트를 만들어보세요.
이전 강의 정답
23강 연습문제 해설:
- non-root 전환의 핵심은 단순히
USER한 줄을 추가하는 것이 아니라, 애플리케이션이 실제로 쓰는 경로 권한까지 함께 정리하는 것입니다.Permission denied가 난다고 root로 되돌리기보다, 필요한 디렉터리 소유권/권한을 정확히 맞추는 방식이 정답입니다. --cap-drop ALL과no-new-privileges는 "당장 문제 없으면 생략"이 아니라 기본값으로 두고, 필요한 capability만 근거를 남겨 제한적으로 추가해야 합니다.- read-only root filesystem은 보안과 안정성에 모두 도움이 됩니다. 다만 임시 파일 경로(
/tmp)나 앱 캐시 경로를 명시적으로 허용해야 하므로, 사전 점검과 관측(로그)이 함께 가야 운영에서 유지됩니다.
실습 환경/재현 정보
- OS: macOS 15+ 또는 Ubuntu 22.04+
- Docker 버전: Docker Engine/Desktop 25.x 이상
- 사용 도구: Docker Compose v2
- 실행 순서:
- 단일 네트워크 Compose로 기본 동작 확인
- frontend/backend 네트워크 분리 및 서비스 재배치
- internal 네트워크 적용 후 네트워크 속성 검증
- 포트 공개 현황 점검 및 내부 서비스
ports제거 - 서비스 간 이름 해석/접근 성공·실패 시나리오 테스트
- 재현 체크:
- web이 db에 직접 접근하지 못하는가?
- api는 web·db 양쪽과 필요한 통신만 가능한가?
- data 계층 네트워크가 internal로 설정되었는가?
- 내부 서비스의 불필요한 외부 공개 포트가 제거되었는가?
- 운영 디버깅 대체 경로가 문서화되어 있는가?