Commit graph hyperhive/hivectl/src/wg.rs
Author SHA1 Message Date
atlas
433b294099 refactor(nix): swarm.peers becomes swarm.hives, a directory of every hive
One attrset describing every hive in the swarm including this one,
identical on every host, with hiveName selecting which entry is us.
"My peers" is derived (swarm.peerHives) rather than declared.

Every field in the old per-host peer list was intrinsic to the hive it
described, never to the pair -- so the list was a directory each host
kept its own copy of. Beyond the deduplication it removes a bug class:
two hosts could hold different endpoints for the same third hive with
nothing to detect the disagreement.

Drops the per-hive caCert. Trust inside a swarm derives from the swarm
root, which every hive chains to. What that genuinely removes is
trusting a hive whose root this swarm does not own -- a cross-swarm
problem that wants a mechanism of its own, not a field that happened to
work.

The matrix container's certificateFiles block goes with it and could
NOT be migrated: that list is read at build time and the swarm root is
a runtime file (its key must never enter the store), so there is no
build-time name to put there. caCert being a nix path was precisely
what made it the build-time distribution channel. Agents are unaffected
-- hive-tls folds the root into the hive trust bundle and the meta
renderer embeds that one file. Tracked separately.

Migration is an assertion plus warnings, not a rename: hives is peers
union {self}, and the set gains a member no existing config has written
down. A rename migrates a name and a default can re-root a meaning;
neither can conjure a new member. The warning explains, the self-entry
assertion stops the build.
2026-08-05 20:44:16 +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
atlas
f108c72f25 hivectl: say which of the three socket failures actually happened
`hivectl open forge` on a host where the daemon is fine and the socket is
fine printed "could not reach the hive-c0re daemon for URLs — is hive-c0re
running?". It was running. The operator was not in `hive-admin` in that
shell, and the connect got EACCES.

The message was a guess, not a diagnosis, because `query_hive_urls`
returned `Option` and threw the cause away with `.ok()`. Three different
failures — not in the group, no socket at all, nobody listening — all
arrived as the same sentence, and only one of the three is fixed by
looking at the daemon.

Classify the connect error in `client::request`, which every
daemon-assisted verb goes through, and keep the io error as the anyhow
cause so the output reads fix-first. EACCES names `hive-admin`,
`services.hyperhive.adminUsers`, and — the part that actually bites — the
re-login, since secondary group membership is only applied at login, so a
shell opened before the grant still cannot connect. ENOENT and
ECONNREFUSED point at the units instead.

Then stop discarding it: `query_hive_urls` returns `Result<Option<_>>`,
`open` and `require_hive_domain` propagate, and `daemon_request` drops its
own "connect to daemon socket" context, which only buried the actionable
line under a vaguer one. `wg init`'s domain lookup stays best-effort by
an explicit `.ok().flatten()` rather than by accident.

Same footgun `agent_exists` was already fixed for: a permission error
collapsed into a value that reads as a different, wrong story.
2026-07-26 17:35:44 +02:00
damocles
fc7720572b split hivectl main.rs into per-domain modules (#2509) 2026-07-16 11:28:43 +02:00