The push side modelled a store per peer hive: a --peer argument, a swarm.peers.<domain>.snapshotStorePort option, and a swarm_peers module whose entire job was answering "which peer". A swarm has exactly one store, so none of that had anything to select between. The receiver already proved it. It keys destination directories by agent, not by sending hive, precisely so an agent that migrates keeps one unbroken incremental chain -- which only makes sense if every hive pushes to the same place. Per-hive stores would split the chain in two, the case that keying exists to prevent. So the destination moves to services.hyperhive.swarm.snapshotStore, rendered into HYPERHIVE_SNAPSHOT_STORE, and swarm_peers is deleted rather than adapted. address has no default because it is a deployment fact this host cannot derive; port defaults because it is a convention both ends read from the same option docs. An unset or empty address fails naming the option instead of connecting somewhere arbitrary, and a test asserts the message suggests no value.
166 lines
7 KiB
Nix
166 lines
7 KiB
Nix
# Swarm peering: who the peer hives are. Serialised into hive-c0re's
|
|
# environment (HYPERHIVE_PEERS / HIVE_PEER_CA_PATHS — see ./hive-c0re)
|
|
# and consumed by identity.rs + the dashboard's P33RS tab.
|
|
#
|
|
# Declaration only — this module has no `config` block. The mesh that
|
|
# uses the `wireguard*` fields below lives in ./swarm-wireguard.nix,
|
|
# because bringing up an interface is host networking rather than swarm
|
|
# bookkeeping, and a host that runs no hive still needs it.
|
|
{
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
{
|
|
# Peer hives in the same swarm. Each entry declares a remote hive
|
|
# reachable from this host.
|
|
options.services.hyperhive.swarm.peers = lib.mkOption {
|
|
type = lib.types.attrsOf (
|
|
lib.types.submodule {
|
|
options = {
|
|
certFingerprint = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "sha256:b1946ac92492d2347c6235b4d2611184a3f5b6cae6c19d6e3c2f0a8e7d4c9f12";
|
|
description = ''
|
|
Expected TLS certificate fingerprint for this peer's HTTPS
|
|
endpoint. Null = trust the system CA bundle (for Let's
|
|
Encrypt peers). Set to pin a self-signed cert.
|
|
|
|
Format: the literal `sha256:` followed by exactly 64
|
|
hex digits (case-insensitive, no colon separators) — the
|
|
SHA-256 digest of the peer's DER-encoded leaf certificate.
|
|
Generate with `openssl x509 -noout -fingerprint -sha256`,
|
|
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.
|
|
'';
|
|
};
|
|
|
|
wireguardPublicKey = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "base64pubkey=";
|
|
description = ''
|
|
WireGuard public key for this peer host. Required when
|
|
`services.hyperhive.swarm.wireguard.enable = true` and
|
|
you want this peer reachable over the mesh. Null = TLS-
|
|
only peering (public internet, no mesh tunnel).
|
|
'';
|
|
};
|
|
|
|
wireguardEndpoint = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "203.0.113.1:51820";
|
|
description = ''
|
|
WireGuard endpoint for this peer in `host:port` form.
|
|
Required when the peer host is behind a firewall and
|
|
this host needs to initiate the tunnel. Null = this host
|
|
waits for the peer to connect (peer-initiates; peer must
|
|
have an endpoint pointing back at this host).
|
|
'';
|
|
};
|
|
|
|
wireguardAddress = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "10.100.0.2/32";
|
|
description = ''
|
|
IP address (with prefix) of the peer host on the
|
|
WireGuard mesh. Used as the `allowedIPs` for the peer's
|
|
WireGuard config entry and injected into `HYPERHIVE_PEERS`
|
|
so hive-c0re can route intra-swarm traffic to the mesh
|
|
address rather than the public domain. Required to include
|
|
the peer in the WireGuard mesh (peers missing this field
|
|
are silently excluded from `wg-hive`).
|
|
'';
|
|
};
|
|
|
|
};
|
|
}
|
|
);
|
|
default = { };
|
|
example = {
|
|
"lab.example.com" = {
|
|
certFingerprint = "sha256:b1946ac92492d2347c6235b4d2611184a3f5b6cae6c19d6e3c2f0a8e7d4c9f12";
|
|
};
|
|
"edge.corp" = { };
|
|
};
|
|
description = ''
|
|
Peer hives in the same swarm. The attrset key is the peer's DNS
|
|
domain — used for dashboard links and Matrix federation discovery.
|
|
Null `certFingerprint` trusts the system CA bundle; set it to pin
|
|
a self-signed TLS cert. Add `wireguardPublicKey` + `wireguardAddress`
|
|
(and optionally `wireguardEndpoint`) to include the peer in the
|
|
WireGuard mesh when `swarm.wireguard.enable = true`.
|
|
'';
|
|
};
|
|
|
|
options.services.hyperhive.swarm.snapshotStore = {
|
|
address = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "10.100.0.1";
|
|
description = ''
|
|
Mesh address of the swarm's snapshot store — the single
|
|
`btrfs receive` endpoint every hive in this swarm pushes agent
|
|
snapshots to. Bare IP, no prefix.
|
|
|
|
There is exactly **one** store per swarm, not one per peer: the
|
|
receiver keys destinations by *agent*, so an agent that migrates
|
|
between hives keeps a single unbroken incremental chain. Per-hive
|
|
stores would split that chain in two, which is the case the store
|
|
exists to serve.
|
|
|
|
Null means this swarm has no store configured, and pushing fails
|
|
saying so rather than guessing an address. Set it on every hive
|
|
that pushes; the receiving host separately sets
|
|
`services.hyperhive.snapshotStore.enable`.
|
|
'';
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 51821;
|
|
description = ''
|
|
TCP port the swarm's snapshot store listens on. Must match the
|
|
receiving host's `services.hyperhive.snapshotStore.port`.
|
|
|
|
Defaulted (unlike `address`) because it is a shared convention
|
|
both sides read from the same option docs — whereas an address
|
|
is deployment-specific and cannot be guessed.
|
|
'';
|
|
};
|
|
};
|
|
|
|
}
|