以为有掩码,令牌却以 base64 形式留在了日志里
目标
通过运行确认变量进入位置的优先级和环境范围,把要放入生产机密的作业限定为 main,看到在没有掩码的日志中机密留下来的样子,并创建日志泄漏检查器和引用固定检查器。
为什么重要
流水线是仓库中持有最强权限、执行任意代码的地方。同名变量从多个位置进来时,必须知道哪个值获胜,才能防止生产值被覆盖的事故,并且要用环境范围和分支规则,减少放入机密的作业数量。掩码只会遮住与值完全相同的字符串,所以改变了形态的输出会原样留下,如果把 include 和镜像用标签来引用,总有一天有人移动了那个标签,我们的流水线就会执行别人的代码。
步骤
- 把
/root/glci-secrets创建为 Git 仓库,并在.gitignore中放入.gitlab-ci-local/和.gitlab-ci-local-variables.yml。.gitlab-ci.yml放入 stages[build, deploy]、全局 variablesREGION: yaml-global、LOG_LEVEL: yaml-global,以及作业show(build,作业 variablesLOG_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。 - 再添加三个作业(全部是 deploy stage)。
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)。 - 给
deploy-prod加上rules: - if: $CI_COMMIT_BRANCH == "main"并提交。评分器会在副本的 feature 分支上,检查列表中是否没有 deploy-prod,也就是放入生产令牌的作业本身是否不会生成。 - 添加作业
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,使其不会被提交。 - 创建
/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。 - 删除
bad-debug,改为添加作业check-token(deploy,environment production,只在 main 上),只执行test -n "$DEPLOY_TOKEN" && echo token-present。提交之后,leak-scan.sh必须输出 OK。 - 创建
/root/glci-secrets/pin-audit.sh <설정파일>(占位符为配置文件)。如果 include 的 project 条目的 ref 不是 40 位提交 SHA,或者是 remote include,或者 component 不是以@<40자리 SHA>(占位符为 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 服务器和 Runner,由 gitlab-ci-local 4.75.1 按与 GitLab 相同的规则解析 .gitlab-ci.yml,并用 shell 执行作业。写了
image:就会试图用 Docker 运行,所以不要使用。受保护变量、掩码、CI_JOB_TOKEN、Runner tag、合并请求流水线的生成是服务器功能,在这里不会重现。 - 运行:在仓库根目录用
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(优先级、保护、掩码) · CI/CD job token · include · Environments · Protected branches
同名变量有四个——谁获胜
把 /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 stage)。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)。
如果给变量设置了环境范围,值就只会进入声明了该 environment 的作业。通配符范围(review/*)匹配动态环境名称。要确认是否需要值,只打印长度或是否存在,而不打印值。
放入生产令牌的作业只在 main 上创建
给 deploy-prod 加上 rules: - 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>(占位符为 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。
分支名称和 tag 是人可以移动的标签,所以昨天和今天同样的配置,可能会拉取到不同的代码。提交 SHA 和镜像摘要是内容本身的地址,无法移动。远程文件(remote)没有办法固定,所以最好把它拉取到仓库里。