Compare commits

..
2 changed files with 34 additions and 111 deletions

View file

@ -78,51 +78,15 @@ on.
## Adopting the hierarchy on an existing hive ## Adopting the hierarchy on an existing hive
A hive that predates the swarm root carries a self-signed `ca.pem`, and An existing `ca.pem` is never re-rooted automatically — swapping it
adopting the hierarchy means replacing it. That invalidates an anchor would break every consumer that already trusts it, and agents only pick
consumers already trust, and they refresh on their own schedule — agents up new trust when their container restarts. To adopt, delete `ca.pem` +
only pick up new trust when their container restarts, peers only on `ca-key.pem` under `tls.stateDir` and restart `hive-tls-ca.service`;
their own rebuild. **Who is allowed to decide that is what splits the the CA is re-issued under the root and the leaf re-signed. Until then
two cases.** the hive serves TLS exactly as before and is simply not part of the
swarm's trust hierarchy.
### Where this host owns the root (`autoConfigure`) Making that adoption a first-class, non-disruptive operation — rather
than a documented `rm` — is tracked separately; the mechanism it needs
Adoption happens by itself, once. `hive-tls-ca.service` notices that (carrying the previous CA in the trust bundle across the overlap)
`ca.pem` does not chain to the root, keeps the old certificate as already exists.
`ca-previous.pem`, and re-issues under the root; the next leaf is signed
by the new CA.
It is safe to automate here precisely because this is the
all-on-one-host shape: every consumer is on this box, so "when will they
have refreshed" is knowable rather than guessed.
The old CA stays in `trust-bundle.pem` afterwards, so adoption is
**additive to the anchor set before it is subtractive** — a container
that has not restarted yet still validates. Removing `ca-previous.pem`
is a deliberate later step: how long is long enough is a property of the
deployment, not something the unit can know.
A marker file (`.swarm-ca-adopted`) records that this ran. Its *absence*
is the trigger, so adoption fires once per hive rather than being
re-decided on every activation.
### Everywhere else
No automatic adoption. `hive-tls-ca.service` fails, loudly, naming both
certificates and giving the two-command recipe:
```
rm <tls.stateDir>/ca.pem <tls.stateDir>/ca-key.pem
systemctl restart hive-tls-ca.service
```
Failing rather than warning is deliberate: a hive whose CA does not
chain to the root it has been given is misconfigured, and a warning in a
build log is not something anyone reads twice.
To keep the current CA on purpose — a hive that deliberately stays
outside the hierarchy, or one mid-migration — `touch` the marker file
named in the message. That is a decision, and it is recorded as one.
A hive with **no** root configured at all is not affected by any of
this: it self-signs exactly as it always has.

View file

@ -248,49 +248,6 @@ in
root=${lib.escapeShellArg "${swarmCaCfg.stateDir}/root.pem"} root=${lib.escapeShellArg "${swarmCaCfg.stateDir}/root.pem"}
rootk=${lib.escapeShellArg "${swarmCaCfg.stateDir}/root-key.pem"} rootk=${lib.escapeShellArg "${swarmCaCfg.stateDir}/root-key.pem"}
prev="$d/ca-previous.pem"
marker="$d/.swarm-ca-adopted"
# --- Adoption: a hive whose CA predates the swarm root.
#
# Runs ONLY where this host also owns the root (`autoConfigure`),
# because adoption invalidates an anchor that consumers already
# trust and they refresh on their own schedule. On one box that
# schedule is knowable; across hosts it is not, so there the
# operator does it and this unit only says so, loudly.
#
# Guarded by a marker rather than by the state of the world: the
# marker's ABSENCE is the trigger, so this fires once per hive
# instead of re-deciding every activation. That is the difference
# between a migration and a kicking machine.
if [ -s "$root" ] && [ -s "$ca" ] && [ ! -e "$marker" ] \
&& ! openssl verify -CAfile "$root" "$ca" >/dev/null 2>&1; then
${
if swarmCaCfg.autoConfigure then
''
echo "adopting the swarm CA: $ca does not chain to $root" >&2
# Keep the old CA as an anchor across the overlap. Consumers
# read the bundle, so adoption is additive before it is
# subtractive — agents pick up new trust only when their
# container restarts, which is a window even on one host.
cp "$ca" "$prev"
chmod 0644 "$prev"
rm -f "$ca" "$cak"
touch "$marker"''
else
''
echo "hive-tls: this hive's CA does not chain to the swarm root." >&2
echo " hive CA: $ca" >&2
echo " swarm root: $root" >&2
echo "Adopting the hierarchy is not automatic here: it invalidates an" >&2
echo "anchor that peers and agents on OTHER hosts still trust, and they" >&2
echo "refresh on their own schedule only you know when that is safe." >&2
echo "To adopt: rm $ca $cak && systemctl restart hive-tls-ca.service" >&2
echo "To keep the current CA deliberately: touch $marker" >&2
exit 1''
}
fi
# --- CA: generated once and reused across leaf rotations, in # --- CA: generated once and reused across leaf rotations, in
# whichever of the two shapes `caGenScript` selected. # whichever of the two shapes `caGenScript` selected.
# Regenerated only if missing or already expired (checkend 0). # Regenerated only if missing or already expired (checkend 0).
@ -322,29 +279,31 @@ in
fi fi
# --- Trust bundle: what a consumer must TRUST, as opposed to # --- Trust bundle: what a consumer must TRUST, as opposed to
# `ca.pem`, which is what this host SIGNS with. Why they stopped # `ca.pem`, which is what this host SIGNS with. The two were the
# being the same file, and why `ca-previous.pem` stays in the set # same file while the hive CA was self-signed, and stopped being
# after an adoption, are in docs/swarm/ca.md. Three constraints # the same file the moment it became an intermediate: openssl
# the code cannot state: # refuses to end a chain at a trusted cert that isn't
# self-signed (that's what `-partial_chain` is for), so an
# agent's curl handed only `ca.pem` fails with "unable to get
# issuer certificate". Verified in both directions before this
# was written — rustls and Go accept a trusted intermediate,
# which is what makes the breakage partial and easy to miss.
# #
# Written IN PLACE, never renamed into position — containers # Consumers therefore trust hive CA + swarm root; on a hive that
# bind-mount this file and a bind mount follows the inode, so a # still has a self-signed CA the bundle is just that CA, so the
# rename leaves every consumer holding the old one. # consumer side needs no condition at all. Trusting the root is
# also the point of the hierarchy — it is what lets a peer hive
# validate without being hand-pinned.
# #
# Dropping `ca-previous.pem` is deliberately NOT done here: # Written IN PLACE, never renamed into position: containers bind
# "long enough" is a deployment fact, not a unit's call. # -mount this file, and a bind mount follows the inode. A
# # rename would leave every consumer holding the old one.
# Built as an array with `if` guards, not
# `[ -s f ] && anchors+=(f)`: under `set -e` an && list whose test
# fails IS a failing command and kills the unit — on exactly the
# hive where the optional file is legitimately absent. The
# reflexive `|| true` is worse; it also swallows a real failure to
# read the hive CA.
bundle="$d/trust-bundle.pem" bundle="$d/trust-bundle.pem"
anchors=("$ca") if [ -s "$root" ]; then
if [ -s "$prev" ]; then anchors+=("$prev"); fi cat "$ca" "$root" > "$bundle"
if [ -s "$root" ]; then anchors+=("$root"); fi else
cat "''${anchors[@]}" > "$bundle" cat "$ca" > "$bundle"
fi
chmod 0644 "$bundle" chmod 0644 "$bundle"
''; '';
}; };