Skip to content

fix: make sops-nix work beyond the default configuration - #534

Open
aldoborrero wants to merge 4 commits into
numtide:mainfrom
aldoborrero:aldoborrero/533
Open

fix: make sops-nix work beyond the default configuration#534
aldoborrero wants to merge 4 commits into
numtide:mainfrom
aldoborrero:aldoborrero/533

Conversation

@aldoborrero

Copy link
Copy Markdown
Member

Closes #533.

What

Turns nix/modules/upstream/sops-nix.nix from a set of activation-script stubs into a compatibility module that pins sops-nix to its systemd code path and fixes the places where that path still assumes NixOS.

The premise is that system-manager can never run NixOS activation scripts, so sops.useSystemdActivation is not one of two valid options here — it is the only one. Everything else follows from that.

  • Default sops.useSystemdActivation to true regardless of userborn, and assert it stays enabled when secrets are configured. Previously, services.userborn.enable = false disabled secrets management with no diagnostic.
  • Assert that neededForUsers is used together with userborn. sops-nix computes its own condition for the for-users unit and ignores useSystemdActivation there, so this is the one case that cannot be papered over.
  • Re-point sops-install-secrets-for-users.service from systemd-sysusers.service (never run here) to userborn.service, and hook it into sysinit-reactivation.target so it also runs on activation and not only on boot.
  • Run the generate-age-key activation script from a sops-generate-age-key.service oneshot, ordered before the units that need the key. The script text is taken from the option sops-nix writes to, not reimplemented, so it stays in sync; if sops-nix ever stops emitting it, the unit simply is not created.

Everything is guarded by lib.optionalAttrs (options ? sops.secrets), so configurations that do not import sops-nix are unaffected.

No changes to the Rust engine were needed: it already restarts sysinit-reactivation.target on every activation, so hooking units there is sufficient.

Tests

Two new container tests, following the existing sops-ssh-key pattern, so they run across the whole distribution matrix:

  • container-*-sops-generate-age-key — asserts the key is generated by a unit, has mode 600, is ordered before sops-install-secrets.service, that secrets still decrypt, and that re-activation does not replace an existing key.
  • container-*-sops-needed-for-users — consumes a neededForUsers secret as a user's hashedPasswordFile and asserts that userborn picks the decrypted hash up in /etc/shadow. This only passes if the for-users unit runs before userborn.

testFlake/sops/secrets-for-users.yaml is new test material, encrypted with the age key already committed at testFlake/sops/age-keys.txt.

Docs

docs/site/how-to/import-nixos-module.md claimed the activation-script options were pure stubs and that "the actual secret decryption is handled differently in system-manager through a systemd service" without saying how. Corrected, and a how-to/manage-secrets.md guide added, since sops-nix support was previously undocumented outside a CHANGELOG line.


Verification status

Run and passing:

  • The same assertions as the two new tests, executed as VM tests on Debian 13 (vm-debian-13-sops-generate-age-key, vm-debian-13-sops-needed-for-users) before they were converted to container tests.
  • Regressions: vm-debian-13-example (which exercises sops), vm-debian-13-prepopulate, vm-debian-13-sudo-module.
  • mypy and pyflakes over the generated container test scripts, replicating what the test derivation's buildCommand does.
  • Evaluation of seven configurations: with and without sops-nix imported, with and without userborn, plus both new assertions firing.
  • treefmt --fail-on-change, and nix build .#systemConfigs.default.
  • toplevel, driver and closureInfo of all six new container checks build on all three distributions.

Not run: the container tests themselves. The Nix daemon on the machine they were developed on does not offer the uid-range system feature that makeContainerTest requires, and the feature cannot be toggled from the command line — it is resolved daemon-side. To run them:

nix build -L .#checks.x86_64-linux.container-debian-13-sops-generate-age-key \
             .#checks.x86_64-linux.container-debian-13-sops-needed-for-users

Investigated and implemented with the help of Claude Code. Everything claimed above was checked against actual builds and runs at the revisions in the issue, not from model recall; in particular, the "not run" item is accurate and not an oversight.

sops-nix installs secrets either from a NixOS activation script or, when
`sops.useSystemdActivation` is set, from a sops-install-secrets.service
oneshot. system-manager can only ever take the second path, and until now
it relied on that option happening to default to true because userborn is
enabled by default. Three configurations silently installed no secrets at
all:

- `sops.age.generateKey`: only implemented upstream as an activation
  script, so the age key was never generated and secret installation
  failed on a machine that did not already have the key.
- `sops.secrets.<name>.neededForUsers`: the generated unit is ordered
  around systemd-sysusers.service, which system-manager never runs, so it
  was neither started on activation nor before userborn creates the users
  that consume the secret.
- `services.userborn.enable = false`: turned `useSystemdActivation` off,
  which dropped secret installation entirely with no diagnostic.

Default `sops.useSystemdActivation` to true and assert it stays enabled,
re-point the for-users unit at userborn and sysinit-reactivation.target,
and run the `generate-age-key` script from a oneshot unit ordered before
the units that need the key.
Both tests run against every distribution in the container test matrix,
following the same pattern as the existing sops-ssh-key test.

sops-generate-age-key asserts that the age key is generated by a unit and
that re-activation does not replace it. sops-needed-for-users consumes a
neededForUsers secret as a user's hashedPasswordFile and asserts that
userborn picks the decrypted hash up, which only works if the for-users
unit is ordered before userborn.
Add a how-to guide for encrypted secrets, and correct the sops-nix
section of the NixOS module import guide, which described the
activation-script options as pure stubs and claimed secret decryption
was handled elsewhere without saying how.
The restart path uses `try-restart`/`try-reload-or-restart` with
--no-block, which are not incidental details: a stopped unit stays
stopped, and the restart is requested rather than awaited because
sops-install-secrets runs before sysinit-reactivation.target with
DefaultDependencies=no.

Also note the `keys` group fallback to `nogroup`, which is what happens
on Debian and Ubuntu.
@mergify

mergify Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sops-nix silently installs no secrets in three configurations

1 participant