跳转到内容

NixOS 部署

Secret Guard 的 flake 提供 nixosModules.secret-guard,其 services.secret-guard.* 结构化选项在未显式设 configFile 时自动生成 secret-guard.toml — 字段校验 (构造 判别 / base_url 卫生 / 路由悬空与环检测) 全部在 eval 期 fail-fast,配错在 nixos-rebuild 时即报错而非部署后 crash-loop。

# 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"
'');

适合 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 同样支持 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)}
'');
}

Secret Guard 当前硬编码 session cookie 不带 Secure flag (本地 HTTP 开发必须 — 带 Secure 的 cookie 在 HTTP 连接上不会被浏览器回传)。若经反向代理终止 TLS 暴露到公网, 务必在反代层配置 HTTP 到 HTTPS 的 301 重定向,避免浏览器误用 HTTP 访问时 cookie 被明文发送。