LabHub
学习 学习路径 课程

CI/CD 流水线

以为已经删掉的令牌,却从镜像层和提交历史里原样冒了出来

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

비밀이 남는 네 자리(빌드 로그·이미지 레이어·이미지 설정의 Env 와 history·커밋 이력)를 각각 손으로 재현해 보이고, 비밀을 이미지에서 빼 실행 시점에 주입하는 설계로 바꾼 뒤, 세 자리를 한 번에 훑는 게이트를 세웁니다.

왜 중요한가

비밀이 새는 사고는 대개 악의가 아니라 편의에서 옵니다. 디버깅하려고 추적을 켰고, 빌드 중간에 파일로 쓰는 것이 가장 간단했고, 설정 파일을 그대로 커밋했습니다. 각각은 몇 분을 아끼려던 일인데, 결과는 그 값을 읽을 수 있는 사람의 범위가 통째로 넓어지는 것입니다. 여기서 사람들이 가장 자주 기대는 것이 마스킹인데, 마스킹은 아는 문자열과 똑같은 것만 가립니다 — base64 로 바꾸거나 두 조각으로 쪼개면 그대로 지나갑니다. 그래서 순서는 찍지 않게 만드는 것이 먼저이고 마스킹은 마지막 그물입니다. 이미지 쪽은 더 조용합니다. 이미지는 파일 시스템만이 아니라 어떻게 만들어졌는지를 함께 담기 때문에, 중간 레이어에서 파일로 썼다가 나중 레이어에서 지워도 앞 레이어 블롭에는 그대로 남고 설정의 Env 와 history 에도 값이 적힙니다. 커밋 이력도 같습니다 — 다음 커밋에서 지워도 블롭은 남습니다. 그리고 가장 자주 틀리는 자리가 마지막에 있습니다. 한 번 샌 비밀에 대한 대응은 지우는 것이 아니라 폐기하고 새로 발급하는 것입니다. 이력 수정은 이미 복제된 사본과 포크, 캐시, 백업에 아무 영향을 주지 못하고, 그 사이에 누가 읽었는지도 알 수 없기 때문입니다.

단계

  1. 작업 디렉터리는 /root/secrets-lab 입니다. 먼저 /root/secrets-lab/secret/api.token 을 만들고 첫 줄에 이 실습의 가짜 토큰 sk-labhub-fake-9f3a2b1c8d7e6f5a4b3c2d1e0f9a8b7c 을 적으세요. 파일 권한은 600 이어야 합니다. 그다음 /root/secrets-lab/publish.sh <토큰파일> <산출물경로> 를 만드세요. 이 스크립트는 (1) 토큰 파일의 첫 줄을 읽고, (2) 산출물 경로의 상위 디렉터리가 없으면 만든 뒤 그 파일에 published <산출물 파일 이름> 한 줄을 쓰고, (3) 셸 추적을 켠 상태에서 Authorization: Bearer <토큰> 을 찍습니다. 실행 권한을 주세요. 이어서 bash /root/secrets-lab/publish.sh /root/secrets-lab/secret/api.token /root/secrets-lab/out/app-1.0.0.txt 를 돌려 표준 출력과 표준 오류를 함께 /root/secrets-lab/logs/publish.log 에 저장하고, 그 로그에서 토큰이 보이는 줄 수/root/secrets-lab/leak-count.txt 에 수 하나로 적으세요. 두 줄 이상이어야 합니다.
  2. /root/secrets-lab/mask.sh <토큰파일> 을 만드세요. 표준 입력을 읽어 토큰과 똑같은 문자열을 전부 ***MASKED*** 로 바꿔 표준 출력으로 내보내는 필터입니다. 토큰이 비어 있으면 첫 줄이 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 그다음 /root/secrets-lab/leaky-shapes.sh <토큰파일> 을 만드세요. 같은 값을 네 가지 모양으로 정확히 네 줄 찍습니다(순서도 이대로입니다). raw <토큰>, base64 <토큰을 줄바꿈 없이 base64 로 인코딩한 값>, split <앞 절반> <뒤 절반>, hex <토큰 바이트를 16진수 소문자로 이어 붙인 값>. 둘 다 실행 권한을 주고 bash /root/secrets-lab/leaky-shapes.sh /root/secrets-lab/secret/api.token | bash /root/secrets-lab/mask.sh /root/secrets-lab/secret/api.token 의 결과를 /root/secrets-lab/logs/mask-probe.log 에 저장하세요. 마지막으로 /root/secrets-lab/mask-report.txt 에 네 줄을 <이름> <masked|leaked> 꼴로 적으세요 — 그 줄에 가려진 자리가 보이면 masked, 값이 그대로 보이면 leaked 입니다.
  3. 컨테이너를 띄우지 않고 OCI 이미지를 손으로 만듭니다. /root/secrets-lab/build-image.sh <레이아웃디렉터리> <태그> <토큰파일> 을 만드세요. 레이어는 두 장입니다. 첫 레이어의 루트에는 app/main.py(내용은 print("labhub demo") 한 줄)와 etc/build.env(내용은 API_TOKEN=<토큰> 한 줄)가 들어가고, 둘째 레이어의 루트에는 그 파일을 지우는 화이트아웃 항목 etc/.wh.build.env(빈 파일)만 들어갑니다. 설정 JSON 은 architectureuname -m 에서 뽑아(x86_64amd64, aarch64arm64) 적고 oslinux, config.EnvPATH=/usr/local/bin:/usr/bin:/binAPI_TOKEN=<토큰> 두 항목, rootfs.diff_ids압축 전 tar 의 sha256 을 레이어 순서대로, history 는 두 항목으로 첫째의 created_by 에 토큰이 적힌 명령을, 둘째의 created_by 에 그 파일을 지우는 명령을 적습니다. 매니페스트의 config·layersdigestsize압축 후 값이고, 모든 블롭은 blobs/sha256/<접두어 없는 다이제스트> 에 둡니다. index.json 에는 매니페스트 하나를 두고 주석 org.opencontainers.image.ref.name 에 둘째 인자의 태그를 적으며, oci-layout{"imageLayoutVersion":"1.0.0"} 입니다. 실행 권한을 준 뒤 bash /root/secrets-lab/build-image.sh /root/secrets-lab/oci/app v1 /root/secrets-lab/secret/api.token 으로 만들고, skopeo inspect oci:/root/secrets-lab/oci/app:v1 의 출력을 /root/secrets-lab/inspect.json 에 저장하세요. 마지막으로 첫 레이어 블롭을 풀어 거기서 꺼낸 etc/build.env 의 줄을 /root/secrets-lab/layer-leak.txt 에 그대로 적으세요 — 둘째 레이어에서 지웠는데도 나옵니다.
  4. 규칙을 데이터로 적습니다. /root/secrets-lab/rules.txt 에 한 줄이 <이름> <확장정규식> 인 규칙 셋을 적으세요(이름 뒤 공백부터 줄 끝까지가 정규식입니다). 이름은 정확히 labhub-fake-token, aws-access-key-id, private-key-header 이고, 각각 이 실습의 가짜 토큰 꼴(sk-labhub-fake- 뒤 16진수 32자), AWS 액세스 키 ID 꼴(AKIA 뒤 대문자와 숫자 16자), PEM 개인 키 머리글(-----BEGINPRIVATE KEY----- 사이에 대문자와 공백)을 잡아야 합니다. 그다음 /root/secrets-lab/image-scan.sh <이미지참조> <규칙파일> 을 만드세요. 이미지의 네 자리를 훑어 정확히 네 줄을 이 순서로 냅니다 — env, history, layer, manifest. 각 줄은 걸리면 <자리> hit <걸린 규칙 이름들>, 안 걸리면 <자리> clean 입니다. 하나라도 걸리면 1 로, 하나도 안 걸리면 0 으로 끝냅니다. 규칙 파일이 없거나 비었거나 이미지를 읽지 못하면 첫 줄이 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 실행 권한을 준 뒤 bash /root/secrets-lab/image-scan.sh oci:/root/secrets-lab/oci/app:v1 /root/secrets-lab/rules.txt 의 출력을 /root/secrets-lab/scan-v1.txt 에 저장하세요.
  5. /root/secrets-lab/build-image-clean.sh <레이아웃디렉터리> <태그> 를 만드세요. 토큰 파일을 인자로 받지 않는 것이 설계의 핵심입니다. 레이어는 한 장이고 루트에 app/main.py(앞 단계와 같은 한 줄)와 etc/app.conf(내용은 api_token_file=/run/secrets/api.token 한 줄)가 들어갑니다. 설정의 config.EnvPATH=/usr/local/bin:/usr/bin:/binAPI_TOKEN_FILE=/run/secrets/api.token 두 항목이고, history 는 비밀이 적히지 않은 한 항목입니다. 나머지 구조(다이제스트·크기·diff_ids·index.json·oci-layout)는 앞 단계와 같습니다. 그리고 /root/secrets-lab/run.sh 를 만드세요. 환경 변수 API_TOKEN_FILE(없으면 /run/secrets/api.token)이 가리키는 파일을 읽어 ready token_sha256=<그 값의 sha256 앞 12자리> 한 줄을 찍습니다. 값 자체는 절대 찍지 않습니다. 파일이 없거나 비어 있으면 첫 줄이 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 둘 다 실행 권한을 준 뒤 bash /root/secrets-lab/build-image-clean.sh /root/secrets-lab/oci/app-clean v1 로 이미지를 만들고, bash /root/secrets-lab/image-scan.sh oci:/root/secrets-lab/oci/app-clean:v1 /root/secrets-lab/rules.txt 의 출력을 /root/secrets-lab/scan-clean.txt 에 저장하세요. 네 줄 모두 clean 이어야 합니다.
  6. /root/secrets-lab/with-secret.sh <토큰파일> <명령> [인자...] 를 만드세요. 이 껍데기는 (1) 자기만의 임시 디렉터리를 만들어 권한을 700 으로 좁히고, (2) 그 안에 토큰 파일의 첫 줄을 권한 600 인 파일로 복사하고, (3) 첫 줄에 secret_file <만든 파일 경로> 를 찍고, (4) 환경 변수 API_TOKEN_FILE 에 그 경로를 담아 넘겨받은 명령을 실행하고, (5) 명령의 종료 코드를 그대로 자기 종료 코드로 삼고, (6) 성공하든 실패하든 끝날 때 그 디렉터리를 지웁니다. 비밀 값 자체는 화면에 찍지 않습니다. 실행할 명령이 없거나 토큰 파일이 없거나 비었으면 첫 줄이 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 실행 권한을 준 뒤 bash /root/secrets-lab/with-secret.sh /root/secrets-lab/secret/api.token bash /root/secrets-lab/run.sh 의 출력을 /root/secrets-lab/logs/with-secret.log 에 저장하세요. 그리고 /root/secrets-lab/secret 디렉터리의 권한을 700 으로 맞춘 뒤 /root/secrets-lab/perm-audit.txt 에 두 줄을 <모드> <경로> 꼴로 적으세요 — 첫 줄은 /root/secrets-lab/secret, 둘째 줄은 /root/secrets-lab/secret/api.token 입니다.
  7. 연습용 저장소 /root/secrets-lab/repo 를 만들고 커밋 셋을 쌓으세요. 첫 커밋은 config/app.conf(비밀 없음), 둘째 커밋은 config/deploy.env(내용은 DEPLOY_TOKEN=sk-labhub-fake-9f3a2b1c8d7e6f5a4b3c2d1e0f9a8b7c 한 줄), 셋째 커밋은 그 config/deploy.env지우는 커밋입니다. 그다음 지워진 값을 이력에서 도로 꺼내세요 — /root/secrets-lab/git-leak.txt 에는 그 블롭의 내용을, /root/secrets-lab/git-leak-commit.txt 에는 그 파일을 처음 넣은 커밋의 40자리 해시를 적습니다. 마지막으로 /root/secrets-lab/scan-repo.sh <저장소> <규칙파일> 을 만드세요. 작업 트리가 아니라 모든 커밋에 딸린 모든 블롭을 앞 단계의 규칙 파일로 훑어, 걸린 것마다 <규칙 이름> <블롭 해시> <경로> 한 줄을 내고 하나라도 걸리면 1, 하나도 없으면 아무것도 내지 않고 0 으로 끝냅니다. 저장소가 아니거나 규칙 파일이 없거나 비었으면 첫 줄이 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 실행 권한을 준 뒤 bash /root/secrets-lab/scan-repo.sh /root/secrets-lab/repo /root/secrets-lab/rules.txt 의 출력을 /root/secrets-lab/repo-scan.txt 에 저장하세요.
  8. /root/secrets-lab/secret-gate.sh <로그디렉터리> <이미지참조> <저장소> <규칙파일> <보고서파일> 을 만드세요. 로그 디렉터리 아래의 모든 파일, 이미지, 저장소 이력 세 자리를 한 번에 훑습니다(이미지와 저장소는 앞 단계에서 만든 두 검사기를 그대로 부릅니다). 걸린 것이 하나라도 있으면 화면과 보고서의 첫 줄에 SECRETS-FOUND <전체 건수> 를 내고 3 으로, 하나도 없으면 SECRETS-CLEAN 을 내고 0 으로 끝냅니다. 로그 디렉터리가 없거나 규칙 파일이 없거나 저장소가 아니면 첫 줄이 ERROR 로 시작하는 줄을 내고 1 로 끝냅니다. 보고서에는 첫 줄 뒤에 세 자리별 결과를 이어 적되, 로그에서 걸린 줄에는 그 파일 경로가, 저장소에서 걸린 줄에는 그 경로가 보여야 합니다. 보고서 경로의 상위 디렉터리가 없으면 만듭니다. 실행 권한을 준 뒤 bash /root/secrets-lab/secret-gate.sh /root/secrets-lab/logs oci:/root/secrets-lab/oci/app-clean:v1 /root/secrets-lab/repo /root/secrets-lab/rules.txt /root/secrets-lab/report/gate.txt 로 한 번 돌리세요. 마지막으로 /root/secrets-lab/revoke-checklist.txt정확히 다섯 줄<순번> <열쇠> <한 줄 설명> 꼴로 적으세요. 열쇠는 이 순서입니다 — revoke, reissue, rotate-consumers, audit-access, prevent-recurrence. 설명은 각각 열 자 이상으로 자기 말로 적습니다.

참고

디버깅하려고 추적을 켰더니 토큰이 로그에 그대로 찍혔다

작업 디렉터리는 /root/secrets-lab 입니다. 먼저 /root/secrets-lab/secret/api.token 을 만들고 첫 줄에 이 실습의 가짜 토큰 sk-labhub-fake-9f3a2b1c8d7e6f5a4b3c2d1e0f9a8b7c 을 적으세요. 파일 권한은 600 이어야 합니다. 그다음 /root/secrets-lab/publish.sh <토큰파일> <산출물경로> 를 만드세요. 이 스크립트는 (1) 토큰 파일의 첫 줄을 읽고, (2) 산출물 경로의 상위 디렉터리가 없으면 만든 뒤 그 파일에 published <산출물 파일 이름> 한 줄을 쓰고, (3) 셸 추적을 켠 상태에서 Authorization: Bearer <토큰> 을 찍습니다. 실행 권한을 주세요. 이어서 bash /root/secrets-lab/publish.sh /root/secrets-lab/secret/api.token /root/secrets-lab/out/app-1.0.0.txt 를 돌려 표준 출력과 표준 오류를 함께 /root/secrets-lab/logs/publish.log 에 저장하고, 그 로그에서 토큰이 보이는 줄 수/root/secrets-lab/leak-count.txt 에 수 하나로 적으세요. 두 줄 이상이어야 합니다.

셸 추적은 set -x 로 켜고 set +x 로 끕니다. 추적은 표준 오류로 나가므로 > 파일 2>&1 처럼 둘을 함께 받아야 보입니다. 값을 찍는 줄 하나와 그 줄의 추적 하나, 그래서 같은 값이 두 곳에 남습니다. 권한을 좁혀 파일을 만들 때는 umask 077 을 서브셸에 걸거나 만든 뒤 chmod 합니다. 줄 수는 grep -c 가 셉니다.

마스킹을 붙였는데 base64 로 바꾼 값이 그대로 지나갔다

/root/secrets-lab/mask.sh <토큰파일> 을 만드세요. 표준 입력을 읽어 토큰과 똑같은 문자열을 전부 ***MASKED*** 로 바꿔 표준 출력으로 내보내는 필터입니다. 토큰이 비어 있으면 첫 줄이 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 그다음 /root/secrets-lab/leaky-shapes.sh <토큰파일> 을 만드세요. 같은 값을 네 가지 모양으로 정확히 네 줄 찍습니다(순서도 이대로입니다). raw <토큰>, base64 <토큰을 줄바꿈 없이 base64 로 인코딩한 값>, split <앞 절반> <뒤 절반>, hex <토큰 바이트를 16진수 소문자로 이어 붙인 값>. 둘 다 실행 권한을 주고 bash /root/secrets-lab/leaky-shapes.sh /root/secrets-lab/secret/api.token | bash /root/secrets-lab/mask.sh /root/secrets-lab/secret/api.token 의 결과를 /root/secrets-lab/logs/mask-probe.log 에 저장하세요. 마지막으로 /root/secrets-lab/mask-report.txt 에 네 줄을 <이름> <masked|leaked> 꼴로 적으세요 — 그 줄에 가려진 자리가 보이면 masked, 값이 그대로 보이면 leaked 입니다.

sed 로 치환하면 값 안의 /·&·. 을 일일이 이스케이프해야 합니다. 파이썬 한 줄로 문자열을 그대로 바꾸는 편이 안전합니다. base64 는 base64 -w0 이 줄바꿈 없이 냅니다. 16진수는 od -An -tx1 의 출력에서 공백과 줄바꿈을 지우면 됩니다. 절반은 bash 의 부분 문자열 ${변수:시작:길이} 로 자릅니다. 마스킹이 가리는 것은 '아는 문자열' 하나뿐이라는 점을 네 줄로 확인하는 것이 이 단계의 전부입니다.

다음 레이어에서 지웠는데 앞 레이어 블롭에 그대로 있었다

컨테이너를 띄우지 않고 OCI 이미지를 손으로 만듭니다. /root/secrets-lab/build-image.sh <레이아웃디렉터리> <태그> <토큰파일> 을 만드세요. 레이어는 두 장입니다. 첫 레이어의 루트에는 app/main.py(내용은 print("labhub demo") 한 줄)와 etc/build.env(내용은 API_TOKEN=<토큰> 한 줄)가 들어가고, 둘째 레이어의 루트에는 그 파일을 지우는 화이트아웃 항목 etc/.wh.build.env(빈 파일)만 들어갑니다. 설정 JSON 은 architectureuname -m 에서 뽑아(x86_64amd64, aarch64arm64) 적고 oslinux, config.EnvPATH=/usr/local/bin:/usr/bin:/binAPI_TOKEN=<토큰> 두 항목, rootfs.diff_ids압축 전 tar 의 sha256 을 레이어 순서대로, history 는 두 항목으로 첫째의 created_by 에 토큰이 적힌 명령을, 둘째의 created_by 에 그 파일을 지우는 명령을 적습니다. 매니페스트의 config·layersdigestsize압축 후 값이고, 모든 블롭은 blobs/sha256/<접두어 없는 다이제스트> 에 둡니다. index.json 에는 매니페스트 하나를 두고 주석 org.opencontainers.image.ref.name 에 둘째 인자의 태그를 적으며, oci-layout{"imageLayoutVersion":"1.0.0"} 입니다. 실행 권한을 준 뒤 bash /root/secrets-lab/build-image.sh /root/secrets-lab/oci/app v1 /root/secrets-lab/secret/api.token 으로 만들고, skopeo inspect oci:/root/secrets-lab/oci/app:v1 의 출력을 /root/secrets-lab/inspect.json 에 저장하세요. 마지막으로 첫 레이어 블롭을 풀어 거기서 꺼낸 etc/build.env 의 줄을 /root/secrets-lab/layer-leak.txt 에 그대로 적으세요 — 둘째 레이어에서 지웠는데도 나옵니다.

레이어는 그냥 tar 입니다. 재현되게 묶으려면 --sort=name --mtime=@1735689600 --owner=0 --group=0 --numeric-owner 를 주고 gzip -n -9 로 압축합니다. 한 레이어에서 세 값이 필요합니다 — 압축 전 tar 의 sha256(diff_id), 압축 뒤 바이트의 sha256(블롭 이름이자 매니페스트의 digest), 압축 뒤 크기(size). 셋 중 하나만 어긋나도 skopeo copy 가 거절하므로 그것이 곧 검산입니다. JSON 은 jq -n --arg 로 만들면 따옴표를 손으로 맞추지 않아도 됩니다. 매니페스트 블롭도 다른 블롭과 같은 자리에 들어가고 index.json 이 그 다이제스트와 크기를 가리킵니다. 레이어 블롭에서 파일 하나만 꺼내는 것은 gzip -dc <블롭> | tar -xO ./etc/build.env 입니다.

레이어를 뒤질 것도 없이 설정의 Env 와 history 에 적혀 있었다

규칙을 데이터로 적습니다. /root/secrets-lab/rules.txt 에 한 줄이 <이름> <확장정규식> 인 규칙 셋을 적으세요(이름 뒤 공백부터 줄 끝까지가 정규식입니다). 이름은 정확히 labhub-fake-token, aws-access-key-id, private-key-header 이고, 각각 이 실습의 가짜 토큰 꼴(sk-labhub-fake- 뒤 16진수 32자), AWS 액세스 키 ID 꼴(AKIA 뒤 대문자와 숫자 16자), PEM 개인 키 머리글(-----BEGINPRIVATE KEY----- 사이에 대문자와 공백)을 잡아야 합니다. 그다음 /root/secrets-lab/image-scan.sh <이미지참조> <규칙파일> 을 만드세요. 이미지의 네 자리를 훑어 정확히 네 줄을 이 순서로 냅니다 — env, history, layer, manifest. 각 줄은 걸리면 <자리> hit <걸린 규칙 이름들>, 안 걸리면 <자리> clean 입니다. 하나라도 걸리면 1 로, 하나도 안 걸리면 0 으로 끝냅니다. 규칙 파일이 없거나 비었거나 이미지를 읽지 못하면 첫 줄이 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 실행 권한을 준 뒤 bash /root/secrets-lab/image-scan.sh oci:/root/secrets-lab/oci/app:v1 /root/secrets-lab/rules.txt 의 출력을 /root/secrets-lab/scan-v1.txt 에 저장하세요.

이미지 안을 컨테이너 없이 보려면 skopeo copy --insecure-policy <참조> dir:<임시디렉터리> 가 가장 쉽습니다. 그러면 manifest.json 과 블롭들이 평범한 파일로 떨어지고, 블롭 파일 이름이 곧 그 블롭의 다이제스트입니다. 설정 블롭은 매니페스트의 .config.digest 가 가리킵니다. 레이어 블롭은 gzip 이므로 풀어서 봐야 하고, tar 안에는 NUL 바이트가 섞여 있으니 grep-a 를 주거나 NUL 을 미리 지웁니다. 정규식이 - 로 시작하면 grep -E -- "$re" 처럼 옵션 끝을 표시해야 합니다. 규칙 파일이 비었는데 0 으로 끝나는 검사기는 '문제가 없다' 와 '아무것도 못 봤다' 를 같은 것으로 보고하는 셈입니다 — 그래서 종료 코드를 따로 둡니다.

비밀을 이미지에서 빼고 실행 시점에 주니 같은 검사가 조용해졌다

/root/secrets-lab/build-image-clean.sh <레이아웃디렉터리> <태그> 를 만드세요. 토큰 파일을 인자로 받지 않는 것이 설계의 핵심입니다. 레이어는 한 장이고 루트에 app/main.py(앞 단계와 같은 한 줄)와 etc/app.conf(내용은 api_token_file=/run/secrets/api.token 한 줄)가 들어갑니다. 설정의 config.EnvPATH=/usr/local/bin:/usr/bin:/binAPI_TOKEN_FILE=/run/secrets/api.token 두 항목이고, history 는 비밀이 적히지 않은 한 항목입니다. 나머지 구조(다이제스트·크기·diff_ids·index.json·oci-layout)는 앞 단계와 같습니다. 그리고 /root/secrets-lab/run.sh 를 만드세요. 환경 변수 API_TOKEN_FILE(없으면 /run/secrets/api.token)이 가리키는 파일을 읽어 ready token_sha256=<그 값의 sha256 앞 12자리> 한 줄을 찍습니다. 값 자체는 절대 찍지 않습니다. 파일이 없거나 비어 있으면 첫 줄이 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 둘 다 실행 권한을 준 뒤 bash /root/secrets-lab/build-image-clean.sh /root/secrets-lab/oci/app-clean v1 로 이미지를 만들고, bash /root/secrets-lab/image-scan.sh oci:/root/secrets-lab/oci/app-clean:v1 /root/secrets-lab/rules.txt 의 출력을 /root/secrets-lab/scan-clean.txt 에 저장하세요. 네 줄 모두 clean 이어야 합니다.

앞 단계의 빌더를 복사해 레이어를 한 장으로 줄이고 비밀이 들어가던 자리를 '읽을 경로' 로 바꾸면 됩니다. 이미지가 담는 것은 값이 아니라 계약입니다 — 어디에 있을 것이라고 약속하는 경로 하나. 값을 그 자리에 가져다 놓는 일은 실행 환경의 몫이고, 그래서 같은 이미지를 개발과 운영이 그대로 나눠 쓸 수 있습니다. sha256sum 은 표준 입력도 받습니다. 검사기가 네 줄 모두 clean 을 내면 종료 코드가 0 이라는 점도 확인하세요.

파일로 넘긴 비밀이 작업이 끝난 뒤에도 그 자리에 남아 있었다

/root/secrets-lab/with-secret.sh <토큰파일> <명령> [인자...] 를 만드세요. 이 껍데기는 (1) 자기만의 임시 디렉터리를 만들어 권한을 700 으로 좁히고, (2) 그 안에 토큰 파일의 첫 줄을 권한 600 인 파일로 복사하고, (3) 첫 줄에 secret_file <만든 파일 경로> 를 찍고, (4) 환경 변수 API_TOKEN_FILE 에 그 경로를 담아 넘겨받은 명령을 실행하고, (5) 명령의 종료 코드를 그대로 자기 종료 코드로 삼고, (6) 성공하든 실패하든 끝날 때 그 디렉터리를 지웁니다. 비밀 값 자체는 화면에 찍지 않습니다. 실행할 명령이 없거나 토큰 파일이 없거나 비었으면 첫 줄이 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 실행 권한을 준 뒤 bash /root/secrets-lab/with-secret.sh /root/secrets-lab/secret/api.token bash /root/secrets-lab/run.sh 의 출력을 /root/secrets-lab/logs/with-secret.log 에 저장하세요. 그리고 /root/secrets-lab/secret 디렉터리의 권한을 700 으로 맞춘 뒤 /root/secrets-lab/perm-audit.txt 에 두 줄을 <모드> <경로> 꼴로 적으세요 — 첫 줄은 /root/secrets-lab/secret, 둘째 줄은 /root/secrets-lab/secret/api.token 입니다.

mktemp -d 는 다른 사람이 못 들어가는 자리에 디렉터리를 만들어 주지만, 그 안에 파일을 만들 때의 권한은 umask 가 정합니다 — ( umask 077; ... ) 를 서브셸에 걸거나 만든 뒤 chmod 합니다. 지우는 일은 trap 에 맡깁니다. EXIT 만 걸면 신호로 죽을 때 남으니 INTTERM 도 함께 겁니다. 명령이 실패해도 껍데기가 먼저 죽으면 안 되니 set -e 를 잠깐 풀고 종료 코드를 받아 두었다가 마지막에 그대로 내보냅니다. 파일 권한만 좁히고 디렉터리를 열어 두면 소용이 없다는 점도 잊지 마세요. 그리고 이렇게 해도 끝이 아닙니다 — 그 값을 이미 읽어 간 프로세스가 무엇을 했는지는 이 껍데기가 알지 못합니다.

나중 커밋에서 지운 파일이 git cat-file 로 그대로 나왔다

연습용 저장소 /root/secrets-lab/repo 를 만들고 커밋 셋을 쌓으세요. 첫 커밋은 config/app.conf(비밀 없음), 둘째 커밋은 config/deploy.env(내용은 DEPLOY_TOKEN=sk-labhub-fake-9f3a2b1c8d7e6f5a4b3c2d1e0f9a8b7c 한 줄), 셋째 커밋은 그 config/deploy.env지우는 커밋입니다. 그다음 지워진 값을 이력에서 도로 꺼내세요 — /root/secrets-lab/git-leak.txt 에는 그 블롭의 내용을, /root/secrets-lab/git-leak-commit.txt 에는 그 파일을 처음 넣은 커밋의 40자리 해시를 적습니다. 마지막으로 /root/secrets-lab/scan-repo.sh <저장소> <규칙파일> 을 만드세요. 작업 트리가 아니라 모든 커밋에 딸린 모든 블롭을 앞 단계의 규칙 파일로 훑어, 걸린 것마다 <규칙 이름> <블롭 해시> <경로> 한 줄을 내고 하나라도 걸리면 1, 하나도 없으면 아무것도 내지 않고 0 으로 끝냅니다. 저장소가 아니거나 규칙 파일이 없거나 비었으면 첫 줄이 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 실행 권한을 준 뒤 bash /root/secrets-lab/scan-repo.sh /root/secrets-lab/repo /root/secrets-lab/rules.txt 의 출력을 /root/secrets-lab/repo-scan.txt 에 저장하세요.

저장소를 만들 때 git config user.emailuser.name 을 저장소 안에 먼저 정해야 커밋이 됩니다. 이력에 있는 모든 객체는 git rev-list --objects --all<해시> <경로> 로 냅니다. 그중 무엇이 블롭인지는 git cat-file --batch-check 에 그 목록을 그대로 흘려 넣으면 한 번에 알 수 있습니다(%(rest) 가 경로를 돌려줍니다). 내용은 git cat-file -p <해시> 입니다. 파일을 처음 넣은 커밋은 git log --diff-filter=A 로 찾습니다. 검사기를 만들 때 가장 흔한 실수는 아무것도 못 찾고 0 으로 끝나 놓고 '깨끗하다' 고 보고하는 것입니다 — 규칙이 없는 상태와 위반이 없는 상태를 종료 코드로 갈라 두세요.

세 자리를 한 번에 훑는 게이트를 세우고 폐기부터 적었다

/root/secrets-lab/secret-gate.sh <로그디렉터리> <이미지참조> <저장소> <규칙파일> <보고서파일> 을 만드세요. 로그 디렉터리 아래의 모든 파일, 이미지, 저장소 이력 세 자리를 한 번에 훑습니다(이미지와 저장소는 앞 단계에서 만든 두 검사기를 그대로 부릅니다). 걸린 것이 하나라도 있으면 화면과 보고서의 첫 줄에 SECRETS-FOUND <전체 건수> 를 내고 3 으로, 하나도 없으면 SECRETS-CLEAN 을 내고 0 으로 끝냅니다. 로그 디렉터리가 없거나 규칙 파일이 없거나 저장소가 아니면 첫 줄이 ERROR 로 시작하는 줄을 내고 1 로 끝냅니다. 보고서에는 첫 줄 뒤에 세 자리별 결과를 이어 적되, 로그에서 걸린 줄에는 그 파일 경로가, 저장소에서 걸린 줄에는 그 경로가 보여야 합니다. 보고서 경로의 상위 디렉터리가 없으면 만듭니다. 실행 권한을 준 뒤 bash /root/secrets-lab/secret-gate.sh /root/secrets-lab/logs oci:/root/secrets-lab/oci/app-clean:v1 /root/secrets-lab/repo /root/secrets-lab/rules.txt /root/secrets-lab/report/gate.txt 로 한 번 돌리세요. 마지막으로 /root/secrets-lab/revoke-checklist.txt정확히 다섯 줄<순번> <열쇠> <한 줄 설명> 꼴로 적으세요. 열쇠는 이 순서입니다 — revoke, reissue, rotate-consumers, audit-access, prevent-recurrence. 설명은 각각 열 자 이상으로 자기 말로 적습니다.

게이트가 옆에 있는 검사기를 부를 때는 자기 위치에서 찾습니다 — here=$(cd "$(dirname "$0")" && pwd). 두 검사기의 종료 코드는 0(깨끗)·1(걸림)·2(오류) 셋이니, 2가 오면 게이트도 멈춰야 합니다. 건수는 세 자리에서 나온 줄을 더한 값입니다. 이미지 검사기는 네 줄을 늘 내므로 그중 걸린 줄만 세야 합니다. 그리고 이 단계의 진짜 결론은 스크립트가 아니라 점검표에 있습니다 — 이력을 고쳐 지우는 일은 이미 복제된 사본·포크·캐시·백업에 아무 영향을 주지 못하고, 그 사이에 누가 읽었는지도 알 수 없습니다. 그래서 첫 줄이 폐기입니다.