Compare commits
2 changed files with 34 additions and 111 deletions
|
|
@ -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.
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue