Nixos-artifacts: one interface for secrets across agenix, sops-nix, and more

Hi everyone,

I’ve been working on nixos-artifacts for a while now, a small framework that gives you one common interface for managing secrets and generated files in NixOS — regardless of which backend stores them (agenix, sops-nix, …).

Status: experimental (like “nix flakes” :grinning_face_with_smiling_eyes:). The user-facing API is usable today; backend internals may still change as new features land, but those changes shouldn’t affect your declarations.

Why?

In larger setups, secret management often gets in my way when spinning up test scenarios. I lean heavily on NixOS modules, but they are not useful when I’m developing or updating modules. So I deploy test setups, but usually I don’t want to deploy real secrets (sometimes I do, e.g. API keys, but most of the time I don’t), So I needed to separate secret generation from secret serialization/deployment. nixos-artifacts pulls the declaration apart from the storage:

  • You describe what the secret is and how it should be created (files, prompts, generator).
  • The backend decides how it’s stored.
  • You can mix backends per artifact in the same configuration.

Inspired by Clan vars and NixOS PR #370444.

What it looks like

A simple secret with a user prompt:

artifacts.store.test = {
  files.secret = { };
  prompts.test.description = "test input";
  generator = pkgs.writers.writeBash "test" ''
    cat $prompts/test > $out/secret
  '';
};

A generated secret used by a service:

artifacts.store.attic = {
  files.env = {
    owner = "atticd";
    group = "atticd";
    path = "/var/lib/attic/secrets/env";
  };
  generator = pkgs.writers.writeBash "generate-attic" ''
    cat >"$out/env" <<EOF
    ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64=$(${pkgs.openssl}/bin/openssl genrsa -traditional 4096 | base64 -w0)
    EOF
  '';
};

services.atticd.environmentFile =
  config.artifacts.store.attic.files.env.path;

Same declaration — change the backend, not the code.

Pick a default backend for the whole machine:

artifacts.default.backend = "agenix";

…or override it for a single artifact (e.g. keep most secrets in agenix, but
ship one through sops-nix):

artifacts.store.attic.backend = "sops-nix";

Status

Working today

  • :white_check_mark: agenix backend
  • :white_check_mark: Home Manager support
  • :white_check_mark: Per-machine and shared artifacts
  • :white_check_mark: CLI written in Rust

Planned

  • :construction: sops-nix backend
  • :construction: On-the-fly “dummy” backend — generates secrets at activation time, no persistence; great for testing more complex setups
  • :construction: Public / private artifact parts — e.g. a WireGuard pair where the public key is shareable and the private key is sealed by the backend
  • :construction: Artifact dependencies — one artifact can consume another’s output (CA chains, or config templates that need a generated secret in place)
  • :construction: Systemd integration — per-artifact units so your service can BindsTo= “this secret is present” and start/stop accordingly

The backend interface is small (check + serialize scripts), so adding new backends is straightforward. There’s a BACKEND_GUIDE if you want to write one.

What I’m looking for

  • Feedback on the design — Does the abstraction make sense? What’s missing?
  • Use cases — does this fit your setup? What would block adoption?

Thanks!

13 Likes

Thanks for sharing! I might give it a try.

Before I decide whether this is acceptable for me to use, I have to ask: what keyboard layout did you use while writing it ?

Just kidding, obviously. :wink:

2 Likes

looks like the recently greenlit vars GSoC proposal has some overlap with this, curious how they’ll end up comparing

1 Like

True, the reason I’m posting this here is the Summit. I realized that basically nobody knows about my project, so I wanted to put it out there.

This is cool, could you post when sops-nix is finished?

1 Like

Yes, keep you informed here, on new backends and features

1 Like