LabHub
学习 学习路径 课程

GitLab CI/CD

以为有掩码,令牌却以 base64 形式留在了日志里

在 LabHub 中继续学习

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

목표

변수가 들어오는 자리의 우선순위와 환경 범위를 실행으로 확인하고, 운영 비밀이 들어갈 잡을 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-prodproduction, review-appreview/$CI_COMMIT_REF_SLUG 이고, 각자 echo "<staging|prod|review> token-len=${#DEPLOY_TOKEN}" 으로 토큰 값이 아니라 길이만 출력합니다. 커밋하고 실행하면 세 잡이 각자 자기 환경의 토큰 길이(staging 14·prod 15·review 17)를 출력해야 합니다.
  3. deploy-prodrules: - 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 에 저장하세요.

참고

같은 이름의 변수가 넷 — 누가 이기나

/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 여야 합니다.

GitLab 은 프로젝트 설정의 변수를 YAML 의 잡 변수·전역 변수보다 우선합니다. 파이프라인을 실행할 때 직접 준 값(여기서는 --variable)은 그보다 앞섭니다. 변수 파일은 비밀을 담으므로 저장소에 올리지 않습니다 — git check-ignore 로 확인하세요.

환경마다 다른 비밀이 들어간다

잡 셋을 더하세요(모두 deploy 스테이지). deploy-staging 은 environment staging, deploy-prodproduction, review-appreview/$CI_COMMIT_REF_SLUG 이고, 각자 echo "<staging|prod|review> token-len=${#DEPLOY_TOKEN}" 으로 토큰 값이 아니라 길이만 출력합니다. 커밋하고 실행하면 세 잡이 각자 자기 환경의 토큰 길이(staging 14·prod 15·review 17)를 출력해야 합니다.

변수에 환경 범위를 두면 그 environment 로 선언한 잡에만 값이 들어갑니다. 와일드카드 범위(review/*)는 동적 환경 이름에 맞춥니다. 값이 필요한지 확인하려면 값 대신 길이나 존재 여부만 찍습니다.

운영 토큰이 들어갈 잡은 main 에서만 만든다

deploy-prodrules: - if: $CI_COMMIT_BRANCH == "main" 을 더해 커밋하세요. 채점기는 사본의 feature 브랜치에서 deploy-prod 가 목록에 없어 운영 토큰이 들어갈 잡 자체가 생기지 않는지 봅니다.

GitLab 은 보호 변수를 보호 브랜치의 파이프라인에만 넘깁니다. 그 기능이 없는 이 환경에서는 같은 의도를 '운영 잡은 main 에서만 만든다' 는 규칙으로 표현합니다. 둘 다 '누가 이 비밀을 가진 잡을 실행시킬 수 있나' 를 줄이는 장치입니다.

마스킹이 없으면 로그에 무엇이 남는가

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 에 더해 커밋되지 않게 하세요.

gitlab-ci-local 은 마스킹을 하지 않아 값이 그대로 찍힙니다. GitLab 의 마스킹은 값과 정확히 같은 문자열만 가리므로, base64 처럼 모양을 바꾸면 그대로 남습니다. 로그는 저장되고 여러 사람이 봅니다.

로그에 비밀이 찍혔는지 기계가 본다

/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 이 나와야 합니다.

base64 는 값 끝에 줄바꿈이 붙었는지에 따라 결과가 다릅니다. 두 모양을 모두 찾으세요. 사본에는 변수 파일(커밋하지 않은 파일)도 함께 복사돼야 파이프라인이 같은 값을 받습니다.

비밀은 찍지 않고 있는지만 확인한다

bad-debug 를 지우고, 대신 잡 check-token(deploy, environment production, main 에서만)을 더해 test -n "$DEPLOY_TOKEN" && echo token-present 만 실행하게 하세요. 커밋한 뒤 leak-scan.sh 가 OK 를 내야 합니다.

디버깅에 필요한 것은 대개 값이 아니라 '들어왔나' 입니다. 존재·길이·해시의 앞 몇 자처럼 되돌릴 수 없는 정보만 남기면 로그를 공유해도 안전합니다. set -x 도 변수 값을 펼쳐 찍으니 배포 잡에서는 피합니다.

움직일 수 있는 참조를 찾는다

/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 에 저장하세요.

브랜치 이름이나 태그는 사람이 옮길 수 있는 이름표라, 어제와 오늘 같은 설정이 다른 코드를 가져올 수 있습니다. 커밋 SHA·이미지 다이제스트는 내용 자체의 주소라 옮길 수 없습니다. 원격 파일(remote)은 고정할 방법이 없으니 저장소 안으로 가져오는 편이 낫습니다.