SecurityDevOps 학습 로드맵 · 9/9

인터넷을 완전히 끊고도 클러스터가 돌아가는지 - 폐쇄망(에어갭) 시뮬레이션 절차

에어갭이 뭔지부터, Harbor 미러 캐시로 이미지·apt·pip·Helm을 전부 내부에 적재하고, 노드 egress를 차단한 뒤 미러만으로 조인·배포·설치가 되는지 검증하는 절차. ISMS-P 2.6 망분리 증적과 연결.

2026-08-0611 min read
#에어갭#Harbor#네트워크 격리#Kubernetes#ISMS-P

목표

지금까지 만든 클러스터는 인터넷에서 이미지·패키지를 자유롭게 내려받는다는 전제로 돌아간다. 이 시리즈는 그 전제를 강제로 없앤다 - 노드가 인터넷에 전혀 나가지 못하는 상태에서도 클러스터 조인, 서비스 배포, 패키지 설치가 되는지를 검증한다. 이 시리즈는 확장(선택)이지만, 통과하면 ISMS-P 2.6(접근통제·망분리) 항목의 실제 증적을 손에 쥐게 된다.

개념: 에어갭(폐쇄망)이란 무엇인가, 왜 필요한가

**에어갭(air-gap)**은 말 그대로 "공기로 끊어져 있다"는 뜻이다 - 네트워크가 물리적/논리적으로 외부 인터넷과 완전히 분리된 환경을 말한다. 보안·국방·금융처럼 데이터 유출 위험을 극단적으로 낮춰야 하는 조직은 서버가 외부와 통신할 수 있는 경로 자체를 없애버린다. 문제는 일반적인 DevOps 도구·관행(apt install, docker pull, helm install - 전부 인터넷에서 뭔가를 받아온다)이 전부 "인터넷이 있다"는 걸 전제로 만들어졌다는 것이다. 에어갭 환경에서 이 도구들을 쓰려면, 인터넷에서 받아올 것들을 미리 내부망 어딘가에 미러링해두고, 모든 도구가 그 내부 미러만 보도록 재설정해야 한다.

egress 차단이란: 노드가 외부로 나가는(egress) 트래픽 자체를 방화벽에서 막는 것이다. 여기서 중요한 구분이 하나 있다 - NetworkPolicy(시리즈 6)는 클러스터 안, 파드 사이의 트래픽을 통제하는 것이고, egress 차단(이 시리즈)은 노드 자체가 클러스터 바깥, 인터넷으로 나가는 걸 막는 것이다. 계층이 다르다 - NetworkPolicy를 아무리 촘촘히 짜도, 노드가 인터넷에 나갈 수 있으면 에어갭이 아니다.

실제 구축 절차

1. Harbor를 프록시 캐시(미러)로 구성 - 이미지 미러링

시리즈 2에서 Harbor를 사설 레지스트리로 썼다면, 이번엔 그 앞단에 프록시 캐시 기능을 추가한다 - 외부 레지스트리(Docker Hub 등)에서 받은 이미지를 Harbor가 한 번 받아서 내부에 캐싱해두고, 이후 요청은 전부 내부 Harbor가 응답하게 만드는 것이다.

# Harbor 관리 콘솔 또는 API로 프록시 캐시 프로젝트 등록
curl -X POST "https://harbor.local/api/v2.0/registries" \
  -u admin:<password> \
  -d '{
    "name": "docker-hub-proxy",
    "type": "docker-hub",
    "url": "https://hub.docker.com"
  }'

curl -X POST "https://harbor.local/api/v2.0/projects" \
  -u admin:<password> \
  -d '{"project_name": "proxy-cache", "registry_id": 1, "metadata": {"proxy_speed": "0"}}'

이미지 pull 경로도 바꿔야 한다 - 클러스터의 모든 노드가 docker.io/library/postgres 대신 harbor.local/proxy-cache/library/postgres를 보게 containerd 설정을 바꾼다.

# /etc/containerd/config.toml - registry mirror 설정
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
  endpoint = ["https://harbor.local/proxy-cache"]

이미지 pull 경로를 제대로 이해하기: kubectl이 파드를 만들면, 그 요청을 받은 노드의 kubelet이 containerd에게 "이 이미지를 실행해라"고 지시하고, containerd가 실제로 레지스트리에서 이미지를 내려받는다. 위 설정은 이 마지막 단계(containerd → 레지스트리)의 목적지를 외부에서 내부 Harbor로 바꾸는 것이다 - kubectl이나 kubelet은 이 사실을 몰라도 된다.

2. apt/pip/Helm 미러 적재

컨테이너 이미지만 미러링하면 끝이 아니다 - 노드 자체에 apt install containerd를 하거나, 파이썬 앱이 pip install을 하거나, Helm이 차트 저장소에서 뭔가를 받아오는 경로도 전부 내부로 돌려야 한다.

# apt 미러 (예: aptly로 내부 미러 서버 구성)
aptly mirror create ubuntu-mirror https://archive.ubuntu.com/ubuntu jammy main restricted universe
aptly mirror update ubuntu-mirror
aptly publish mirror ubuntu-mirror

# 각 노드의 /etc/apt/sources.list를 내부 미러로 변경
echo "deb http://mirror.internal/ubuntu-mirror jammy main restricted universe" | sudo tee /etc/apt/sources.list
# pip 미러 (사내 PyPI 미러 - devpi 또는 단순 파일 서버 + pip download로 사전 수집)
pip download -r requirements.txt -d ./offline-packages
pip install --no-index --find-links=./offline-packages -r requirements.txt
# Helm 차트 미러 - 차트를 내부 저장소(예: ChartMuseum, 또는 Harbor의 Helm 지원)에 재배포
helm pull bitnami/rabbitmq --version 14.0.0
helm push rabbitmq-14.0.0.tgz oci://harbor.local/helm-mirror

apt/pip/Helm 미러의 공통 원리: 셋 다 "패키지 이름을 URL로 바꿔서 내려받는" 방식이라는 점은 같다. 미러링이란 그 URL이 가리키는 목적지를 외부 저장소에서 내부 저장소로 바꿔치기하는 작업이다 - 클라이언트(apt, pip, helm) 입장에서는 "어디서" 받는지만 바뀔 뿐, 사용 방법 자체는 그대로다.

3. 노드 egress 방화벽 차단

미러가 준비됐으면, 이제 진짜로 인터넷 경로를 끊는다.

# 각 노드에서 - 미러 서버(내부 IP)만 허용하고 나머지 아웃바운드는 차단
sudo iptables -A OUTPUT -d <내부 미러 서버 IP 대역> -j ACCEPT
sudo iptables -A OUTPUT -d <클러스터 내부 IP 대역> -j ACCEPT
sudo iptables -A OUTPUT -o lo -j ACCEPT
sudo iptables -A OUTPUT -j DROP    # 그 외 모든 아웃바운드 차단

# 확인
curl -m 5 https://google.com   # 반드시 타임아웃/실패해야 정상
curl -m 5 https://harbor.local # 내부 미러는 정상 응답해야 함

4. 미러만으로 검증 - 조인 / 배포 / 설치

egress를 차단한 상태에서 시리즈 1~2의 절차를 다시 밟아본다.

# 신규 워커 노드 조인 (containerd가 내부 미러에서 pause 이미지 등을 받아오는지)
sudo kubeadm join <VIP>:6443 --token <토큰> --discovery-token-ca-cert-hash sha256:<해시>

# 새 서비스 배포 (이미지가 harbor.local/proxy-cache 경로에서 pull되는지)
kubectl apply -f api-server-deployment.yaml
kubectl describe pod <pod> | grep Image   # 실제로 어느 레지스트리에서 받았는지 확인

# 노드에 패키지 설치 (내부 apt 미러로 받아지는지)
sudo apt-get install -y htop

5. 실패 지점 기록

이 검증 과정에서 여전히 외부망을 필요로 하는 부분이 반드시 나온다 - 예를 들어 Helm 차트의 values.yaml이 하드코딩된 외부 URL을 참조하거나, 어떤 컨테이너 이미지의 엔트리포인트 스크립트가 내부적으로 외부 API를 호출하는 경우다. 이런 지점을 발견할 때마다 표로 남긴다.

| 실패 지점 | 증상 | 원인 | 조치 |
|---|---|---|---|
| RabbitMQ Helm 차트 | 설치 중 timeout | values.yaml이 GitHub raw URL을 fetch | 내부 미러 URL로 오버라이드 |
| ... | ... | ... | ... |

6. (심화) egress 차단 상태에서 Day-0 재구축

시리즈 3의 Terraform/Ansible 재구축 실험을 이번엔 egress가 차단된 상태에서 그대로 실행한다. Ansible이 apt 모듈로 패키지를 설치하는 태스크, Terraform이 프로바이더 플러그인을 받는 과정까지 전부 내부 미러를 보도록 설정이 완결되어 있어야 이 실험이 통과한다 - 하나라도 빠지면 재구축이 그 지점에서 멈춘다.

검증 방법

  • egress 차단 후 외부 도메인 접근이 100% 실패하는지 (curl 타임아웃)
  • 클러스터 조인·서비스 배포·패키지 설치가 전부 내부 미러만으로 성공하는지
  • Day-0 재구축(심화)이 egress 차단 상태에서 사람 개입 없이 끝까지 도는지

ISMS-P 연계

이 시리즈에서 만든 egress 차단 정책 자체가 ISMS-P 2.6(접근통제) 항목의 "망 분리" 증적이 된다. 시리즈 6의 매핑표에 이 결과를 추가한다.

| 2.6 접근통제 | ..., (확장) egress 차단 정책 | ..., 폐쇄망 시뮬레이션 결과(방화벽 규칙 + 차단 테스트 로그) |

체크리스트

  • Harbor를 프록시 캐시로 구성해 이미지 미러링
  • apt/pip/Helm 미러 적재
  • 노드 egress 방화벽 차단 적용
  • 미러만으로 클러스터 조인/서비스 배포/패키지 설치 검증
  • 여전히 외부망이 필요한 실패 지점 표로 기록
  • (심화) egress 차단 상태에서 Day-0 재구축 성공