Skip to content

安全模型

本文讲系统的安全姿态:鉴权与 token、注册审批闸门、被刻意禁止的控制模型、私钥处理、写盘与容器边界。配置项见 配置参考,接口鉴权见 API 参考

鉴权边界

接口面鉴权说明
Admin API(/control/v1/admin/*/control/v1/ui/*Authorization: Bearer <凭据>(静态 DN42_CONTROL_ADMIN_TOKEN 或账号会话令牌)fail-closed:未配置 admin token 时全部 403(含账号登录);凭据错 / 会话过期或被吊销 401
账号登录(POST /control/v1/auth/login无 Bearer;用户名 + 密码 + Cap 人机验证服务端硬校验人机验证失败 400;凭据错统一 401 invalid credentials(防枚举);连续失败限速 429;见 管理面 API
Agent HTTP(/control/v1/agent/*,除 register)Authorization: Bearer <agent token>token 绑定 node_id
Agent WebSocket(/agent/ws/{node_id}Bearer,且 principal.node_id == URL node_id不符 4403、无效 4401
POST /agent/register请求体内 enrollment token唯一不需 Bearer 的写接口
GET /healthzDB 探针

所有 admin 写操作(含鉴权失败)由中间件记入 admin_audit_log,见 账号、审计与自动对等表

SQL 注入面由三层约束覆盖:结构上一律走 ORM / Core 参数化表达式,ruffS608 拦 f-string 拼 DML/DQL,AST 纪律测试补 DDL 与动态标识符两处盲区。全仓库的动态 SQL 豁免点可用 grep -rn "sql-ok:" apps/ 列出。完整约束与判据见 SQL 构造与防注入

token 哈希模型

  • 数据库只存 sha256 哈希,不存明文。
  • Agent token:格式 <id>.<secret>id 为非密查找键)或固定字面量(literal_token_id 从哈希派生 id)。明文只在签发响应里出现一次,不落日志、不入库。resolve() 校验未撤销、未过期。rotate() 撤旧签新。
  • Enrollment token:同哈希模型。node_id 为空 = 任意节点可用,非空 = 仅该节点;一次性(used_at 标记消费)。
  • 账号会话令牌admin_sessions):登录签发 secrets.token_urlsafe(32),DB 只存 sha256;TTL 默认 24h,改密吊销该用户全部存活会话。账号密码admin_users)只存 argon2id 散列;不存在的用户名也跑一次对照校验,抹平时间侧信道。

注册审批闸门

未知节点无法自助上线,必须经管理员放行(agent_http.py register + PendingRegistrationStore):

  1. agent POST /agent/register(enrollment token + requested_node_id + inventory)。
  2. 校验 enrollment token(全局或绑定、未消费、未过期)。
  3. 查审批状态:
    • rejected403(显式否决)。
    • pending → 返回 PENDING_APPROVAL不消费 enrollment token,agent 周期重试。
    • 未知节点 → 记为 pending 注册申请。
    • 已落库且有 generation → 签发 agent token,标记 enrollment 已用。

"approve 只是放行名单,provision 才下发 DesiredState"——两步分离避免误放行即生效。流程见 节点接入

私钥处理

节点 WireGuard 密钥的单一事实源是控制面的 node_wireguard_keys:控制面生成或收编密钥对,materialize 时把字面私钥无条件注入每个 WG 接口。公钥恒由私钥派生,Node.wireguard_public_key 只是它的只读投影。

私钥以明文列存在库里node_wireguard_keys.private_key),节点不再生成也不再持有独立密钥文件。因此:

⚠️ 控制面数据库泄露等于全 fleet WireGuard 私钥泄露。 库备份、对公网发布的 PostgreSQL 端口、以及任何能读该表的凭据,都要按「持有全网隧道密钥」的级别对待。

节点侧同样是明文落盘。 私钥随 DesiredState 下发后被渲染进 <state-dir>/nodes/<node>/rendered/wireguard/<iface>.conf,再 bind 挂载给 wg-gateway 容器。实测(hkg1):该节点全部 20 个 WG 接口的 conf 携带同一把字面私钥,文件模式 0644、目录链一路 0755——节点上任何本地用户都能读到全网隧道私钥

⚠️ 这意味着密钥的暴露面是「控制面数据库 + 每一台节点的文件系统」。节点上不应存在不受信任的本地用户;给渲染目录收权限是独立于本文档的改进项。

一致性闸门(始终生效):agent 每轮从 DesiredState 取密钥、推导公钥上报,控制面与权威投影比对——不符返回 409 并回滚事务,agent 中止本轮 reconcile。节点绝不用偏离的密钥拉隧道;要改密钥只能走显式轮换。

离线托管(可选,当前生产未配置):配了 [server] recovery_public_key 时,agent 用它 RSA-OAEP 封装私钥随报上交,控制面存密文。恢复私钥永不在控制面或节点出现,只在离线的 dn42-recover 工具里使用。

托管防的是「数据库整体丢失」,不防「数据库被读取」——密文与明文私钥在同一个库里。

private_key_ref 字段只接受字面私钥。历史上的 secret:// 占位符会让 agent 另生成一把托管密钥、造成公钥分叉,该概念已彻底废除,schema 层强制拒绝 URI 形态的取值。

完整模型与恢复演练见 密钥托管与恢复

禁止的控制模型

系统刻意不提供以下能力,这是核心安全设计而非缺失:

  • ❌ 远程 shell / 任意命令执行接口。
  • ❌ 控制面直接 push 命令到节点执行。
  • ❌ 节点接收并执行控制面下发的脚本片段。

控制面能表达的只有声明式期望状态DesiredState)。怎么达成完全由节点本地 Agent 基于本机观测决定(见 架构)。即使控制面被攻陷,攻击者也只能改"想要什么",无法直接在节点上执行任意命令——节点渲染的是受 schema 约束、经模板生成的配置,不是可执行指令流。

写盘与容器边界

  • Agent 渲染文件路径强校验(禁 ..、绝对路径、NUL、Windows 盘符,见 dn42_runtime),原子写盘。
  • 容器由受管 label(dn42.managed 等)标识;Agent 只动带本节点 label 的容器,不碰其它容器。
  • systemd 单元用 ProtectSystem=strictNoNewPrivileges=true、受限 ReadWritePaths(见 部署)。

多管理员 / RBAC

管理面凭据为**静态 admin token(机器与自动化)加管理员账号(人机登录)**双轨。账号侧目前没有细粒度 RBAC——admin_users.role 是预留字段,当前恒为 admin

生产应妥善保管 admin token、用 TLS 终止反代(全链路无明文段:CDN 代理时 SSL 模式取 Full (strict),源站强制 HTTPS 与 HSTS)、按需收紧 CORS 白名单。

信任域隔离

控制面上并存四种凭据,彼此不可跨越:

凭据解锁范围越界表现
静态 admin token / 账号会话/admin/*/ui/*
agent token/agent/*,且只限绑定的那个节点payload node_id 不符 403;WS 路径 node_id 不符 4403
门户会话令牌/autopeering/*,按会话内 ASN 划权打不进管理面;他人的资源一律 404(防枚举)
auth-server 会话 JWT/autopeering/session 回显与 OAuth access token 有 audience 区隔,互不通用

门户侧复用管理面的 builder 函数,但不复用端点——require_admin 挂在路由级,另起一条挂 require_portal_session 的路由,只共享纯函数。对外字段一律白名单投影而非黑名单剥除,将来 builder 新增字段默认不出网。