Watch
0
0
Fork
You've already forked hyperhive
0

swarm-bao-pki: sign every other hive's three store leaves

The store host's `swarm-bao-pki` now also signs, for every hive in
`swarm.hives` other than its own, the reader (`<hive>`), queue-agent
(`<queueAgentCommonNamePrefix>-<hive>`) and gateway-leaf
(`<hiveLeafCommonNamePrefix>-<hive>`) leaves, into
/var/lib/swarm-bao-pki/hives/<hive>/ with the basenames the own hive's
leaves use. Same CA, same `signLeaf`, idempotent on absence like every
other leaf in the unit. `signLeaf` now takes the CA from the top of the
PKI directory rather than from the leaf's directory; every existing
caller passed that directory, so their output is unchanged.

The operator still copies the files to the other hive's host and names
them in config; only the openssl step against the CA goes away
(mara on #5016, c95685: "ok mint").

Closes #5016
This commit is contained in:
atlas 2026-10-12 19:15:23 +02:00 • committed by mara
commit 48a728df17
6 changed files with 84 additions and 14 deletions

View file

@ -103,8 +103,23 @@ certificates come from a real internal CA; the store has no opinion about
which. A host that does **not** deploy the store names the reader's three which. A host that does **not** deploy the store names the reader's three
itself, plus a pair for every principal it runs — see itself, plus a pair for every principal it runs — see
[per-principal identities](secrets.md#per-principal-identities) for the list. [per-principal identities](secrets.md#per-principal-identities) for the list.
The operator issues those leaves out of band; they're the credentials the They're the credentials the store can't hand you, being what opens it, so the
store can't hand you, being what opens it. store's host signs them and the operator copies them to the host that needs
them and names them in config. The service principals' leaves are at the top of
`/var/lib/swarm-bao-pki/`, named after the principal.
For every hive in `swarm.hives` other than its own, the store's host signs
that hive's three leaves into `/var/lib/swarm-bao-pki/hives/<hive>/`:
| file | subject | name it in |
| ---------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------- |
| `client.pem`, `client-key.pem` | `<hive>` | `deploy.bao.clientCertFile`, `.clientKeyFile` |
| `queue-agent.pem`, `queue-agent-key.pem` | `<deploy.bao.queueAgentCommonNamePrefix>-<hive>` | `deploy.bao.queueAgentClientCertFile`, `.queueAgentClientKeyFile` |
| `hive-leaf.pem`, `hive-leaf-key.pem` | `<deploy.bao.hiveLeafCommonNamePrefix>-<hive>` | `deploy.hive-controller.tls.baoHiveIdentityCertFile`, `.baoHiveIdentityKeyFile` |
Copy `/var/lib/swarm-bao-pki/ca.pem` with them and name it in
`deploy.bao.serverCaFile`. A hive added to `swarm.hives` gets its directory on
the store host's next deploy; a leaf already there is never re-signed.
⚠️ Not the gateway's HTTPS certificates and not the store's `pki` root — this is **mTLS ⚠️ Not the gateway's HTTPS certificates and not the store's `pki` root — this is **mTLS
between services and the store**, a separate trust domain, because a store between services and the store**, a separate trust domain, because a store

View file

@ -35,8 +35,10 @@ from the store's own client CA, with the subject
- On the host running the store, `swarm-bao-pki.service` mints it at - On the host running the store, `swarm-bao-pki.service` mints it at
`/var/lib/swarm-bao-pki/hive-leaf.pem` and the two options default to it. `/var/lib/swarm-bao-pki/hive-leaf.pem` and the two options default to it.
- On every other host, copy a leaf with that subject from the store's host - On every other host, copy `hive-leaf.pem` and `hive-leaf-key.pem` from
and set both options, before the host's next deploy. `/var/lib/swarm-bao-pki/hives/<hiveName>/` on the store's host, which
`swarm-bao-pki.service` mints for every other hive in `swarm.hives`, and
set both options, before the host's next deploy.
Without it, `hive-tls-resign.service` fails with a message naming both Without it, `hive-tls-resign.service` fails with a message naming both
options, and so does `swarm-services-cert.service` on a hive fronting no options, and so does `swarm-services-cert.service` on a hive fronting no

View file

@ -426,8 +426,11 @@ leaf two units share: the forge's host holds one store identity for the token it
publishes and the OIDC client secret it reads. publishes and the OIDC client secret it reads.
On a host that mints its own PKI, `glue-bao-tls.nix` signs all seven and defaults On a host that mints its own PKI, `glue-bao-tls.nix` signs all seven and defaults
all fourteen options, and there is nothing to do. Elsewhere you issue each leaf from all fourteen options, and there is nothing to do. Elsewhere you copy each leaf from
that CA out of band and name it here — one file per principal rather than one file the store's host and name it here: a service principal's from the top of
`/var/lib/swarm-bao-pki/`, another hive's queue-agent pair from
`/var/lib/swarm-bao-pki/hives/<hive>/` ([bao.md § TLS](bao.md#tls) lists that
directory). That's one file per principal rather than one file
shared by seven, which is the whole of what this buys. `swarm-ci-runner.nix` shared by seven, which is the whole of what this buys. `swarm-ci-runner.nix`
refuses the build wherever the runner runs without its pair, as refuses the build wherever the runner runs without its pair, as
`swarm-review-classifier.nix` does for the classifier and `swarm-grafana.nix` `swarm-review-classifier.nix` does for the classifier and `swarm-grafana.nix`

View file

@ -49,7 +49,13 @@ let
# thing. # thing.
clientCn = hyperhiveCfg.hiveName; clientCn = hyperhiveCfg.hiveName;
# Every other hive in the swarm directory. This host's own hive is signed
# into the top of the directory, beside the leaves its units read.
otherHives = lib.filter (h: h != clientCn) (lib.attrNames hyperhiveCfg.swarm.hives);
# $1 dir $2 basename $3 CN $4 SAN or "" $5 EKU # $1 dir $2 basename $3 CN $4 SAN or "" $5 EKU
# The CA is always the one at the top of `pkiDir`, whichever dir the leaf
# lands in.
signLeaf = pkgs.writeShellScript "swarm-bao-sign-leaf" '' signLeaf = pkgs.writeShellScript "swarm-bao-sign-leaf" ''
set -euo pipefail set -euo pipefail
d="$1"; base="$2"; cn="$3"; sans="$4"; eku="$5" d="$1"; base="$2"; cn="$3"; sans="$4"; eku="$5"
@ -65,7 +71,7 @@ let
printf 'keyUsage=critical,digitalSignature,keyEncipherment\n' printf 'keyUsage=critical,digitalSignature,keyEncipherment\n'
printf 'extendedKeyUsage=%s\n' "$eku" printf 'extendedKeyUsage=%s\n' "$eku"
} > "$ext" } > "$ext"
openssl x509 -req -in "$csr" -CA "$d/ca.pem" -CAkey "$d/ca-key.pem" \ openssl x509 -req -in "$csr" -CA ${pkiDir}/ca.pem -CAkey ${pkiDir}/ca-key.pem \
-CAcreateserial -days 3650 -sha256 -extfile "$ext" -out "$d/$base.pem" -CAcreateserial -days 3650 -sha256 -extfile "$ext" -out "$d/$base.pem"
chmod 0600 "$d/$base-key.pem" chmod 0600 "$d/$base-key.pem"
chmod 0644 "$d/$base.pem" chmod 0644 "$d/$base.pem"
@ -162,9 +168,9 @@ in
fi fi
# The store's own identity, and the identity of a reader on this host. # 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 # A reader elsewhere gets its leaf from this CA too — that is what
# what makes the store reachable from another machine at all, and why # makes the store reachable from another machine at all, and why the
# the CA is a file rather than a service. # CA is a file rather than a service.
[ -s ${pkiDir}/server.pem ] || ${signLeaf} ${pkiDir} server \ [ -s ${pkiDir}/server.pem ] || ${signLeaf} ${pkiDir} server \
${lib.escapeShellArg cfg.domain} ${lib.escapeShellArg "DNS:${cfg.domain}"} serverAuth ${lib.escapeShellArg cfg.domain} ${lib.escapeShellArg "DNS:${cfg.domain}"} serverAuth
[ -s ${pkiDir}/client.pem ] || ${signLeaf} ${pkiDir} client \ [ -s ${pkiDir}/client.pem ] || ${signLeaf} ${pkiDir} client \
@ -288,11 +294,30 @@ in
# `pki/issue/<hiveLeafPkiRolePrefix>-<hive>` and nothing else. Its # `pki/issue/<hiveLeafPkiRolePrefix>-<hive>` and nothing else. Its
# subject carries this hive's name, like the queue-agent leaf # subject carries this hive's name, like the queue-agent leaf
# above, because ./swarm-bao.nix writes one role # above, because ./swarm-bao.nix writes one role
# per hive. Only where a hive runs; hives on other hosts are issued # per hive. Only where a hive runs; the other hives' are below.
# theirs out of band.
[ -s ${pkiDir}/hive-leaf.pem ] || ${signLeaf} ${pkiDir} hive-leaf \ [ -s ${pkiDir}/hive-leaf.pem ] || ${signLeaf} ${pkiDir} hive-leaf \
${lib.escapeShellArg "${deployCfg.bao.hiveLeafCommonNamePrefix}-${clientCn}"} "" clientAuth ${lib.escapeShellArg "${deployCfg.bao.hiveLeafCommonNamePrefix}-${clientCn}"} "" clientAuth
''} ''}
# The reader, queue-agent and gateway-leaf leaves of every other hive in
# the swarm directory: the files an operator copies to that hive's
# host. One directory per hive, with the basenames this hive's leaves
# use, so a hive's set is one directory to copy.
${lib.concatMapStrings (
h:
let
d = lib.escapeShellArg "${pkiDir}/hives/${h}";
in
''
install -d -m 0700 ${d}
[ -s ${d}/client.pem ] || ${signLeaf} ${d} client \
${lib.escapeShellArg h} "" clientAuth
[ -s ${d}/queue-agent.pem ] || ${signLeaf} ${d} queue-agent \
${lib.escapeShellArg "${deployCfg.bao.queueAgentCommonNamePrefix}-${h}"} "" clientAuth
[ -s ${d}/hive-leaf.pem ] || ${signLeaf} ${d} hive-leaf \
${lib.escapeShellArg "${deployCfg.bao.hiveLeafCommonNamePrefix}-${h}"} "" clientAuth
''
) otherHives}
''; '';
}; };
}; };

View file

@ -147,8 +147,9 @@ in
would evaluate, deploy and log in — and undo the split. would evaluate, deploy and log in — and undo the split.
On a hive that runs the store, glue-bao-tls.nix supplies both as On a hive that runs the store, glue-bao-tls.nix supplies both as
defaults and there is nothing to do. Elsewhere the leaf is issued defaults and there is nothing to do. Elsewhere copy the pair from
from that CA out of band and named here — see docs/swarm/secrets.md. /var/lib/swarm-bao-pki/hives/<hive>/ on the store's host and name it
here — see docs/swarm/bao.md.
''; '';
} }
]; ];

View file

@ -1289,6 +1289,30 @@ let
t.baoHiveIdentityCertFile t.baoHiveIdentityCertFile
!= baoGrantHere.services.hyperhive.deploy.bao.servicesIssuerClientCertFile; != baoGrantHere.services.hyperhive.deploy.bao.servicesIssuerClientCertFile;
} }
{
# The other hives' side: every hive in the directory but this one gets
# the same three leaves, under its own name, in a directory of its own
# that the operator copies to its host.
name = "the PKI unit signs every other hive's reader, queue-agent and gateway-leaf identities";
ok =
let
s = baoGrantTwoHives.systemd.services.swarm-bao-pki.script;
b = baoGrantTwoHives.services.hyperhive.deploy.bao;
signs =
file: cn: lib.hasInfix "/hives/h2/${file}.pem ]" s && lib.hasInfix " ${cn} \"\" clientAuth" s;
in
signs "client" "h2"
&& signs "queue-agent" "${b.queueAgentCommonNamePrefix}-h2"
&& signs "hive-leaf" "${b.hiveLeafCommonNamePrefix}-h2"
# This hive's own leaves stay at the top of the directory.
&& !(lib.hasInfix "/hives/h1/" s);
}
{
# What makes the one above mean something: a hive outside the directory
# gets nothing, so a store with only its own hive signs no other set.
name = "the PKI unit signs no other hive's leaves when the directory holds only its own";
ok = !(lib.hasInfix "swarm-bao-pki/hives/" baoGrantHere.systemd.services.swarm-bao-pki.script);
}
{ {
name = "a gateway-leaf role prefix outside swarm-* is refused, naming the option"; name = "a gateway-leaf role prefix outside swarm-* is refused, naming the option";
ok = ok =