glue-bao-tls: mint the store an identity it can hold before it is up
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.
This commit is contained in:
parent
fa540ca1ec
commit
9bbc74ee51
2 changed files with 120 additions and 0 deletions
|
|
@ -23,6 +23,7 @@
|
|||
./hive-priv.nix
|
||||
./hive-tls.nix
|
||||
./otel.nix
|
||||
./glue-bao-tls.nix
|
||||
./swarm-authelia.nix
|
||||
./swarm-bao.nix
|
||||
./swarm-ca.nix
|
||||
|
|
|
|||
119
nix/host-modules/glue-bao-tls.nix
Normal file
119
nix/host-modules/glue-bao-tls.nix
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# 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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue