feat(#1886): trust a peer hive's root CA hive-wide for self-signed federation

Add swarm.peers.<domain>.caCert (path to a peer hive's root CA PEM),
trusted everywhere the hive's own internal CA is — so a self-signed
peer hive can federate (matrix) and any in-hive consumer validates its
certs.

Mechanism (reuses the existing hive-CA embedding): the meta-flake
renderer embeds a LIST of CA files next to each agent's flake —
hive-ca.pem (the hive's own self-signed CA, when active) plus each peer
caCert as peer-ca-<N>.pem — and emits them all in
security.pki.certificateFiles, so every agent trusts them at build
time. The matrix container trusts the same peer CAs for federation TLS.
Nothing is installed in the host trust store; the certs live in the nix
store (no mutable host file).

- meta.rs: embedded_ca_files() = hive CA + peer CAs (from new
  HIVE_PEER_CA_PATHS env); ca_embed_state() tracks the list (content +
  add/remove); sync_agents materialises + stages the list; render emits
  the multi-entry certificateFiles. Tests cover hive-only / hive+peers
  / peers-only / none.
- hive-c0re.nix: HIVE_PEER_CA_PATHS service env (colon-joined caCerts);
  caCert / certFingerprint option docs updated to the hive-wide scope.
- hive-matrix.nix + docs/swarm.md: scope + comment updates.

certFingerprint stays the c0re-only leaf-pin path.
This commit is contained in:
atlas 2026-06-22 14:29:24 +02:00 committed by mara
commit edad6f863c
4 changed files with 288 additions and 70 deletions

View file

@ -213,6 +213,35 @@ in
then strip the colons and prepend `sha256:`. A malformed
value is ignored with a warning rather than weakening
trust. See docs/swarm.md for the full recipe.
Scopes only to hive-c0re's own peer HTTPS checks it does
NOT help Matrix federation (tuwunel validates against its
container trust bundle). For a self-signed peer whose root
CA you want trusted hive-wide (every agent + Matrix
federation), set `caCert` below.
'';
};
caCert = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "./peers/edge-ca.pem";
description = ''
Path to this peer hive's root CA certificate (PEM). When
set, the CA is embedded (at build time, into the nix store
no runtime file on the host) and trusted **everywhere the
hive's own internal CA is**: it rides alongside `hive-ca.pem`
in each agent's `security.pki.certificateFiles` (via the
meta-flake renderer), and is added to the Matrix homeserver
container's trust bundle so tuwunel validates *federation*
TLS from a self-signed peer hive whose cert chains to it.
This is the CA-trust path that `certFingerprint`
(leaf-pinning, c0re-only) can't cover, and is what unblocks
Matrix federation with a self-signed peer hive. Trust stays
inside the hive (agents + the Matrix container), never the
host system trust store. Mutually complementary with
`certFingerprint`; set `caCert` for the federation case. See
docs/swarm.md.
'';
};
@ -825,7 +854,26 @@ in
}
) config.services.hyperhive.swarm.peers
);
};
}
//
lib.optionalAttrs
(lib.any (p: p.caCert != null) (lib.attrValues config.services.hyperhive.swarm.peers))
{
# Peer-hive root CA file paths (colon-joined), one per peer that
# declares `swarm.peers.<domain>.caCert`. hive-c0re's meta-flake
# renderer (meta.rs) embeds each next to every agent's flake and
# adds it to `security.pki.certificateFiles`, so a peer CA is
# trusted everywhere the hive's own internal CA (`hive-ca.pem`)
# is — i.e. by every agent. The matrix container trusts the same
# CAs separately for federation TLS. The `caCert` files are
# copied into the nix store at build, so these are store paths —
# nothing mutable lives on the host.
HIVE_PEER_CA_PATHS = lib.concatStringsSep ":" (
lib.filter (c: c != null) (
lib.mapAttrsToList (_domain: p: p.caCert) config.services.hyperhive.swarm.peers
)
);
};
serviceConfig = {
ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --config ${serveConfig}";
# Migrate hive-c0re's *own* state to the service user after an

View file

@ -358,6 +358,21 @@ in
{
system.stateVersion = "26.05";
# Peer-hive root CAs (`swarm.peers.<domain>.caCert`) added to THIS
# container's trust bundle so tuwunel validates *federation* TLS
# from a self-signed peer hive (it checks the peer's federation
# cert against its trust bundle). Peer CAs are trusted everywhere
# the hive's own internal CA is — agents get them via the
# meta-flake renderer (`HIVE_PEER_CA_PATHS` → each agent's
# `security.pki.certificateFiles`); this block is the matrix
# container's copy, since the host `security.pki` store doesn't
# cross the container boundary. They are never installed in the
# HOST trust store. Null entries (CA-bundle / fingerprint-pinned
# peers) drop out.
security.pki.certificateFiles = lib.filter (c: c != null) (
lib.mapAttrsToList (_domain: p: p.caCert) config.services.hyperhive.swarm.peers
);
# tuwunel hard-fails to boot if `/etc/resolv.conf` has no
# `nameserver` line (`Failed to configure DNS resolver ... no
# nameservers found in config` → exit 1). This declarative