Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions rfcs/0201-security-artifacts.md
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`):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The abstraction supports pluggable backends (`security.artifacts.provider`):
The abstraction supports pluggable backends (`security.artifacts.defaultProvider`):

Nit:

- `sops-nix`
- `agenix`
- `systemd-creds`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 x is not decryptable". In this case it is probably better to have a distinct target per-key so that most services can still boot up. Providers that do just copy secrets in one batch or otherwise can't have partial failures can just have these all be the same but providers that do have this issue can create a target per 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";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 path an "out parameter" which is set by the provider and the provider picks what directory to put files in (possibly with some provider-specific configuration).

};
};
```

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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>`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- **colmena**: Similar pattern using `deployment.keys.<name>` (inherited from nixops).

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?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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:

  1. Generated per-deploy.
  2. Generated one for the lifetime of the host.
  3. Generated deterministically but just to use logic (decrypt, base64, ...)

For example colmena supports 1 and 3 via a keyCommand option.


# 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).