先看结论与判断条件
- 退场清单从主体和资源两个方向盘点,人员账号、服务身份、共享账号、令牌与第三方支持入口不能只查一个系统。
- 每条访问决定必须是 revoke、transfer 或 retain-with-boundary 之一,并明确资源 owner、批准人、完成条件与回执。
- 样本、mapping、符号、日志和工单是否保留属于资料治理;权限退场只决定谁在什么条件下继续访问。
- 生产签名私钥不应成为供应商日常持有物,Android 与 Apple 的签名身份、账号和托管责任要分开回收。
- 服务账号、CI 凭据、API 令牌和 webhook 要按实际依赖顺序轮换或撤销,避免先关账号导致无法识别残余调用。
- 退场完成必须由独立复核确认登录失败、令牌失效、共享资源有新 owner,不能只凭工单状态或口头通知。
权限退场是访问关系收尾,不等于删除全部资料
项目结束常被简化为关闭供应商账号,但软件加固过程可能跨越文件传输、代码或制品仓库、处理平台、CI、对象存储、诊断日志、工单、即时沟通、签名服务和渠道后台。人员账号只是主体之一,服务身份、临时令牌、共享链接、受邀组织和支持通道也可能继续有效。退场要以主体到资源的访问关系为基本记录。
本任务只解决谁还能访问什么以及如何撤销、转移或受限保留,不决定样本和证据应该保存多久。mapping、符号、测试回执或构建证明可能仍需由客户指定角色保管,但供应商不必因此继续保持原权限。资料保留和销毁按独立政策处理,访问清单只记录保留责任人、读取边界和最终回执。
NIST SP 800-218 SSDF 把来源、构建、验证、变更与供应链风险纳入安全软件开发实践。退场可以把这些实践转成可执行问题:谁仍能读来源与产物,谁能改变构建或配置,谁持有验证证据,谁能继续触发发布。SSDF 不规定具体产品能力或统一撤权步骤,因此实际系统和组织角色仍需逐项盘点。
| 对象 | 示例 | 主要风险 | 退场动作 |
|---|---|---|---|
| 人员主体 | 个人账号和访客 | 离场后继续登录 | 禁用并验证 |
| 服务主体 | CI bot 与集成 | 无人识别残余调用 | 迁移或撤销 |
| 临时能力 | 令牌和共享链接 | 过期策略失效 | 轮换或作废 |
| 样本资源 | 输入输出候选 | 继续下载复制 | 转移 owner |
| 证据资源 | 日志 mapping 符号 | 诊断材料外泄 | 最小化读取 |
| 高权限入口 | 签名与渠道 | 发布身份失控 | 专门复核 |
先从身份提供方和应用侧同时导出访问清单
身份提供方可以列出用户、组、角色、登录时间和应用授权,但某些平台还维护本地账号、部署密钥、机器人、访问令牌和共享链接。资源系统又可能有直接 ACL,未必完全继承企业身份。因此盘点要双向进行:从所有供应商人员和服务身份查资源,也从每个加固资源查当前成员与凭据,最后对差异逐项解释。
主体记录包含公开安全的 subjectRef、类型、所属组织、业务联系人、认证方式、当前角色和离场日期,不保存密码、令牌内容或恢复码。资源记录包含 resourceRef、类别、owner、敏感级别和系统管理员。访问关系再记录 permission、来源、到期、使用目的、处理决定和回执。三层分开后,可以看出同一账号通过组、直接授权和令牌获得的多条路径。
共享账号与共同负责必须被拆开。共享账号无法可靠证明实际使用人,应在退场前迁移到具名主体或受控服务身份,并轮换认证材料。资源 owner 也必须唯一,研发团队或双方不能代替可执行负责人。尚未找到 owner 的仓库或日志空间标记 blocked,不能为赶进度直接删除或默认继续开放。
| 字段 | 来源 | 用途 | 缺失处置 |
|---|---|---|---|
| subjectRef | 身份系统 | 识别访问主体 | 停止关闭声明 |
| resourceRef | 资源目录 | 识别目标系统 | 补充资源 owner |
| permission | ACL 或角色 | 判断能力范围 | 按高权限处理 |
| grantSource | 组直接或令牌 | 发现多条路径 | 逐条撤销 |
| expiresAt | 授权记录 | 识别临时访问 | 不得假设已过期 |
| receiptRef | 执行回执 | 证明动作完成 | 保持 blocked |
人员账号与服务身份需要不同的撤权顺序
人员账号适合先冻结新增授权,再导出当前会话、组和资源成员关系,通知负责人后禁用登录、撤销会话与应用授权,最后验证无法重新认证。服务身份则可能支撑构建、加固任务、报告同步或通知,不能直接照搬人员离场流程。先识别调用方、权限和依赖,迁移到客户控制身份,再轮换凭据并观察旧身份不再产生调用。
临时令牌、部署密钥、SSH 公钥、API key、OAuth 应用授权、webhook secret 和机器证书都要作为独立访问关系。清单只记录公开标识、用途、scope、创建者、到期与撤销回执,不保存秘密值。若令牌由个人账号创建,禁用人员账号后仍可能有效,必须在资源侧确认其状态。
撤权顺序由风险与恢复能力共同决定。高权限人工登录可以优先禁用,自动化凭据需要在替代身份验证通过后切换;仍处于事件调查或紧急修复窗口的访问可以受限保留,但必须有到期、最小权限、审批和监控。临时保留不是无限延期,每次延长都产生新批准记录。
| 身份 | 先决检查 | 主要动作 | 验证 |
|---|---|---|---|
| 个人账号 | 组和直接授权 | 禁用撤销会话 | 登录失败 |
| 访客账号 | 外部组织邀请 | 移除访客关系 | 成员列表消失 |
| 服务账号 | 任务和依赖 | 迁移后禁用 | 旧调用归零 |
| API 令牌 | scope 与调用者 | 轮换撤销 | 旧令牌失败 |
| 部署密钥 | 仓库和流水线 | 替换删除 | 旧 key 无权 |
| 紧急访问 | hold 与批准 | 降权限加到期 | 到期自动关闭 |
样本、日志、mapping 与符号要转移 owner 后再撤权
加固输入、输出候选、崩溃日志、mapping、符号、测试回执和工单附件可能分散在上传区、对象存储、处理平台和支持系统。退场前为每类资源确认客户接收人、候选摘要、访问边界和状态。需要保留的材料先转移 owner 与受控副本,再撤销供应商读取;不再需要的材料进入批准的销毁流程,而不是由退场人员自行删除。
OWASP MASVS-PRIVACY-1 强调最小化敏感数据访问,并把第三方 SDK 的数据行为纳入责任边界。应用到离场时,应问供应商是否仍能读取含业务数据的样本或日志,第三方支持工具是否保留副本,以及诊断导出是否包含用户信息。该控制不替代业务数据分类、同意与合规审查,但支持按最小必要原则收紧访问。
mapping 和符号既可能支持崩溃诊断,也可能暴露代码结构,不能放在无人负责的公开共享空间。转移时记录材料摘要、适用候选、权限组、加密与审计位置;退场回执确认供应商成员和旧分享链接失效。保留期限由证据政策决定,本文不重复时长和销毁细则。
| 资源 | 接收条件 | 撤权条件 | 验证回执 |
|---|---|---|---|
| 输入样本 | 候选与来源清单 | 客户副本已核对 | 旧下载失败 |
| 输出候选 | 输入输出关系 | 发布方已接收 | 供应商权限移除 |
| 诊断日志 | 脱敏和案件索引 | 调查状态明确 | 成员列表与审计 |
| mapping | 绑定版本与摘要 | 诊断 owner 接收 | 分享链接失效 |
| 符号文件 | 候选与平台身份 | 崩溃系统接收 | 旧 token 作废 |
| 工单附件 | 缺陷和结论索引 | 后续联系人确定 | 外部访客移除 |
构建证明和测试证据保留,不等于原访问继续有效
SLSA Provenance v1.1 把产物主体、构建者、构建类型、外部参数和材料绑定。项目结束后,客户可能需要保留 provenance 来解释候选来源,但可以把证明复制到客户控制的证据库并验证签名,不必继续开放原构建系统。访问退场记录应区分证据内容的保管责任与生成系统的操作权限。
in-toto Attestation Statement v1 用 subject 摘要和有类型的 predicate 组织声明。转移声明时核对 subject 是否指向实际候选、签名者是否仍可信、验证工具是否可用,并记录新 owner。声明格式不保证事实真实,转移成功也不意味着供应商原账号可以保留;相反,旧主体权限应在接收验证完成后按清单撤销。
测试报告、加固配置说明和缺陷回执采用同样原则:保留可审计副本,关闭生成或编辑权限。只读访问也需要明确用途和到期,不能因为不能修改就无限开放。若后续支持需要供应商重新查看证据,应从新工单发起临时最小权限,而不是复活整个项目角色。
签名、渠道和发布入口必须单独做高权限复核
Sign your Android app 区分应用签名密钥、上传密钥、证书和 Play App Signing 责任。退场清单要确认供应商是否拥有 Play 组织成员、上传能力、签名服务调用权、证书下载或发布流水线权限。生产私钥原则上不应成为普通项目材料;若存在特殊托管或调用关系,必须由密钥 owner 另行轮换、撤销和复核。
Apple app code signing process 说明 Apple 平台通过代码签名、证书和运行验证建立应用身份。Apple 团队成员、证书、profile、CI 签名身份和分发账号应按平台机制独立检查,不能沿用 Android 清单字段推测。移除人员成员不代表机器证书或 CI 凭据已失效,必须在签名服务和流水线两侧验证。
高权限复核采用双人确认:资源 owner 执行或见证撤销,发布负责人验证旧主体不能签名、上传或改变渠道配置。验证使用受控的无副作用动作,例如权限查询或被拒绝的测试认证,不发布真实版本。任何无法验证的入口保持 blocked,并冻结项目完成声明。
| 入口 | 身份对象 | 撤权动作 | 验证边界 |
|---|---|---|---|
| Android 组织 | 人员与服务成员 | 移除角色 | 无法查看应用 |
| 上传能力 | 上传身份 | 撤销或轮换 | 测试认证失败 |
| 签名服务 | 调用角色 | 移除策略 | 无签名权限 |
| Apple 团队 | 成员角色 | 移除成员 | 团队资源不可见 |
| CI 签名 | 机器身份 | 轮换证书凭据 | 旧任务失败 |
| 渠道配置 | 发布管理员 | 转移 owner | 变更被拒绝 |
用顺序、回执与例外状态避免不可恢复的误关
退场顺序一般是冻结新增授权、完成访问盘点、指定资源 owner、转移必要资料、迁移自动化、撤销高风险人工权限、轮换机器凭据、关闭共享链接,最后执行独立验证。具体顺序按系统依赖调整,但每步要有前置条件。先删除唯一服务账号可能中断证据导出,先移除 owner 又可能让共享资源无人管理。
回执来自执行系统而不是关闭人自述。账号禁用保留身份平台状态,令牌撤销保留资源端结果,成员移除保留最新 ACL,owner 转移保留新主体接受,材料转移保留摘要核对。工单完成只是索引,必须链接真实系统回执。若系统不提供不可变回执,至少由第二人读取状态并签署复核。
例外使用 blocked 或 retained-with-boundary,写清资源、剩余权限、理由、批准人、补偿控制、到期与复核日期。事件调查、法务 hold 或紧急支持可能需要短期读取,但不能恢复写入、签名或发布权限。例外到期仍未关闭时自动回到阻塞,不因项目行政结束而从清单消失。
| 阶段 | 前置条件 | 动作 | 回执 |
|---|---|---|---|
| 冻结 | 项目结束决定 | 禁止新增授权 | 策略状态 |
| 盘点 | 系统清单完整 | 双向对账 | 差异报告 |
| 转移 | 新 owner 接受 | 迁移资料与服务 | 接收摘要 |
| 撤销 | 依赖已迁移 | 禁用账号令牌 | 系统状态 |
| 验证 | 动作全部提交 | 独立认证检查 | 失败回执 |
| 例外 | 阻塞理由明确 | 受限保留 | 到期与批准 |
用代码发现离场后仍有效的账号和无人负责资源
下面的 Python 示例读取公开安全的访问退场清单,检查 subjectRef、resourceRef、permission、expiresAt、disposition、status、resourceOwner、retentionOwner 和 receiptRef。它会发现离场日期之后仍 active 的访问、已 revoked 却没有回执的记录、需要 retain 却没有保留责任人的资源,以及 owner 写成双方、共同负责或待定的模糊记录。
示例不读取真实身份系统、不执行撤销,也不包含令牌、密码、私钥、客户数据或内部地址。日期采用明确格式,离场后仍有效并非自动违法,而是必须进入 retained-with-boundary 并给出批准和到期。转移状态要求新 resourceOwner,撤销状态要求 receiptRef。实际系统还应使用唯一索引防止重复关系。
校验通过只说明清单字段闭合,不能证明账号真的无法登录。每条 completed 记录仍需由资源系统状态、访问失败测试或 ACL 快照验证。若代码发现阻塞,不要删除记录或改离场日期来取得通过,应补 owner、回执或受限保留决定。
import json
import re
import sys
from datetime import date
from pathlib import Path
REQUIRED = {
'accessId',
'subjectRef',
'resourceRef',
'permission',
'expiresAt',
'disposition',
'status',
'resourceOwner',
'retentionOwner',
'receiptRef',
}
FORBIDDEN = {'token', 'password', 'privateKey', 'customerData'}
DISPOSITIONS = {'revoke', 'transfer', 'retain-with-boundary'}
STATUSES = {'planned', 'blocked', 'active', 'completed'}
AMBIGUOUS = re.compile(r'^(both|joint|tbd|双方|共同负责|待定)$', re.I)
def stop(message):
raise SystemExit(message)
def parse_day(value, field, index):
try:
return date.fromisoformat(str(value))
except ValueError:
stop(f'record {index} has invalid {field}')
def required_text(record, field, index):
value = str(record[field]).strip()
if not value:
stop(f'record {index} has empty {field}')
return value
def validate(record, index, offboarding_date):
if not isinstance(record, dict):
stop(f'record {index} is not an object')
if FORBIDDEN.intersection(record):
stop(f'record {index} contains forbidden fields')
missing = sorted(REQUIRED - record.keys())
if missing:
stop(f'record {index} lacks fields: {missing}')
disposition = record['disposition']
status = record['status']
if disposition not in DISPOSITIONS or status not in STATUSES:
stop(f'record {index} has invalid disposition or status')
owner = required_text(record, 'resourceOwner', index)
if AMBIGUOUS.fullmatch(owner):
stop(f'record {index} has ambiguous resource owner')
expires = parse_day(record['expiresAt'], 'expiresAt', index)
if status == 'active' and expires >= offboarding_date:
stop(f'record {index} remains active after offboarding')
if disposition == 'revoke' and status == 'completed':
if not str(record['receiptRef']).strip():
stop(f'record {index} is revoked without receipt')
if disposition == 'transfer' and status == 'completed':
required_text(record, 'resourceOwner', index)
if disposition == 'retain-with-boundary':
retention_owner = str(record['retentionOwner']).strip()
if not retention_owner or AMBIGUOUS.fullmatch(retention_owner):
stop(f'record {index} has no retention owner')
identity = (required_text(record, 'subjectRef', index), required_text(record, 'resourceRef', index), required_text(record, 'permission', index))
return required_text(record, 'accessId', index), identity
if len(sys.argv) != 2:
stop('usage: validate_offboarding.py offboarding.json')
source = Path(sys.argv[1])
if not source.is_file():
raise SystemExit('offboarding file is missing')
data = json.loads(source.read_text(encoding='utf-8'))
if not isinstance(data, dict):
raise SystemExit('offboarding root must be an object')
offboarding_date = parse_day(data.get('offboardingDate'), 'offboardingDate', 'root')
records = data.get('access')
if not isinstance(records, list) or not records:
raise SystemExit('access records are missing')
results = [validate(record, index, offboarding_date) for index, record in enumerate(records)]
access_ids = [item[0] for item in results]
identities = [item[1] for item in results]
if len(access_ids) != len(set(access_ids)):
raise SystemExit('accessId is not unique')
if len(identities) != len(set(identities)):
raise SystemExit('subject resource permission relation is duplicated')
print(json.dumps({'accessRecords': len(records), 'status': 'pass'}))最终复核只关闭有系统证据的访问关系
最终会议逐项打开主体、资源、权限、处理决定、owner、到期和回执。completed 表示系统动作与独立验证都已完成;blocked 表示缺少 owner、依赖未迁移或回执不可得;retained-with-boundary 表示经批准继续最小访问。unknown 不能默认视为已关闭,也不能因供应商联系人离职而从清单删除。
复核还要抽查反向路径:从关键样本仓库、日志、mapping、符号、工单、签名与渠道系统重新导出成员和令牌,确认没有清单外主体。随后验证旧身份无法登录或调用,新 owner 能接收必要资料,自动化在新身份下正常运行。任何验证失败都重新打开对应关系,不需要重跑无关系统。
准备退场时,可先整理主体、资源、权限、凭据类型、资料 owner、依赖、离场日期和例外,再参考本站的软件加固证据保留政策区分访问与保留。需要进一步评估项目收尾,可携带脱敏访问清单和回执索引通过御盾中央平台提交申请。实际撤权与签名状态必须以目标系统回执为准。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 安全项目应保留来源、构建、验证和变更证据,并处理供应链风险。 | NIST SP 800-218 SSDF 提供组织级安全软件开发实践与供应链风险方向。 | SSDF 不定义具体加固产品功能,也不规定所有组织统一的账号退场步骤。 |
| 移动端应最小化敏感数据访问,并把第三方数据行为纳入责任边界。 | OWASP MASVS-PRIVACY-1 明确要求最小化敏感数据访问并审视第三方 SDK。 | 该控制不能替代业务数据分类、同意记录、合同和合规审查。 |
| 构建证明可以把产物主体与构建者、构建类型、外部参数和材料关联。 | SLSA Provenance v1.1 定义 provenance 模型及主要字段语义。 | provenance 可转移保管但不单独证明运行时安全,也不要求原构建访问继续存在。 |
| 供应链声明应把产物摘要与有类型的声明负载绑定。 | in-toto Attestation Statement v1 定义 subject、predicateType 与 predicate 的结构。 | 声明格式不保证内容真实,仍需可信签名者、验证门禁与候选摘要复算。 |
| Android 发布链需要区分应用签名密钥、上传密钥、证书和 Play App Signing 责任。 | Sign your Android app 说明 Android 应用签名与托管签名职责。 | 官方文档不能确认某个实际账号或候选使用正确身份,仍需目标系统回执。 |
| Apple 平台通过代码签名、证书与运行验证建立 App 身份。 | Apple app code signing process 说明 Apple 平台应用签名与验证过程。 | Apple 签名链与 Android 签名机制不同,退场清单必须分别核对。 |
| 权限退场应从人员、服务身份、临时能力和资源 ACL 两个方向交叉盘点。 | 工程判断:单一身份系统无法覆盖本地账号、令牌、共享链接和资源直接授权。 | 具体系统清单取决于真实架构,文章不声称任何项目已经完成盘点。 |
| 资料保留与原访问持续有效是两个不同决定。 | 工程判断:证据可以转移到客户控制资源,同时撤销供应商账号和编辑能力。 | 保留期限、销毁、hold 和法律责任由独立政策及授权角色决定。 |
工程常见问题
停用供应商主账号后,软件加固项目权限就全部收回了吗?
不能这样判断。还要检查服务账号、组成员、直接 ACL、临时令牌、部署密钥、共享链接、工单访客、签名服务和渠道成员,并逐项验证失效。
项目结束后 mapping 和符号需要删除吗?
本文不决定保留期限。先指定客户保管 owner、绑定候选并转移受控副本,再撤销供应商访问;是否销毁由证据保留和 hold 政策决定。
服务账号为什么不能和人员账号一起立即禁用?
服务身份可能支撑构建、同步或通知。应先识别依赖并迁移到客户控制身份,再轮换凭据、观察旧调用停止,避免误关导致证据或业务中断。
只读访问是否可以在项目结束后一直保留?
不应默认保留。只读仍可复制样本和诊断信息,需要明确用途、最小范围、批准人、到期和审计;后续支持可以按新工单临时授权。
如何证明令牌和账号真的已经撤销?
使用目标系统状态、ACL 快照、令牌撤销结果和受控认证失败测试,并由独立复核人确认。工单完成或口头通知只能作为索引。
向御盾中央平台提交项目退场评估前准备什么?
准备主体、资源、权限、授权来源、临时凭据标识、资料 owner、自动化依赖、签名渠道入口、离场日期、处理决定、例外、批准人与系统回执索引。