hyperhive/nix/host-modules/glue-bao-tls.nix
atlas c22db5eb57 swarm-bao-tls: drop the unreachable CN fallback
`clientCn` fell back to `cfg.domain` when `hiveName` was unset. That branch
cannot run: `hive-network.nix` asserts `hiveName != null` under
`mkIf services.hyperhive.enable`, and this file's `config` is gated on the same
predicate, so any host that evaluates the conditional has already failed the
assertion.

Worse than dead, it read as a second supported spelling of a hive's identity —
which is what a cert-auth role matches on. It was not even the hive's own
domain: `cfg` here is `services.hyperhive.swarm.bao`, so the fallback resolved
to the store's address, one string shared by every hive in the swarm and the
same CN the server leaf carries.

Reading the option directly matches what other modules needing the name already
do (`hive-c0re/environment.nix`).
2026-09-10 00:25:07 +02:00

146 lines
6.6 KiB
Nix

# Glue: give the secret store a PKI of its own, and point it at it.
#
# ONE PAIRING PER FILE — `glue-<consumer>-<what>.nix`. A single module holding
# every co-location default becomes the file nobody dares change, because a
# reader cannot tell which of its rules their deployment is subject to. Each
# of these should be deletable on its own, and deleting this one leaves a
# store that takes operator-provided certificates and nothing else.
#
# ⚠️ Why the PKI lives HERE and not in ./swarm-bao.nix: the store must have no
# opinion about where its identity comes from. Minting is an opinion — the
# most consequential one available — so it belongs to the glue that decides
# this deployment self-signs, not to the service that merely serves what it is
# handed. A deployment with a real internal CA drops this file and names its
# own paths; nothing in the store changes.
#
# ⚠️ Not the hive CA and not the swarm CA. The store will eventually
# distribute both, and an authority you must already hold a certificate from
# cannot be one the store hands out — reach the store to get the CA material,
# need a cert from that CA to reach the store. This CA signs a fixed, short
# list of leaves and distributes nothing, so it cannot enter that cycle.
#
# ⚠️ Files like this are the only place a `deploy.<foo>` value may derive from
# a `deploy.<bar>.enable`. Everywhere else that is forbidden. The exception
# earns itself: the derivation happens either way, and the alternative is
# having it spread through the service modules where it is invisible.
#
# Everything is `mkDefault`. An operator naming their own paths wins.
{
pkgs,
lib,
config,
...
}:
let
hyperhiveCfg = config.services.hyperhive;
deployCfg = hyperhiveCfg.deploy;
cfg = hyperhiveCfg.swarm.bao;
# Host-side, outside the container's tree, for the same reason the raft data
# is: `nixos-container destroy` must not take it. Losing the CA key means
# re-issuing every client certificate in the swarm.
pkiDir = "/var/lib/swarm-bao-pki";
# What a reader calls itself to the store. The hive's name, because a bao
# cert-auth role matches on the CN — this is an interface, not a label.
# No fallback: `hiveName` is asserted set for every hyperhive host, which is
# the same condition this file's `config` is gated on. A fallback here reads
# as a second supported spelling and there is no such thing.
clientCn = hyperhiveCfg.hiveName;
# $1 dir $2 basename $3 CN $4 SAN or "" $5 EKU
signLeaf = pkgs.writeShellScript "swarm-bao-sign-leaf" ''
set -euo pipefail
d="$1"; base="$2"; cn="$3"; sans="$4"; eku="$5"
csr="$(mktemp "$d/$base.csr.XXXXXX")"
ext="$(mktemp "$d/$base.ext.XXXXXX")"
trap 'rm -f "$csr" "$ext"' EXIT
openssl req -newkey rsa:4096 -nodes -sha256 \
-keyout "$d/$base-key.pem" -out "$csr" -subj "/CN=$cn"
{
[ -n "$sans" ] && printf 'subjectAltName=%s\n' "$sans"
printf 'basicConstraints=critical,CA:FALSE\n'
printf 'keyUsage=critical,digitalSignature,keyEncipherment\n'
printf 'extendedKeyUsage=%s\n' "$eku"
} > "$ext"
openssl x509 -req -in "$csr" -CA "$d/ca.pem" -CAkey "$d/ca-key.pem" \
-CAcreateserial -days 3650 -sha256 -extfile "$ext" -out "$d/$base.pem"
chmod 0600 "$d/$base-key.pem"
chmod 0644 "$d/$base.pem"
'';
in
{
config = lib.mkIf (hyperhiveCfg.enable && deployCfg.bao.enable) {
services.hyperhive.deploy.bao = {
serverCertFile = lib.mkDefault "${pkiDir}/server.pem";
serverKeyFile = lib.mkDefault "${pkiDir}/server-key.pem";
clientCaFile = lib.mkDefault "${pkiDir}/ca.pem";
# A reader on this host, which happens to be the host that mints. Only
# these three are what a reader elsewhere needs placed by hand; that they
# collapse to the same CA file here is a property of self-signing, not of
# the pairing.
clientCertFile = lib.mkDefault "${pkiDir}/client.pem";
clientKeyFile = lib.mkDefault "${pkiDir}/client-key.pem";
serverCaFile = lib.mkDefault "${pkiDir}/ca.pem";
};
# Idempotent on ABSENCE, never on content. Re-issuing the CA invalidates
# every client certificate already trusting it, so a rebuild that
# "refreshed" it would lock every reader in the swarm out at once — the
# same rule the store's TPM PIN unit follows, for a sharper reason.
# Declared beside the unit it names, not in the store's module: an entry
# exists only where the unit does, and this one is minted by glue that not
# every hive runs.
services.hyperhive.swarm.otel.journaldUnits = [ "swarm-bao-pki" ];
systemd.services.swarm-bao-pki = {
description = "mint the swarm secret store's own CA and leaves";
before = [ "swarm-bao-certs.service" ];
requiredBy = [ "swarm-bao-certs.service" ];
path = [
pkgs.openssl
pkgs.coreutils
];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
set -euo pipefail
install -d -m 0700 ${pkiDir}
if [ ! -s ${pkiDir}/ca.pem ]; then
openssl req -x509 -newkey rsa:4096 -nodes -sha256 -days 3650 \
-keyout ${pkiDir}/ca-key.pem -out ${pkiDir}/ca.pem \
-subj "/CN=swarm-bao-ca ${cfg.domain}" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
chmod 0600 ${pkiDir}/ca-key.pem
chmod 0644 ${pkiDir}/ca.pem
fi
# The store's own identity, and the identity of a reader on this host.
# A reader elsewhere gets its leaf from this CA out of band that is
# what makes the store reachable from another machine at all, and why
# the CA is a file rather than a service.
[ -s ${pkiDir}/server.pem ] || ${signLeaf} ${pkiDir} server \
${lib.escapeShellArg cfg.domain} ${lib.escapeShellArg "DNS:${cfg.domain}"} serverAuth
[ -s ${pkiDir}/client.pem ] || ${signLeaf} ${pkiDir} client \
${lib.escapeShellArg clientCn} "" clientAuth
# Minted whether or not a controller runs here, because the case it
# serves is the one where it does not: a controller elsewhere needs a
# leaf from this CA and has no way to sign one. Issuing it here turns
# "obtain a certificate out of band" into "copy this file".
#
# Its own CN rather than the reader's above: the controller's policy
# lets it create roles for every hive, and the reader's leaf carries
# this hive's name.
[ -s ${pkiDir}/controller.pem ] || ${signLeaf} ${pkiDir} controller \
${lib.escapeShellArg deployCfg.bao.controllerCommonName} "" clientAuth
'';
};
};
}