Skip to content

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.

modules/fleet.nix
# 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 of yen.lib.kits.
  • (2) is the exports sink. Members write their own element there.
  • (3) is the generators sink, 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.

modules/access.nix
# 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 kit sshd, with the roles server and client. (2) puts db in the server role. The client role is web.
  • (3) says where the public host keys are read from.
  • (4) is the kit users, with the role default. A user has user, groups and shell.
  • (5) is the kit emergency-access: an authorizedKeys list for root, on db only.

web trusts the key of db, read from vars/per-machine/db/openssh/ssh.id_ed25519.pub. db has no known hosts:

result
{
knownHosts = {
db = {
hostNames = [
"db"
];
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDocsKeyNotReal db";
};
};
}
result
null

The secrets are in the plan of the generators, each on one machine:

result
[
"emergency-root@db"
"openssh@db"
"user-ops@web"
]

The server settings, and root on db only:

result
{
enable = true;
}
result
{
authorizedKeys = {
keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBreakGlassKeyNotReal admin"
];
};
}
result
null
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).

An sshd assembly with no server is a count of zero, and a client alone has nobody to trust:

variants/no-server.nix
# 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;
};
};
};
}
result
[
"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:

variants/missing-key.nix
# 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;
};
};
};
}
result
[
"known-hosts: edge has no openssh/ssh.id_ed25519.pub public key"
]

One user in two assemblies on one machine:

variants/same-user.nix
# 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";
};
};
}
result
[
"users: ops is set by assemblies ops and ops-again on web"
]
ContributeCommunitySponsor