AegisDB
EnglishGitHub
工作原理

闸门装在哪一层,决定了它挡不挡得住

这一页讲的是几个设计决定,以及每个决定背后被否掉的那个更省事的做法。

逐语句判定

判定链:4 道,逐语句

语句进入网关后依次过 4 层:菜单权限决定这个角色开不开这扇门;能力矩阵按「角色 × 能力 × 分层」给出三态结论(放行 / 转审批 / 拒绝);高危命令字典按「命令 × 分层」给出这条命令在这一层的档位;最后一层拦下不带 WHERE 的 DELETE 与 UPDATE。这 4 层不在同一个地方:第一层是挂在每条路由上的守卫,后三层在同一个判定函数里。

字典那一档最容易被读反:high 不是「禁止」,是拦下并强制走审批;mid 同样转审批;off 表示这一层不给结论,落回能力矩阵。出厂字典里只有 DROP · TRUNCATE · DELETE · ALTER · RENAME · GRANT · REVOKE,UPDATE / INSERT / CREATE 不在里面,只过能力矩阵那一层;字典是运维可编辑的,出厂那几条是默认值不是上限。

4 层规则都按分层存,内置 5 个分层:生产环境 · 法务环境 · 预发布环境 · 演练环境 · 开发环境。无 WHERE 拦截原来是一个全局开关,想让开发同学能跑 DELETE 练手,就得把生产的那道也一起卸了。ADR 0013 之后它改为按分层:同一条 DROP 在 PROD 拦下转审批、在 DEV 直接放行,是同一张表里的两行,不是两套配置。

两处刻意的短路要说在明处:会话级设置(SET search_path …、Oracle 的 ALTER SESSION SET …)只查「查询数据」这一维,跳过字典与无 WHERE;只看计划的 EXPLAIN 同样跳过后两层。而 EXPLAIN ANALYZE 与 DWS/GaussDB 的 EXPLAIN PERFORMANCE 会真的执行被包住的语句,它们按真实动词走完整判定。

能力矩阵

角色:二线运维(L2)(固定展示)

PRODGLISTAGINGUATDEV
查询数据放行放行放行放行放行
写入数据转审批转审批转审批转审批放行
结构变更转审批转审批转审批转审批放行
授权与账号拦截拦截拦截拦截拦截
连接实例拦截拦截拦截拦截拦截
审批工单拦截拦截拦截拦截拦截
执行计划放行放行放行放行放行
发起发布单转审批转审批转审批转审批放行

控制台界面复刻 · 非截图

8 个维度控制台上都有,参与逐语句判定的只有 6 个 —— 连接实例、审批工单在判定层没有读者,改了它们不改变任何一条语句的裁决。

授权与执行

审批通过,网关不替你执行

终端工单通过后,AegisDB 只做一件事:通知发起人。审批本身不发任何 SQL,审计那一行停在「已批准、尚未执行」。回来按下「执行」的不必是发起人 —— 任何够得到那台实例的人都可以,而每一道闸都按实际按按钮的那个人重算:标签授权、实例维护态、能力矩阵复判,一律按 actor 算而不是按发起人算。

「通过不代执行」只对终端工单成立,另外三类单各不相同。发布单:执行归流水线的执行阶段,插队执行会把同一次变更应用两次。执行窗口单:批准即生效,那扇门从此到点自己开。导出申请单:批准即入队,由导出 worker 跑。后两类根本没有可执行的命令 —— 它们的 Command 是一句中文描述,放它走执行路径,网关会把那句中文当 SQL 发给数据库。

副作用产品自己认下了:审批人可以执行自己批过的单 —— 这条两人控制被 ADR 0010 末段刻意放弃了。下发前会再判一次,只在结论变成「拒绝」时拦下。但复判挡得住的只是「规则变严了」,挡不住「当时的判断依据已经不成立」—— 表结构变了、数据量涨了一个量级,复判都看不出来。

审批单
AP-20260919-0042已通过
发起人
linwei@vela.io
目标实例 / 库
PRODprod-mysql-01 · orders

SQL

ALTER TABLE orders ADD COLUMN note varchar(64)

命中规则

dictDenyPROD 禁止直接执行,需转审批

审批链

  • zhaoyun@vela.io
  • wangfang@vela.io

执行前会再判一次规则 —— 这张单可能在待办里停了几个小时。

示意按钮,不可点击 —— 通过 ≠ 代为执行(见工作原理页「第二层」)。

控制台界面复刻 · 非截图

按时间放宽

执行窗口:把口头约定变成到点自动关上的东西

维护车次里一晚上几十条 DDL,逐条把审批人叫醒不现实。现实里的解法通常是「今晚先把规则改松,明早改回来」—— 而这比没有审批更糟:改回来这件事没有系统记着,没人知道门还开着。

执行窗口把那个约定搬进系统:任何人可以申请,提交后生成审批单,通过后才生效。按库开,库名必填,前端刻意不预选默认库 —— 空库名会让一扇门悄悄覆盖整台实例。时间模型两种:周期班车按 IANA 时区给出每班起止,或一次性窗口用绝对起止时刻。

「到点自动关上」是准确的说法,「过期后申请自动失效」不是:没有任何清扫窗口的定时任务,过期只是「时间表不再覆盖当下时刻」这个算给界面看的判断 —— 窗口行不消失、审批不作废,判定恢复原样而已。

执行窗口 · 班车

夜间维护班车

PRODprod-mysql-01 · orders

待审批
周期班车
02:00–04:00 Asia/Shanghai
发车日
周一至周五

订单库紧急发布

PRODprod-pg-02 · billing

进行中
一次性窗口
2026-09-20 01:00 – 2026-09-20 05:00
倒计时
还剩 1 小时 23 分

周末例行维护

STAGINGstaging-mysql-03 · reports

已到期
周期班车
02:00–04:00 Asia/Shanghai
发车日
每天

控制台界面复刻 · 非截图

结果出口

脱敏发生在结果离开网关之前

回给浏览器的那个 JSON 里,身份证号已经不在了。整个网关只有两处把用户 SQL 的结果行读出来:终端结果与数据导出。脱敏就挂在这两处,新增的调用路径天然被覆盖。端到端测试直接断言原始响应体里明文一个字节都不能出现,而不是断言界面上显示成了星号。

规则按「表名 + 字段名」配置(表名留空 = 所有表),方式有留头留尾、全部遮蔽、同值同码三种,第三种用于对账:能核对是不是同一个人,看不出是谁。终端与导出算的是同一份打码计划,两边各算一遍会让导出文件和界面上看到的不一样。

脱敏挂在结果离开网关的那一层目标库把带原值的结果行交给网关。整个网关进程里只有两处把用户 SQL 的结果行读出来:终端、异步执行与发布代执行走 RealRun,数据导出的流式路径走 RealQueryEach。两处共用同一份掩码计划,打完码结果才越过服务端与客户端的分界,所以浏览器和导出文件里都已经没有原值。换成让前端打码,原值会先整份越过这条分界。网关进程进来的结果行带着原值RealRun终端 · 异步 · 代执行RealQueryEach数据导出(流式)同一份掩码计划 maskPlan两边各算一遍,导出与界面就会不一样结果离开服务端浏览器 / 导出文件这里已经没有原值被否掉的:让前端打码那样原值会先整份越过这条分界
留痕

审计链:写下之后改不动

每条审计的 hash = SHA256(前一条 hash + 本条内容),创世行的前驱是空串。prev_hash 上建着单列唯一索引 —— 两条记录不可能挂在同一个前驱上,链分叉由数据库层面拒绝,不靠应用侧自觉。改动中间任何一条,它之后的每一条都对不上。

校验是控制台上一个人工触发的动作,从创世行一路重算到链尾,报出第一条对不上的行号。它不拦截任何写入、不阻止服务、不自动告警 —— 它是一次显式的整表扫描,产出的是一份带结论的报告。报告自己写清了覆盖不到什么:从链尾整段截断查不出来,要查出这种得把链尾定期锚到网关之外。

审计链怎么连起来,以及校验覆盖不到什么每条审计记录的哈希是前一条哈希与本条内容拼起来做 SHA256,创世行的前驱是空串。prev_hash 上建着单列唯一索引,两条记录挂不到同一个前驱,链分叉由数据库层面拒绝。改动中间任何一条,它之后的每一条都对不上,从链中间抽掉一行也接不上 —— 这两种校验都查得出。查不出的是从链尾整段截断:那要把链尾定期锚到网关之外。prev_hash 上有单列唯一索引:两条记录挂不到同一个前驱,链分叉由数据库拒绝prev_hash创世行第 2 行第 3 行第 4 行链尾被整段截断prev_hash 是空串hash = SHA256(prev_hash ‖ payload)改过中间一行、或把中间一行抽掉 —— 它之后每一条都对不上从链尾整段截断 —— 校验看不出来
变更发布

变更发布流水线:为什么执行阶段一律先停在等待确认

在网关出现之前,一次数据库变更靠人在终端里按顺序做对若干件事:看一眼 SQL 规不规范,发起审批,批完回终端执行,再手工查一下结果。每一步都有,但顺序、是否做过、做的结果只存在于做事人的记忆里。发布流水线把这条路写下来:一条流程就是一个有序的阶段列表。(ADR 0005)

阶段类型是一个封闭集合,七种:规范审查、人工审批、备份 / 回滚点、执行变更、执行后校验(必须只读)、人工确认(发起人不能自己放行)、结果通知。封闭是因为每一种都需要一个执行器:未知类型要么被跳过,那就是一次没人跑的审查,要么让调度器崩溃。

到了执行阶段,发布单无条件先停在等待确认:代码里这道闸没有任何条件分支。不是「多一道确认更安全」—— 那句话是废话。它有四条互相独立、层层递进的理由。

一、审批回答「可不可以做」,执行确认回答「现在做」—— 两个问题,答案在两个不同的人手里。业务低峰、应用停没停、备份就不就绪,这些只有发起人和当班的人知道。

二、没有人在看着它。一条 DROP 在半夜被批准并自动落库,发起人不在现场;出了事,发现得比谁都晚 —— 而 DDL 恰恰是最需要有人盯着回滚窗口的那一类。

三、这道闸就是「按当时规则复判」的那个「当时」。发布单可能在审批里停留数小时,这期间高危字典可以改、实例可以被挪进更严的分层、发起人的角色可以变。自动落库的话,「那一刻」是一个没有人选择的时刻;有了确认闸,它就是一个有人负责的时刻 —— 确认之后代码立刻做的第一件事就是复判。

四、发布流水线是这个网关里最有吸引力的一条执行旁路,而这个仓库每一条新执行通道都栽在同一个地方。ER6 就是其中一次:异步通道曾经让在终端里被拦下的人,把同一条语句改成后台任务提交,然后它就跑了。而流水线是异步的,它以服务身份运行,而它的全部目的就是把变更打到生产上。

复判按谁算,这里必须说准,而且它与终端工单正好相反。终端工单那条路,每一道闸都按实际按按钮的人算。发布单不是:确认执行只检查确认人的角色,而且不重新检查他够不够得到这台实例;确认之后那次复判,用的是发起人的角色。两条路各自讲得通,但没有一句话能同时概括它们。

最后一句最重要:这条流水线没有自动回滚,而且它不假装有。备份阶段只执行运维显式配置的备份语句,没配就跳过并如实说明 —— 报一个绿色的「备份完成」而底下什么都没发生,是整条流水线里最危险的一行。多条语句之间没有事务,失败时前面成功的那几条不会被撤销。

发布流水线的阶段与状态一条流程是一个有序的阶段列表,阶段类型是一个封闭集合的七种:规范审查、人工审批、备份或回滚点、执行变更、执行后校验、人工确认、结果通知。其中人工审批、执行变更、人工确认三种会把发布单停在等待状态。等待与运行中是两件事:等待表示流水线活着但卡在一个人身上,运行中表示它正在干活。到达执行阶段一律先停在等待确认,代码里这道闸没有任何条件分支。运行中正在干活等待卡在一个人身上规范审查自动人工审批等人备份 /回滚点自动执行变更等人执行后校验自动人工确认等人结果通知自动到达执行阶段一律先停在等待确认 —— 代码里这道闸没有任何条件分支。不是「多一道确认更安全」:审批回答可不可以做,执行确认回答现在做;而确认之后代码立刻做的第一件事,就是按当时的规则复判一次。

失败朝严 —— 以及两条刻意不朝严的例外

判定层的失败一律朝严:读不到规则表、时区解析不出来、库名为空、定义写不通,一律不生效或当场拒绝;任何一层读不出来,结论是拒绝加最高风险,终端上打出来的那句话是「风险控制暂时不可用 · 已按最严处理」。认不出的 SQL 动词按「写入数据」算,打码列匹配不确定时宁可多打一列。这意味着某些故障会表现为「什么都跑不了」,但不会表现为「什么都跑得了」。诚实起见,两条刻意不朝严的例外也该说:判断不出发起人账号时不把它算成自审(不过度拦截),以及角色存在性查询失败时保留原有的那组角色 id —— 一次数据库抖动不该让所有人瞬间失去全部角色,而保留的是真实存在过的 id,不是把未知当成放行。