工程卷宗

云端 Mac CI 构建日志脱敏:从源头阻止敏感信息外泄

云端 Mac CI 构建日志脱敏:从源头阻止敏感信息外泄

流水线失败后,团队通常会把完整日志、测试附件和结果包上传到共享存储,再由另一位工程师接手排查。真正危险的不是那句显眼的 TOKEN=,而是调试模式回显的命令参数、请求头、临时签名路径和测试代码附带的环境快照。云端 Mac 长时间承接多个任务时,这些内容还可能进入缓存与归档,因此日志脱敏必须发生在输出源、管道和上传门禁三个位置。

先画出日志的数据流

不要先写一条覆盖所有文本的正则。第一步是列出信息从哪里进入、经过什么进程、最终保存在哪里。

入口 常见泄漏方式 优先处理
Shell set -x 回显展开后的变量 敏感步骤前关闭追踪
命令参数 密钥、令牌直接出现在进程参数中 改用权限受限的临时文件或标准输入
环境变量 诊断脚本打印完整环境 只输出允许名单
测试 请求头、账户数据写入附件 在测试辅助层统一清洗
结果包 截图、活动日志和诊断文件被整体上传 导出后扫描再发布

脱敏不能让已经泄漏的密钥重新安全。只要确认真实值进入过可共享日志,就应停止发布该产物,并按密钥类型执行轮换。

先在 OakVM 节点上为每次任务建立独立目录,并设置仅当前用户可读写的权限。日志、结果包和待上传文件分开放置,避免扫描后的文件与原始文件混在一起。

umask 077
run_id="$(date -u +%Y%m%dT%H%M%SZ)-$$"
root="$HOME/ci-runs/$run_id"
mkdir -p "$root/raw" "$root/safe" "$root/results"

从输出源关闭泄漏入口

不把秘密放进参数

进程参数可能被诊断工具、崩溃记录或同一用户启动的脚本读取。调用内部上传脚本时,不要写成 --token "$TOKEN"。更稳妥的方式是由脚本读取权限为 600 的临时文件,用完立即删除;若工具支持标准输入,也可以通过标准输入传递。

Shell 追踪应默认关闭。确实需要观察普通命令时,只在不接触敏感变量的短区间启用,并在加载凭据前执行 set +x。同时禁止直接输出完整环境,诊断信息改为允许名单:

printf 'PATH=%s
' "$PATH"
printf 'DEVELOPER_DIR=%s
' "$DEVELOPER_DIR"
xcodebuild -version
sw_vers

路径也要按敏感信息处理。用户名、项目代号和临时签名目录可能暴露团队结构。日志发布前可以把工作根目录替换为 $WORKSPACE,但应保留文件名和行号,避免失去排错价值。

建立不会吞掉失败的脱敏管道

替换应优先匹配真实秘密值,而不是只查找 passwordsecret 之类的字段名。后者既容易误报,也挡不住没有标签的令牌。下面的过滤器从受限文件读取待遮罩值,按长度从长到短替换,避免短值破坏长值匹配。

import os
import sys
from pathlib import Path

secret_file = Path(os.environ["REDACT_FILE"])
values = [
    line.rstrip("
")
    for line in secret_file.read_text(encoding="utf-8").splitlines()
    if line.strip()
]
values.sort(key=len, reverse=True)

for line in sys.stdin:
    for value in values:
        line = line.replace(value, "[REDACTED]")
    sys.stdout.write(line)

运行构建时要保留 xcodebuild 的真实退出码。若只看管道末端的 tee,失败构建可能被误判为成功。

#!/bin/bash
set -o pipefail

set +x
REDACT_FILE="$root/secrets.txt" \
xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -resultBundlePath "$root/results/App.xcresult" \
  build 2>&1 |
python3 ci/redact.py |
tee "$root/safe/build.log"

statuses=("${PIPESTATUS[@]}")
exit "${statuses[0]}"

secrets.txt 必须使用 600 权限,并在任务退出钩子中删除。多行私钥不适合逐行替换;正确做法是从源头禁止打印,只把稳定、可能出现在文本中的标识值加入遮罩文件。

扫描结果包与测试附件

文本日志通过并不代表整个任务可以上传。结果包可能包含测试截图、失败附件、活动日志与诊断信息。先复制到隔离目录,导出确实需要共享的内容,再对导出目录执行扫描,不要默认发布完整结果包。

用两类规则降低误报

第一类规则匹配本次任务的真实秘密值,命中即阻断。第二类规则检查高风险结构,例如授权请求头、私钥边界、带凭据的 URL 和疑似长令牌。结构规则命中后进入人工复核,不要自动把所有 token 字样认定为泄漏,因为源码文件名和测试说明也可能包含这个词。

扫描报告只记录文件路径、行号和规则编号,不要把命中的原文再次复制进报告。对于二进制附件,可先识别类型;无法安全解析的文件默认不进入共享产物,而不是用 strings 抽取后直接公开。

把脱敏变成发布门禁

一条可维护的门禁至少包含四个状态:构建退出码、精确值扫描、结构规则复核、产物允许名单。任何一项未完成,上传步骤都不应启动。

建议在每次任务结束时检查:

  • 原始目录权限是否仍为仅当前用户可访问;
  • 日志中是否残留工作根目录、授权头或真实秘密值;
  • 结果包是否包含无关截图、网络响应和环境快照;
  • 上传清单是否只有脱敏日志、必要报告与明确选中的附件;
  • 清理脚本是否删除秘密文件,而不删除仍需排查的安全副本。

最后做一次反向测试:向临时任务注入专用假令牌,让它分别经过普通输出、错误输出和测试附件,确认门禁能够阻断;随后让构建主动返回非零状态,确认脱敏管道不会把失败改成成功。只有这两类测试同时通过,日志安全机制才算真正进入工程流程。

常见问题

只在上传日志前执行关键词替换,是否足够?

不够。敏感值可能先被终端回显、缓存或写入结果包,应先关闭调试输出并避免把密钥放进命令参数,再把替换和发布前扫描作为第二道防线。

日志经过脱敏管道后,怎样保留 xcodebuild 的真实退出码?

使用 Bash 的 pipefail,并在管道结束后立即保存 PIPESTATUS[0]。不要直接采用 tee 或脱敏脚本的退出码,否则失败构建可能被错误标记为成功。

哪些构建产物需要与纯文本日志一起检查?

至少检查结果包、测试附件、诊断压缩包、导出配置副本和自定义脚本生成的报告;这些文件可能保存请求头、环境快照、路径或测试账户信息。

OAKVM BUILD NODE

在独享物理节点上运行下一次构建

选择 OakVM M4 或 OakVM M4 Pro,按天、周、月或季租用非虚拟机的云端 Mac。节点实际可用状态以控制台实时返回为准。

选择配置并租用