HashiCorp Vault Integration
HashiCorp Vault Integration
sops-nix and age cover static secrets in git. Large desks that already run Vault want short-lived database passwords and certs. The boring default is: Vault Agent on the node, templates to /run/vault, Nix only knows the paths — and if you do not already operate Vault, stay on sops-nix.
Mental model
Vault issues leases. The agent:
- Authenticates (AppRole, AWS, Kubernetes — pick one).
- Reads secrets / PKI.
- Renders a file on tmpfs.
- Signals the unit (
SIGHUPor restart) when the file changes.
NixOS may ship services.vault-agent — check your 26.05 pin (nixos-option services.vault-agent). If the option is missing, Case 1’s systemd.services.vault-agent-desk is the boring wrapper. The role-id can live in the store. The secret-id cannot. Vault Agent is a node sidecar, not a reason to put VAULT_TOKEN in desk-api’s Nix module.
Vault --lease--> vault-agent --template--> /run/vault/db.env
│
▼
desk-api EnvironmentFile=
Worked examples
Case 1: Agent module
Save as vault-agent.nix:
# vault-agent.nix
{ pkgs, ... }:
{
systemd.services.vault-agent-desk = {
description = "Vault Agent for desk-api";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
serviceConfig = {
ExecStart = "${pkgs.vault}/bin/vault agent -config /etc/vault-agent.hcl";
Restart = "on-failure";
DynamicUser = true;
ProtectSystem = "strict";
PrivateTmp = true;
RuntimeDirectory = "vault-agent";
LoadCredential = "secret-id:/run/secrets/vault-secret-id";
};
};
environment.etc."vault-agent.hcl".text = ''
vault { address = "https://vault.desk.internal:8200" }
auto_auth {
method "approle" {
config = {
role_id_file_path = "/etc/vault/role-id"
secret_id_file_path = "/run/secrets/vault-secret-id"
}
}
}
template {
source = "/etc/vault/templates/db.ctmpl"
destination = "/run/vault-agent/db.env"
perms = "0400"
}
'';
}role-id is not a secret in many setups. secret-id is — put it in sops-nix, not in environment.etc.
Case 2: Template without secret values in Nix
Save as db.ctmpl (provisioned via environment.etc — this file has Go template placeholders, not passwords):
# db.ctmpl
{{ with secret "database/creds/desk-api" }}
DESK_DB_USER={{ .Data.username }}
DESK_DB_PASSWORD={{ .Data.password }}
{{ end }}
The rendered /run/vault-agent/db.env never enters /nix/store.
Case 3: Reload the app when the lease rotates
# vault-reload.nix
{
systemd.services.desk-api = {
serviceConfig.EnvironmentFile = "/run/vault-agent/db.env";
unitConfig.StartLimitIntervalSec = 0;
};
systemd.paths.desk-api-reload = {
wantedBy = [ "multi-user.target" ];
pathConfig.PathModified = "/run/vault-agent/db.env";
};
systemd.services.desk-api-reload = {
serviceConfig = {
Type = "oneshot";
ExecStart = "/run/current-system/sw/bin/systemctl reload-or-restart desk-api.service";
};
};
}If the app cannot reload credentials without a restart, restart. Document that. Leases that expire while the process still holds the old password are outages.
Case 4: AppRole files
# vault-approle.nix
{ config, ... }:
{
environment.etc."vault/role-id".text = "00000000-0000-0000-0000-000000000000";
# secret-id from sops, not from etc
sops.secrets."vault/secret-id" = {
mode = "0400";
};
}Use a real UUID from vault read auth/approle/role/desk-api/role-id. The example zeros are a placeholder so this listing does not pretend to be a live role.
Case 5: When not to use Vault
If the desk has five hosts and two humans, Vault is a second production system. sops-nix + age is the boring stack. Add Vault when the org already requires it, or when credentials must rotate every hour.
The trap
The trap is a root Vault token in configuration.nix. That token is in the store and in every cache copy. AppRole + a sops-backed secret-id. Never VAULT_TOKEN in the flake.
The other trap is ignoring lease TTL. Agent templates that nobody reloads leave the DB password valid in Vault’s eyes for twenty minutes after the process started — then Postgres rejects it at 3 a.m.
The boring rule
- Nix knows addresses, role ids, and paths. Bytes stay in Vault and
/run. - AppRole (or cloud auth). No root tokens in git.
- Reload or restart on template change.
- secret-id via sops-nix. role-id may be public.
- Skip Vault if sops-nix already solves the desk.
Try this
nixos-option services.vault-agent(orrg vault-agentin nixpkgs) on a 26.05 pin and write down the module path your release actually ships.- Render a dummy
db.ctmplwith static{{ printf "x" }}locally usingconsul-template/ vault agent-devif you have a lab Vault. Confirm the output file is not a store path. git grep VAULT_TOKENon the desk repo. It should be empty.- Time a database lease TTL and confirm desk-api restarts before expiry in the lab.