A hive that does not host authelia has no path to its own agent queue client secret. The mint writes the plaintext to a host directory whose other reader lives in a different container, so the host that mints is the only place both trees are addressable — which is where this unit runs. Four pieces, in the order they depend on each other: the leaf (glue-bao-tls.nix signs it, because the thing that owns a private key owns issuing from it), the module declaring its own cert/key options, the one-pairing glue file pointing them at that leaf, and the imports. The unit is gated on holding a client identity, never on deploy.bao.enable — that option is the co-location assumption itself, and the publisher is the case that assumption excludes. The secret is passed to bao as `value=@<path>`, never as an argv element: bao is an external binary, so an argument is world-readable in /proc for the life of the call. Refs #3853
153 lines
7.1 KiB
Nix
153 lines
7.1 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
|
|
|
|
# The secret publisher's, minted here for the reason the controller's
|
|
# line above gives — and it serves that case more often, not less: the
|
|
# publisher runs beside AUTHELIA, which is the one host guaranteed not
|
|
# to be this one whenever the store has a host of its own.
|
|
[ -s ${pkiDir}/secret-publisher.pem ] || ${signLeaf} ${pkiDir} secret-publisher \
|
|
${lib.escapeShellArg deployCfg.bao.secretPublisherCommonName} "" clientAuth
|
|
'';
|
|
};
|
|
};
|
|
}
|