Watch
0
0
Fork
You've already forked hyperhive
0

semi-spread example: serve internal vhosts on the VPN addresses only, over IPv4 and IPv6

This commit is contained in:
atlas 2026-11-10 00:07:38 +01:00 • committed by mara
commit 7800f92425
9 changed files with 356 additions and 89 deletions

View file

@ -4,6 +4,7 @@
{
imports = [
./wireguard.nix
./gateway.nix
./dns.nix
];

View file

@ -1,6 +1,6 @@
# Names inside the VPN. Every swarm name that is not public resolves to the
# VPN address of the machine serving it, through a static table written on
# each host. Identical on every host.
# Names inside the VPN. Every swarm name, the public ones included, resolves to
# the VPN addresses (IPv4 and IPv6) of the machine serving it, through a static
# table written on each host. Identical on every host.
#
# Two readers need the table. Processes on the host read /etc/hosts
# (`networking.hosts`). Containers (agents, swarm services) ask the hive's
@ -49,17 +49,20 @@ let
};
elsewhere = removeAttrs servedBy [ self ];
# Each machine's names, once per address family: A and AAAA answers.
addressesOf = machine: [
mesh.${machine}.address
mesh.${machine}.address6
];
table = lib.concatMap (
machine: map (address: lib.nameValuePair address elsewhere.${machine}) (addressesOf machine)
) (builtins.attrNames elsewhere);
in
{
networking.hosts = lib.mapAttrs' (
machine: names: lib.nameValuePair mesh.${machine}.address names
) elsewhere;
networking.hosts = lib.listToAttrs table;
services.dnsmasq.settings.addn-hosts = "${builtins.toFile "swarm-vpn-hosts" (
lib.concatStringsSep "\n" (
lib.mapAttrsToList (
machine: names: "${mesh.${machine}.address} ${lib.concatStringsSep " " names}"
) elsewhere
)
lib.concatMapStrings (entry: "${entry.name} ${lib.concatStringsSep " " entry.value}\n") table
)}";
}

View file

@ -0,0 +1,20 @@
# Which addresses the gateway's private vhosts answer on: this machine's VPN
# addresses (IPv4 and IPv6) and the hive bridge. Identical on every host.
# Each machine's own file lists its private vhosts (`public = false`); a vhost
# that stays `public` also answers on these addresses.
#
# The bridge is how this machine's own containers (agents, swarm services)
# reach a name it serves: the hive's resolver answers it with the bridge
# address.
{ config, ... }:
let
mesh = import ./mesh.nix;
me = mesh.${config.networking.hostName};
in
{
services.hyperhive.gateway.privateListenAddresses = [
me.address
me.address6
config.services.hyperhive.network.bridgeIp
];
}

View file

@ -2,6 +2,9 @@
# hive `<name>`.
{ config, lib, ... }:
let
hyperhive = config.services.hyperhive;
name = lib.removePrefix "hive-" config.networking.hostName;
# The leaves for this hive, copied from swarm-central, where the secret store
# runs: `client.pem` and `client-key.pem` from
# `/var/lib/swarm-bao-pki/hives/<name>/`, along with that directory's
@ -10,19 +13,30 @@ let
pki = "/var/lib/swarm-pki";
in
{
services.hyperhive.deploy = {
hive-controller = {
enable = true;
hiveName = lib.removePrefix "hive-" config.networking.hostName;
};
bao = {
clientCertFile = "${pki}/client.pem";
clientKeyFile = "${pki}/client-key.pem";
services.hyperhive = {
deploy = {
hive-controller = {
enable = true;
hiveName = name;
};
bao = {
clientCertFile = "${pki}/client.pem";
clientKeyFile = "${pki}/client-key.pem";
};
};
# The hive's dashboard and its domain answer on the private addresses only.
gateway.vhosts =
lib.genAttrs
[
hyperhive.swarm.dashboardDomains.${name}
hyperhive.swarm.hives.${name}.domain
]
(_: {
public = false;
});
};
# The dashboard is reachable over the VPN only.
networking.firewall.interfaces.wg0.allowedTCPPorts = [
config.services.hyperhive.gateway.httpsPort
];
networking.firewall.interfaces.wg0.allowedTCPPorts = [ hyperhive.gateway.httpsPort ];
}

View file

@ -1,32 +1,40 @@
# The VPN's facts: one entry per machine, read by every host (wireguard.nix,
# dns.nix) and nothing else. Keyed by `networking.hostName`.
# gateway.nix, dns.nix) and nothing else. Keyed by `networking.hostName`.
#
# `address` is the machine's address inside the VPN; `endpoint` is where the
# other machines dial it; `publicKey` is the output of `wg pubkey` on that
# machine. The keys below are placeholders: replace each with the real one.
# `address` and `address6` are the machine's addresses inside the VPN (IPv4
# 10.77.0.0/24 and the unique-local IPv6 prefix fd77:77a1:5e::/64);
# `endpoint` is where the other machines dial it; `publicKey` is the output of
# `wg pubkey` on that machine. The keys below are placeholders: replace each
# with the real one. Generate your own IPv6 prefix (RFC 4193) rather than
# keeping this one.
{
swarm-central = {
address = "10.77.0.1";
address6 = "fd77:77a1:5e::1";
endpoint = "203.0.113.10:51820";
publicKey = "REPLACE-with-swarm-central-wg-pubkey";
};
swarm-services = {
address = "10.77.0.2";
address6 = "fd77:77a1:5e::2";
endpoint = "203.0.113.11:51820";
publicKey = "REPLACE-with-swarm-services-wg-pubkey";
};
swarm-heavyduty = {
address = "10.77.0.3";
address6 = "fd77:77a1:5e::3";
endpoint = "203.0.113.12:51820";
publicKey = "REPLACE-with-swarm-heavyduty-wg-pubkey";
};
hive-one = {
address = "10.77.0.11";
address6 = "fd77:77a1:5e::11";
endpoint = "198.51.100.11:51820";
publicKey = "REPLACE-with-hive-one-wg-pubkey";
};
hive-two = {
address = "10.77.0.12";
address6 = "fd77:77a1:5e::12";
endpoint = "198.51.100.12:51820";
publicKey = "REPLACE-with-hive-two-wg-pubkey";
};

View file

@ -1,6 +1,6 @@
# swarm-central: the swarm controller (and the swarm UI it serves) and the
# secret store.
{ config, ... }:
{ config, lib, ... }:
let
mesh = import ./mesh.nix;
hyperhive = config.services.hyperhive;
@ -14,8 +14,13 @@ in
# and hives on other machines.
deploy.bao.extraListenAddresses = [ mesh.swarm-central.address ];
# The swarm UI is public.
# The gateway's ports are open to everyone. The swarm UI is the one vhost
# that answers there; the store's browser UI answers on the private
# addresses only.
gateway.openFirewall = true;
gateway.vhosts = lib.genAttrs [ hyperhive.swarm.bao.ui.domain ] (_: {
public = false;
});
};
# The store is reachable over the VPN only.

View file

@ -17,6 +17,11 @@ in
deploy.ci-runner.enable = true;
deploy.attic.enable = true;
# The cache answers on the private addresses only.
gateway.vhosts = lib.genAttrs [ hyperhive.swarm.attic.ownDomain ] (_: {
public = false;
});
deploy.bao = lib.mergeAttrsList [
(pair "builder" "builder")
(pair "ciRunner" "ci-runner")

View file

@ -19,9 +19,22 @@ in
# Everything once-per-swarm that is not placed elsewhere below.
deploy.allSwarmServices = true;
# The forge and the matrix homeserver are public, and so is the SSO login
# they send browsers to.
# The gateway's ports are open to everyone. The forge and the matrix
# homeserver answer there, and so does the SSO login they send browsers
# to. Every other vhost here answers on the private addresses only.
gateway.openFirewall = true;
gateway.vhosts =
lib.genAttrs
(with hyperhive.swarm; [
nats.domain
otel.domain
grafana.domain
victoriametrics.domain
victorialogs.domain
])
(_: {
public = false;
});
deploy.bao = lib.mergeAttrsList [
{ enable = false; }

View file

@ -1,6 +1,6 @@
# The VPN: plain NixOS WireGuard, one interface `wg0`, a full mesh built from
# ./mesh.nix. Identical on every host; the host's own entry is picked by
# `networking.hostName`.
# ./mesh.nix, carrying IPv4 and IPv6. Identical on every host; the host's own
# entry is picked by `networking.hostName`.
{ config, lib, ... }:
let
mesh = import ./mesh.nix;
@ -8,13 +8,19 @@ let
in
{
networking.wireguard.interfaces.wg0 = {
ips = [ "${mesh.${self}.address}/24" ];
ips = [
"${mesh.${self}.address}/24"
"${mesh.${self}.address6}/64"
];
listenPort = 51820;
# Created on the host by `wg genkey`, never in the store.
privateKeyFile = "/etc/wireguard/wg0.key";
peers = lib.mapAttrsToList (_: peer: {
inherit (peer) publicKey endpoint;
allowedIPs = [ "${peer.address}/32" ];
allowedIPs = [
"${peer.address}/32"
"${peer.address6}/128"
];
persistentKeepalive = 25;
}) (removeAttrs mesh [ self ]);
};