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

CI/CD 流水线

以为已经删掉的令牌,却从镜像层和提交历史里原样冒了出来

在 TT Lab 中继续学习

目标

分别亲手重现机密残留的四个位置(构建日志、镜像层、镜像配置的 Env 与 history、提交历史),把设计改为机密不放进镜像、而在运行时注入,然后建立一个一次性扫描三个位置的门禁。

为什么重要

机密泄露的事故,多半不是出于恶意,而是出于图方便。为了调试打开了追踪,在构建中途写成文件最省事,把配置文件原样提交了。每一件都是想省几分钟的事,结果却是能读到这个值的人的范围整体扩大了。人们在这里最常依赖的是掩码,而掩码只遮盖与已知字符串完全相同的东西——转成 base64 或拆成两段,就会直接穿过去。所以顺序是先让它不被打印出来,掩码只是最后一张网。镜像方面更加悄无声息。镜像里装的不只是文件系统,还有它是如何构建出来的,所以在中间层写成文件、后面的层再删除,前面层的 blob 里依然保留,配置的 Env 和 history 里也会写着这个值。提交历史也一样——即使在下一个提交中删掉,blob 也还在。而最常出错的位置在最后。对已经泄露的机密,应对措施不是删除,而是撤销并重新签发。修改历史对已经被复制的副本、fork、缓存、备份毫无影响,也无法知道在此期间有谁读取过。

步骤

  1. 工作目录是 /root/secrets-lab。先创建 /root/secrets-lab/secret/api.token,在第一行写入本实验的假令牌 sk-labhub-fake-9f3a2b1c8d7e6f5a4b3c2d1e0f9a8b7c。文件权限必须是 600。然后创建 /root/secrets-lab/publish.sh <토큰파일> <산출물경로>(占位符依次为令牌文件与产物路径)。这个脚本要(1)读取令牌文件的第一行,(2)产物路径的上级目录不存在就创建,然后往该文件写一行 published <산출물 파일 이름>(占位符为产物文件名),(3)在开启 shell 追踪的状态下打印 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진수 소문자로 이어 붙인 값>(占位符依次为:令牌;不含换行的令牌 base64 编码值;前一半与后一半;令牌字节按小写十六进制拼接的值)。两个脚本都赋予执行权限,把 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=<토큰>,占位符为令牌),第二层的根目录里只放删除该文件的 whiteout 条目 etc/.wh.build.env(空文件)。配置 JSON 中,architecture 取自 uname -m(x86_64 写作 amd64,aarch64 写作 arm64),os 是 linux,config.Env 有 PATH=/usr/local/bin:/usr/bin:/bin 和 API_TOKEN=<토큰> 两项,rootfs.diff_ids 按层的顺序写压缩前 tar 的 sha256,history 有两项,第一项的 created_by 写入带有令牌的命令,第二项的 created_by 写入删除该文件的命令。清单中 config、layers 的 digest 和 size 是压缩后的值,所有 blob 都放在 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。最后解开第一层的 blob,把从中取出的 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- 后跟 32 位十六进制数)、AWS 访问密钥 ID 的形态(AKIA 后跟 16 位大写字母和数字)、PEM 私钥头(-----BEGIN 与 PRIVATE 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.Env 有 PATH=/usr/local/bin:/usr/bin:/bin 和 API_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자리>(占位符为该值 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 中写该 blob 的内容,在 /root/secrets-lab/git-leak-commit.txt 中写首次加入该文件的提交的 40 位哈希。最后创建 /root/secrets-lab/scan-repo.sh <저장소> <규칙파일>(占位符依次为仓库与规则文件)。用前面步骤的规则文件扫描的不是工作树,而是挂在所有提交上的所有 blob,对每个命中项输出一行 <규칙 이름> <블롭 해시> <경로>(占位符依次为规则名称、blob 哈希、路径),只要有一处命中就以 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。每条说明不少于十个字符,用自己的话写。

参考

为了调试打开了追踪,结果令牌被原样打印到日志里

工作目录是 /root/secrets-lab。先创建 /root/secrets-lab/secret/api.token,在第一行写入本实验的假令牌 sk-labhub-fake-9f3a2b1c8d7e6f5a4b3c2d1e0f9a8b7c。文件权限必须是 600。然后创建 /root/secrets-lab/publish.sh <토큰파일> <산출물경로>(占位符依次为令牌文件与产物路径)。这个脚本要(1)读取令牌文件的第一行,(2)产物路径的上级目录不存在就创建,然后往该文件写一行 published <산출물 파일 이름>(占位符为产物文件名),(3)在开启 shell 追踪的状态下打印 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。必须至少有两行。

shell 追踪用 set -x 打开,用 set +x 关闭。追踪输出到标准错误,所以要像 > 파일 2>&1(占位符为文件)这样把两者一起接收才能看到。打印值的那一行有一条,这一行的追踪又有一条,所以同一个值会留在两个地方。要以收窄的权限创建文件,可以在子 shell 中设置 umask 077,或者创建之后用 chmod。行数由 grep -c 统计。

接上了掩码,转成 base64 的值却原样通过了

创建 /root/secrets-lab/mask.sh <토큰파일>(占位符为令牌文件)。这是一个过滤器:读取标准输入,把所有与令牌完全相同的字符串替换为 ***MASKED*** 并输出到标准输出。令牌为空时,输出第一行以 ERROR 开头的行并以 2 结束。然后创建 /root/secrets-lab/leaky-shapes.sh <토큰파일>。把同一个值以四种形态打印恰好四行(顺序也照此):raw <토큰>、base64 <토큰을 줄바꿈 없이 base64 로 인코딩한 값>、split <앞 절반> <뒤 절반>、hex <토큰 바이트를 16진수 소문자로 이어 붙인 값>(占位符依次为:令牌;不含换行的令牌 base64 编码值;前一半与后一半;令牌字节按小写十六进制拼接的值)。两个脚本都赋予执行权限,把 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。

用 sed 替换的话,必须把值里的 /、&、. 逐个转义。用一行 python 按字符串原样替换更安全。base64 用 base64 -w0,输出时不带换行。十六进制只要把 od -An -tx1 输出中的空格和换行删掉即可。一半一半用 bash 的子串 ${변수:시작:길이}(占位符依次为变量、起点、长度)来截取。掩码所能遮盖的只有“已知的字符串”这一个,用这四行来确认这一点,就是这一步的全部。

在后面的层里删掉了,前面层的 blob 里却还在

不启动容器,手工构建 OCI 镜像。创建 /root/secrets-lab/build-image.sh <레이아웃디렉터리> <태그> <토큰파일>(占位符依次为布局目录、标签、令牌文件)。层有两层。第一层的根目录里放 app/main.py(内容是一行 print("labhub demo"))和 etc/build.env(内容是一行 API_TOKEN=<토큰>,占位符为令牌),第二层的根目录里只放删除该文件的 whiteout 条目 etc/.wh.build.env(空文件)。配置 JSON 中,architecture 取自 uname -m(x86_64 写作 amd64,aarch64 写作 arm64),os 是 linux,config.Env 有 PATH=/usr/local/bin:/usr/bin:/bin 和 API_TOKEN=<토큰> 两项,rootfs.diff_ids 按层的顺序写压缩前 tar 的 sha256,history 有两项,第一项的 created_by 写入带有令牌的命令,第二项的 created_by 写入删除该文件的命令。清单中 config、layers 的 digest 和 size 是压缩后的值,所有 blob 都放在 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。最后解开第一层的 blob,把从中取出的 etc/build.env 的那一行原样写入 /root/secrets-lab/layer-leak.txt——虽然在第二层中删除了,它还是会出现。

层就是普通的 tar。要让打包可复现,就加上 --sort=name --mtime=@1735689600 --owner=0 --group=0 --numeric-owner,并用 gzip -n -9 压缩。一个层需要三个值——压缩前 tar 的 sha256(diff_id)、压缩后字节的 sha256(blob 的名称,同时也是清单的 digest)、压缩后的大小(size)。三者只要有一个对不上,skopeo copy 就会拒绝,这本身就是校验。JSON 用 jq -n --arg 生成,就不必手工对齐引号。清单 blob 也放在与其他 blob 相同的位置,由 index.json 指向它的摘要和大小。从层 blob 中只取出一个文件,命令是 gzip -dc <블롭> | tar -xO ./etc/build.env(占位符为 blob)。

不必翻层,配置的 Env 和 history 里就写着

把规则写成数据。在 /root/secrets-lab/rules.txt 中写三条规则,每行的形式是 <이름> <확장정규식>(占位符依次为名称与扩展正则)(名称后面的空格直到行尾都是正则)。名称恰好是 labhub-fake-token、aws-access-key-id、private-key-header,分别要能匹配本实验假令牌的形态(sk-labhub-fake- 后跟 32 位十六进制数)、AWS 访问密钥 ID 的形态(AKIA 后跟 16 位大写字母和数字)、PEM 私钥头(-----BEGIN 与 PRIVATE 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。

不启动容器就想查看镜像内部,最容易的办法是 skopeo copy --insecure-policy <참조> dir:<임시디렉터리>(占位符依次为引用与临时目录)。这样 manifest.json 和各个 blob 就会落成普通文件,blob 文件名就是该 blob 的摘要。配置 blob 由清单的 .config.digest 指向。层 blob 是 gzip,必须解压后才能看,而 tar 里混有 NUL 字节,所以要给 grep 加 -a,或者预先删掉 NUL。如果正则以 - 开头,必须像 grep -E -- "$re" 那样标明选项结束。规则文件为空却以 0 结束的检查器,等于把“没有问题”和“什么也没看到”当成了同一件事来报告——所以要另外设置退出码。

把机密从镜像里拿掉、在运行时提供,同样的检查就安静了

创建 /root/secrets-lab/build-image-clean.sh <레이아웃디렉터리> <태그>(占位符依次为布局目录与标签)。不接收令牌文件作为参数,是这个设计的核心。 层只有一层,根目录里放 app/main.py(与前一步相同的那一行)和 etc/app.conf(内容是一行 api_token_file=/run/secrets/api.token)。配置的 config.Env 有 PATH=/usr/local/bin:/usr/bin:/bin 和 API_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자리>(占位符为该值 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。

把前一步的构建脚本复制过来,把层缩减为一层,再把原来放机密的位置换成“要读取的路径”就行。镜像里装的不是值,而是契约——一个承诺“会在哪里”的路径。把值放到那个位置,是运行环境的职责,所以同一个镜像,开发和生产可以原样共用。sha256sum 也接受标准输入。还要确认:检查器输出四行全是 clean 时,退出码是 0。

通过文件传递的机密,在任务结束之后仍留在原处

创建 /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。

mktemp -d 会在别人进不去的位置创建目录,但在其中创建文件时的权限由 umask 决定——要在子 shell 中设置 ( umask 077; ... ),或者创建之后用 chmod。删除交给 trap。只设置 EXIT 的话,被信号杀死时会残留,所以同时设置 INT 和 TERM。命令失败时,包装脚本也不能先死掉,所以要暂时解除 set -e,接住退出码,最后原样交出去。也别忘了:如果只收窄文件权限、却敞开着目录,是没有用的。而且即使这样做,也还没有结束——已经读走那个值的进程做了什么,这个包装脚本无从知晓。

后来的提交里删掉的文件,用 git cat-file 原样取了出来

创建练习用仓库 /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 中写该 blob 的内容,在 /root/secrets-lab/git-leak-commit.txt 中写首次加入该文件的提交的 40 位哈希。最后创建 /root/secrets-lab/scan-repo.sh <저장소> <규칙파일>(占位符依次为仓库与规则文件)。用前面步骤的规则文件扫描的不是工作树,而是挂在所有提交上的所有 blob,对每个命中项输出一行 <규칙 이름> <블롭 해시> <경로>(占位符依次为规则名称、blob 哈希、路径),只要有一处命中就以 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。

创建仓库时,必须先在仓库中设定 git config user.email 和 user.name,才能提交。历史中所有的对象,由 git rev-list --objects --all 以 <해시> <경로>(占位符依次为哈希与路径)的形式输出。其中哪些是 blob,把那份列表原样灌入 git cat-file --batch-check 就能一次性得知(%(rest) 会返回路径)。内容用 git cat-file -p <해시>(占位符为哈希)。首次加入文件的提交用 git log --diff-filter=A 查找。构建检查器时最常见的失误,是什么也没找到就以 0 结束,却报告“很干净”——要用退出码把没有规则的状态和没有违规的状态区分开来。

建立一次扫描三个位置的门禁,并把撤销写在最前面

创建 /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。每条说明不少于十个字符,用自己的话写。

门禁调用旁边的检查器时,要从自己所在的位置去找——here=$(cd "$(dirname "$0")" && pwd)。两个检查器的退出码有 0(干净)、1(命中)、2(错误)三种,所以收到 2 时,门禁也要停下。件数是三个位置所输出的行相加的值。镜像检查器总是输出四行,所以只能统计其中命中的行。而且这一步真正的结论不在脚本里,而在检查清单里——修改并删除历史,对已经被复制的副本、fork、缓存、备份毫无影响,也无法知道在此期间有谁读取过。所以第一行是撤销。