-
-
Notifications
You must be signed in to change notification settings - Fork 161
[RFC 0201] security.artifacts: backend-agnostic secret management #201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,120 @@ | ||||||||
| --- | ||||||||
| feature: security-artifacts-backend-agnostic | ||||||||
| start-date: 2026-05-13 | ||||||||
| author: Mutasem Kharma | ||||||||
| co-authors: | ||||||||
| shepherd-team: | ||||||||
| shepherd-leader: | ||||||||
| related-issues: https://github.com/NixOS/nixpkgs/pull/519619 | ||||||||
| --- | ||||||||
|
|
||||||||
| # Summary | ||||||||
| [summary]: #summary | ||||||||
|
|
||||||||
| Introduce `security.artifacts`, a backend-agnostic abstraction layer for managing secrets in NixOS. This feature decouples NixOS service modules from specific secret providers (like `sops-nix`, `agenix`, or `systemd-creds`), allowing users to uniformly declare and inject secrets without vendor lock-in. | ||||||||
|
|
||||||||
| # Motivation | ||||||||
| [motivation]: #motivation | ||||||||
|
|
||||||||
| Currently, NixOS modules handle secrets inconsistently. Some modules hardcode support for specific secret managers, while most simply provide options like `passwordFile` or `secretFile` and leave it to the user to manually wire up their chosen secret manager. This results in fragile configurations, tight coupling, and difficult migrations when switching between secret backends (e.g., from `agenix` to `sops-nix`). | ||||||||
|
|
||||||||
| A unified interface will: | ||||||||
| 1. Provide a standard way for NixOS services to consume secrets natively. | ||||||||
| 2. Standardize filesystem permissions, systemd ordering, and deployment paths. | ||||||||
| 3. Allow users to easily switch secret backends without rewriting their entire service configuration. | ||||||||
|
|
||||||||
| # Detailed design | ||||||||
| [design]: #detailed-design | ||||||||
|
|
||||||||
| This RFC proposes the `security.artifacts` module tree: | ||||||||
|
|
||||||||
| 1. **Option Declaration**: | ||||||||
| `security.artifacts.secrets.<name>` accepts a submodule configuring the owner, group, mode, target path, and **optional per-secret provider**. | ||||||||
|
|
||||||||
| 2. **Providers**: | ||||||||
| The abstraction supports pluggable backends (`security.artifacts.provider`): | ||||||||
| - `sops-nix` | ||||||||
| - `agenix` | ||||||||
| - `systemd-creds` | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you have an article/blog posts etc that explains the use of systemd-creds with NixOS you have in mind? I haven't encountered it yet and wasn't able to find anything with a quick web search. |
||||||||
| - `external` (Assumes secret is provisioned outside of NixOS, e.g. cloud-init) | ||||||||
| - `dummy` (for CI/CD environments where secrets are mocked plaintext) | ||||||||
|
|
||||||||
| Users can set a global provider or override it per-secret, allowing for mixed configurations (e.g. TPM2-bound local secrets via `systemd-creds` alongside shared GitOps secrets via `sops-nix`). | ||||||||
|
|
||||||||
| 3. **Systemd Synchronization**: | ||||||||
| A global systemd target `nixos-artifacts-secrets.target` is introduced. All secrets must be provisioned before this target is reached. Downstream services that consume secrets simply need `Wants = [ "nixos-artifacts-secrets.target" ]` and `After = [ "nixos-artifacts-secrets.target" ]`. | ||||||||
|
Comment on lines
+44
to
+45
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suspect this code be an issue for some providers like sops that do decryption on-host and can have any number of failure modes such as "New key |
||||||||
|
|
||||||||
| 4. **Security Assertions**: | ||||||||
| Evaluation-time assertions guarantee that the resolved paths of the secrets do not leak into `/nix/store` and that required source files are specified for providers that need them. | ||||||||
|
|
||||||||
| # Examples and Interactions | ||||||||
| [examples-and-interactions]: #examples-and-interactions | ||||||||
|
|
||||||||
| ### Basic Usage with `sops-nix`: | ||||||||
|
|
||||||||
| ```nix | ||||||||
| security.artifacts.enable = true; | ||||||||
| security.artifacts.provider = "sops-nix"; | ||||||||
|
|
||||||||
| security.artifacts.secrets."postgres-pw" = { | ||||||||
| owner = "postgres"; | ||||||||
| group = "postgres"; | ||||||||
| mode = "0400"; | ||||||||
| source = ./secrets/postgres.yaml; | ||||||||
| }; | ||||||||
|
|
||||||||
| services.postgresql = { | ||||||||
| enable = true; | ||||||||
| # Explicitly reference the artifact path | ||||||||
| passwordFile = config.security.artifacts.secrets."postgres-pw".path; | ||||||||
| }; | ||||||||
| ``` | ||||||||
|
|
||||||||
| ### Mixed Providers (TPM2 + GitOps): | ||||||||
|
|
||||||||
| ```nix | ||||||||
| security.artifacts.enable = true; | ||||||||
| security.artifacts.provider = "sops-nix"; # Global default | ||||||||
|
|
||||||||
| security.artifacts.secrets = { | ||||||||
| "shared-app-key" = { | ||||||||
| source = ./secrets/app.yaml; | ||||||||
| }; | ||||||||
|
|
||||||||
| "local-tpm-secret" = { | ||||||||
| provider = "systemd-creds"; # Override global default | ||||||||
| path = "/run/secrets/tpm-key"; | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I worry that figuring out how to ensure that the provider can write to this directory will be complicated. Is it assumed that this directory exists? Do we assume that all providers run as root and can write to any existing directory? Maybe it would be better to make the |
||||||||
| }; | ||||||||
| }; | ||||||||
| ``` | ||||||||
|
|
||||||||
| If the user switches to `agenix`, they simply change the `provider` string, and the system seamlessly uses the new backend without requiring any changes to the `postgresql` configuration or the file paths. | ||||||||
|
|
||||||||
| # Drawbacks | ||||||||
| [drawbacks]: #drawbacks | ||||||||
|
|
||||||||
| - **Ecosystem Fragmentation during Transition**: Until all major NixOS services adopt this standard, some services will use `security.artifacts` while others will continue using `LoadCredential` or raw paths. | ||||||||
| - **Maintenance of Translation Layers**: The translation layers for out-of-tree backends (`agenix`, `sops-nix`) will need to be kept up to date with upstream changes. | ||||||||
|
|
||||||||
| # Alternatives | ||||||||
| [alternatives]: #alternatives | ||||||||
|
|
||||||||
| - **Status Quo**: Continue using `passwordFile` options and leave integration up to the user. This keeps NixOS core simpler but pushes complexity to the end-user. | ||||||||
| - **systemd-creds Only**: Force all secrets to use `systemd-creds`. While powerful, `systemd-creds` does not natively support GitOps workflows (e.g. encrypting with age/PGP and committing to a repo) as easily as `sops` or `agenix` without significant wrapper tooling. | ||||||||
|
|
||||||||
| # Prior art | ||||||||
| [prior-art]: #prior-art | ||||||||
|
Comment on lines
+105
to
+106
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A lot of the prior art allows controlling the mode, user, groups of the secrets but this proposal doesn't mention any of this. This need is somewhat mitigated by systemd's LoadCredential but IMHO there are still many use cases (that for better or worse) still need to have files lying around owned by a particular user or group. What is the plan for supporting these use cases? |
||||||||
|
|
||||||||
| - **sops-nix**: Pioneered the `sops.secrets.<name>` pattern, which heavily inspired this RFC's interface. | ||||||||
| - **agenix**: Similar pattern using `age.secrets.<name>`. | ||||||||
|
|
||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Not critical but relevant IMHO. It also shows a different style where the secrets are just copied from the deployment host without any on-host decryption step. |
||||||||
| # Unresolved questions | ||||||||
| [unresolved]: #unresolved-questions | ||||||||
|
|
||||||||
| - Should `security.artifacts` also handle dynamically generated secrets (e.g. `nixos-generate-config` generating an SSH key) rather than just static deployed secrets? | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be very cool. For example sometimes services need secrets for auth between themselves but the secret doesn't matter as long as both services know it. I see a few use cases:
For example colmena supports 1 and 3 via a |
||||||||
|
|
||||||||
| # Future work | ||||||||
| [future]: #future-work | ||||||||
|
|
||||||||
| - Porting all `services.*` modules to natively consume `security.artifacts` instead of `passwordFile` strings. | ||||||||
| - Implementing automatic systemd dependency injection (automatically adding `After = [ "nixos-artifacts-secrets.target" ]` to services that reference an artifact). | ||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: