From 92980f65b443b7fbc84ef5321e171aaa9e867790 Mon Sep 17 00:00:00 2001 From: Mutasem-mk4 <140179052+Mutasem-mk4@users.noreply.github.com> Date: Wed, 13 May 2026 05:39:58 +0300 Subject: [PATCH 1/3] rfcs: add RFC for security.artifacts --- rfcs/0000-security-artifacts.md | 98 +++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 rfcs/0000-security-artifacts.md diff --git a/rfcs/0000-security-artifacts.md b/rfcs/0000-security-artifacts.md new file mode 100644 index 000000000..f89e6d28b --- /dev/null +++ b/rfcs/0000-security-artifacts.md @@ -0,0 +1,98 @@ +--- +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.` accepts a submodule configuring the owner, group, mode, and target path of the secret. + +2. **Providers**: + The abstraction supports pluggable backends (`security.artifacts.provider`): + - `sops-nix` + - `agenix` + - `systemd-creds` + - `dummy` (for CI/CD environments where secrets are mocked plaintext) + +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" ]`. + +4. **Security Assertions**: + Evaluation-time assertions guarantee that the resolved paths of the secrets do not leak into `/nix/store`. + +# Examples and Interactions +[examples-and-interactions]: #examples-and-interactions + +A user deploying a Postgres database password using `sops-nix`: + +```nix +security.artifacts.enable = true; +security.artifacts.provider = "sops-nix"; + +security.artifacts.secrets."postgres-pw" = { + owner = "postgres"; + group = "postgres"; + mode = "0400"; +}; + +services.postgresql = { + enable = true; + # ... + # The module can directly read the provisioned artifact or accept the path. +}; +``` + +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 + +- **sops-nix**: Pioneered the `sops.secrets.` pattern, which heavily inspired this RFC's interface. +- **agenix**: Similar pattern using `age.secrets.`. + +# 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? + +# 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). From 94b126ea9d4e054f87de5d1b27190c5e616ee917 Mon Sep 17 00:00:00 2001 From: Mutasem-mk4 <140179052+Mutasem-mk4@users.noreply.github.com> Date: Wed, 13 May 2026 06:59:06 +0300 Subject: [PATCH 2/3] chore: rename RFC file to match PR number 0201 --- rfcs/{0000-security-artifacts.md => 0201-security-artifacts.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rfcs/{0000-security-artifacts.md => 0201-security-artifacts.md} (100%) diff --git a/rfcs/0000-security-artifacts.md b/rfcs/0201-security-artifacts.md similarity index 100% rename from rfcs/0000-security-artifacts.md rename to rfcs/0201-security-artifacts.md From d299fda695a33e8c9253e35797cded9e09126dd7 Mon Sep 17 00:00:00 2001 From: Mutasem-mk4 <140179052+Mutasem-mk4@users.noreply.github.com> Date: Wed, 13 May 2026 16:47:41 +0300 Subject: [PATCH 3/3] docs: update RFC to reflect per-secret providers and external backend --- rfcs/0201-security-artifacts.md | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/rfcs/0201-security-artifacts.md b/rfcs/0201-security-artifacts.md index f89e6d28b..88967540a 100644 --- a/rfcs/0201-security-artifacts.md +++ b/rfcs/0201-security-artifacts.md @@ -29,25 +29,28 @@ A unified interface will: This RFC proposes the `security.artifacts` module tree: 1. **Option Declaration**: - `security.artifacts.secrets.` accepts a submodule configuring the owner, group, mode, and target path of the secret. + `security.artifacts.secrets.` 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` + - `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" ]`. 4. **Security Assertions**: - Evaluation-time assertions guarantee that the resolved paths of the secrets do not leak into `/nix/store`. + 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 -A user deploying a Postgres database password using `sops-nix`: +### Basic Usage with `sops-nix`: ```nix security.artifacts.enable = true; @@ -57,12 +60,31 @@ security.artifacts.secrets."postgres-pw" = { owner = "postgres"; group = "postgres"; mode = "0400"; + source = ./secrets/postgres.yaml; }; services.postgresql = { enable = true; - # ... - # The module can directly read the provisioned artifact or accept the path. + # 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"; + }; }; ```