Air-Gapped Mirrors and a Private CA
An intake bundle: from request to signature verification
한국어 원문으로 표시합니다.
목표
반입 요청서에서 시작해 번들을 받고, 상대 경로 해시 목록에 서명하고, 개인 키 없는 매체로 옮겨, 폐쇄망 쪽에서 서명 먼저·해시 다음으로 검증한 뒤 번들만으로 설치하고 기록을 남긴다.
왜 중요한가
해시 목록만 함께 넣은 USB 는 전송 중 손상은 잡지만 바꿔치기는 못 잡는다 — 바꾼 사람이 목록도 고치면 그만이다. 목록에 서명하고 공개 키를 다른 길로 미리 건네 두어야 "누가 만든 목록인가" 가 보증된다. 그리고 검증 스크립트는 실패할 때 0 이 아닌 값으로 끝나야 자동화가 멈춘다. 이 파드는 인터넷(80/443)에 닿으므로 받는 쪽 역할을 하고, 설치 단계는 바깥을 닫힌 프록시로 막은 채 번들만으로 한다.
단계
/root/intake/request.txt에 반입 요청을생태계 이름 버전두 줄로 적는다:pypi requests 2.32.3,go rsc.io/quote v1.5.2.- 번들
/root/intake/bundle/에 파이썬 휠(pypi/, 의존성 포함, 휠만)과 Go 모듈 프록시(goproxy/, 새 모듈 캐시의 cache/download 내용)를 받는다. - 번들 디렉터리 안에서 상대 경로로
/root/intake/bundle/SHA256SUMS를 만든다(목록과 서명 파일 자신은 뺀다). /root/intake/keys/intake.key(Ed25519 개인 키)·intake.pub를 만들고SHA256SUMS에 서명해/root/intake/bundle/SHA256SUMS.sig에 둔다.- 매체
/root/intake/media/bundle.tar를 만들고(개인 키는 넣지 않는다), 폐쇄망 쪽/root/intake/inside/에 풀고, 공개 키는 따로/root/intake/inside/trusted.pub로 둔다. - 폐쇄망 쪽 검증 스크립트
/root/intake/verify.sh <번들> <공개키>를 쓴다. 서명 먼저, 해시 다음, 목록에 없는 파일까지 보고, 실패하면 0 이 아닌 값으로 끝나야 한다. - 바깥을 막은 채
/root/intake/inside/bundle만으로 설치한다: venv/root/intake/venv에 requests, 그리고/root/intake/goapp의 Go 프로그램(rsc.io/quote 의quote.Hello()출력)을 빌드해/root/intake/bin/hello로. - 반입 기록
/root/intake/record.json을 남긴다(키:request,files,sha256sums_sha256,signer_pub_sha256,verified,installed).
참고
- Ed25519 키:
openssl genpkey -algorithm ed25519 -out <키>· 공개 키:openssl pkey -in <키> -pubout -out <공개키> - 서명:
openssl pkeyutl -sign -inkey <키> -rawin -in <파일> -out <서명>· 검증:openssl pkeyutl -verify -pubin -inkey <공개키> -rawin -in <파일> -sigfile <서명> - 흔한 실수 1: 해시 목록을 절대 경로로 만들기. 폐쇄망 쪽에서 다른 디렉터리에 풀면 전부 깨집니다.
- 흔한 실수 2: 검증을 해시부터 하기. 목록까지 바꿔치기된 번들이 통과합니다.
- 흔한 실수 3: 매체에 개인 키나 신뢰할 공개 키를 함께 넣기.
반입 요청서는 정확한 버전으로
/root/intake/request.txt 에 pypi requests 2.32.3 과 go rsc.io/quote v1.5.2 두 줄을 적는다.
한 줄에 생태계, 이름, 버전 세 칸입니다. 범위(>=)나 latest 를 쓰면 받는 날마다 결과가 달라집니다. Go 모듈의 버전은 v 로 시작합니다.
요청서대로 번들 받기
/root/intake/bundle/pypi/ 에 requests 2.32.3 과 의존성 휠을, /root/intake/bundle/goproxy/ 에 rsc.io/quote v1.5.2 의 모듈 프록시 내용을 받는다.
pip 는 휠만 받게 하는 옵션과 함께 download 를, Go 는 새 모듈 캐시(GOMODCACHE)로 한 번 받은 뒤 그 cache/download 를 옮기면 됩니다. Go 는 받을 모듈이 쓰일 작은 모듈이 필요합니다 — 7단계에서 쓸 /root/intake/goapp 을 지금 만들어 두세요.
상대 경로 해시 목록
번들 디렉터리 안에서 상대 경로로 /root/intake/bundle/SHA256SUMS 를 만든다(목록·서명 파일 자신은 뺀다).
번들 디렉터리로 들어가 find 로 파일을 모으고 sha256sum 에 넘기면 경로가 ./ 로 시작합니다. 목록 파일이 자기 자신을 포함하지 않게 이름으로 빼세요. 만든 뒤 같은 디렉터리에서 -c 로 확인합니다.
목록에 서명하기
/root/intake/keys/intake.key(Ed25519)·intake.pub 를 만들고 SHA256SUMS 의 서명을 /root/intake/bundle/SHA256SUMS.sig 에 둔다.
OpenSSL 3 의 genpkey 로 ed25519 키를 만들고, pkey 로 공개 키를 뽑습니다. Ed25519 는 원문을 그대로 받으므로 pkeyutl 에 -rawin 이 필요합니다. 개인 키는 번들 밖(keys/)에 둡니다.
개인 키 없는 매체와 따로 건네는 공개 키
/root/intake/media/bundle.tar 를 만들고 /root/intake/inside/ 에 풀며, 공개 키는 따로 /root/intake/inside/trusted.pub 로 둔다.
tar 는 bundle 디렉터리 하나만 담습니다. 풀면 inside/bundle 이 생기게 하세요. 공개 키가 매체 안에 있으면 바꿔치기한 사람이 자기 키를 넣으면 그만이라, 신뢰할 키는 다른 길로 미리 가 있어야 합니다.
서명 먼저, 해시 다음 — 검증 스크립트
/root/intake/verify.sh <번들> <공개키> 를 쓴다. 서명 → 해시 → 목록에 없는 파일 순으로 보고, 실패하면 0 이 아닌 값으로 끝난다.
채점기는 여러분의 스크립트를 번들 사본 두 개(파일 한 바이트를 바꾼 것, 그 파일에 맞춰 목록까지 고친 것)에 돌려 둘 다 거절하는지 봅니다. 두 번째는 해시만 보는 스크립트로는 막을 수 없습니다.
번들만으로 설치
바깥을 막은 채 /root/intake/inside/bundle 만으로 venv /root/intake/venv 에 requests 를, /root/intake/goapp 을 빌드해 /root/intake/bin/hello 를 만든다.
설치 전에 6단계 스크립트로 검증부터 하세요. pip 는 인덱스를 보지 않고 번들 디렉터리만 보게, Go 는 번들의 goproxy 디렉터리를 file:// 프록시로 가리키고 새 모듈 캐시로 빌드합니다.
반입 기록 남기기
/root/intake/record.json 에 request(요청서 줄 목록), files(번들 목록의 파일 수), sha256sums_sha256, signer_pub_sha256(trusted.pub 파일의 sha256), verified(true), installed(설치한 것 목록)를 적는다.
값은 손으로 옮기지 말고 파일에서 계산하세요. files 는 SHA256SUMS 의 줄 수입니다. installed 에는 requests 와 Go 모듈처럼 실제로 설치·빌드에 쓴 것을 이름==버전 또는 모듈@버전 모양으로 적습니다.