Secrets Don’t Belong in Config | SecretSpec<br>Skip to content
Secrets Don’t Belong in Config
Jul 20, 2026<br>Domen Kožar
Applications should not require passwords, API keys, or tokens in their<br>configuration files.
Configuration describes behavior. It belongs in git, code review, bug reports,<br>and developer machines.
A secret grants authority. It needs restricted access and independent rotation.
Putting both in one file couples different lifecycles and audiences. If rotating<br>a password requires regenerating application configuration, the interface has<br>coupled them too tightly.
NixOS contains 110 workarounds for this<br>Section titled “NixOS contains 110 workarounds for this”
We audited all 445 NixOS modules that handle a real secret<br>in nixpkgs at commit 141f212, classifying each by where its secret value ends<br>up.
Where the secret value ends upModulesShareMerged into a config file at runtime11025%Inlined into a config in /nix/store429%Delivered as an environment variable16136%Left in a dedicated file opened by the app5813%Loaded through systemd credentials5312%Passed as a command-line argument194%Classification uncertain2—<br>The interesting number is 110. A quarter of the modules retrieve a secret<br>safely, then copy it into configuration because that is the only interface the<br>application accepts.
These modules use envsubst, replace-secret, jq, yq, sed, or custom<br>code to assemble a restricted file at startup. The result can be secure, but<br>every module now owns application-specific, security-sensitive glue just to<br>combine two inputs that should have remained separate.
This is not unique to NixOS. The same workaround appears as an entrypoint<br>script, Helm template, init container, or CI interpolation step on other<br>platforms.
As a side note, 42 modules can inline secrets into the world-readable<br>/nix/store. That direct security problem is tracked in<br>nixpkgs issue #24288. The 110<br>runtime mergers make the broader point: even when deployment authors avoid the<br>leak, the missing separation still creates work.
Give secrets their own interface<br>Section titled “Give secrets their own interface”
Applications should accept secret values through a dedicated runtime channel,<br>such as:
a password_file or token_file setting;
a systemd credential;
a narrowly scoped environment variable;
or an external secret provider.
These mechanisms are not equally safe: environment variables can be inherited,<br>arguments can appear in process listings, and files still need correct<br>permissions. What separation does guarantee is that the deployer no longer has<br>to manufacture a second, secret-bearing version of the configuration.
The principle is simple; implementing it across environments is not. Local<br>development might use a system keyring, CI environment variables, and<br>production 1Password or Vault. Without a shared abstraction, each environment<br>needs its own naming, lookup, validation, and injection glue.
How I got it wrong in Cachix<br>Section titled “How I got it wrong in Cachix”
Cachix historically stored its auth token and per-cache signing keys in<br>~/.config/cachix/cachix.dhall, alongside cache names and other configuration.<br>It was convenient, but the file had to be treated as a secret even though much<br>of it was ordinary configuration.
A typical file mixed them directly:
~/.config/cachix/cachix.dhall{ authToken = "XXX-AUTH-TOKEN"
, binaryCaches =
[ { name = "mycache"
, secretKey = "XXX-SIGNING-KEY"
The cache name is configuration; the auth token and signing key are secrets.<br>You could not share the cache configuration without also sharing credentials.
devenv 2.2 separates the token through SecretSpec.<br>The project declares CACHIX_AUTH_TOKEN, devenv resolves it from the configured<br>provider, and the value is passed to Cachix without being added to devenv’s<br>configuration.
Cachix PR #737 brings the same<br>boundary into the client through the SecretSpec Haskell SDK. It resolves<br>CACHIX_AUTH_TOKEN and CACHIX_SIGNING_KEY from SecretSpec and can store them<br>in the user’s chosen provider instead of cachix.dhall. Existing environment<br>variables and config files remain higher-priority fallbacks for compatibility.<br>The PR is still open and is not available in a released Cachix version yet.
That is the problem SecretSpec is designed to solve: configuration declares the<br>requirement, while each environment chooses where the value lives.
Declare once, resolve anywhere<br>Section titled “Declare once, resolve anywhere”
SecretSpec applies that separation by making secretspec.toml a declaration of<br>what an application needs, without storing the values:
secretspec.toml[project]
name = "myapp"
[profiles.production]
DATABASE_URL = { description = "Postgres connection string" }
STRIPE_API_KEY = { description = "Stripe secret key" }
Providers decide where the values live. A developer can<br>use the system keyring, CI can use environment variables, and production can use<br>1Password, Vault/OpenBao, or a cloud secret manager without changing...