P1 of the storage backend: hives push agent snapshots over the WireGuard mesh that swarm.nix already brings up. No controller dependency — a btrfs subvolume tree, a socket-activated receiver, and the existing mesh. The mesh is the authentication. Cryptokey routing already binds a peer's source address to its public key (allowedIPs = [ peer.wireguardAddress ]), so the store adds no key material and no certs; anything else would authenticate the same fact twice. Destination is keyed per AGENT, not per hive: after a migration the same agent's next incremental send arrives from a different hive, and a per-hive prefix would split its snapshot chain and break the incremental parent lookup — the exact case this store exists to serve. The sender unavoidably contributes the agent name (a btrfs stream carries no such notion, and the subvolume name inside it is the sender's). So the receiver owns the destination root and VALIDATES the sender-supplied leaf against a whitelist charset — no slash, no dot, so neither traversal nor an absolute path can survive it. ListenStream binds this host's mesh address, never a wildcard, and that is asserted rather than commented: bound to 0.0.0.0 the socket would be an unauthenticated remote write into agent state. swarm.nix: the mesh config moves off the c0re.enable gate onto swarm.wireguard.enable. The mesh is host networking, not a c0re feature — a swarm host that runs no hive (this store) previously got no wg-hive interface at all. Nothing in that block was c0re-specific; the peer data c0re consumes is rendered in hive-c0re and stays gated there. Confinement is deliberately not in the module: it is a property of the deployment (a dedicated VM, or a container in the all-local case). The systemd hardening is defence in depth only — btrfs receive needs CAP_SYS_ADMIN, which can mount() its way out of the namespace those directives set up. The `dedicated` option turns "this host runs nothing else" into an assertion the build checks instead of an assumption the deployer remembers.
250 lines
10 KiB
Nix
250 lines
10 KiB
Nix
# Swarm peering: the peer-hive declarations and the optional
|
|
# WireGuard inter-hive mesh. The peers 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; the mesh
|
|
# config below is host-level networking.
|
|
{
|
|
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`.
|
|
'';
|
|
};
|
|
|
|
# WireGuard mesh config for the local host.
|
|
# When enabled, a `wg-hive` interface connects to all peers that have
|
|
# `wireguardPublicKey` declared. Peers reachable over the mesh are
|
|
# preferred for inter-hive traffic (no public TLS round-trip needed);
|
|
# peers without a public key still work via normal HTTPS.
|
|
options.services.hyperhive.swarm.wireguard = {
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Enable the WireGuard inter-hive mesh. When true, a `wg-hive`
|
|
interface is brought up connecting to all swarm peers that
|
|
declare a `wireguardPublicKey`. Requires
|
|
`privateKeyFile` to be set.
|
|
'';
|
|
};
|
|
|
|
privateKeyFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
example = "/etc/wireguard/hive.key";
|
|
description = ''
|
|
Path to the host's WireGuard private key file. The file must
|
|
be readable by root and should have mode 0400. Generate with
|
|
`wg genkey > /etc/wireguard/hive.key`. Required when
|
|
`swarm.wireguard.enable = true`.
|
|
'';
|
|
};
|
|
|
|
address = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
example = "10.100.0.1/24";
|
|
description = ''
|
|
IP address (with prefix) of this host on the WireGuard mesh.
|
|
Use a /24 (or broader) prefix so the routing table covers all
|
|
peer /32 routes. Example: `"10.100.0.1/24"` for a 256-host mesh.
|
|
'';
|
|
};
|
|
|
|
listenPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 51820;
|
|
description = ''
|
|
UDP port the local WireGuard interface listens on. Must be
|
|
reachable from peer hosts when they initiate the tunnel.
|
|
Default: 51820 (standard WireGuard port).
|
|
'';
|
|
};
|
|
|
|
persistentKeepalive = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.int;
|
|
default = 25;
|
|
example = 25;
|
|
description = ''
|
|
Seconds between keepalive packets sent to each peer. Useful
|
|
when this host (or a peer) is behind NAT — keeps the UDP hole
|
|
open. Set to null to disable. Default: 25 seconds.
|
|
'';
|
|
};
|
|
};
|
|
|
|
# Gated on the mesh itself, NOT on the c0re daemon. The mesh is host
|
|
# networking, not a c0re feature: a swarm host that runs no hive —
|
|
# the snapshot store, for one — still has to join the mesh, and under
|
|
# the old `c0re.enable` gate it silently got no `wg-hive` interface
|
|
# at all. Nothing below is c0re-specific; the peer data
|
|
# c0re consumes (HYPERHIVE_PEERS / HIVE_PEER_CA_PATHS) is rendered in
|
|
# ./hive-c0re and stays gated there.
|
|
config = lib.mkIf config.services.hyperhive.swarm.wireguard.enable {
|
|
assertions = [
|
|
{
|
|
assertion = config.services.hyperhive.swarm.wireguard.privateKeyFile != null;
|
|
message = ''
|
|
services.hyperhive.swarm.wireguard.enable requires
|
|
services.hyperhive.swarm.wireguard.privateKeyFile to be set.
|
|
Generate a key: wg genkey > /etc/wireguard/hive.key
|
|
'';
|
|
}
|
|
{
|
|
assertion = config.services.hyperhive.swarm.wireguard.address != "";
|
|
message = ''
|
|
services.hyperhive.swarm.wireguard.enable requires
|
|
services.hyperhive.swarm.wireguard.address to be set
|
|
(e.g. "10.100.0.1/24").
|
|
'';
|
|
}
|
|
];
|
|
|
|
# WireGuard inter-hive mesh. Brings up a `wg-hive` interface and
|
|
# connects to each peer that has `wireguardPublicKey` set.
|
|
networking.wireguard.interfaces =
|
|
let
|
|
wgCfg = config.services.hyperhive.swarm.wireguard;
|
|
meshPeers = lib.filterAttrs (
|
|
_: p: p.wireguardPublicKey != null && p.wireguardAddress != null
|
|
) config.services.hyperhive.swarm.peers;
|
|
in
|
|
{
|
|
wg-hive = {
|
|
ips = [ wgCfg.address ];
|
|
listenPort = wgCfg.listenPort;
|
|
privateKeyFile = wgCfg.privateKeyFile;
|
|
peers = lib.mapAttrsToList (
|
|
_domain: p:
|
|
{
|
|
publicKey = p.wireguardPublicKey;
|
|
allowedIPs = [ p.wireguardAddress ];
|
|
}
|
|
// lib.optionalAttrs (p.wireguardEndpoint != null) {
|
|
endpoint = p.wireguardEndpoint;
|
|
}
|
|
// lib.optionalAttrs (wgCfg.persistentKeepalive != null) {
|
|
persistentKeepalive = wgCfg.persistentKeepalive;
|
|
}
|
|
) meshPeers;
|
|
};
|
|
};
|
|
|
|
# Open the WireGuard UDP port on the host firewall (host-level
|
|
# networking — not inside containers).
|
|
networking.firewall.allowedUDPPorts = [
|
|
config.services.hyperhive.swarm.wireguard.listenPort
|
|
];
|
|
};
|
|
}
|