NixOS 部署
Secret Guard 的 flake 提供 nixosModules.secret-guard,其 services.secret-guard.*
结构化选项在未显式设 configFile 时自动生成 secret-guard.toml — 字段校验 (构造
判别 / base_url 卫生 / 路由悬空与环检测) 全部在 eval 期 fail-fast,配错在
nixos-rebuild 时即报错而非部署后 crash-loop。
接入 flake
Section titled “接入 flake”# flake.nix:{ inputs.secret-guard.url = "github:luochen1990/secret-guard"; # 本 flake 的 nixosModules.secret-guard 自带 overlay (pkgs.secret-guard 可用)}姿势 0: 结构化选项 + 自动 render (推荐)
Section titled “姿势 0: 结构化选项 + 自动 render (推荐)”api_key_file / value_file 等文件选项让 toml 本身完全脱敏;secret 由
LoadCredential 或 sops-nix 解密到独立路径后注入 (见下节):
# NixOS module 内:{ # 凭据注入: *File 选项是纯路径直通, 本模块不做 sops/LoadCredential 派生 — # 部署侧自选注入姿势 (下方 LoadCredential 为推荐) systemd.services.secret-guard.serviceConfig.LoadCredential = [ "zai_key:${config.sops.secrets."llm__zai_coding_plan_api_key".path}" ];
services.secret-guard = { enable = true;
providers."zai-coding-plan" = { name = "Zhipuai Coding Plan"; kind = "direct"; protocol = "openai"; baseUrl = "https://open.bigmodel.cn/api/coding/paas/v4"; apiKeyFile = "/run/credentials/secret-guard.service/zai_key"; }; # router 形态 (虚拟端点, 按 model 通配符路由; attrsOf 跨模块可合并) providers."default-route" = { kind = "router"; routes = [{ modelPattern = "*"; target = "zai-coding-plan"; priority = 100; }]; };
# redact 保护清单 ([[secrets.entries]] 段) secrets.entries = [ { id = "llm__zai_coding_plan_api_key"; valueFile = "/run/credentials/secret-guard.service/zai_key"; } ];
# 账号系统 (可选; [auth] 段) auth = { enable = true; oidc = { issuerUrl = "https://idp.example.com/realms/main/"; clientId = "secret-guard"; clientSecretFile = "/run/credentials/secret-guard.service/oidc_secret"; redirectUrl = "https://sg.example.com/oauth2/callback"; # 省略 = host/port 派生 }; apiKeys = [ { label = "opencode"; keyFile = "/run/credentials/secret-guard.service/sdk_key"; } ]; }; };}自动生成的 toml 暴露在只读选项 services.secret-guard.resolvedConfigFile,调试可直接:
nix eval .#nixosConfigurations.<host>.config.services.secret-guard.resolvedConfigFile*File 字段 (api_key_file / value_file / key_file) 是纯路径直通,注入姿势由部署侧
自选。两种主流写法 (均配合手写 configFile,结构化选项同理):
姿势 1: sops.secrets + systemd LoadCredential
Section titled “姿势 1: sops.secrets + systemd LoadCredential”适合 secret 被多个模块共享的场景 (例如其他模块也用同一个 api_key,已经设了别的
owner)。LoadCredential 让 systemd 在服务启动时把 secret mount 到
/run/credentials/<service>/<id>,自动设 mode=0400 与 service User,不需要修改
sops.secrets 的 owner:
systemd.services.secret-guard.serviceConfig.LoadCredential = [ "zai_key:${config.sops.secrets."llm__zai_coding_plan_api_key".path}"];
services.secret-guard.configFile = (pkgs.writeText "secret-guard.toml" '' [[providers]] id = "zai-coding-plan" kind = "direct" api_key_file = "/run/credentials/secret-guard.service/zai_key" enabled = true
[[providers.endpoints]] protocol = "openai" base_url = "https://open.bigmodel.cn/api/coding/paas/v4"'');姿势 2: sops.secrets + 直接路径
Section titled “姿势 2: sops.secrets + 直接路径”适合 secret 只给 Secret Guard 用的场景。与姿势 1 的唯一差异是 api_key_file 直接指向
sops 解密路径 (需要把 secret 的 owner 设为 secret-guard 用户):
sops.secrets."zai_api_key" = { owner = "secret-guard"; };
services.secret-guard.configFile = (pkgs.writeText "secret-guard.toml" '' [[providers]] id = "zai-coding-plan" kind = "direct" api_key_file = "${config.sops.secrets."zai_api_key".path}" enabled = true
[[providers.endpoints]] protocol = "openai" base_url = "https://open.bigmodel.cn/api/coding/paas/v4"'');Secret entries 的批量注入
Section titled “Secret entries 的批量注入”Secret 同样支持 value_file (启动时一次性 resolve,读取失败拒绝启动),所以
redact 用的 secrets 列表 也能完全脱敏地注入,姿势与 provider 的 api_key_file
完全对称。适合把一批符合命名模式的 sops secrets (如 *_api_key / *_api_token /
*_secret) 批量注入 Secret Guard 做 Redact:
let # 从 config.sops.secrets 中按模式筛选要 redact 的 key (SSOT: 只列一次). redactKeys = lib.filter (k: lib.hasSuffix "_api_key" k || lib.hasSuffix "_api_token" k || lib.hasSuffix "_secret" k ) (builtins.attrNames config.sops.secrets);in { # LoadCredential 与 toml entries 都从 redactKeys 派生, 永远同步. systemd.services.secret-guard.serviceConfig.LoadCredential = map (k: "${k}:${config.sops.secrets.${k}.path}" ) redactKeys;
services.secret-guard.configFile = (pkgs.writeText "secret-guard.toml" '' ${...providers 段...} ${lib.concatStrings (map (k: '' [[secrets.entries]] id = "${k}" value_file = "/run/credentials/secret-guard.service/${k}" '') redactKeys)} '');}HTTPS 反向代理注意
Section titled “HTTPS 反向代理注意”Secret Guard 当前硬编码 session cookie 不带 Secure flag (本地 HTTP 开发必须 — 带
Secure 的 cookie 在 HTTP 连接上不会被浏览器回传)。若经反向代理终止 TLS 暴露到公网,
务必在反代层配置 HTTP 到 HTTPS 的 301 重定向,避免浏览器误用 HTTP 访问时 cookie
被明文发送。