Motivation#
In the previous post, we introduced Home Manager for personal configuration. Ordinary settings can go into Nix files, but passwords, API tokens and private keys need separate handling. Putting them directly into configuration is unsuitable even when the repository is private.
During a build, Nix puts many configuration and source files into the store. Being able to reproduce a configuration does not mean that only its owner can read the resulting files. Here I will explain the distinction between configuration and secrets, and introduce sops-nix.
The examples use a fictional host, laptop, and a service that only checks whether it can read a file. The demonstration password has no real use. No existing service configuration or real keys are used.
The Nix Store and Secrets#
Files in the Nix store are normally readable by other local users. A password written into environment.etc.*.text, Home Manager’s home.file.*.text, or a file generated during a build may end up in the store. Setting 0600 when installing the final file does not remove copies already created earlier.
Flake source files are another easily overlooked place. Local Git flakes read Git-tracked files, so plaintext secrets should stay outside a repository used as a flake input, even if no module refers to them. See the Nix flake input documentation.
It helps to distinguish these kinds of content:
| Content | Where to keep it |
|---|---|
| Service names, configuration structure and secret file paths | Nix configuration |
| Encrypted secret files | Configuration repository, with metadata checked before publication |
| Private decryption keys | Protected storage outside the repository and store |
| Plaintext used by a service | Runtime files or the service’s credential directory |
A path tells a program where to read a file; it is different from reading that file into a Nix expression. Applying builtins.readFile to a runtime secret brings its plaintext into evaluation and defeats the arrangement in which a build machine needs no decryption key.
Runtime files still need access controls. Root, programs with service privileges and authorized backup tools may read them. Encryption at rest addresses exposure in the repository and build stages. System accounts, service permissions and logging still need attention.
Introduction to sops-nix#
This example uses three tools:
- age generates encryption and decryption keys;
- SOPS edits encrypted YAML and other files;
- sops-nix declares secret sources, destinations and permissions in NixOS, and decrypts them on the target system at runtime.
SOPS supports several encryption methods. Here we use age. Its public key, also called a recipient, is used for encryption. The corresponding private key decrypts the file and must stay out of Git. See the age documentation.
We will use the NixOS module, which is a separate integration from the Home Manager module in the previous post. The example continues with NixOS 26.05 and a sops-nix version supporting useSystemdActivation. Your flake.lock records the actual dependencies.
Configuring sops-nix#
Adding a Flake Input#
Add the following to the existing flake.nix:
| |
Accept sops-nix in outputs, then add its module to the existing host’s module list. For example, this is a fragment of the output attribute set:
| |
Keep the existing modules. There is no need to replace the entire flake. The input branch will continue to change, while the local lock file records a specific revision. Check flake.lock after adding the input, then update it deliberately.
Creating a Demonstration Key#
This walkthrough assumes you edit the configuration and run the demonstration service on the same computer. From the configuration repository root, open a shell containing the tools:
| |
In that shell, create a key specifically for the demonstration:
| |
The last command prints only the public key. The private key stays outside the repository. If this file already exists, check its purpose before proceeding; do not overwrite it. SOPS_AGE_KEY_FILE tells subsequent SOPS commands where to find the demonstration identity. Set it again when opening another shell.
For several computers, it is usually better to separate the administrator’s editing identity, each computer’s decryption identity and a protected recovery identity. Sharing one demonstration key here keeps the local process easy to follow. I would not copy the same private key to every computer.
Creating an Encrypted File#
Create .sops.yaml at the repository root:
| |
Replace <AGE_RECIPIENT> with the public key printed above. It is a placeholder and cannot be used for encryption as written. Then create the secret directory and open the editor:
| |
Inside the editor opened by SOPS, enter:
| |
After saving and exiting, secrets/demo.yaml on disk should be encrypted, rather than containing the plaintext above. Use sops edit for later changes too. Avoid writing a real password into an ordinary YAML file and relying on remembering to encrypt it before committing. Consider editor swap files, backups and temporary files as well.
SOPS normally retains YAML key names and encryption metadata. An encrypted file still needs review before publication: service names, recipient lists and comments can reveal configuration structure. See the SOPS usage documentation for editing and configuration rules.
Providing the Target System’s Private Key#
On this demonstration computer, install the demonstration private key in a root-managed location:
| |
These commands prepare a new demonstration environment. Do not overwrite an existing key.txt: other configurations may depend on it. A real remote computer needs its own private key provisioned through a trusted method. The build result does not automatically contain that key.
Keep /var/lib/sops-nix/key.txt persistent. Restoring only the configuration repository after reinstalling cannot recreate the ability to decrypt.
Declaring the Secret and Demonstration Service#
Create modules/secrets-demo.nix:
| |
defaultSopsFile is relative to the module and points to the encrypted repository file. The name demo/password selects the nested YAML key. Its default destination is /run/secrets/demo/password.
This example disables automatic SSH host key imports and uses only the prepared age identity. keyFile is a string describing a runtime path. It does not copy the private key as a Nix source file.
The example explicitly enables useSystemdActivation, so the service depends on sops-install-secrets.service. An older configuration decrypting through an activation script cannot use this dependency unchanged. Home Manager user services have a separate startup arrangement too. See the sops-nix module source for these options.
The service uses systemd’s LoadCredential. Systemd reads the root-owned secret and provides a credential directory to the service. The service reads it through CREDENTIALS_DIRECTORY, so this check does not require opening access to the original file for ordinary users. It checks for a nonempty file and tries reading one byte without printing the password. See the NixOS systemd documentation for credential delivery.
For a real service, prefer its supported passwordFile, credentialsFile or credential interface. Avoid reading a runtime secret with Nix and assembling it into a store configuration again. If a service requires a complete configuration file, sops-nix also offers runtime templates that can be explored separately.
Building and Verification#
Add the configuration and encrypted file to the Git index, then build:
| |
Before committing, confirm that the staged YAML is in SOPS encrypted format and review the newly created flake.lock and add it to the index for recording together. Keep private keys out of the index. A successful build means the system can be generated. Correct target identities and service access still need runtime verification.
After preparing to apply the configuration using the process in post five, run:
| |
The second command runs the demonstration check again if it already ran during the rebuild. Expect Result=success, ExecMainStatus=0, and 400 root root permissions on the original secret file. This is a oneshot service, so inactive after successful exit is normal.
There is no need to cat the secret. With a real service, also test its function, such as completing an authorized request, and check that its logs do not accidentally contain the password.
Troubleshooting#
Decryption Failure#
Distinguish a missing encrypted file, a mismatched YAML key and an identity unable to decrypt. Git tracking and module paths can be checked before building. The match between the private key and recipient needs checking on the target computer.
Inspect this example’s decryption service with:
| |
Review logs before copying or sharing them. Do not print private keys, decrypted YAML or the entire environment into a chat to troubleshoot.
A Service Still Uses the Old Password#
Updating a secret file does not mean a running program has read it again. Systemd also loads credentials when a service starts. Depending on the service, you may need a restart, reload or a change to the password stored on its server.
sops-nix secret options can declare restartUnits or reloadUnits, but first check which operation the service supports. For a database, changing an external password file does not necessarily update the database account’s password.
User Passwords and Early Decryption#
NixOS account passwords may need to be available before user creation, unlike ordinary service secrets. sops-nix provides neededForUsers for early decryption. When using it with hashedPasswordFile, the content must be a password hash. Early decryption cannot depend on a user that has yet to be created for ownership, so the ordinary service example above needs adaptation.
Key Updates and Recovery#
When adding a computer, include its public key in the appropriate rule. With an administrator identity that can still decrypt, run:
| |
This applies the .sops.yaml recipient rules to an existing file. Editing the rules alone does not update previously encrypted files. Before committing and deploying, check the result with the newly added decryption identity.
SOPS can also replace the file’s data encryption key:
| |
updatekeys and rotate change the encrypted file. Changing an API token or password at its issuing service is a separate operation. See the SOPS key management documentation. If a key or secret has leaked, removing a recipient cannot recall old Git revisions or ciphertext, nor erase a password someone already knows. Revoke the old secret at the actual service. Before storing a replacement password or token, remove the compromised recipient, run updatekeys and rotate, then enter the new value and update its consumers.
For recovery, keep a separately protected recovery identity and confirm it can decrypt the required files. Backing up ciphertext while losing every private key usually leaves the content unrecoverable. Keeping only one private key on the computer also risks losing it during disk failure or reinstallation. Practice recovery in an isolated temporary environment with a disposable secret before you need it.
For example, in an isolated environment with only the recovery identity available, check that the demonstration file decrypts and discard the output to keep its content off the terminal:
| |
Replace the recovery key path, and first add its public key to the rule and run updatekeys. Check that the exit status is zero and that no other decryption identities are available in the test environment, so the check cannot silently use your everyday key.
A system rollback may redeploy old ciphertext, but it cannot make a revoked token valid again. Plan separately for restoring system configuration, decryption access and service accounts.
Recommended Usage#
I recommend testing the entire encryption, build, decryption and reading process with a demonstration secret before migrating one real service. Give each service only what it needs, use separate decryption identities for computers, and keep recovery keys elsewhere.
When asking AI for help, provide option structures, placeholders and reviewed error messages. Ask it to explain where plaintext will appear and who can read it. Enter real secrets and handle private keys in a terminal or tool you control, and review files, screenshots and logs before sharing.
The next post will cover system state, backups and recovery: what rebuilding NixOS cannot restore, and how to check that a backup actually works.
