现象:共享桶部署下网关启动即报 gateway_auditor_key_required,残留过期 审计文件则每次 Execute 报 gateway_exclusive_writer_evidence_unverified; 用户已改为“停目标写入容器→更新→启动”的维护式更新。 根因:Gateway.Ready 强制 Isolation.Verify,main 强制 Ed25519 公钥,等于 把整桶独占证明当作发布前置;这与共享桶、沿用原 AK 的实际部署矛盾。 取舍:删除审计校验与相关 env 依赖,保留 journal、治理 PG 只读 authority、 plan/owner/attempt/epoch 绑定、intent-before-send、单次写与精确版本回滚。 create 仅在精确版本读回且仍为 current 时提交;发现外部 latest 只返回冲突, 不在 confirm 阶段删除(journal create intent 已提交,删除会变成无 delete intent/receipt 的未记账副作用并让 intent 永久 held)。同 key 不自动合并。 证据:真实二进制无审计/残留审计两环境通过;kg-pgtest-writeset 一次性库 + 随机 MinIO 桶跑 14 个网关用例(含共享桶共存写者、同 key 既有对象拒绝、 精确回滚)全过;umask 022 下 make verify 本地必需项全 success。
14 KiB
对象写入网关(开发候选,尚不可部署业务)
协议与持久化
cmd/object-gateway 提供 kg-object-gateway-v1,只监听 loopback;外部接入需要另行审核的 TLS 终结与网络隔离。治理 PG 保存不可变计划、grant、intent、逐版本 receipt 和 attempt→grant delivery。部署迁移身份显式应用 internal/objectgateway/schema.sql;启动不自动迁移。运行身份只授必要 SELECT/INSERT 和 scope 行 UPDATE,不授 DDL/TRUNCATE/证据修改或业务库写权限。
scope 是 provider、原 endpoint、bucket 的 NUL 分隔 SHA256。同一物理桶在本网关内只使用一个 scope 和一套共享 journal,不能用 endpoint 别名或另一日志库建立平行写域。这不要求整桶只有本网关一个写者:别的服务可以在同一桶的其它 key 上正常读写。
数据认证端点:POST /v1/acquire、/v1/resolve、/v1/execute、/v1/reconcile、/v1/observe,以及 GET /v1/capabilities。所有请求核验协议和 scope。capabilities.automatic_grants 必须为真,正式 stack 才接受自动授权能力;未接控制源、只读角色权限异常、协议不符或不可达均拒绝,无直写回退。
PG 行锁负责原子准入,不是远端存储 fence。intent 必须持久提交后才发送一次请求;连接死亡、租约过期不清除未知 intent。任何未终态 intent 均阻止 scope 换 epoch。重复 operation 只读回执或精确对账,不重发 PUT;暂时查不到版本仍 held。删除必须绑定已确认 create 的精确 VersionID,不删 latest、不生成删除标记。
同 key 竞争按 key 判定,不按桶判定:create 前已存在任何版本 → 回执 rejected 且不修改该对象;PUT 后除精确版本读回外还核对当前 latest,若并发写者已产生更新的当前版本,则只按精确 VersionID 撤回本次写入,保留对方版本并返回冲突;读不到当前版本时保持 held,不删除、不猜测。版本化存储的 PUT 不是原子 create-if-absent,因此“本 key 多数情况下归本计划”不是存储层保证。
不可变证据表拒绝 UPDATE/DELETE/TRUNCATE。Ready 核 fsync、synchronous_commit、full_page_writes、主库可写及必要表/触发器;管理员与迁移身份仍是可信边界,触发器不能抵御 DBA 的 DDL。
正式控制交付路径
生产代码路径已接入,并已在专属本机隔离栈运行正式 worker 的两次 SIGKILL 接管;这不是生产部署验收:
cmd/worker.runOwnedLibraryAttempt → attempt context → librarystack → GatewayObjectStore(Automatic) → ProductionLibraryTarget.Apply → EnsureObjectGrant → /v1/acquire → GatewayGovernanceAuthority → 原持久 WriteSet → journal delivery → 数据 execute。
冻结/采用计划时只保存自校验后的 WriteSet,不提前索取 grant;observer 先把摘要锚定到原预备份事件,首笔业务 PG Apply 前才请求授权。worker 只提交 job/operation/attempt epoch/owner/plan digest,不提交可替换的 key/body allowlist,也不能选择网关 epoch。
可信控制域使用独立 SELECT-only 治理连接,核对目标身份、job/operation、当前 attempt、owner、有效租约、running 状态和原预备份事件中的摘要、job、backup、目标快照;再从归档加载并验 hash、范围、provider/bucket。客户端自述不能替代这些持久事实。控制读角色不得是 superuser、CREATEDB、CREATEROLE,不能写/截断/建触发器到这些治理表。每次加载和能力查询重验角色。
网关在 scope 锁内再次核对授权事实,原子写入 grant、当前 epoch、不可变 delivery。同一 attempt 重试返回原 grant;owner/operation/plan 替换拒绝;同一 job 的后续 attempt 必须沿用原 operation 和 plan。旧发送未终态时,新 attempt 可先只读对账,但无充分证据仍 held,不能交接。
控制服务配置了治理 authority 后,旧 /v1/plans、/v1/advance 手动控制端点关闭,控制 token 也不能绕过原计划绑定。旧 cmd/object-gateway-control 仅保留离线/协议兼容路径,不是正式 worker 的交付方案。
独立恢复请求
library.recover 的队列ID由原窗口job确定;它具有独立恢复operation与request digest,绑定scope、queue、原window/operation、target、source、backup和原WriteSet。原计划的job、operation和摘要不改写。Claim中的recovery_job_id与recovery_operation_id仅为独立授权上下文,原job_id/operation_id仍是发送域。网关重新读取持久队列payload、running状态、当前owner/attempt/有效租约与恢复operation,不信任worker自行声明。可选显式来源/目标/backup/plan字段只能收紧原绑定,不能覆盖它。
SELECT-only治理角色需要读取library_jobs、library_operations、library_attempts、library_versions、jobs,均不能INSERT/UPDATE/DELETE/TRUNCATE/TRIGGER。已有原窗口恢复请求时不自动重启原发布;过期running恢复队列只在相同target、未释放维护窗口、原业务租约也过期时重新投递,业务attempt/epoch仍重新申请。
索引unknown恢复边界
原冻结before-image非空时,恢复先持久化restore-mark-intent,再以原CAS(或确认缺失的create)写入包含原计划/index/id归属标记的完整before-image。丢响应后仅匹配完整标记正文及有效CAS才能登记restore-claimed并CAS去标记;已知回执的代次不一致拒绝。恢复完成的restored回执仍须匹配正文和CAS,不能接受同正文的新代次。
去标记前持久化restore-finalize-intent。发布clean-finalization或恢复clean-finalization已提交但响应丢失时,当前干净正文并非充分归属证据;继续held,不盲删、不重发、不把相同外观当回执。旧unknown缺少写前标记intent亦不自动升级认领。本候选没有服务端可验证的clean提交关联凭证,因此这部分自动收敛仍是明确缺口,不是可部署完整恢复保证。
跨 epoch 的不可变发送身份
Request.Grant 继续代表首次发送的不可变身份,后端 claim、intent 和 receipt 都不改。新 ExecuteRequest.authorization 仅代表本次准入权限。/v1/resolve 根据相同 operation/key/action/hash/length/owner/plan 取回旧请求;只允许 epoch 差异,不能换内容。
新 attempt 获得新 grant 后,已发送 key 沿用旧 request/claim,未发送 key 使用新授权 epoch 建立首次 intent。当前授权必须匹配 journal 的当前 epoch;旧 epoch 的新写拒绝。新 epoch 补偿删除仍使用原 create 回执和精确版本,删除本身有独立不可变发送身份。
配置与凭据归属
网关进程环境:
OBJECT_GATEWAY_LISTEN:仅127.0.0.1:<port>或[::1]:<port>。OBJECT_GATEWAY_DATABASE_URL:独立 journal 连接,迁移与运行角色分离。OBJECT_GATEWAY_GOVERNANCE_DATABASE_URL:仅可信网关持有的 SELECT-only 治理读连接;独立于 journal 运行身份和普通 worker 身份。OBJECT_GATEWAY_ARCHIVE_ROOT:原持久计划所在归档绝对路径,部署只读挂载。OBJECT_GATEWAY_TARGET_ID:与正式 workerLibraryTargetIdentity()完全相同的逻辑目标身份;不能用另一个环境的值。OBJECT_GATEWAY_PROVIDER、OBJECT_GATEWAY_ENDPOINT、OBJECT_GATEWAY_BUCKET、OBJECT_GATEWAY_REGION:精确存储身份。OBJECT_GATEWAY_ACCESS_KEY、OBJECT_GATEWAY_SECRET_KEY:网关持有的对象凭据,需在目标桶 PUT/GET/按版本 DELETE/列举版本。沿用现有 AK 即可,不要求另建独立只读或独占写凭据。OBJECT_GATEWAY_DATA_TOKEN、OBJECT_GATEWAY_CONTROL_TOKEN:不同且至少 32 字节;普通 worker 仅持有 data token。OBJECT_GATEWAY_ID、OBJECT_GATEWAY_AUDIT_FILE、OBJECT_GATEWAY_AUDIT_PUBLIC_KEY:已退役。旧部署残留的值被忽略,不参与启动、Ready、每次 Execute 或能力判断;不需要管理员签发整桶独占证明,也不会因过期文件拒绝发布。
秘密只经服务环境/私有文件注入,不进 argv、URL 或日志。所有调用端错误都使用固定原因码。
正式 worker 使用 LIBRARY_OBJECT_GATEWAY_URL 和 LIBRARY_OBJECT_GATEWAY_TOKEN;不再依赖预先生成的 grant 文件。LIBRARY_OBJECT_GATEWAY_GRANT_DIRECTORY 保留旧离线兼容用途,不是自动模式的授权来源。发布 stack 对所有对象 provider 强制网关;S3 构造仍用于只读捕获/预备份,可继续使用现有凭据,不再要求独立只读 OSS AK。
已关闭的本仓旁路
ObjectImporter.Stage/Promote无条件拒绝,不留配置开关,不执行 PUT/COPY。- legacy
release-workerruntime/route 在读凭据及连接目标前拒绝。 release-diag、release-channel-e2e旧写型入口启动即非零退出,避免 probe PUT 或文档级旁路。librarystack不再为缺网关的 MinIO/S3 返回直写 writer。libraryctlcapture 的 S3 构造与读取保留;未把读取当作写入口禁用。
这不等于已经撤销运行环境旧进程的凭据;源码变更尚未部署。librarydrill 等无 owned-attempt 的旧写型工具不能靠静态 grant 绕过自动模式,需补正式执行身份后才可用于写入。低层 S3 adapter 仍用于网关后端、读取及显式隔离夹具;共享桶不要求撤除其它写者,跨写者的同 key 竞争按冲突/held 处理。
实测与未完成边界
已实跑:真实治理 PG(独立 SELECT-only 登录角色)、原 archive、数据 HTTP、业务 PG 与 MinIO 的自动授权/跨 epoch 链;未锚定计划在第一笔 PG 前拒绝,接管保持原发送身份且不增加 PUT。另有真实网关二进制的两次中断验证,以及共享桶下“无整桶证明启动 + 其它 key 并发写入 + 同 key 既有对象拒绝”的真实 PG/MinIO 用例。历史隔离审计 authority/故障注入是当时的测试设施,不是当前部署前置。
正式 worker 隔离进程证据:真实 Docker 入口闸门/队列、只读 worker 对象身份、独立治理读取角色、网关与业务 PG/ES/MinIO;首次 PG 后和对象提交后两次 SIGKILL,同一原 job/operation/plan 的三代 attempt 完成发布,原对象发送 epoch 与回执不变。隔离构建标签 kg_isolated_drill 仅把真实租约缩短至4秒,并拒绝非 loopback 一次性 r8 库;正常构建仍为10分钟。维护上游是无业务凭据的独立 HTTP 夹具,不是运行中的 WeKnora;签名证据绑定本次真实拒写/排空材料,不代表生产独占审核。
已补独立 library.recover 正式worker链、恢复中再次SIGKILL、完整before-image/CAS/精确版本读回,以及来源/目标/原job/计划/operation/任意队列身份拒绝。ES故障代理只转发真实ES并丢弃已提交响应,未伪造服务响应。
尚未完成:clean-finalization unknown的充分归属与自动收敛;网关真实 OSS 端到端与真实部署验收(父执行);原D0/F11/F12与55项其余缺口。测试/隔离验收与真实部署分列。
整桶独占审核、全部 writer 排空、独立只读对象凭据已不再是发布前置;维护窗口内停目标写入容器并读回确认即可更新。网关按 key 拒绝冲突、精确版本回滚,但不承诺无关写者同时修改同一 key 时仍能自动完成——那种情况会明确冲突或 held,需要人工按回执处理。未改生产权限或上游 WeKnora 源码/Schema。
维护式更新(已采用)
目标测试 KB 的更新走维护窗口,不再要求证明整桶只有一个写者:
- 停目标测试 WeKnora 的写入应用容器(只停该服务,不停机器人、适配器、共享 PG/ES 或其它 KB 的服务),并由容器状态/健康读回确认已停止。这是本轮文档化的运维步骤;代码侧没有新增停容器实现。
- 用既有维护/预备份链记录本次目标 KB/对象范围:实例级维护窗口与写者停写证据由
internal/libraryfreeze+internal/librarymaintenance采集,libraryctl capture(页面入口POST /api/v1/library/captures)完成闸门排空→停写者→导出快照并保留post_stop_digest;不把通用主机备份当作 KB 预备份的替代。 - 用原 plan、原 WriteSet 摘要和限定对象清单走既有
ProductionLibraryTarget.Apply(经正式 worker owned attempt 与网关 grant)更新资源;网关沿用现有 AK,不换凭据、不签独占证明。 - 启动原容器,健康检查与页面业务验收;失败走既有
library.recover/RestoreDrill恢复链按原预备份回放并启动原服务,不无限留停机。 - 无关服务与无关 key 全程不需要停;同 key 若仍有外部写者,按上节的冲突/held 处理:双方版本都保留,由人按证据处置,不自动合并。
部署需变更的组件:object-gateway(本候选镜像)、worker(同提交镜像),以及两者 env 中移除/留空已退役的 OBJECT_GATEWAY_ID、OBJECT_GATEWAY_AUDIT_FILE、OBJECT_GATEWAY_AUDIT_PUBLIC_KEY。api、web 无协议变更,可不同步。journal、治理 PG 只读 authority、archive 与 target identity 配置保持不变。
部署与回退(未执行)
测试主机 120.76.217.51、生产 120.76.218.137 各自独立批准;本候选未在任一主机执行停机、备份、更新或启动。r5 的已完成 OSS 测试授权不能重复使用。
按“停目标写入容器→读回确认→备份目标 KB 范围→更新资源→启动→健康/页面验收”执行;失败按原备份恢复并启动原服务。回退先关新写准入并保留 journal;所有已发送请求有终态才可撤网关,unknown 继续 held。不得因网关故障自动恢复旧直写凭据。当前整模块仍未完成真实 OSS 端到端与 clean-finalization 收敛,禁止把本候选当作完整生产就绪。