Access kits
The library ships kits for access to machines: ssh servers and clients, user accounts, and a break-glass key. A kit is data with roles, and an assembly puts your machines in its roles. See Kits batteries for the model. You import a kit as a module. The fleet below imports three, and it writes the sinks they need: the fleet’s own words.
# The fleet's words: machines, kits and assemblies, and the sinks the access kits write to and read from.{ yen, lib, inputs, ...}:let kits = yen.lib.batteries.kits; inherit (yen.lib) opt holds; attrs = yen.lib.attrs; assemblies = lib.attrValues yen.assembly; # tree requests: every assembly and machine, with their data own.instances = yen.kinds.assembly; hosts.instances = yen.kinds.machine; # what a machine gets, the NixOS values of its modules merged for the page merged = ms: lib.foldl' lib.recursiveUpdate { } (lib.filter lib.isAttrs ms);in{ imports = [ # (1) inputs.yen.lib.kits.sshd inputs.yen.lib.kits.users inputs.yen.lib.kits.emergency-access ]; yen.kinds.fleet = { spawns.machine = _: yen.machine; sinks.out.reduce = yen.lib.reduce.merge; sinks.exports.reduce = kits.exports { isMember = holds yen.kinds.member; }; # (2) sinks.generators.reduce = yen.lib.batteries.vars.generators { # (3) machineOf = s: if holds yen.kinds.member s then s.parent else s; }; }; yen.kinds.machine = { spawns.member = kits.members { inherit assemblies holds; }; # (4) sinks.nixos.out = { scope, nixos, ... }: { nixos.${scope.name} = merged nixos; }; sinks.os.out = { scope, os, ... }: { os.${scope.name} = merged os; }; }; yen.kinds.member = { }; yen.kinds.kit.fields = { roles = opt attrs { }; constraints = opt attrs { }; exports = opt (yen.lib.listOf yen.lib.str) [ ]; }; yen.kinds.assembly.fields = { kit = yen.kinds.kit; roles = opt attrs { }; }; yen.fleet.main = { }; yen.machine = { web = { }; db = { }; }; yen.flake = { out, ... }: out;
yen.aspects = { # the fleet is read through a tree request, never as refs: an aspect reads instance data only through args contract = { at = yen.kinds.fleet; cycle = { tree, ... }: { tree = [ own hosts ]; errors = [ (kits.contract { # (5) assemblies = tree.select own; machines = tree.select hosts; inherit holds; }) ]; }; }; settings-typed = { at = yen.kinds.member; errors = kits.settings-typed; # (6) }; };}(1)imports the three kits ofyen.lib.kits.(2)is theexportssink. Members write their own element there.(3)is thegeneratorssink, for the secrets that the kits generate.(4)gives each machine one member scope for each (assembly, role) it takes.(5)reads the assemblies and the machines through a tree request, and checks the contract.(6)checks that the settings of every member have their types.
The assemblies. No machine is named by a string: each role’s machines are a ref.
# Four assemblies: db serves ssh and trusts nothing, web trusts db, ops logs in on web, a break-glass key on db.{ yen, ... }:{ yen.assembly = { ssh = { kit = yen.kit.sshd; # (1) roles = { server.at = yen.machine.db; # (2) client = { at = yen.machine.web; settings.root = ../vars/per-machine; # (3) where the public host keys are read from }; }; }; ops = { kit = yen.kit.users; roles.default = { at = yen.machine.web; settings = { # (4) user = "ops"; groups = [ "wheel" ]; }; }; }; break-glass = { kit = yen.kit.emergency-access; roles.default = { at = yen.machine.db; settings.authorizedKeys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBreakGlassKeyNotReal admin" ]; # (5) }; }; };}(1)is the kitsshd, with the rolesserverandclient.(2)putsdbin the server role. The client role isweb.(3)says where the public host keys are read from.(4)is the kitusers, with the roledefault. A user hasuser,groupsandshell.(5)is the kitemergency-access: anauthorizedKeyslist for root, ondbonly.
web trusts the key of db, read from vars/per-machine/db/openssh/ssh.id_ed25519.pub. db has no known hosts:
{ knownHosts = { db = { hostNames = [ "db" ]; publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDocsKeyNotReal db"; }; };}nullThe secrets are in the plan of the generators, each on one machine:
[ "emergency-root@db" "openssh@db" "user-ops@web"]The server settings, and root on db only:
{ enable = true;}{ authorizedKeys = { keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBreakGlassKeyNotReal admin" ]; };}nullThe kits
Section titled “The kits”| Kit | Roles | Does |
|---|---|---|
sshd |
server, client |
a server runs openssh, generates a host key (the private file secret, the public file not) and exports where the public key is; a client gets programs.ssh.knownHosts for the servers of its own assembly |
users |
default |
user, groups, shell (a function from pkgs to a package: the fleet has no package ref); a password hash, secret, generated on the machine of that member only |
emergency-access |
default |
authorizedKeys (public keys are data) for root, and a root password hash, secret, on machines that hold a member only |
yen.lib.batteries.vars.known-hosts { from; rootOf; entry; keep ? } is the battery behind the sshd client. It reads the
public keys with public-value and gives two values: nixos (the programs.ssh.knownHosts records) and
errors (one record for each key file that is missing, never a throw).
Errors
Section titled “Errors”An sshd assembly with no server is a count of zero, and a client alone has nobody to trust:
# an sshd assembly with no server: a count of zero, and a client with no server in its assembly{ yen, ... }:{ yen.assembly = { none = { kit = yen.kit.sshd; roles.server.at = _: false; }; alone = { kit = yen.kit.sshd; roles.client = { at = yen.machine.web; settings.root = ../vars/per-machine; }; }; };}[ "kits: assembly none role server: 0 machines, at least 1" "sshd: alone.client has no server in its assembly"]A server without a public key file is an error record. The fleet still evaluates:
# a server whose public host key was never generated: there is no file under vars/per-machine/edge{ yen, ... }:{ yen.machine.edge = { }; yen.assembly.ssh-edge = { kit = yen.kit.sshd; roles = { server.at = yen.machine.edge; client = { at = yen.machine.web; settings.root = ../vars/per-machine; }; }; };}[ "known-hosts: edge has no openssh/ssh.id_ed25519.pub public key"]One user in two assemblies on one machine:
# the users kit twice for one user on one machine{ yen, ... }:{ yen.assembly.ops-again = { kit = yen.kit.users; roles.default = { at = yen.machine.web; settings.user = "ops"; }; };}[ "users: ops is set by assemblies ops and ops-again on web"]