Air-Gapped Mirrors and a Private CA
Pull images by their original names from an internal mirror
한국어 원문으로 표시합니다.
이 실습은 진짜 k3s 에서 돕니다
VM 안에 k3s 한 대(v1.33.3+k3s1)가 떠 있고, skopeo·podman 과 우분투의 docker-registry 패키지(CNCF distribution 레지스트리)가 깔려 있습니다. 처음 뜨는 데 몇 분이 걸립니다. VM 은 처음에는 인터넷(80/443)에 닿습니다 — 3단계까지가 연결된 반입 준비 이고, 4단계에서 여러분이 바깥 송신을 막아 폐쇄망 으로 바꿉니다. 채점은 바깥에서 VM 의 8899 포트로 들어오는 에이전트가 하므로, 이미 맺은 연결과 루프백을 막으면 채점이 끊깁니다.
목표
사설 CA 로 사내 레지스트리를 HTTPS 로 세우고 이미지를 다이제스트 그대로 옮긴 뒤, 바깥이 막힌 상태에서 이미지 이름을 바꾸지 않고 podman·containerd·쿠버네티스가 사내 미러에서 받게 한다.
왜 중요한가
폐쇄망에서 매니페스트의 이미지 이름을 사내 레지스트리 주소로 전부 바꾸면, 차트와 매니페스트가 바깥과 갈라지고 다음 버전을 받을 때마다 다시 고쳐야 한다. 미러는 이름은 그대로 두고 "그 레지스트리는 여기서 받는다" 만 런타임에 알려 준다. 런타임마다 설정 파일이 다르다는 것이 함정이다 — podman 은 registries.conf, containerd 는 hosts.toml, k3s 는 registries.yaml. 그리고 모두 사설 CA 를 따로 알려 줘야 한다.
예상 60분이다. 세션이 끝나면 VM 이 사라지니 남기고 싶은 파일은 종료 전에 따로 보관한다.
단계
/root/registry/pki에 루트(ca.crt·ca.key, CNAirgap Internal Root CA)와registry.airgap.internal인증서(registry.crt·registry.key, SAN)를 만들고/etc/hosts에127.0.0.1 registry.airgap.internal을 더한다./etc/docker/registry/config.yml로 사내 레지스트리가127.0.0.1:5000에서 그 인증서로 HTTPS 를 내주게 하고, 서비스를 살려 부팅 때도 뜨게 한다. 레지스트리 서비스 계정이 키를 읽을 수 있어야 한다.- 연결된 동안
registry.k8s.io/e2e-test-images/busybox:1.36.1-1과registry.k8s.io/pause:3.10을 사내 레지스트리의 같은 경로(registry.airgap.internal:5000/e2e-test-images/busybox:1.36.1-1,.../pause:3.10)로 다이제스트를 보존해 옮기고,/root/registry/digests.txt에원래이름 다이제스트를 한 줄씩 적는다. - 다시 돌려도 규칙이 한 벌만 남는
/root/registry/egress.sh로 바깥 송신을 막는다(체인AIRGAP-EGRESS, OUTPUT·FORWARD 에서 한 번씩, 루프백·이미 맺은 연결·10.42.0.0/16·10.43.0.0/16 은 살린다). - podman:
/etc/containers/registries.conf.d/50-airgap-mirror.conf로registry.k8s.io의 미러를 사내 레지스트리로 두고 사설 CA 를 알려 준 뒤, 원래 이름registry.k8s.io/e2e-test-images/busybox:1.36.1-1로 pull 한다. - containerd:
/root/registry/certs.d/registry.k8s.io/hosts.toml을 손으로 쓰고,k3s ctr -n airgap-ctr images pull --hosts-dir /root/registry/certs.d registry.k8s.io/pause:3.10으로 받는다. - 쿠버네티스:
/etc/rancher/k3s/registries.yaml로 k3s 가 사내 미러를 쓰게 하고 k3s 를 다시 띄운 뒤, 네임스페이스airgap에 원래 이름·imagePullPolicy: Always인 파드mirror-busybox(매니페스트/root/registry/mirror-busybox.yaml)를 Running 으로 만든다. /root/registry/mirror-record.json에 반입 기록을 남긴다 —images배열의 항목마다ref·mirror_ref·digest·pulled_by(podman·ctr·kubelet 가운데 실제로 받은 것들), 최상위에egress_blocked.
참고
- 인증서: 사설 CA 모듈과 같은 절차입니다(SAN 필수).
- 레지스트리 응답:
curl --cacert /root/registry/pki/ca.crt https://registry.airgap.internal:5000/v2/_catalog - 다이제스트:
skopeo inspect --format '{{.Digest}}' docker://<이미지>· 보존해 옮기기:skopeo copy --all --preserve-digests ... - podman·skopeo 의 레지스트리 CA 자리:
/etc/containers/certs.d/<호스트:포트>/ca.crt - k3s 가 만든 containerd 설정:
/var/lib/rancher/k3s/agent/etc/containerd/ - 흔한 실수 1: 레지스트리 키를 root 만 읽게 두기. 서비스는 전용 계정으로 돌아 키를 못 읽고 죽습니다 —
journalctl -u docker-registry. - 흔한 실수 2:
--all없이 옮기기. 여러 아키텍처 목록이 한 아키텍처로 줄어 다이제스트가 바뀌고, 다이제스트로 고정한 매니페스트가 미러에서 이미지를 못 찾습니다.
사내 레지스트리 인증서
/root/registry/pki 에 루트(ca.crt·ca.key)와 registry.airgap.internal 인증서(registry.crt·registry.key, SAN)를 만들고 /etc/hosts 에 이름을 더한다.
루트는 자기 서명에 CA:TRUE, 서버 인증서는 루트로 서명하면서 확장 파일로 SAN 을 줍니다. 사설 CA 모듈과 같은 절차입니다.
사내 레지스트리를 HTTPS 로
/etc/docker/registry/config.yml 로 레지스트리가 127.0.0.1:5000 에서 그 인증서로 HTTPS 를 내주게 하고, 서비스를 살려 부팅 때도 뜨게 한다.
설정의 http 절에 주소와 tls(인증서·키 경로)를 줍니다. 서비스는 전용 계정으로 돌기 때문에 키를 그 계정이 읽을 수 있는 곳·권한으로 두어야 합니다. 안 뜨면 journalctl -u docker-registry 부터 보세요.
다이제스트를 보존해 옮기기
두 이미지를 사내 레지스트리의 같은 경로로 다이제스트를 보존해 옮기고, /root/registry/digests.txt 에 원래이름 다이제스트 를 한 줄씩 적는다.
skopeo copy 는 레지스트리에서 레지스트리로 바로 옮깁니다. 여러 아키텍처 목록을 통째로 옮겨야 다이제스트가 바깥과 같습니다. 사내 레지스트리의 CA 는 /etc/containers/certs.d/<호스트:포트>/ca.crt 에 두면 skopeo 와 podman 이 함께 씁니다.
바깥 송신을 막아 폐쇄망 만들기
다시 돌려도 규칙이 한 벌만 남는 /root/registry/egress.sh 로 바깥 송신을 막는다.
새 체인을 만들어 OUTPUT 과 FORWARD 맨 앞에서 그리로 보내면, 비우고 다시 채우는 방식으로 몇 번을 돌려도 같은 모양이 됩니다. 채점 에이전트와 쿠버네티스 대역을 먼저 살리세요.
podman: registries.conf 의 mirror
/etc/containers/registries.conf.d/50-airgap-mirror.conf 로 registry.k8s.io 의 미러를 사내 레지스트리로 두고, 원래 이름으로 busybox 를 pull 한다.
registries.conf 의 [[registry]] 는 prefix 로 어떤 이름에 적용할지, [[registry.mirror]] 는 먼저 시도할 위치를 적습니다. 이름은 바꾸지 않습니다. 사설 CA 는 3단계에서 둔 certs.d 가 그대로 쓰입니다.
containerd: hosts.toml 을 손으로
/root/registry/certs.d/registry.k8s.io/hosts.toml 을 쓰고, k3s ctr -n airgap-ctr images pull --hosts-dir /root/registry/certs.d registry.k8s.io/pause:3.10 으로 받는다.
디렉터리 이름이 원래 레지스트리이고, 파일 안의 server 가 원래 주소, [host."..."] 가 먼저 시도할 미러입니다. 미러에는 pull·resolve 권한과 CA 경로를 적습니다.
쿠버네티스: registries.yaml 과 Always 파드
/etc/rancher/k3s/registries.yaml 로 k3s 가 사내 미러를 쓰게 하고 다시 띄운 뒤, imagePullPolicy: Always 인 파드 mirror-busybox(매니페스트 /root/registry/mirror-busybox.yaml)를 네임스페이스 airgap 에서 Running 으로 만든다.
mirrors 에 원래 레지스트리와 endpoint 를, configs 에 미러 주소의 tls ca_file 을 적습니다. k3s 문서대로 바꾼 뒤에는 k3s 를 다시 띄워야 합니다. 이미지 이름은 원래 그대로 둡니다 — Always 여도 미러에서 태그를 풀 수 있으면 뜹니다.
반입 기록을 레지스트리·런타임과 대조
/root/registry/mirror-record.json 에 이미지마다 ref·mirror_ref·digest·pulled_by 와 최상위 egress_blocked 를 남긴다.
다이제스트는 사내 레지스트리에 물어 뽑고, pulled_by 는 podman·ctr·kubelet 가운데 실제로 그 이미지를 받은 쪽을 적습니다. 값은 손으로 옮기지 말고 명령 출력에서 모으세요.