GitLab CI/CD · 현장에서의 파이프라인 · 실습
마스킹을 믿었는데 base64 로 토큰이 로그에 남았다
목표
변수가 들어오는 자리의 우선순위와 환경 범위를 실행으로 확인하고, 운영 비밀이 들어갈 잡을 main 으로 좁힌 뒤, 마스킹이 없는 로그에 비밀이 남는 모습을 보고 로그 누출 검사기와 참조 고정 검사기를 만듭니다.
왜 중요한가
파이프라인은 저장소에서 가장 강한 권한을 들고 임의의 코드를 실행하는 자리입니다. 같은 이름의 변수가 여러 곳에서 들어오면 어느 값이 이기는지 알아야 운영 값이 덮이는 사고를 막고, 환경 범위와 브랜치 규칙으로 비밀이 들어갈 잡의 수를 줄여야 합니다. 마스킹은 값과 똑같은 문자열만 가리므로 모양을 바꾼 출력은 그대로 남고, include 와 이미지를 이름표로 참조하면 누군가 그 이름표를 옮기는 날 우리 파이프라인이 남의 코드를 실행합니다.
단계
1. /root/glci-secrets 을 git 저장소로 만들고 .gitignore 에 .gitlab-ci-local/ 와 .gitlab-ci-local-variables.yml 을 두세요. .gitlab-ci.yml 은 stages [build, deploy], 전역 variables REGION: yaml-global·LOG_LEVEL: yaml-global, 잡 show(build, 잡 variables LOG_LEVEL: yaml-job, echo "region=$REGION log=$LOG_LEVEL")를 둡니다. 프로젝트 변수 파일 .gitlab-ci-local-variables.yml 에는 REGION: project-var, LOG_LEVEL: project-var 와 환경마다 값이 다른 DEPLOY_TOKEN(production prod-tok-7f3a9c, staging stg-tok-41be02, review/* review-tok-9d0c11)을 둡니다. 설정만 커밋하세요. show 로그는 region=project-var log=project-var 여야 합니다.
2. 잡 셋을 더하세요(모두 deploy 스테이지). deploy-staging 은 environment staging, deploy-prod 는 production, review-app 은 review/$CI_COMMIT_REF_SLUG 이고, 각자 echo "<staging|prod|review> token-len=${#DEPLOY_TOKEN}" 으로 토큰 값이 아니라 길이만 출력합니다. 커밋하고 실행하면 세 잡이 각자 자기 환경의 토큰 길이(staging 14·prod 15·review 17)를 출력해야 합니다.
3. deploy-prod 에 rules: - if: $CI_COMMIT_BRANCH == "main" 을 더해 커밋하세요. 채점기는 사본의 feature 브랜치에서 deploy-prod 가 목록에 없어 운영 토큰이 들어갈 잡 자체가 생기지 않는지 봅니다.
4. 잡 bad-debug(deploy, environment production, main 에서만)를 더해 echo "debugging with $DEPLOY_TOKEN" 과 echo -n "$DEPLOY_TOKEN" | base64 를 실행하게 하고 커밋하세요. 실행한 뒤 이 잡의 로그(.gitlab-ci-local/output/bad-debug.log)를 /root/glci-secrets/leak-evidence.log 로 복사하고, 이 파일을 .gitignore 에 더해 커밋되지 않게 하세요.
5. /root/glci-secrets/leak-scan.sh <저장소> 를 만드세요. 저장소를 임시 사본으로 떠 커밋하고 파이프라인을 돌린 뒤, .gitlab-ci-local/output/*.log 에 프로젝트 변수 파일 가운데 이름에 TOKEN·PASSWORD·SECRET·KEY 가 들어간 변수의 값(8자 이상, 환경별 값 포함)이 그대로나 base64 로 들어 있으면 LEAK <잡> <변수> 를 한 줄씩(정렬) 출력하고 3, 없으면 OK 와 0, 파이프라인이 실패하면 ERROR 와 1 로 끝냅니다. 원래 저장소에는 흔적을 남기지 않습니다. 이 저장소에 돌리면 LEAK bad-debug DEPLOY_TOKEN 이 나와야 합니다.
6. bad-debug 를 지우고, 대신 잡 check-token(deploy, environment production, main 에서만)을 더해 test -n "$DEPLOY_TOKEN" && echo token-present 만 실행하게 하세요. 커밋한 뒤 leak-scan.sh 가 OK 를 내야 합니다.
7. /root/glci-secrets/pin-audit.sh <설정파일> 을 만드세요. include 의 project 항목 ref 가 40자리 커밋 SHA 가 아니거나, remote include 이거나, component 가 @<40자리 SHA> 로 끝나지 않으면 UNPINNED include <값>, 잡의 image 에 @sha256: 다이제스트가 없으면 UNPINNED image <값> 을 한 줄씩(정렬, 중복 없이) 출력하고 3, 없으면 OK 와 0 으로 끝냅니다. project 항목의 값은 <project>@<ref> 로 적습니다. /root/glci-secrets/pin-sample.yml 에 표본 설정(과제 본문 아래 파일 예시와 같은 내용)을 두고 결과를 /root/glci-secrets/pin-report.txt 에 저장하세요.
참고
- 이 VM 에는 GitLab 서버와 러너가 없고, gitlab-ci-local 4.75.1 이 .gitlab-ci.yml 을 GitLab 과 같은 규칙으로 해석해 shell 로 잡을 실행합니다.
image:를 적으면 도커로 돌리려 하므로 쓰지 않습니다. 보호 변수·마스킹·CI_JOB_TOKEN·러너 태그·병합 요청 파이프라인 생성은 서버 기능이라 여기서 재현되지 않습니다. - 실행: 저장소 루트에서
gitlab-ci-local --shell-isolation --no-artifacts-to-source(잡마다 따로 된 작업 디렉터리, 산출물을 저장소에 되쓰지 않음), 잡 목록:gitlab-ci-local --list-csv-all, 해석된 설정:gitlab-ci-local --preview. gitlab-ci-local 은 git 이 추적하는 파일만 잡에 넘기므로 파일을 만들면git add하세요. 채점기는 저장소를 사본으로 떠 모든 파일을 커밋한 뒤 같은 도구로 다시 돌립니다. .gitlab-ci-local-variables.yml은 GitLab 프로젝트 설정의 CI/CD 변수를 대신합니다(환경 범위 values 지원). GitLab 의 보호 변수·마스킹·숨김 변수·CI_JOB_TOKEN 은 서버 기능이라 재현하지 않습니다. 참고로 GitLab CE 19.3.2 를 실제로 띄워 본 결과(2026-09-15) 마스킹된 변수는 로그에 [MASKED] 로, CI_JOB_TOKEN 은 glcbt- 로 시작하는 값으로 찍혔습니다.- 흔한 실수: 변수 파일을 커밋하는 것. 흔한 실수: 디버깅하려고 set -x 나 env 전체를 찍는 것.
- [GitLab CI/CD variables(우선순위·보호·마스킹)](https://docs.gitlab.com/ci/variables/) · [CI/CD job token](https://docs.gitlab.com/ci/jobs/ci_job_token/) · [include](https://docs.gitlab.com/ci/yaml/includes/) · [Environments](https://docs.gitlab.com/ci/environments/) · [Protected branches](https://docs.gitlab.com/user/project/repository/branches/protected/)
단계 7개
- 같은 이름의 변수가 넷 — 누가 이기나
- 환경마다 다른 비밀이 들어간다
- 운영 토큰이 들어갈 잡은 main 에서만 만든다
- 마스킹이 없으면 로그에 무엇이 남는가
- 로그에 비밀이 찍혔는지 기계가 본다
- 비밀은 찍지 않고 있는지만 확인한다
- 움직일 수 있는 참조를 찾는다