TT Lab
开始
学习 学习路径 课程

GitLab CI/CD

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

在 TT Lab 中继续学习

目标

通过运行确认变量进入位置的优先级和环境范围,把要放入生产机密的作业限定为 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 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)。
  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>(占位符为 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 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)没有办法固定,所以最好把它拉取到仓库里。