A CA that signs exactly two things -- the store's server certificate and the client certificate of whoever reads from it -- and distributes nothing. Not the hive CA, not the swarm CA: the store will distribute both, and an authority you must already hold a certificate from cannot be one the store hands out. Not the gateway's HTTPS material either, self-signed or ACME; that is a different trust domain with a different audience. The minting lives here rather than in swarm-bao.nix because it is an opinion about where the store's identity comes from -- the most consequential one available. The service serves what it is handed. A deployment with a real internal CA drops this file and names its own paths in serverCertFile / clientCaFile, and nothing in the store changes. Ordering simplifies too: with the unit and its consumer in one module, before/requiredBy is internal rather than a cross-module fact. Idempotent on ABSENCE only. Re-issuing the CA invalidates every client certificate already trusting it, so a rebuild that refreshed it would lock out every reader in the swarm at once.
119 lines
5.1 KiB
Nix
119 lines
5.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 exactly two
|
|
# things 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.
|
|
clientCn = if hyperhiveCfg.hiveName != null then hyperhiveCfg.hiveName else cfg.domain;
|
|
|
|
# $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";
|
|
};
|
|
|
|
# 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.
|
|
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
|
|
'';
|
|
};
|
|
};
|
|
}
|