凭据、审计与软件供应链:轮换权限并核验制品来源
应用读取数据库口令后建立连接,执行操作后留下审计记录,部署时又需要判断运行的 JAR 来自哪里。这三个过程分别涉及访问资格、操作记录和软件来源。它们共用一些密码技术,但撤销方式和信任对象各不相同。
运行中的应用
├── 取得凭据:谁可以读取哪个环境、哪个版本的秘密
├── 访问资源:数据库角色、已有连接、连接池与最小权限
├── 记录操作:结构化事件、追加权限、独立保存与查询
└── 加载制品:依赖清单、文件摘要、签名者与构建来源把凭据交给正确的进程
口令、签名私钥与加密密钥各自保护什么
| 材料 | 主要用途 | 需要限制的能力 | 轮换时最容易遗漏的对象 |
|---|---|---|---|
| 数据库口令、API key | 向服务证明调用身份 | 登录、读取或修改资源 | 已建立连接、客户端缓存、后台作业 |
| 签名私钥、HMAC 密钥 | 对消息或制品生成可验证的认证信息 | 签发某类消息或制品 | 验证端信任列表、旧签名和已签发令牌 |
| 数据加密密钥 | 加密和解密业务数据 | 读取历史密文、生成新密文 | 旧数据、备份、包裹后的数据密钥 |
| 证书、公钥、key ID | 分发验证材料或定位密钥 | 修改信任配置、替换映射 | 验证端缓存和用途限制 |
公钥和证书通常可以公开,私钥必须受保护;key ID 只是选择线索,验证端仍要限定允许的来源、算法与用途。数据库地址、超时和功能开关属于普通配置,但配置中混入的口令仍按秘密处理。签名密钥、数据库身份和加密密钥应分别管理,避免一项泄漏同时获得多种权限。
每个秘密至少关联用途、环境、获准消费者、权限、版本和撤销入口。应用、迁移作业、批处理与人工应急账号分别使用身份,轮换时才能逐个迁移。支持工作负载身份或动态凭据的平台,可以让应用用短期身份换取短期数据库凭据,减少长期共享口令;获取身份的入口和租约续期也要受控。OWASP Secrets Management
先运行一次真实轮换和权限测试
下载 凭据、审计与供应链实验工程,在 Linux 开发机解压。宿主需要 Docker CLI、unzip,当前普通用户已获准访问开发用 Docker daemon。
工程使用 Maven 3.9.12、Java 17 编译目标;Java 17 和 25 都可运行。Spring Boot 4.1.1 仅以依赖 BOM 导入,不继承其构建父 POM;工程生成普通 JAR,不启动 Spring Web 应用。实际依赖为 pgJDBC 42.7.13、Jackson 3.1.5、JUnit 6.0.3、Testcontainers 2.0.5;数据库为 PostgreSQL 18.6。构建插件在 POM 中分别固定版本,CycloneDX Maven 插件为 2.9.1。
先准备目录并确认 daemon 可达:
unzip secret-supply-lab.zip
cd secret-supply-lab
mkdir -p .m2
LAB_DIR="$(pwd -P)"
MAVEN_IMAGE='maven:3.9.12-eclipse-temurin-17'
docker version
test -S /var/run/docker.sock
SOCKET_GID="$(stat -c '%g' /var/run/docker.sock)"
docker pull "$MAVEN_IMAGE"
docker pull postgres:18.6docker version 应同时显示 Client 和 Server。没有 Server、socket 不存在或 permission denied 时,先修复当前开发环境的 daemon 连接。原生 Linux 默认 Unix socket 下,运行命令如下:
docker run --rm --user "$(id -u):$(id -g)" --group-add "$SOCKET_GID" \
-e HOME=/tmp -e MAVEN_CONFIG=/tmp/.m2 \
--mount "type=bind,src=$LAB_DIR,dst=/work" \
--mount "type=bind,src=$LAB_DIR/.m2,dst=/m2" \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
--workdir /work "$MAVEN_IMAGE" \
mvn -B -ntp -Duser.home=/tmp -Dmaven.repo.local=/m2 clean verify--user 让 Maven 按宿主 UID/GID 写入源码目录与缓存;--group-add 让容器中的测试 JVM 访问 socket。daemon 随后创建独立 PostgreSQL 容器。控制 Docker socket 的能力仍然很高,不因 Maven 使用普通 UID 而消失,测试应运行在专用开发环境。Docker Linux 权限说明
Docker Desktop 的容器化 Maven 需要再加 -e TESTCONTAINERS_HOST_OVERRIDE=host.docker.internal,使测试 JVM 能访问兄弟容器的随机映射端口。rootless 或远程 daemon 按实际 socket 和可达地址配置;宿主已有 JDK/Maven 时可直接执行 mvn -B -ntp clean verify。容器内运行 Testcontainers
正常结果为 3 次测试执行,零失败、零错误、零跳过。其中一项检查双角色轮换,一项检查审计表权限,一项检查 JSON 事件字段。报告在 target/surefire-reports;构建还生成 target/secret-supply-lab-1.0.0.jar、target/bom.json 和 target/bom.xml。
临时数据库和口令由测试创建,测试结束关闭数据库容器、删除临时凭据文件。测试没有连接现有业务数据库。内网运行需要先为 Maven 依赖、PostgreSQL 及 Testcontainers 辅助镜像准备获准仓库与认证,下载失败不能按“没有执行危险操作”当成测试成功。
文件注入需要同时检查挂载与读取权限
凭据文件与应用文件分开保存,可以使同一制品在不同环境读取不同身份。常见布置为:
受控凭据目录
└── db.properties 仅应用 UID 或授权组可读
│ 只读挂载
▼
应用容器 /run/secrets/db.properties
│ 解析 user/password,不记录值
▼
JDBC Properties → 数据库登录 → 已建立连接应用已具备读取权限的文件,可在部署时通过以下挂载选项交付;SECRET_FILE 必须是管理系统已准备好的绝对路径:
SECRET_FILE='/srv/example-service/secrets/db.properties'
test -f "$SECRET_FILE"
stat -c '%a %u:%g %n' "$SECRET_FILE"
# 加入应用原有 docker run 命令:
# --mount "type=bind,src=$SECRET_FILE,dst=/run/secrets/db.properties,readonly"常见权限是由应用 UID 拥有的 0600,或受控应用组可读的 0640;父目录还要允许该 UID 穿过。readonly 限制容器写入挂载,不负责加密文件,也不能阻止已获准读取的应用把内容复制到其他地方。检查挂载时只查看路径、权限和 RW 状态,不打印文件内容。Docker Bind mounts
Compose 的 secrets 可以按服务声明文件授权,通常在容器的 /run/secrets/ 下提供文件。应用需要主动读取文件,或使用镜像明确支持的 _FILE 选项;普通环境变量名称后面追加 _FILE 并不会自动实现文件读取。基于本机文件的 Compose secret 也不能代替上游密库和宿主文件权限。Docker Compose Secrets
工程中的 CredentialFile.connect(jdbcUrl, file) 检查文件大小、必需字段,再把凭据单独交给驱动:
Properties connection = new Properties();
connection.setProperty("user", user);
connection.setProperty("password", password);
connection.setProperty("connectTimeout", "3");
connection.setProperty("socketTimeout", "5");
connection.setProperty("ApplicationName", "sec15-supply");
return DriverManager.getConnection(jdbcUrl, connection);完整实现使用 UTF-8 读取 Properties,拒绝超过 4096 字节或缺少字段的文件。实验口令为随机十六进制字符串;接入真实配置时还要按 Properties 的转义规则编码值。生产 JDBC 连接应使用组织规定的 TLS 和服务器身份校验,文件权限只能保护本机副本。pgJDBC 连接参数
文件更新只改变后续读取。应用若在启动时读取一次,替换文件后需要重新加载配置或重建连接池;单文件 bind mount 还可能继续指向旧文件对象,不能假设宿主原子替换路径会立即刷新容器视图。部署系统应明确版本化挂载、重新创建实例或受控重新加载的方式,再通过新连接验证身份。
追踪已经离开密库的副本
环境变量可能进入子进程、诊断配置和容器检查输出,命令行参数可能进入进程列表与 Shell 历史,Dockerfile 中写入后再删除的文件还可能留在旧镜像层。构建需要凭据时,应使用构建系统的临时 secret 挂载,避免写进 ARG、ENV 或最终镜像。Docker Build secrets
堆转储、崩溃报告、APM 标签、备份和工单附件也是潜在副本。应用日志只记录秘密的管理 ID、版本与失败类别,不记录值或完整连接串;低熵口令即使只记录普通哈希,也可能被离线猜测。诊断文件按敏感数据控制读取、保留期限和对外分享,不能只对主配置文件设置权限。
分别撤销数据库登录、已有连接与审计修改权限
用两套登录身份完成数据库轮换
PostgreSQL 的一个角色保存一份当前口令配置。平滑迁移可以建立两个登录角色,让它们继承同一最小权限组:
app_rights(NOLOGIN)
├── SELECT business.account
├── INSERT business.audit_event
├── app_v1(LOGIN,旧凭据)
└── app_v2(LOGIN,新凭据)
audit_owner(NOLOGIN)
└── 拥有 business.audit_event;应用不继承此角色实验先由管理连接创建这些角色和表。应用连接使用 app_v1 或 app_v2,不使用准备数据的管理员。生产准备登录角色时,使用获准的管理连接执行 CREATE ROLE ... LOGIN IN ROLE app_rights,再通过 psql 的 \password app_v2 交互设置口令,避免把明文写进命令历史;分发完成后再允许该身份进入业务连接池。PostgreSQL ALTER ROLE
轮换过程的不同观察点如下:
| 操作 | 新建 app_v1 连接 | 已建立的 app_v1 连接 | app_v2 连接 |
|---|---|---|---|
| 两角色都可登录 | 成功 | 可查询 | 可查询 |
| 应用配置切到 app_v2 | 仍可成功 | 旧池仍可能保留 | 新池使用新身份 |
ALTER ROLE app_v1 NOLOGIN | 被拒绝,实验 SQLState 为 28000 | 仍可查询 | 不受此操作影响 |
| 精确终止旧 backend | 继续被拒绝 | 连接结束 | 继续可查询 |
SupplyTest 保持旧连接不关闭,读取账户金额 100;建立新连接后读取同一值,再执行 NOLOGIN。它随后分别尝试旧身份的新连接与旧连接上的查询,得到的行为不同。测试输出为:
new_v2=accepted old_new_login=28000 old_existing_before_terminate=usable old_existing_after_terminate=closed例行轮换时,先让新连接健康工作,再停止旧池发放连接,等待在途事务结束并关闭旧池;确认所有服务、定时任务和迁移工具已切换后禁用旧身份。泄漏响应可能需要立即禁用并终止连接,代价是中断在途工作,应用要按业务操作 ID 查询结果,再决定是否重试。
终止的是精确会话,不是所有数据库连接
应用可用 SELECT pg_backend_pid() 获取当前连接对应的 backend PID。实验管理连接使用下面的预编译查询,? 只绑定刚才记录的旧 PID:
SELECT pg_terminate_backend(pid, 1000)
FROM pg_stat_activity
WHERE pid = ?
AND usename = 'app_v1'
AND datname = current_database();这里同时限制 PID、角色和当前实验数据库。1000 表示最多等待 1000 毫秒确认进程终止;未指定超时时的 true 只表示信号已成功发送,不能直接推导连接已经退出。调用者需要相应管理权限;pg_cancel_backend 取消当前查询,pg_terminate_backend 才结束会话。PostgreSQL 管理函数
实验断言只命中一条旧连接,返回 true,随后旧连接查询失败,而 v2 仍能读取金额。实际连接结束可能表现为服务端终止或驱动连接失效,测试接受相应的 57P01、08006、08003。
NOLOGIN 只约束新登录。还需要检查角色成员关系、代理访问和其他认证机制是否允许间接使用旧权限。单角色改密码方案同样要处理连接池与已有连接;VALID UNTIL 的口令有效期也不能代替主动关闭旧会话。撤销后不要通过恢复旧文件、恢复旧账号 LOGIN 来解决生产回滚失败。
签名与加密的旧密钥为什么不能同样删除
签名轮换通常先向验证端分发新公钥,再用新私钥签发。旧公钥保留到需要验证的旧消息、令牌或制品退出使用窗口;私钥已经泄漏时,则要撤销其可信签发资格,并处理尚未到期的旧凭证。HMAC 的验证端持有同一秘密,也具有生成有效认证码的能力,因此分发面和非对称签名不同。
数据加密还要保留历史密文的读取能力。常见信封加密关系为:
业务数据 ──数据密钥 DEK──► 密文
数据密钥 DEK ──密钥加密密钥 KEK──► 包裹后的 DEK
读取:授权使用 KEK → 解开 DEK → 解密业务数据在 DEK 未泄漏且算法策略允许时,轮换 KEK 可以重新包裹 DEK,减少重写全部业务数据的成本;DEK 泄漏则要评估该密钥保护的数据并用新 DEK 重新加密。删除唯一可用密钥可能使备份一并无法恢复,执行销毁前要核对历史版本与恢复策略。信封加密
审计事件先限定字段,再交给序列化器
审计需要能关联主体、动作、对象和结果。请求正文、Authorization、Cookie、密码与完整个人资料不应因为“便于排查”直接进入事件。工程将输入限制为五个字段:
public record AuditEvent(
String actor, String action, String objectId,
String outcome, String requestId) {
public String json() {
return JsonMapper.shared().writeValueAsString(this);
}
}JsonMapper 的包名为 tools.jackson.databind.json。调用方构造明确事件,不传任意请求 Map。若 actor 中包含换行,序列化器将它编码在 JSON 字符串内部,解析后仍得到原字段值;直接拼接“用户 + 换行 + 文本”则可能伪造另一条日志。测试同时检查五字段集合,确认没有 password、token 字段。
JSON 编码让换行留在字段值内,字段长度、内容最小化和读取权限仍需另行设置。生产审计还应按任务增加租户、有效主体、事件 ID、操作关联 ID、规则版本和服务端事件时间;来源由可信上下文生成,不能直接相信客户端提交的 actor。日志收集端应保留必要关联信息,并对查看、导出、保留和删除实行独立权限。OWASP Logging
应用只能追加,不能改写旧记录
实验的审计表由独立 audit_owner 持有,业务权限组仅有 INSERT:
ALTER TABLE business.audit_event OWNER TO audit_owner;
GRANT USAGE ON SCHEMA business TO app_rights;
GRANT INSERT ON business.audit_event TO app_rights;应用插入时使用参数化语句:
try (var insert = app.prepareStatement(
"INSERT INTO business.audit_event VALUES(?, ?::jsonb)")) {
insert.setObject(1, eventId);
insert.setString(2, event.json());
int inserted = insert.executeUpdate();
}INSERT 影响 1 行;UPDATE 和 DELETE 均得到 SQLState 42501;独立管理连接查询时,原事件仍存在。应用不是表 owner,也没有超级用户或绕过控制的角色属性。对象所有权、角色继承、PUBLIC 权限和迁移新增表的默认授权都需要一并检查。PostgreSQL Privileges
追加权限限制应用改写已有记录,但应用仍可能漏记或伪造它新写入的事件,数据库管理员也拥有更高能力。对删除历史要求更严格时,把事件送往独立汇聚系统或具有受控保留策略的存储,并监测投递延迟、事件缺口与重复。哈希链若与日志由同一被入侵主体任意重算,也不足以抵御同源伪造。
业务修改与审计放在同一个数据库事务时,可以共同提交或回滚。审计发往外部系统时需要解决两次写入的间隙,可先在本地事务中持久化待投递事件,再异步发送。高风险管理动作遇到审计持久化失败,应按事先定义的策略拒绝或进入受控待处理状态;已经完成的业务不能仅因日志上传失败就向客户端宣称“完全未执行”。
从 Maven 依赖追到受信制品
解析出来的依赖才是构建实际输入
Java 应用的依赖包含直接声明、传递引入、父 POM 与 BOM 管理的版本。Maven 的 dependencyManagement 提供版本和其他管理信息,不会自动把其中全部依赖加入运行时;冲突选择、scope 和 exclusion 会改变最终集合。构建插件又在独立插件环境运行,应用依赖树不包含所有构建工具代码。Maven Dependency Mechanism
沿用前面的 Maven 容器,将目标改为以下命令可以分别查看运行依赖和有效配置;已有本机 Maven 时直接执行:
mvn -B -ntp dependency:tree -Dscope=runtime
mvn -B -ntp help:effective-pom -Doutput=target/effective-pom.xml本工程的运行依赖包括 pgJDBC、checker-qual、Jackson databind、core 和 annotations,共 5 个组件。JUnit 与 Testcontainers 属于 test scope;导入 Boot BOM 不意味着构件包含一个 Spring Boot 服务。对有效 POM 或 settings 的诊断输出按内部资料保存,分享前检查仓库账号和其他敏感配置。
构建机只从组织认可的仓库解析依赖与插件,内部坐标的所有权和发布权限要明确。私有包名、公共仓库回退、可变 SNAPSHOT、版本范围与被覆盖的同名制品,都可能让相同源码取得不同输入。Maven mirror 可以把仓库请求导向受控仓库管理器,但它本身不承担漏洞判断或源代码审查。Maven Repository Mirrors
CycloneDX 描述组件,不替代所有构建信息
POM 将 CycloneDX makeAggregateBom 绑定到 package,并明确输出格式与位置:
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.9.1</version>
<executions>
<execution>
<id>default</id>
<phase>package</phase>
<goals><goal>makeAggregateBom</goal></goals>
<configuration combine.self="override">
<schemaVersion>1.6</schemaVersion>
<includeTestScope>false</includeTestScope>
<outputFormat>all</outputFormat>
<outputName>bom</outputName>
<outputDirectory>${project.build.directory}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>outputFormat=all 同时生成 JSON 与 XML,includeTestScope=false 排除测试依赖,输出为 target/bom.json 和 target/bom.xml。插件在线文档可能描述更新版本,工程始终由 POM 中的 2.9.1 和 schema 1.6 决定格式。CycloneDX makeAggregateBom
宿主安装 jq 后,可以只查看必要字段:
jq '{bomFormat, specVersion, componentCount: (.components | length)}' target/bom.json
jq -r '.components[] | [.group, .name, .version] | @tsv' target/bom.json第一个命令应得到 CycloneDX、1.6、组件数 5;第二个命令可与运行依赖树逐项对照。components 描述组件,dependencies 描述它们的引用关系,purl 用于标识包坐标。项目名称、描述、许可证和仓库地址属于项目元数据,依赖组件各自保留自己的元数据;这些字段与经过验证的构建来源声明有不同用途。
这份依赖 SBOM 不包含容器基础镜像中的系统包、JDK 安装内容或运行时另外下载的插件。普通 JAR 也不会把所有运行依赖直接装进去,部署时仍要管理完整 classpath 或最终镜像。将 SBOM 关联到实际交付制品的摘要,后续发现某个依赖受影响时才能查到对应部署。
摘要固定文件,签名还需要预先建立信任
对刚构建的普通 JAR 计算摘要:
sha256sum target/secret-supply-lab-1.0.0.jar输出是当前文件的 SHA-256 摘要和文件名,摘要由 64 个十六进制字符(256 位)表示。发布系统需要保存本次实际值,不能拿另一台机器的一串示例摘要来比较。攻击者如果能同时替换 JAR 与旁边的摘要文本,两者仍可以相互匹配;接收方需要通过受信发布渠道取得预期摘要,或验证绑定这个制品的签名。
JAR 签名覆盖归档中的条目摘要,签名者证书帮助验证对应密钥。jarsigner -verify -strict 会把严重验证警告纳入非零退出,包括证书链、未签名条目和签名者匹配等问题;普通输出里出现 verified 仍要结合退出码及信任设置读取。签名完成后 JAR 字节已经改变,发布摘要应针对最终交付版本计算。JDK jarsigner
分步签名与验证一个本地 JAR
以下操作只处理实验 JAR 的副本。Linux 宿主沿用 LAB_DIR,启动一个普通 UID 的交互 JDK 容器,源码目录只读挂载:
JDK_IMAGE='eclipse-temurin:17.0.20_8-jdk'
docker pull "$JDK_IMAGE"
docker run --rm -it --user "$(id -u):$(id -g)" -e HOME=/tmp \
--mount "type=bind,src=$LAB_DIR,dst=/work,readonly" \
--workdir /work "$JDK_IMAGE" bash下面几个代码块在这个容器的 Bash 内执行。先建立仅本轮可用的临时目录与随机口令文件,再生成 RSA 密钥:
set -euo pipefail
export LC_ALL=C
umask 077
SIGN_DIR="$(mktemp -d /tmp/sec15-sign.XXXXXXXX)"
od -An -N24 -tx1 /dev/urandom | tr -d ' \n' > "$SIGN_DIR/password"
keytool -genkeypair -alias lab-signer -keyalg RSA -keysize 3072 -validity 2 \
-dname 'CN=Local Laboratory Signer' -storetype PKCS12 \
-keystore "$SIGN_DIR/signer.p12" -storepass:file "$SIGN_DIR/password"signer.p12 含私钥,口令通过文件读取,避免放入命令行值。这个自签证书只为短期实验生成,不适合直接成为组织的发布身份。生产密钥应限定可调用的工作负载与用途,优先避免让每个构建分支都得到可导出的长期签名私钥。JDK keytool
将证书导出,再导入一个单独的验证信任库:
keytool -exportcert -rfc -alias lab-signer \
-keystore "$SIGN_DIR/signer.p12" -storepass:file "$SIGN_DIR/password" \
-file "$SIGN_DIR/cert.pem"
keytool -importcert -noprompt -alias expected-signer \
-file "$SIGN_DIR/cert.pem" -keystore "$SIGN_DIR/trust.p12" \
-storetype PKCS12 -storepass:file "$SIGN_DIR/password"这个导入动作代表验证者已经通过独立可信渠道接受该证书。生产接收未知 JAR 时,不能把随包下载的任意证书自动加入信任库;还要约束预期发布者和允许签名的项目。随后签名并严格验证:
jarsigner -keystore "$SIGN_DIR/signer.p12" -storepass:file "$SIGN_DIR/password" \
-signedjar "$SIGN_DIR/signed.jar" target/secret-supply-lab-1.0.0.jar lab-signer
jarsigner -verify -strict -keystore "$SIGN_DIR/trust.p12" \
-storepass:file "$SIGN_DIR/password" "$SIGN_DIR/signed.jar" expected-signer预期验证退出码为 0,并显示 jar verified。原 JAR 未被修改;所有新材料都在容器的私有 /tmp 目录。实验没有可信时间戳,证书到期后不能沿用这次验证结果解释长期归档签名;实际发布还要按策略检查证书有效性、撤销与时间戳。
第一种反例去掉受信库,确认默认信任配置不接受本轮自签证书:
set +e
jarsigner -verify -strict "$SIGN_DIR/signed.jar" > "$SIGN_DIR/untrusted.log" 2>&1
status=$?
set -e
test "$status" -ne 0
grep -Ei 'certificate chain is invalid|unable to find valid certification path' "$SIGN_DIR/untrusted.log"第二种反例保留正确受信库,但修改已经签名的资源条目:
cp -- "$SIGN_DIR/signed.jar" "$SIGN_DIR/tampered.jar"
printf '%s\n' 'Changed after signing.' > "$SIGN_DIR/payload.txt"
jar --update --file "$SIGN_DIR/tampered.jar" -C "$SIGN_DIR" payload.txt
set +e
jarsigner -verify -strict -keystore "$SIGN_DIR/trust.p12" \
-storepass:file "$SIGN_DIR/password" "$SIGN_DIR/tampered.jar" expected-signer \
> "$SIGN_DIR/tampered.log" 2>&1
status=$?
set -e
test "$status" -ne 0
grep -F 'digest error for payload.txt' "$SIGN_DIR/tampered.log"两组反例都要求非零退出并命中特定原因。缺少 jarsigner、文件不存在或工具启动失败会产生其他错误,不能记作“正确拒绝了不可信制品”。用 exit 退出后,--rm 会删除这个容器及其临时密钥,宿主的构建产物保留。
回到宿主,包内 signature-lab.sh 可重复上述三条路径并自动清理本轮副本:
docker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp \
--mount "type=bind,src=$LAB_DIR,dst=/work,readonly" \
--workdir /work "$JDK_IMAGE" bash signature-lab.sh正常输出为 trusted_original=verified untrusted_signer=rejected tampered_resource=digest_error。Java 25 可改用 eclipse-temurin:25.0.4_7-jdk。脚本在每条路径上检查真实命令及错误原因,不会重新构建或修改源 JAR。
构建来源声明还要绑定源码和构建者
签名者可以签署任意文件。要判断制品由哪个源码和构建过程产生,需要进一步读取并验证 provenance。SLSA 的构建来源声明包含制品 subject/digest、构建类型、外部参数、已解析输入以及 builder 等信息;不同格式由对应 predicate 规范定义。SLSA Build Provenance
验证者应将声明签名关联到预先接受的构建身份,核对 subject 摘要等于当前制品,再检查预期源码仓库、构建类型和参数。声明里写着一个 builder 名称,不能自动使这个 builder 可信;工作流或构建平台自身被攻破时,也要重新评估它签发的材料。预配置的信任根和逐项验证步骤见 SLSA Verifying artifacts。
构建身份只写指定制品仓库路径,发布身份负责允许哪个版本进入环境,二者分开授权。未受信 PR、fork 构建与普通单元测试任务不应获得生产发布权限。可复现构建有助于比较相同输入是否得到相同输出,但还需要检查依赖来源和构建环境;软件开发与交付各阶段的安全实践可按 NIST SSDF 1.1进一步梳理。
处理泄漏、审计缺口与受影响版本
凭据泄漏先阻止继续使用
发现仓库、日志或工单里的秘密后,先识别它对应的服务、权限和消费者,撤销或限制旧资格,并向获准消费者分发新凭据。接着检查旧身份的连接、令牌、任务和已经发生的业务操作。删除 Git 当前文件只减少一个入口,已经复制出去的值仍可使用,必须在服务端失效。
例行轮换保留迁移窗口,泄漏时则根据影响缩短窗口。旧数据库连接可能要精确终止;签名私钥泄漏需要处理其签发材料的可信性;数据密钥泄漏还可能涉及历史数据重新加密。业务回滚只能切回兼容的新身份配置,不能顺手恢复被撤销的秘密。
漏洞公告要对应到实际运行版本
用包坐标、版本与 SBOM 查找受影响构件,再通过部署记录确定哪些实例正在运行对应摘要。随后检查可利用条件:漏洞代码是否打包、是否可达、输入是否受攻击者控制、功能是否启用,以及已有网络或权限控制能否降低风险。暂时没有可达路径可以影响修复优先级,但需要记录具体条件和复核期限。OWASP Vulnerable Dependency Management
修复时升级受影响依赖或基础镜像,重新执行测试,在受控环境重建,生成对应的新 SBOM、摘要和签名,再部署到目标实例。只更改 POM 而未替换运行制品,或者只扫描应用 JAR 而漏掉镜像系统包,都会留下旧风险。回滚版本也要重新检查是否含同一漏洞,不能只因“过去稳定”而重新推广。
根据第一条异常选择检查对象
| 现象 | 优先检查 | 恢复后的确认 |
|---|---|---|
| 凭据文件 permission denied | 容器实际 UID/GID、父目录权限、挂载路径和所有者 | 同一应用身份能读取,日志不输出值 |
| 文件已更新但仍使用旧角色 | 配置读取时机、单文件挂载、旧池与后台作业 | 新建连接的 current_user 为目标角色 |
| NOLOGIN 后仍有旧查询 | 既有 backend 与角色成员关系 | 确认需要结束的精确会话已退出,新身份仍可用 |
| 审计 INSERT 被拒绝 | schema USAGE、表 INSERT、实际 current_user | 正常追加成功,UPDATE/DELETE 仍被拒绝 |
| 审计数量缺口或上传积压 | 本地事务是否提交、持久待投递记录、发送失败与重复 | 缺失窗口已补查,重复按事件 ID 处理 |
| SBOM 不在预期目录 | 有效 POM、execution ID、插件输出配置 | 当前 package 生成的文件与当前依赖树一致 |
| JAR 签名链不受信 | 预期发布者、受控信任库、证书期限与撤销策略 | 正确原件验证成功,未知签名仍拒绝 |
| 已签资源摘要错误 | 下载损坏、签名后的解包/重打包、来源被替换 | 重新取得受信制品,不通过重签未知文件掩盖错误 |
| 仓库或密库不可用 | 网络、身份、租约、已验证缓存与重试预算 | 新身份获取恢复,运行实例与发布摘要重新核对 |
排查数据库身份时,在对应连接上执行 SELECT current_user, session_user, pg_backend_pid();管理员查看 pg_stat_activity 时限定数据库、角色与应用名称。不要把生产口令放进 psql 命令行,也不要为一次轮换执行无筛选的连接终止或全局容器清理。
实验异常留下容器时,先用 docker ps -a --filter label=lab=sec15-supply 确认对象,再按精确容器 ID 处理。修复挂载、依赖或权限后重跑 clean verify,三项测试应恢复成功;签名故障则重新运行受信原件、未知签名者与资源篡改三条路径,确认修复没有扩大信任范围。
权威资料与规范地址
凭据、容器与数据库
OWASP Secrets Management:https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
Docker Linux 权限:https://docs.docker.com/engine/install/linux-postinstall/
容器内运行 Testcontainers:https://java.testcontainers.org/supported_docker_environment/continuous_integration/dind_patterns/
Docker Bind mounts:https://docs.docker.com/engine/storage/bind-mounts/
Docker Compose Secrets:https://docs.docker.com/compose/how-tos/use-secrets/
Docker Build secrets:https://docs.docker.com/build/building/secrets/
pgJDBC 连接参数:https://jdbc.postgresql.org/documentation/use/
PostgreSQL ALTER ROLE:https://www.postgresql.org/docs/18/sql-alterrole.html
PostgreSQL 管理函数:https://www.postgresql.org/docs/18/functions-admin.html
信封加密:https://docs.cloud.google.com/kms/docs/envelope-encryption
OWASP Logging:https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
PostgreSQL Privileges:https://www.postgresql.org/docs/18/ddl-priv.html
依赖、签名与漏洞恢复
Maven Dependency Mechanism:https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html
Maven Repository Mirrors:https://maven.apache.org/guides/mini/guide-mirror-settings.html
CycloneDX makeAggregateBom:https://cyclonedx.github.io/cyclonedx-maven-plugin/makeAggregateBom-mojo.html
JDK jarsigner:https://docs.oracle.com/en/java/javase/25/docs/specs/man/jarsigner.html
JDK keytool:https://docs.oracle.com/en/java/javase/25/docs/specs/man/keytool.html
SLSA Build Provenance:https://slsa.dev/spec/v1.2/build-provenance
SLSA Verifying artifacts:https://slsa.dev/spec/v1.2/verifying-artifacts
NIST SSDF 1.1:https://csrc.nist.gov/pubs/sp/800/218/final
OWASP Vulnerable Dependency Management:https://cheatsheetseries.owasp.org/cheatsheets/Vulnerable_Dependency_Management_Cheat_Sheet.html
