保护一个租户的到底是什么,直说。
没有我们并不持有的认证,也没有用形容词冒充控制。这里写的是什么被强制执行、在哪里执行,以及缺了什么。
隔离是结构性的,不是靠人盯的。
对工作区所属数据的每一次查询,谓词里都带着工作区 id。不存在一条无作用域的访问路径可供人忘记使用。
这个设计消除的是最普通的那种故障:一个本来正确的系统里,某个赶工写出的新接口漏掉了一个 WHERE 子句。如果数据层根本不回答没有指名工作区的问题,那个接口就不会编译成一次泄漏——它根本编译不过。
对抗性泄漏测试把这个性质钉死,而不是信任约定,因为一条约定的可靠程度,只等于最后一个读过那个文件的人。
internal/tenancy · migrations/0002
审计日志的仅可追加是在数据库里,不是在应用里。
任何更新或删除都会触发报错。一个决定不改写历史的应用是政策;一个拒绝改写历史的数据库才是控制。
这正是审计师会问、而供应商通常答不上来的差别。如果阻止追溯修改的唯一理由是没有代码路径会那么做,那么一段支持脚本、一次数据库迁移,或者一位手握控制台的操作员就能做到,而日志不会知道。
internal/tenancy · migrations/0002 audit_log_immutable()
密钥轮换不需要停机。登录可抵御钓鱼。
需要维护窗口的轮换,就是不会发生的轮换。
- 01
带重叠有效期的密钥 id
每个工作区的 API 密钥和 webhook 签名密钥都带一个密钥 id。轮换期间两者都有效,所以您按自己的节奏切换,之后再吊销旧的。internal/tenancy · internal/outbox/sign.go
- 02
会员和操作员都用通行密钥
WebAuthn,强制要求用户验证和可发现凭据——不是可选项,因为这些令牌就挡在资金流前面。可发现登录直接从凭据解析出账户,所以不用输入任何东西,也就没有东西可被钓。internal/passkey
- 03
验证器双因素,存储方式正确
TOTP 密钥无法做哈希——校验要用它重新算出验证码——所以会员的密钥用 AES-256-GCM 在按用途派生的子密钥下密封保存,而不是明文存放。被接受的验证码会回写计数器,所以同一个码在有效窗口内无法重放。internal/membertotp · internal/secretbox
- 04
能哈希就哈希,不能哈希就密封
密码或恢复码会被哈希,因为平台永远只需要回答“是不是这个”。平台必须读回来的密钥则改为密封保存,而早于这套构造的存储正在迁移过来,而不是原样留着。internal/secretbox
没有东西会从您配置的 URL 流出去。
webhook 目的地在构造上就是受攻击者影响的输入——那是租户敲进来的一个 URL,随后平台会从自己的网络内部去请求它。
分发器在发出请求之前先跑一遍出站守卫,而不是信任主机名;只有这个顺序才管用:在域名解析之后做的检查,或者对字符串而不是对地址做的检查,正是 DNS 重绑定专门用来穿过的那种检查。回环地址和内网目的地只在沙箱和端到端测试中被允许,而且这项放宽是一个显式开关,不是靠嗅探环境。
internal/outbox/ssrf.go · internal/outbox/dispatcher.go
我们不是托管方,代码也拒绝变成托管方。
不碰会员资金。不碰私钥。租户的收款通道里不放平台的凭据。
会员付给您
internal/rails
配置会拒绝我们自己的凭据
internal/rails
提取只下指令,从不搬钱
internal/claims
不留证件的身份核验。
服务商会话跑在您自己的账户上。返回的是一个决定和一个不可逆的句柄。
这个句柄只够执行“一人一位置”,仅此而已。它不足以还原一个身份,而这正是重点:保管护照扫描件最安全的方式就是不保管,而泄露也无法泄出从未存过的数据。
internal/verification
我们不宣称的事。
如果您正在填供应商问卷,以下就是我们会不及格的那几行。在这里知道,好过到第四个月才知道。
See /contact#security