docs(swarm): cut the CA material into its own page
Second split under docs/swarm/, following the shape docs/turn-loop/ and docs/web-ui/ already use. The README keeps the `## Swarm CA` heading rather than deleting it: an existing cross-reference targets that anchor, and a heading is the cheapest thing to preserve. It now carries the one-paragraph summary and a pointer, so a reader who lands on the anchor still arrives somewhere that answers the question. The CA page also absorbs three things that were true but undocumented — why the trust bundle rather than the bare intermediate (openssl will not terminate a chain at a non-self-signed anchor without -partial_chain), that autoConfigure derives from enableAllLocalDefaults, and that the name constraint excludes both IP families because a permitted-DNS-only constraint says nothing about IP SANs.
This commit is contained in:
parent
f798662396
commit
bcd4fbd918
3 changed files with 100 additions and 56 deletions
|
|
@ -198,7 +198,7 @@ The issuer is a **host-held hive CA**, not a bare self-signed leaf. A host servi
|
|||
|
||||
**Why a CA, not a bare leaf**: a bare self-signed leaf is its own trust anchor, so every regeneration is a new anchor every consumer must re-trust — and a runtime-generated, in-container leaf can't be wired into an agent's build-time trust store at all. With a stable CA, agents and federation peers trust it *once*; leaf rotation never re-breaks them.
|
||||
|
||||
**What consumers trust**: `trust-bundle.pem` in the same state dir, not `ca.pem`. The hive CA is itself issued under the swarm root ([`swarm/`](swarm/README.md#swarm-ca) has the hierarchy), and an intermediate is not a chain a verifier can terminate at — so the bundle carries the hive CA plus whatever it is rooted at. nginx is handed the leaf with the hive CA appended for the same reason. Everything that trusts the hive's TLS reads the bundle: agents (via `security.pki.certificateFiles`), the CI and forge containers, and a federating peer.
|
||||
**What consumers trust**: `trust-bundle.pem` in the same state dir, not `ca.pem`. The hive CA is itself issued under the swarm root ([`swarm/ca.md`](swarm/ca.md) has the hierarchy), and an intermediate is not a chain a verifier can terminate at — so the bundle carries the hive CA plus whatever it is rooted at. nginx is handed the leaf with the hive CA appended for the same reason. Everything that trusts the hive's TLS reads the bundle: agents (via `security.pki.certificateFiles`), the CI and forge containers, and a federating peer.
|
||||
|
||||
**Why on by default**: matrix-dart-sdk (FluffyChat's SDK) hardcodes `https://<host>/.well-known/matrix/client` for homeserver discovery and refuses to fall back to plain http. Without TLS the browser client cannot bootstrap.
|
||||
|
||||
|
|
|
|||
|
|
@ -57,61 +57,13 @@ and `qualify()` / `qualified_label()` semantics.
|
|||
## Swarm CA
|
||||
|
||||
A hive's internal TLS chains to a **swarm root CA**: the root signs each
|
||||
hive's own CA, and that hive CA signs the gateway leaf. A peer that
|
||||
trusts the root once validates every hive in the swarm, present and
|
||||
future, instead of being pinned to each one by hand.
|
||||
hive's own CA, and that hive CA signs the gateway leaf, so a peer that
|
||||
trusts the root once validates every hive in the swarm rather than being
|
||||
pinned to each one by hand.
|
||||
|
||||
Two provisioning modes, one structure — what differs is who puts the
|
||||
artifacts on disk:
|
||||
|
||||
| | swarm root | this hive's CA |
|
||||
| --- | --- | --- |
|
||||
| default | operator-provided | operator-provided, else self-signed as before |
|
||||
| `autoConfigure = true` (all on one host) | generated by `swarm-ca.service` on first boot | issued by `hive-tls-ca.service` under the root |
|
||||
|
||||
`services.hyperhive.swarm.ca.autoConfigure` selects between them, and is
|
||||
**off by default**: a swarm's services and its hives can live on
|
||||
different hosts, and a host cannot tell whether it is the one holding
|
||||
the root, so setting the swarm CA up is an operator action rather than
|
||||
something a host assumes. Turn it on for an all-on-one-host deployment
|
||||
and the hierarchy costs no configuration.
|
||||
|
||||
**A hive given neither artifact keeps the self-signed CA it has always
|
||||
had.** It serves TLS exactly as before and simply isn't part of a
|
||||
swarm's trust hierarchy — the right outcome for a hive nobody has
|
||||
federated yet. Only `autoConfigure` issues a hive sub-CA, because only
|
||||
that case can: signing one needs the root's private key.
|
||||
|
||||
Moving the swarm CA onto its own host is then a matter of moving
|
||||
`services.hyperhive.swarm.ca.stateDir` and leaving `autoConfigure` off —
|
||||
there is no second code path to switch to.
|
||||
|
||||
The root's private key never reaches the nix store: the store is
|
||||
world-readable, so a key committed to a flake is a key published to
|
||||
everyone who builds it. Only certificates are distributed.
|
||||
|
||||
Each hive CA is **name-constrained** (X.509 `nameConstraints`) to that
|
||||
hive's own domain, so a hive CA that leaks can only mint names inside
|
||||
its own subdomain — enforced by every verifier rather than by
|
||||
convention.
|
||||
|
||||
### What to hand a peer
|
||||
|
||||
`hivectl peer-config` prints the `cp` line. The file is
|
||||
`<tls.stateDir>/trust-bundle.pem` — the hive CA plus the swarm root —
|
||||
not `ca.pem`: an intermediate on its own is not something a verifier can
|
||||
build a chain to. On a hive that predates the swarm root the bundle is
|
||||
just that hive's self-signed CA, so the recipe does not change.
|
||||
|
||||
### Adopting the hierarchy on an existing hive
|
||||
|
||||
An existing `ca.pem` is never re-rooted automatically — swapping it
|
||||
would break every consumer that already trusts it, and agents only pick
|
||||
up new trust when their container restarts. To adopt, delete `ca.pem` +
|
||||
`ca-key.pem` under `tls.stateDir` and restart `hive-tls-ca.service`;
|
||||
the CA is re-issued under the root and the leaf re-signed. Until then
|
||||
the hive serves TLS exactly as before and is simply not part of the
|
||||
swarm's trust hierarchy.
|
||||
Provisioning modes, what to hand a peer (`trust-bundle.pem`, never
|
||||
`ca.pem`), the name constraints on a hive CA, and how an existing hive
|
||||
adopts the hierarchy: [`ca.md`](ca.md).
|
||||
|
||||
## Running the swarm's shared services
|
||||
|
||||
|
|
|
|||
92
docs/swarm/ca.md
Normal file
92
docs/swarm/ca.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Swarm CA
|
||||
|
||||
A hive's internal TLS chains to a **swarm root CA**: the root signs each
|
||||
hive's own CA, and that hive CA signs the gateway leaf. A peer that
|
||||
trusts the root once validates every hive in the swarm, present and
|
||||
future, instead of being pinned to each one by hand.
|
||||
|
||||
That is the whole point of the hierarchy — it turns per-peer trust from
|
||||
O(n²) hand-pinning into one anchor per swarm.
|
||||
|
||||
## Two provisioning modes, one structure
|
||||
|
||||
What differs is who puts the artifacts on disk:
|
||||
|
||||
| | swarm root | this hive's CA |
|
||||
| --- | --- | --- |
|
||||
| default | operator-provided | operator-provided, else self-signed as before |
|
||||
| `autoConfigure = true` (all on one host) | generated by `swarm-ca.service` on first boot | issued by `hive-tls-ca.service` under the root |
|
||||
|
||||
`services.hyperhive.swarm.ca.autoConfigure` selects between them, and is
|
||||
**off by default**: a swarm's services and its hives can live on
|
||||
different hosts, and a host cannot tell whether it is the one holding
|
||||
the root, so setting the swarm CA up is an operator action rather than
|
||||
something a host assumes. Turn it on for an all-on-one-host deployment
|
||||
and the hierarchy costs no configuration.
|
||||
|
||||
It defaults from `services.hyperhive.enableAllLocalDefaults`, the single
|
||||
switch that says "this box is the whole deployment".
|
||||
|
||||
**A hive given neither artifact keeps the self-signed CA it has always
|
||||
had.** It serves TLS exactly as before and simply isn't part of a
|
||||
swarm's trust hierarchy — the right outcome for a hive nobody has
|
||||
federated yet. Only `autoConfigure` issues a hive sub-CA, because only
|
||||
that case can: signing one needs the root's private key.
|
||||
|
||||
Moving the swarm CA onto its own host is then a matter of moving
|
||||
`services.hyperhive.swarm.ca.stateDir` and leaving `autoConfigure` off —
|
||||
there is no second code path to switch to.
|
||||
|
||||
## Constraints on the material
|
||||
|
||||
The root's private key never reaches the nix store: the store is
|
||||
world-readable and content-addressed, so a key committed to a flake is a
|
||||
key published to everyone who builds it. Only certificates are
|
||||
distributed.
|
||||
|
||||
Each hive CA is **name-constrained** (X.509 `nameConstraints`) to that
|
||||
hive's own domain, so a hive CA that leaks can only mint names inside
|
||||
its own subdomain — enforced by every verifier rather than by
|
||||
convention. The constraint excludes both IP families as well, since a
|
||||
permitted-DNS-only constraint says nothing about IP SANs.
|
||||
|
||||
The root is issued with `pathlen:1`: it may sign hive CAs, and those may
|
||||
sign leaves, and the chain stops there.
|
||||
|
||||
## What to hand a peer
|
||||
|
||||
`hivectl peer-config` prints the `cp` line. The file is
|
||||
`<tls.stateDir>/trust-bundle.pem` — the hive CA plus the swarm root —
|
||||
**not `ca.pem`**.
|
||||
|
||||
The distinction is load-bearing rather than cosmetic: once a hive CA is
|
||||
an intermediate, it is no longer something a verifier can build a chain
|
||||
*to*. OpenSSL will not terminate a chain at a trusted non-self-signed
|
||||
certificate without `-partial_chain`, so handing a peer the bare
|
||||
intermediate produces a verification failure that reads like a bad cert
|
||||
rather than like a missing anchor. The bundle carries both, so the same
|
||||
file works whichever mode issued it.
|
||||
|
||||
On a hive that predates the swarm root the bundle is just that hive's
|
||||
self-signed CA, so the recipe does not change.
|
||||
|
||||
⚠️ Consumers must read `trust-bundle.pem`, never `ca.pem` directly. A
|
||||
consumer that reads `ca.pem` works fine on a hive that has always been
|
||||
self-signed and breaks the moment that hive adopts the hierarchy — so
|
||||
the failure is invisible on the deployment you are most likely to test
|
||||
on.
|
||||
|
||||
## Adopting the hierarchy on an existing hive
|
||||
|
||||
An existing `ca.pem` is never re-rooted automatically — swapping it
|
||||
would break every consumer that already trusts it, and agents only pick
|
||||
up new trust when their container restarts. To adopt, delete `ca.pem` +
|
||||
`ca-key.pem` under `tls.stateDir` and restart `hive-tls-ca.service`;
|
||||
the CA is re-issued under the root and the leaf re-signed. Until then
|
||||
the hive serves TLS exactly as before and is simply not part of the
|
||||
swarm's trust hierarchy.
|
||||
|
||||
Making that adoption a first-class, non-disruptive operation — rather
|
||||
than a documented `rm` — is tracked separately; the mechanism it needs
|
||||
(carrying the previous CA in the trust bundle across the overlap)
|
||||
already exists.
|
||||
Loading…
Reference in a new issue