Secrets as data
yen never reads a secret. It plans which host gets which encrypted file, as data that a secrets tool (agenix, sops-nix) reads. A secret is an instance of a data kind; its owners are host refs.
# A secret is data: nothing spawns it. A host's `secrets` sink only takes secret refs.{ yen, lib, ... }:{ yen.kinds.fleet = { spawns.host = _: yen.host; sinks.out.reduce = yen.lib.reduce.merge; }; yen.kinds.secret.fields = { file = lib.types.str; # the encrypted file in the repo owners = yen.lib.is lib.isList; # host refs }; yen.kinds.host.sinks.secrets = { reduce = yen.lib.reduce.set; of = yen.kinds.secret; # the plan a secrets tool reads: agenix, sops-nix, or your own out = { host, secrets, ... }: { secretsPlan.${host.name} = map (s: { inherit (s) name file; path = "/run/secrets/${s.name}"; }) secrets; }; };
yen.fleet.main = { }; yen.flake = { out, ... }: out;}{ yen, ... }:{ yen.host.web = { }; yen.host.db = { }; yen.secret.tls = { file = "secrets/tls.age"; owners = [ yen.host.web ]; }; yen.secret.db-password = { file = "secrets/db-password.age"; owners = [ yen.host.web yen.host.db ]; }; yen.secret.old-token = { file = "secrets/old-token.age"; owners = [ ]; };}One component delivers each secret to its owners. Another reports secrets that nobody owns:
{ yen, lib, ... }:{ # each host gets the secrets that name it yen.aspects.deliver = { at = yen.kinds.host; cycle = { scope, ... }: { secrets = lib.attrValues (lib.filterAttrs (_: s: lib.any (yen.lib.same scope) s.owners) yen.secret); }; }; # a secret that no host owns is an error, not a silent leftover yen.aspects.orphans = { at = yen.kinds.fleet; errors = map (s: "secret ${s.name} (${s.file}) has no owner") ( lib.filter (s: s.owners == [ ]) (lib.attrValues yen.secret) ); };}{ db = [ { file = "secrets/db-password.age"; name = "db-password"; path = "/run/secrets/db-password"; } ]; web = [ { file = "secrets/db-password.age"; name = "db-password"; path = "/run/secrets/db-password"; } { file = "secrets/tls.age"; name = "tls"; path = "/run/secrets/tls"; } ];}[ "secret old-token (secrets/old-token.age) has no owner"]Refs, not names
Section titled “Refs, not names”The sink’s of is the secret kind, so only a ref passes. A name is refused, with the scope and the aspect:
# A secret by name, not by ref: `of` refuses it.{ yen, ... }:{ yen.aspects.shortcut = { at = yen.host.db; secrets = "tls"; };}[ "sink secrets: element of aspect shortcut from fleet:main/host:db fails `of`" "secret old-token (secrets/old-token.age) has no owner"]