ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

自托管Treg的密钥暗礁:Fernet加密、ACL与四级角色,配错一步照样裸奔

自托管Treg的密钥暗礁:Fernet加密、ACL与四级角色,配错一步照样裸奔 自托管Treg的密钥暗礁Fernet加密、ACL与四级角色配错一步照样裸奔【免费下载链接】tregOpenRouter for agent tools. Join community here: https://discord.gg/6mQYYfFMAn项目地址: https://gitcode.com/GitHub_Trending/treg/treg把 3000 个外部 API 端点收敛到一个网关、把几十家供应商的密钥托管在服务器上——treg 这套OpenRouter for agent tools的设计听起来很美Agent 只需一个 token就能替你调用 SEO、社媒、广告、数据富集等真实世界的工具密钥永不出服务器。但密钥不出服务器有个前提服务器上那一坨加密落盘的密钥以及决定谁能用它们的那套权限必须配得对。社区里流传的各种 treg 教程大多停留在怎么把一个 token 调通的层面真正让自托管实例裸奔的往往是四件事Fernet 主密钥没设或丢了、角色权限给了宽、审计日志根本没被当成安全数据看、以及部署时踩了那些默认值暗坑。这篇基于 treg 仓库源码逐一拆解最后给一份可以直接照着执行的自检清单。一、Fernet 主密钥全库密钥的总开关treg 落盘加密选的是 Fernet——对称加密一个 32 字节 urlsafe base64 主密钥加密所有 Secret 值。看 crypto.py 的实现整个模块只有几十行def _fernet() - Fernet: key get_settings().secret_key return Fernet(key.encode() if key else _EPHEMERAL) def encrypt(plaintext: str) - str: return _fernet().encrypt(plaintext.encode()).decode() def new_key() - str: return Fernet.generate_key().decode()关键就在_fernet()里那个兜底secret_key为空时进程会现场铸一把临时密钥_EPHEMERAL。这意味着什么没配TREG_SECRET_KEY的服务照样能跑、密钥照样加密落盘——但重启后所有密文都无法解密。这不是悄悄丢失而是一个响亮的安全信号代码注释原话是secrets wont survive a restart, which is the intended loud signal to set TREG_SECRET_KEY。更狠的防线在 infra/db.py 的verify_db()if not settings.secret_key and sqlite not in settings.database_url: raise RuntimeError( TREG_SECRET_KEY is not set on a non-SQLite database - stored secrets would be lost on the next restart. Set a key (treg keygen) before starting. )也就是说SQLite 本地模式允许临时密钥反正数据在本机、丢了重连即可但只要你不是 SQLite启动直接拒绝。这个设计堵住了拿 Postgres 当生产库却不设密钥的最蠢路径。生成与保存密钥的正确姿势仓库自己给的 self-host 脚本写得很标准见 selfhost.shKEY$($VENV/bin/python -m treg keygen) cat $ENV_FILE EOF TREG_SECRET_KEY$KEY EOF chmod 600 $ENV_FILEpython -m treg keygen见main.py就是new_key()的 CLI 出口.env权限收成 600。这套流程值得照抄因为大部分自托管事故恰恰反着来密钥明文进 shell history、.env 权限 644、或者干脆没设。轮换主密钥轮换其实无解所以要在钥匙环里做文章必须直说一个残酷事实treg 的 Fernet 主密钥不支持原地轮换。encrypt()与decrypt()用的是同一个TREG_SECRET_KEY一旦换新密钥旧密文全部解不开唯一的补救是重新录入/重连所有供应商密钥。仓库文档auth-secrets.md和代码都没有提供双密钥滚动解密的能力。所以对自托管者而言密钥轮换的现实答案分两层主密钥层面能做的只有用 KMS/密钥管理服务注入环境变量 文件权限 600 备份到密码管理器把TREG_SECRET_KEY当根 CA 私钥一样对待。丢了它 丢了一库供应商凭据。真正的轮换粒度在 API Key 层面treg 把谁调用的凭据treg_xxx开头的 token和调用什么的供应商凭据分开管理。调用方 token 只存 SHA-256 哈希hash_token()见 crypto.py库中永远没有明文 token轮换走rotate_agent_key/rotate_human_keyapi_keys.py用条件 UPDATE 做跨进程原子占锁result await db.execute( update(ApiKey).where( ApiKey.id old.id, ApiKey.state.in_((ACTIVE, DISABLED)), ApiKey.replacement_key_id.is_(None), ).values(stateREVOKED, revoked_atnow, deleted_atnow) ) if result.rowcount ! 1: raise RotationConflict旧行置为REVOKED并挂replacement_key_id旧 token 当场失效同时保留审计痕迹。对Default key每名成员每团队一张的签名 token轮换走default_generation自增rotate_default_generationtoken 里签的kgclaim 对不上就直接 401revoked key。教训把轮换的预期放在调用方 token 上而不是主密钥上。主密钥设计成不可轮换意味着你必须保证它从一开始就不会泄露——这比能轮换更考验部署纪律。另外注意 config.py 里一个极易被忽略的细节session_secret签名会话 cookie 的 HMAC 密钥未设置时回退到secret_key。也就是说如果你只设了一个强secret_key而session_secret为空那么拿到secret_key的人同时能伪造会话 cookie——密钥隔离形同虚设。生产环境两个都应显式设置成不同的随机值。二、四级角色与 ACL最危险的权限不是给了 admintreg 的角色模型是四级owner admin member viewer定义在 models.pyROLE_RANK {viewer: 0, member: 1, admin: 2, owner: 3}角色的分界点在哪里看 access.py 的_require_can_registerdef _require_can_register(caller: Caller) - None: if not _role_at_least(caller.role, member): raise HTTPException(status_code403, detailviewers can call and read, but cannot register)一句话概括viewer 能调用、能读但什么都不能注册不能建 Secret、不能注册工具、不能发起 OAuth。routers/api_keys.py里 viewer 连创建附加 key都被拒绝viewers cannot create additional keys。invite 的--role可选值也只有viewer/member/admincli.pyowner 只能通过改角色接口授予。两个最容易被误解的坑坑一owner 不受任何 ACL 限制而 deny 规则连 owner 都拦。看 access.pydef _tool_allowed(caller: Caller, tool_name: str) - bool: if caller.role owner: return True access caller.membership.tool_access return access is None or tool_name in accessowner 在工具级 ACL 和项目级 ACL_project_allowed面前一律放行——这是刻意的owner is never restricted但意味着把一个人升成 owner就等于把团队所有密钥的使用权都交给他。反过来enforce_deny的注释写得明明白白deny 规则对包括 owner 在内的所有角色生效因为deny rule is a guardrail, not a permission tier。坑二四级角色只是注册权限的标尺真正的精细管控在 Membership 行上。每个成员有一行Membershipmodels.py上面可以单独钉三样东西tool_accessNULL 团队全部工具JSON 列表 只允许列出的工具project_accessNULL 整个 orgJSON 列表 只允许这些项目里的工具daily_call_cap每人每日调用上限-1 不限配合local_run_enabled控制能否走本地 run 授权。两把 ACL 锁的关系是AND 组合_tool_usable_tool_allowed且_project_allowed。换句话说最小权限的正确姿势不是给 viewer 还是 member而是给协作方 member 角色 tool_access钉到个别工具 project_access钉到单个项目 daily_call_cap设上限。四级角色管能不能注册ACL 管能用哪些缺一不可。还有一个隐蔽的坑invite 接口models.py 的Invite在tool_access字段上就能预置 ACL——发邀请时就钉好工具名单而不是等人进来再逐项改。自托管团队如果靠先发 member 邀请、事后收权邀请窗口期就是权限敞口。别忽视机器身份这条暗线treg 里 Agent 是不可登录的机器身份邮箱挂在agents.treg.local这种不可路由域上access.py任何登录门都解析不到它而它作为 User 却能继承 Membership 的所有能力。require_identity里专门防了一手if user is not None and _is_machine_email(user.email): raise HTTPException(status_code403, detail( this token belongs to a machine identity — it can call this teams tools, but cannot act as a user))机器 token 不能建 org、不能收邀请——因为机器能建 org 就自动成为 ownerowner 又豁免所有 ACL。这是仓库里自己写明的逃逸路径自托管时给 Agent 发 key 也请遵循同样的最小权限专用 token、钉死的tool_access、独立的daily_call_cap。三、审计日志的盲区它是尽力而为的不是证据链如果你把CallRecordmodels.py当谁在什么时候调了什么的完整账本那就踩进最大的盲区了。审计子系统audit.py的设计哲学模块 docstring 第一句就是Audit writes — deferred and fire-and-forget (rule #2: never block the proxied response).具体机制是单进程单 writer、批量落库、后台连接池以及背压丢行_MAX_PENDING 5000 # shed load past this: drop the audit row rather than grow unbounded _BATCH 200队列超过 5000 条就直接丢审计行只记一个递增计数器并打日志。这是刻意的取舍——audit is best-effort; never OOM or wedge the server for it。所以审计表可能缺行。峰值流量下丢的正是最需要查的那几笔。做安全复盘时不能把审计表里没有当成没发生。审计与计费分离。CallRecord注释明确钱在 ledger 里同步落地审计表是允许丢行的分析数据the money landed in the ledger synchronously... losing a row here costs analytics, not accounting。查成本走 ledger查行为走 audit两套数据源对不齐时别慌。失败静默但有 ERROR 日志。写库失败不会打断调用an audit hiccup must never break a real call但会打ERROR级日志。运维上必须把 treg 的 ERROR 日志当警报接住——正常流量下 audit writer 不该报错一旦出现说明审计链路已破。对安全审计而言更值得盯的三张表/字段ApiKeyEventappend-only 的 key 管理审计记录 created / rotated / revoked / membership_revoked且从不含完整密钥材料——轮换、吊销都有痕。CallRecord.refused_by凡是 treg 主动拒绝的调用ACL 拒绝、deny 规则、密钥失效都标refused_by。大量refused_by就是攻击探测的信号比正常调用日志值钱得多。refresh token 重用检测application/auth.py已轮换的 refresh token 被再次使用会吊销整条 grant family并记oauth.refresh_reuse审计行——这是仓库内置的凭据被复制检测器看到它基本等于有人在偷用你的授权。另一个盲区在本地 run 授权treg run --local的 grant 是唯一被允许把密钥值交给客户端的路径api.py但它有两道闸——local_run_enabled成员开关 共享密钥必须通过持有X-Treg-Run-Proof的隔离 runner。审计里对应GRANT/DENY事件。自托管时如果开了--server运行run_allowed_bins允许列表务必盯RunRecord的argv它是服务器以服务账号执行了哪些命令的唯一记录。四、自托管安全自检清单基于以上源码证据给一份能直接照着执行的清单。按会导致裸奔的严重程度排序第一优先密钥与加密用python -m treg keygen生成TREG_SECRET_KEY写入.env并chmod 600不要进 shell history、不要进代码仓库。非 SQLite 数据库未设密钥会拒绝启动——请把这条当成部署回归测试跑一遍故意去掉变量确认它拒绝。显式设置独立的session_secret不要让它回退到secret_keyconfig.py 第 601 行附近。主密钥不可轮换备份到密码管理器/KMS一旦疑似泄露按全量重新录入供应商凭据 轮换全部调用方 token处理。调用方 token 只存哈希看到treg_明文出现在任何日志/备份里即为事故。第二优先角色与 ACL按能注册才 member、只调用就 viewer分派角色invite 时就钉好tool_access/project_access不留先进来后收权的窗口。给协作方和 Agent 用member/专用 key 工具白名单 项目白名单 每日调用上限四件套而不是简单发 owner/admin。记住 owner 豁免所有 ACL_tool_allowed对 owner 恒真——owner 名额是团队内最敏感的权限。Agent 用机器身份agents.treg.local域 token永远别给机器身份发能建 org的权限。检查require_superadmin依赖的TREG_ADMIN_TOKENconfig.py 注释it sees ALL orgs——这个 env 兜底如果设得短或泄露等于跨租户 admin 全开。默认留空、靠is_superadmin用户是更稳的姿势。第三优先审计与运行审计是尽力而为且会在背压时丢行把 treg 进程的ERROR日志接进监控audit back-pressure出现即扩容或排查。定期扫CallRecord.refused_by非空的行、oauth.refresh_reuse审计事件、ApiKeyEvent的 revoked/rotated 记录。若启用treg run --server严格维护run_allowed_bins白名单默认不允许任意二进制注释明确a member cant namebash/pythonand run arbitrary code as the server user。检查.env里TREG_BLOCKED_EMAIL_DOMAINS、TREG_PAUSED_PROVIDERS、webhook 目标是否按需配置——这些默认都是不拦/不暂停/不告警的安全面配置。最后一条纪律treg 的 selfhost.sh 里那句话值得反复读——Never point TREG_PUBLIC_URL at a public domain while TREG_SINGLE_USER is true: the no-login dashboard refuses to run anywhere but loopback sqlite。单用户免登录模式是被代码强制锁在 loopback 的你只要把TREG_SINGLE_USERtrue的实例暴露到公网域名就等于亲手拆掉认证。配错一步照样裸奔的每一个字都对应上面某一条默认值或某个我以为没关系的开关。把这四组检查放进部署流水线treg 的密钥永不出服务器才真正成立。【免费下载链接】tregOpenRouter for agent tools. Join community here: https://discord.gg/6mQYYfFMAn项目地址: https://gitcode.com/GitHub_Trending/treg/treg创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表