判定层读不出来就拒绝
能力矩阵与高危命令字典都在数据库里,一次瞬时故障会同时让它们失效:没配规则的格子读作放行、空字典读作没开,于是生产上的 DROP TABLE 会一路畅通。所以任何一层读不出来,结论是拒绝加最高风险,终端上打出来的那句话是「风险控制暂时不可用 · 已按最严处理」。
这一页是写给要逐条核我们的人的:算法、参数、失败行为一条条列出来,包括每道闸够不到什么,以及还欠着的几笔账。
先动手试一次:按「校验链」,逐行打勾;按「模拟篡改第 3 行」,第 3 行之后全部变红。变红的不是被改的那一行,是它之后的每一行 —— 这就是 prev_hash 那一列在做的事。
—hash60b8bc3560b8bc35hash049bfebb049bfebbhash013e12a9013e12a9hash6462004264620042hashe436ab86控制台界面复刻 · 非截图
每条审计记录携带前一条的哈希:hash = SHA256(prev_hash 后面接上 payload)。payload 是这一行若干字段的 JSON,键按字典序序列化,时间统一归到 UTC。创世行的 prev_hash 是空串。写入侧与校验侧共用同一段 payload 构造函数 —— 两边各写一份,迟早算出两个不一样的前像,而那时候校验报出来的「被改过」是假的。
链不分叉由数据库保证,不靠应用自觉:prev_hash 上建的是单列唯一索引。每一行同时钉住环境与控制分层两个快照,而且两者都进哈希 —— 一个改了不会破链的快照,证明不了任何事。校验是控制台上一个人工触发的动作,从创世行一路重算到链尾,报出第一条对不上的行;它不拦截任何写入、不发告警,产出的是一份带结论的报告。
可核验的参数
脱敏发生在结果离开网关之前:回给浏览器的那个 JSON 里,身份证号已经不在了。整个网关只有 2 处把用户 SQL 的结果行读出来:RealRun(终端与发布代执行)与 RealQueryEach(导出流式)。唯一的旁路叫 RealQueryEachRaw,名字刺眼是故意的 —— 换成一个默认为 false 的参数,「这一次到底脱没脱敏」就藏进一个布尔值里了。
匹配分两步:先按回传的列名匹配,SELECT * 落在这一步,也是最安全的形态;再按选择列表第 k 项的表达式匹配,兜住 AS 别名与函数包装。每个 UNION 分支都看 —— 结果集的列名只来自第一个分支。失败行为在这一节尤其要紧:读库失败时沿用上一次的规则而不是返回空,空意味着什么都不脱敏,一次数据库抖动就会让敏感数据明文回传。
可核验的参数
每个账户上有一个单调递增的会话代次,签发 JWT 时写进 tv 声明,中间件每个请求拿它跟库里的当前值比 —— 不一致当场 401「会话已失效」。登出、管理员重置口令、把用户移出角色这三处会让它加一,已签发的令牌在那一刻全部作废。改角色(不是移出角色)刻意不加:权限每个请求实时查库,降权不靠踢下线生效。
登录频控锁的是来源 IP,不是账号。代码注释把这件事说成有意为之:攻击者只能锁掉自己那个地址,永远锁不掉受害者的账号。只有跨过阈值的那一次会武装锁定,后续失败不会无限延长 —— 否则一个攻击者就能把一段共享 NAT 出口永久锁死。TOTP 步进按 RFC 6238 做,用过的时间步会被消费;要不要步进由目标分层自己的开关决定,不是靠「prod」这个名字判的,而分层解析不出来一律当作要步进。
可核验的参数
目标库口令以 AES-256-GCM 加密落库。密钥由一个口令短语经 SHA-256 派生成 32 字节;nonce 每次加密现取 12 字节随机数,与密文和认证标签一起存,整体 base64 之后加 enc:v1: 前缀。一份数据库备份泄露,拿不到任何一把可以直接使用的钥匙。
两把密钥分开:签名 JWT 用 VELA_JWT_SECRET,静态加密用 VELA_SECRET_KEY。这里最容易讲反:分开设置之后可以独立轮换的是 JWT 密钥,而不是静态加密密钥 —— VELA_SECRET_KEY 一经设定不可更改,换了它存量口令就解不开。轮换 JWT 密钥的代价是所有在线令牌当场解析失败,等于全员下线:代码里没有密钥版本号,也没有双密钥并行解密。
可核验的参数
网关上有三扇门,各管各的。控制台走 /api/v1 下除另外两面之外的全部路由,认 Authorization 头里的 JWT,调它的是登录的人;开放接口走 /api/v1/open,认 key 加点加 secret 的令牌,调它的是 CI 与 DevOps 平台;回调只有一条路径,认共享密钥,调它的是外部审批服务。
开放接口不是第二条流水线,是同一条流水线的另一扇门:/api/v1/open 下的每个写路径最终都落到与控制台同一个提交函数,同样的流程模板、规则库、审批链、执行前复判与审计链。审计行因此有两个名字:actor 是服务账号,operator 记成 API 加上凭据名。(ADR 0006)
可核验的参数
网关主动发起的每一次出站(事件中心 Webhook、飞书机器人卡片、外部审批 API)都走同一个受控 HTTP 客户端。目标地址检查两次 —— 保存配置时做一次 DNS 解析预检,真正拨号时再检查实际要连的那个 IP,把「名字在这两次之间重新解析到内网」那个时间窗关掉。解析不出来算拒绝,不是放行。
出站带签名。发给事件中心的请求带 X-Vela-Signature 头,值是 sha256= 加上用密钥对请求体算出的 HMAC-SHA256 十六进制;飞书卡片带飞书官方那套签名。这两处都是网关发出去的签名。接收方要知道的一条:密钥为空时网关不盖章,那时请求上根本没有签名头 —— 按「一定带签名」写校验的接收方,会在对方没配密钥的那一刻收到一个自己判不了的请求。
入站不是签名。审批回调没有签名校验,也没有时间戳防重放:它校验的是一个共享密钥,用常量时间比较,外加一份可选的来源 IP 白名单 —— 而那份白名单的默认值是空串,空串等于不限来源。密钥是主控制,IP 白名单是可选的第二道。总开关关着一律拒,密钥没配一律拒。
可核验的参数
下面每一条都是代码里真实存在的分支,不是一句原则声明。代码里给这条原则编了号 —— ED3:一条读不出来的规则不等于没有规则。
能力矩阵与高危命令字典都在数据库里,一次瞬时故障会同时让它们失效:没配规则的格子读作放行、空字典读作没开,于是生产上的 DROP TABLE 会一路畅通。所以任何一层读不出来,结论是拒绝加最高风险,终端上打出来的那句话是「风险控制暂时不可用 · 已按最严处理」。
分层没有一个说得通的零值:拿空分层去扫脚本,字典一行都匹配不到,每条语句包括 DROP TABLE 都会报安全。终端、批量、审批执行、脚本扫描四条路都把它当拒绝处理,MFA 也一样。
认不出的 SQL 动词归入写入维度;认不出的 MongoDB 方法按写处理;引擎认不出就两种读法都试,任一读出「没有 WHERE」就按没有 WHERE 判;打码列匹配不确定时宁可多打一列 —— 漏打一列是敏感数据直接回传。
一条语句多次命中字典取最严那条,ALTER TABLE t DROP PARTITION p 不会因为 ALTER 排在前面就按中风险算;批量粘贴由最严的那一条统辖整批;EXPLAIN 同时受「执行计划」与「查询数据」两维约束,取更严的那一档,拆出一个权限不能发出原权限拒绝过的东西;多角色取并集时,读不懂的那一档是拒绝。
标签授权查询失败时绝不报「不受限」:空结果的含义恰恰是「没有限制」,把错误吞掉,等于在一次数据库抖动里把整个资产交给一个受限角色。菜单表读不出来是 403。
外部审批总开关关着拒、回调密钥没配拒、密钥不匹配拒、来源 IP 不在白名单拒;没拿到厂商任务号就不认外部回调;说通过却没有审批人拒。开放接口凭据在每一项上同样朝严。
执行窗口是这套系统里唯一往松走的东西,而它自己的失败一律不放宽:说不清是哪个库就不放行;窗口表读不到当没有窗口;时区认不出窗口不生效,不拿服务器时区顶上。
脱敏规则读不出来沿用上一次;发布能力矩阵读不出来当场报错;审批链为空当场失败并说清楚,不造一张永远没人能批的死单;运维写的正则编译失败时把自己报成一条 finding 而不是被跳过,因为一条永远不触发的规则和一条永远通过的规则分不出来;从库延迟上限没写等于 30 秒而不是不限,要关掉必须写一个负数,因为 0 同时也是「这一段根本不存在」。
一份只列优点的清单说服不了任何人。下面这两条是代码里刻意不朝严的地方,理由都写在注释里。
禁自审那道网比对的是审批人与发起人。比不出来的时候代码选了放过,注释写的是不要过度拦截 —— 一个匹配不上的账号名不该把一张合法的审批挡在外面。它与上面那条「说通过却没有审批人就拒」是配套的:完全没有审批人是拒,认不出发起人才是放。
注释写得很直白:一次数据库抖动不该让所有人瞬间失去全部角色。方向其实仍然是安全的 —— 保留的是真实存在过的 id,不是把未知当成放行。同一类还有一处:读不懂的设置开关保持它的默认值,而安全相关的那几个默认本身都选在严的一侧。
分寸写在一行测试注释里:多拦在判定层是安全余量,多拦在报告层是假警报。所以上面这些朝严的地方全都在判定层,而扫描报告那一侧刻意不跟着收紧。
下面这些是现在还欠着的。写在这里不是为了显得坦诚 —— 是因为安全团队一定会问到它们,而等被问出来比自己先写出来难看得多。
原话是:整个后端没有下发 Content-Security-Policy,也就是说 XSS 这一层目前没有任何纵深防御。(ADR 0017)
键名是 aegis_token。ADR 0017 把它记成一笔记在账上的取舍,并写明了三条偿还触发条件:页面开始加载任何第三方脚本、出现任何一份 XSS 报告、或者要接外部用户。
ADR 0012 的表里列着:发布流水线的备份与校验两步不过判定引擎;MFA 宽限按用户代次而不是按单个令牌;自助换绑 MFA 没有重认证、也不写审计;子查询别名能绕过脱敏;另有 18 条低危未动。
网关的后台协程里没有链校验这一项。所以「链是完好的」这句话的有效期,是上一次有人去按那个按钮的时刻。
不告警、不阻断、也不发 Webhook 事件。校验接口回的是一份带结论的报告,看不看、看没看见,取决于有没有人去看。
可观测面只有无鉴权的 /healthz、一个统计接口(最近 512 次请求的 p50 / p95,以及生产拦截计数)、gin 的访问日志和 slog 的 JSON 输出。