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. 설명은 각각 열 자 이상으로 자기 말로 적습니다.

참고

단계 8개

  1. 디버깅하려고 추적을 켰더니 토큰이 로그에 그대로 찍혔다
  2. 마스킹을 붙였는데 base64 로 바꾼 값이 그대로 지나갔다
  3. 다음 레이어에서 지웠는데 앞 레이어 블롭에 그대로 있었다
  4. 레이어를 뒤질 것도 없이 설정의 Env 와 history 에 적혀 있었다
  5. 비밀을 이미지에서 빼고 실행 시점에 주니 같은 검사가 조용해졌다
  6. 파일로 넘긴 비밀이 작업이 끝난 뒤에도 그 자리에 남아 있었다
  7. 나중 커밋에서 지운 파일이 git cat-file 로 그대로 나왔다
  8. 세 자리를 한 번에 훑는 게이트를 세우고 폐기부터 적었다