Commit graph hyperhive/nix/host-modules/hive-tls.nix
Author SHA1 Message Date
atlas
d3b40da1c8 deploy: give every option an enable, and name the controller one
Two corrections from review, applied forward on this branch rather than
by rewriting it.

`deploy.<service>` was a bare bool, which makes
`deploy.forgejo = { enable; ci; }` unrepresentable -- the nested
CI-runner sub-option this namespace was designed around. Every entry is
now an attrset with an `enable`, so a second per-host deployment
decision becomes an ordinary addition rather than a migration.

`deploy.controller` is now `deploy.swarm-controller`, consistent with
`deploy.swarm-ui`, which was introduced in the same commit.

89 references rewritten across 24 files -- nix, Rust, docs, and the
repo's own CLAUDE.md.

The prefix-anchored sweep missed exactly one, and it was live code:
hive-tls.nix spells it `hyperhiveCfg.deploy.controller` -- the only
`hyperhiveCfg` prefix among 45 references. A suffix grep
(`\.deploy\.<name>`) finds it; a path-anchored one cannot, because the
head of a reference is whatever alias the reading file happens to bind.
2026-08-30 04:23:22 +02:00
atlas
0b7357d4b8 deploy: move the controller and swarm-ui toggles
One commit rather than two because they are not independent: the UI's
`enable` had the controller's as its literal default, so moving the
controller alone would leave the UI's default naming an option that no
longer exists.

The UI keeps that derivation in its new home — it is a view onto the
controller's state and reaches it over that daemon's unix socket, so the
host running the controller is the host that can serve it.

Three spellings had to move together for the UI, not one: the `default`,
the `defaultText` shown in the options doc, and the description prose
that names the old path in words. A grep for the option path finds the
first two.

The sweep also reached outside nix: `swarm-controller`'s crate README and
its `//!` module doc both named the option, as did this repo's own
CLAUDE.md and four pages under docs/. An option's name is API, and its
documentation lives wherever someone thought to write it down.
2026-08-30 04:23:22 +02:00
atlas
41f0d7e036 fix(#3462): apply the name check in the unit that runs on the deploy
hive-tls-ca re-signs at service activation -- the rebuild itself --
while hive-tls-resign only fires from a weekly timer. The previous commit
put the coverage check in the timer unit, so a corrected serviceDomains
would not have taken effect until up to a week after the deploy that
changed it. Same bug one level along: found a trigger, not the trigger.

Two guards now share one definition rather than each carrying their own,
because a rule enforced in one and not the other is worse than one
enforced in neither -- it looks fixed and only fires on whichever path
you did not take.

Also widens hive-tls-ca's condition to the services leaf. Both leaves are
signed inside that block but only the hive leaf gated it, so a fresh
gateway.pem suppressed the re-signing of a swarm-services.pem that was
missing or stale.
2026-08-18 21:54:38 +02:00
atlas
2d2f16406f fix(#3462): re-sign a leaf when it stops covering the configured names
Expiry was the only re-sign trigger, so a leaf signed when the name set
was smaller stayed valid -- and wrong -- for its whole lifetime. Adding a
service to swarm.serviceDomains reissues the sub-CA (its own .names
reconciliation) but nothing regenerated the leaf nginx actually serves,
which left the previous commit's config change unable to fix anything on
a hive whose gateway leaf was not near expiry.

covers() reads the DNS names back out of the certificate rather than a
sidecar file: the pem is what nginx serves, and a bookkeeping file drifts
from it the moment a leaf is replaced by hand. Applied to both leaves --
the hive leaf has the same defect if the hive domain ever changes.
2026-08-18 21:54:38 +02:00
atlas
e15f1c9fff fix(#3349): do not define a controller env key on hives with no controller
Defining an environment key on a unit that does not exist creates a
fragment for it: inert, never activated, but present on every
non-controller hive that has a CA. hive-c0re runs everywhere so its
line needs no guard; the controller runs on one host in a swarm.

Caught in review. It evaluates and builds clean either way, which is
why it took a reviewer rather than a check.
2026-08-16 20:55:39 +02:00
atlas
ef9339da16 fix(#3349): point the swarm-queue client at the hive's trust bundle
The queue client built a bare reqwest::Client, so it trusted only the
platform roots. Against a swarm whose authelia is signed by the swarm
CA that is fatal: minting a token dies with 'invalid peer certificate:
UnknownIssuer', inside the auth callback, on a four-second retry loop,
with the queue never connecting.

The anchor was never missing. hive-tls.nix assembles trust-bundle.pem
and already hands it to hive-c0re as HIVE_TLS_CA_PATH; nothing pointed
the queue client at it.

QueueConfig gains an optional ca_file from <prefix>_OIDC_CA_FILE, read
outside the all-or-none tuple on purpose: a CA path with no queue is
meaningless rather than half-configured, and requiring it would break a
swarm fronted by a public certificate in order to fix one that is not.
add_root_certificate extends the default roots rather than replacing
them, so both deployments work.

A bad path fails loudly instead of falling back to the platform roots.
An operator who names a CA file wants that anchor; a silent fallback
turns their typo into UnknownIssuer five layers away.

hive-tls.nix names the bundle for both clients, beside the line that
already does it for hive-c0re, rather than having each consumer
re-derive the path.
2026-08-16 20:55:39 +02:00
atlas
b12be26f20 docs(3191): drop the migration history from the gateway comments
Per mara on the PR: how a thing used to be deployed stops being
relevant the moment this is merged and the hives are rebuilt, so the
comments state the current constraint and nothing about the container
that used to hold it.

Removes the two ⚠️ blocks this PR added (hive-tls-ca's ordering, the
resign propagation), the matrix ordering's, dnsmasq's resolv.conf
paragraph, and the pre-existing 'REMOVED WITH THE CONTAINER' inventory
in the gateway module. The 🚨 do-not-simplify warning on the cert-copy
unit keeps both its reasons and loses the container framing.

Kept deliberately: the two operator-facing 'this used to default to
true' option notes (migration information for someone upgrading), and
the SupplementaryGroups block, which documents why an absence is
load-bearing rather than how a deployment used to look.
2026-08-12 13:26:58 +02:00
atlas
d70b1e0a97 fix(3191): order gateway TLS against the units that read it, not a dead container
Removing the gateway container retired two things that were still
depended on: an ordering edge and a delivery path.

- hive-tls-ca ordered itself before/requiredBy
  container@hive-gateway.service. That unit no longer exists, so the CA
  was sequenced against nothing while its real consumer,
  hive-gateway-self-signed-cert (which nginx Requires=), could win the
  race and fail its copy under set -eu, blocking nginx.

- hive-tls-resign propagated a rotated leaf with
  `systemctl -M hive-gateway … || true`. The machine is gone, so both
  calls failed and both failures were swallowed: the unit logged
  "propagating" and exited 0 while nginx served the stale copy until it
  expired. Host units now, so no -M and no || true — a failed
  propagation fails the timer.

- container@hive-matrix ordered after the gateway container to get the
  resolver up first. dnsmasq is a host service now, so it orders after
  dnsmasq.service, which is what the edge always meant.
2026-08-12 12:18:13 +02:00
atlas
a92f7351d9 fix(3086): the hive leaf stops naming two services it already covered
`DNS:forge.<domain>` and `DNS:matrix.<domain>` sat beside
`DNS:*.<domain>`, which already covers them — a wildcard covers exactly
one label and those are one label. Naming them read as policy, and that
is why they outlived the thing they described: a configured service name
no longer has to be under this hive's domain, and when it isn't, this is
the one list it cannot join.

Proved by execution rather than eval, because the SAN list is a printf
argument filled in at runtime and eval can only show the template — the
lesson from the gateway outage. openssl-verified against a CA carrying
the real name constraint: old and new leaves agree on every name the hive
serves, the wildcard genuinely stops at one label, and a sibling name
added to this leaf fails.

That last case came back stronger than expected: a nameConstraints
violation invalidates the certificate, not the offending SAN, so one
foreign name here would break the leaf for the dashboard and every other
vhost sharing it. The comment says so now, and gateway.md's cert-shape
paragraph no longer describes SANs that are gone.
2026-08-11 23:35:06 +02:00
atlas
155df39ee4 feat(nix): warn when a swarm service name has no certificate path here
The swarm's service names default to siblings of the hive domain
(forge.<swarm.domain>, not forge.<hive domain>), and the hive CA's leaf
is a single-label wildcard over its own domain, so it cannot cover them.
The swarm-services leaf can — but only on a host that holds the swarm
root key, i.e. swarm.ca.autoConfigure. Everywhere else the gateway
quietly serves the hive leaf on those names and every client sees a
mismatch, on a config that evaluates and deploys cleanly.

A warning rather than an assertion, per the operator's call. This module
knows what it can issue; it cannot see an operator-installed services
sub-CA or an external ACME setup, so "no certificate path" is a thing it
observes, not a thing it can conclude. A rebuild must not be blocked by
a verdict this host isn't in a position to reach — the message says what
was observed and names both ways out.
2026-08-11 20:43:01 +02:00
müde
b5066ad061 unbork main? 2026-08-06 00:12:28 +02:00
atlas
5a83c40dca feat(nix): issue a swarm-services leaf, and renew it with the hive one
The swarm's service names cannot go on the hive leaf: the hive CA is
name-constrained to the hive domain and those names are siblings of
it. So there is a second leaf, signed by the services sub-CA.

signLeafScript is parameterised rather than duplicated -- same
ceremony, different issuer and names -- so the two cannot drift in
how they are built. The name list itself is derived once, as a
read-only swarm.serviceDomains, and read by both the sub-CA that
name-constrains those names and the leaf that carries them as SANs:
two modules each assembling the list is how they stop agreeing.

The renewal unit is the point of this commit as much as the leaf.
hive-tls-resign now knows about both, because a leaf that first-boot
issuance creates and weekly renewal ignores looks perfect for its
entire validity and then expires with no warning -- the failure is
invisible until it is total. The freshness test became a function
over a leaf rather than a check of one, so adding a third leaf is a
line rather than a rewrite.

The services leaf is skipped where the sub-CA is absent: it exists
only where the swarm CA is autoconfigured, and on a hive whose certs
come from its operator the correct state is no leaf, not a stale one.

Also drops a comment that documented signLeafScript's old signature
from above an unrelated binding.
2026-08-05 22:43:03 +02:00
atlas
01680ee962 docs(tls): move the bundle rationale to the CA page
The comment block tripped the 30-line lint, and the lint was right: the
openssl chain-termination explanation had just been written into
docs/swarm/ca.md, so the comment was a second copy of it. What stays is
the part the code cannot say — the inode constraint, why the previous
anchor is not dropped here, and the set -e alternative that was
rejected.
2026-08-05 18:53:51 +02:00
atlas
0befa4f36d feat(tls): adopt the swarm CA automatically, but only where it is safe
A hive whose CA predates the swarm root keeps a self-signed CA, and
nothing re-roots it. That is what makes the hierarchy non-disruptive,
and it also means the O(1)-trust payoff never arrives for that hive.

Adoption now happens by itself where this host owns the root, and
nowhere else. The split is the whole design: adoption invalidates an
anchor consumers already trust, and they refresh on their own schedule —
on one box that schedule is knowable, across hosts it is not. So the
all-local case migrates itself and every other case gets a failure that
names both files, gives the two-command recipe, says why it is not
automatic, and offers the marker as a deliberate opt-out.

The previous CA rides in the trust bundle afterwards. Consumers read the
bundle rather than ca.pem, so adoption is additive before it is
subtractive; agents pick up new trust only when their container
restarts, which is a window even on a single host. Dropping the old
anchor stays a separate, deliberate step.
2026-08-05 18:53:51 +02:00
atlas
cc74a2c1f6 feat(nix): make the swarm CA hierarchy opt-in
A swarm's services and its hives can live on different hosts, and a host
has no way to tell whether it is the one holding the root — so setting
the swarm CA up is an operator action, not something a host infers.
`swarm.ca.autoConfigure` is off by default and is the only thing that
issues a hive sub-CA, which is also the only case that *can*: signing
one needs the root's private key, and that key is deliberately not on a
hive host otherwise.

With the flag off, a hive self-signs its CA exactly as it always has.
That keeps a plain hive working out of the box; what it gives up is
membership of a swarm's trust hierarchy, which is the right thing to
give up for a hive nobody has federated.

Replaces a default derived from `swarm.peers == { }`. That read "no
peers declared" as "everything is local", which is not the same claim —
a hive can belong to a swarm it has not declared yet, or to one whose
services run elsewhere entirely.
2026-08-05 15:57:50 +02:00
atlas
06710e83b4 feat(nix): issue each hive's CA under a swarm root CA
Cross-hive trust was O(n²) hand-pinning: every hive had to name every
peer's CA. A swarm root makes it O(1) — trust the root once and every
present and future peer validates.

The root is generated by a new `swarm-ca` unit on a single-host swarm
and operator-provided otherwise; `swarm.ca.autoConfigure` picks between
them and derives its default from `swarm.peers` being empty, so "all on
one host" is read off the deployment rather than remembered. Both modes
produce the same artifacts in the same places, so splitting hosts later
is moving the service dirs, not switching code paths. The root key never
enters the nix store, and the root is never regenerated automatically —
replacing it invalidates every peer at once.

Each hive CA carries `nameConstraints` pinned to that hive's domain, so
a leaked hive CA can only mint names inside its own subdomain, enforced
by verifiers rather than by convention.

`ca.pem` was serving as both the issuer and the anchor consumers trust;
those are the same file only while it is self-signed. openssl will not
terminate a chain at a trusted cert that isn't self-signed (rustls and
Go will), so the promotion would have broken some consumers and not
others. `hive-tls-ca` now also writes `trust-bundle.pem` — the hive CA
plus whatever it is rooted at — and every anchor consumer reads that:
agents, the CI and forge containers, and the peer-config recipe. On a
hive with no swarm root the bundle is just that CA, so nothing consuming
it needs a mode to branch on.
2026-08-05 15:57:50 +02:00
müde
79fec131d6 docs: sweep archaeology comments to present state 2026-07-13 22:16:02 +02:00
müde
e0cfed7fe8 refactor: jq for tea-login, build-time avatar png, shared leaf-sign script 2026-07-13 22:14:25 +02:00
müde
4a48ce5024 refactor: nix/host-modules + nix/agent-modules layout, update doc paths 2026-07-13 22:05:49 +02:00
Renamed from nix/modules/hive-tls.nix (Browse further)