Skip to content

NixOS Deployment

Secret Guard’s flake provides nixosModules.secret-guard, whose services.secret-guard.* structured options auto-generate secret-guard.toml when configFile is not explicitly set — field validation (construction discrimination / base_url hygiene / route dangling and cycle detection) all fails fast at eval time, so a misconfiguration surfaces at nixos-rebuild instead of crash-looping after deployment.

# flake.nix:
{
inputs.secret-guard.url = "github:luochen1990/secret-guard";
# The flake's nixosModules.secret-guard ships an overlay (pkgs.secret-guard is available)
}
Section titled “Approach 0: Structured Options + Auto-Render (Recommended)”

The api_key_file / value_file file-style options keep the toml itself free of secrets; secrets are decrypted to separate paths by LoadCredential or sops-nix and injected from there (see the next section):

# Inside a NixOS module:
{
# Credential injection: the *File options are pure path pass-throughs — this module
# does no sops/LoadCredential derivation; pick your injection mechanism on the
# deployment side (LoadCredential below is the recommended one)
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 form (virtual endpoint, routing by model wildcard; attrsOf merges across modules)
providers."default-route" = {
kind = "router";
routes = [{ modelPattern = "*"; target = "zai-coding-plan"; priority = 100; }];
};
# The redact protection list (the [[secrets.entries]] section)
secrets.entries = [
{ id = "llm__zai_coding_plan_api_key";
valueFile = "/run/credentials/secret-guard.service/zai_key"; }
];
# Account system (optional; the [auth] section)
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"; # omitted = derived from host/port
};
apiKeys = [
{
label = "opencode";
keyFile = "/run/credentials/secret-guard.service/sdk_key";
}
];
};
};
}

The generated toml is exposed via the read-only option services.secret-guard.resolvedConfigFile; for debugging you can simply run:

Terminal window
nix eval .#nixosConfigurations.<host>.config.services.secret-guard.resolvedConfigFile

The *File fields (api_key_file / value_file / key_file) are pure path pass-throughs; the injection mechanism is the deployment side’s choice. The two mainstream styles (both shown with a hand-written configFile; the same applies to the structured options):

Approach 1: sops.secrets + systemd LoadCredential

Section titled “Approach 1: sops.secrets + systemd LoadCredential”

Suits secrets shared across multiple modules (e.g. another module uses the same api_key and has already set a different owner). LoadCredential has systemd mount the secret to /run/credentials/<service>/<id> at service start, automatically setting mode=0400 and the service User — no need to touch the 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"
'');

Suits secrets used only by Secret Guard. The sole difference from approach 1 is that api_key_file points directly at the sops-decrypted path (which requires setting the secret’s owner to the secret-guard user):

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

Secrets also support value_file (resolved once at startup; a failed read refuses to start), so the redact secrets list can be injected fully sanitized too, symmetrically to the provider api_key_file. This suits batch-injecting a set of sops secrets matching naming patterns (like *_api_key / *_api_token / *_secret) into Secret Guard for Redact:

let
# Filter the keys to redact from config.sops.secrets by pattern (SSOT: listed once).
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 and the toml entries both derive from redactKeys — always in sync.
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 section...}
${lib.concatStrings (map (k: ''
[[secrets.entries]]
id = "${k}"
value_file = "/run/credentials/secret-guard.service/${k}"
'') redactKeys)}
'');
}

Secret Guard currently hardcodes its session cookie without the Secure flag (mandatory for local HTTP development — a Secure cookie wouldn’t be returned by browsers over HTTP). If you expose it publicly behind a TLS-terminating reverse proxy, make sure to configure an HTTP-to-HTTPS 301 redirect at the proxy layer, so the cookie can’t be sent in plaintext should the browser ever hit the origin over HTTP.