Networking
Source is
docs/assets/network.d2;mise run docs:diagramsre-renders it. Every address in it is restated from the topology SSOTs below, so change those first.
Networking spans all four layers: UniFi VLANs and BGP at Layer 1/3 (
terraform/network/), Cilium and the Gateway API inside each cluster at Layer 2 (clusters/*/networking/), Cloudflare and Tailscale gluing sites together at Layer 3. This page is the single place the whole story lives. Cluster composition is on Architecture/Kubernetes; host hardware is on Fleet; live discovery of the running UniFi controller is theunifi-networkskill (Runbooks/Inspect UniFi Network).Sites and fabric
Two UniFi consoles, each its own Terraform root:
terraform/network/unifi/folly/on-site andterraform/network/unifi/offsite/at the remote site. They're joined by exactly one inter-site data plane: a UniFi Site Magic WireGuard tunnel (wgsts1000).Each site's k8s nodes run Cilium with a BGP control plane (ASN 64513) peering eBGP with that site's own UniFi gateway (ASN 64512) β folly's UDM Pro, offsite's UCG Max.
LAN / VLANs
folly (
terraform/network/unifi/folly/), all networks domainlolwtf.caunless noted:network VLAN CIDR notes Management β 10.1.0.0/24domain fml.pulsifer.ca; WLANfml(WPA3)Lab Net 2 10.2.0.0/24WLAN lab(open, hidden SSID); SSOT belowKubernetes 8 10.3.0.0/26node network; DHCP hands out iPXE boot info pointing at spore future 1337 10.13.37.0/28IPv6 PD enabled iot 666 10.66.6.0/26domain iot.fml.pulsifer.ca
offsite (
terraform/network/unifi/offsite/):network VLAN CIDR Default β 192.168.1.0/24Kubernetes 2 10.89.0.0/28
folly isolates Lab Net and Kubernetes together in a custom
Labfirewall zone (firewall.tf); offsite's Kubernetes network sits in the defaultInternalzone β this split is the root of the cross-site reachability gap below.Client MACs/DHCP reservations for both sites are declared in
terraform/network/unifi/folly/clients.yaml(cameras,unmanaged-infra,lab,k8s,rpis, β¦ groups) β point at that file rather than enumerating hosts here.
Lab-network SSOT:
lab-topologyclusters/folly/config/lab-topology.jsonis the flat-string Fluxlab-topologyConfigMap. It owns the Lab/future CIDRs and the full host addresses consumed by NixOS, folly storage, and folly monitoring.nix/lib/lab.nixprojects the ConfigMap into the attribute shape host and service modules consume. The folly UniFi root reads it throughterraform/modules/cluster-topology, preservinglocal.labfor network resources and deriving the gateway-host form of the future CIDR.The
unifi_network.labprecondition compares every selected ConfigMap host address with itsclients.yamlDHCP-reservation octet. The ConfigMap owns full addresses;clients.yamlowns MACs and reservation octets; disagreement fails the Atlantis plan.
Cluster network facts
The per-cluster
cluster-topologyConfigMaps (clusters/<site>/config/cluster-topology.json) are the SSOT for every cluster network fact β full mechanism (FluxsubstituteFrom,conftestschema check, Nix/Terraform consumers) is on Architecture/Kubernetes. The current values:key folly offsite API_SERVER_IP10.3.0.1010.89.0.10API_SERVER_HOSTNAMEfolly.lolwtf.caoffsite.lolwtf.caROUTER_IP10.3.0.110.89.0.1K8S_NODE_CIDR10.3.0.0/2610.89.0.0/28CILIUM_POD_CIDR10.100.0.0/2010.101.0.0/20SERVICE_CIDR10.10.0.0/1610.11.0.0/16CLUSTER_DNS10.10.0.25410.11.0.254LB_RANGE10.3.0.64/2610.89.0.64/26BGP_GATEWAY_ASN6451264512BGP_CILIUM_ASN6451364513
Cilium: CNI + BGP load balancer
clusters/<site>/networking/cilium/ip-pools.yamldeclares aCiliumPodIPPoolfrom${CILIUM_POD_CIDR}and aCiliumLoadBalancerIPPoolfrom${LB_RANGE}with a catch-allserviceSelectorβ every Service/Gateway of type LoadBalancer gets a VIP from that pool.bgp.yamlin the same directory sets up aCiliumBGPClusterConfig(nodes labelledbgp-enabled: "true"β every node in both clusters, per the Terraform bootstrap'snode-labels.tf) peering to${ROUTER_IP}at${BGP_GATEWAY_ASN}, and twoCiliumBGPAdvertisements: pod IP pools, and Service addresses. The two clusters advertise different address types β folly advertises onlyLoadBalancerIP; offsite advertisesClusterIP,ExternalIP, andLoadBalancerIP.On the gateway side,
unifi_bgp(in each site'sbgp.tf) uploads a raw FRR config file (bgp-folly.conf/bgp.conf) rather than using the provider's structured ASN/peer schema, because the config needs custom prefix-lists and route-maps the structured form can't express.
Cross-site reachability
The single Site Magic tunnel carries two control-plane protocols, but both resolve through the same tunnel, so they are not independent paths:
OSPF (Site Magic's own) auto-shares the LAN/node subnets (
10.3.0.0/26β10.89.0.0/28) and wins the RIB for them.iBGP between the gateways (sourced from each gateway's LAN router-id via
update-source, so sessions and reachability survive WAN failover) is the only way the Cilium LoadBalancer/32VIPs and pod CIDRs (10.100.0.0/20/10.101.0.0/20) cross sites at all β OSPF never carries them.
Because there's one tunnel, which protocol wins the RIB doesn't matter for reachability. What matters is the gateway firewall: it only forwards what it's told to allow across the
Lab/InternalβVpnforward.folly isolates Lab Net + Kubernetes in a custom
Labzone (firewall.tf), so the cross-site allow policies (nest_k8s_to_folly_k8s,folly_k8s_to_nest_k8s) must explicitly list the full k8s address space β node CIDR, LB VIP pool, and pod CIDR β not just the node subnet. Matching only the node subnet was the actual cause of a real outage: nodeβnode traffic worked (node subnets were allowed) while pod-sourced packets got dropped on theLab β Vpnforward.offsite has no custom firewall policies at all β its Kubernetes network sits in the default
Internalzone, whose predefinedInternal β Vpnrules already permit the full k8s address space across the tunnel. If offsite's k8s network is ever moved into a custom/isolated zone, it needs folly's explicit pod-CIDR + VIP-pool allow policies mirrored, not just the node subnet.
Gateway API ingress
Every cluster runs a shared
cluster-gateway(gatewayClassName: cilium) serving*.lolwtf.caoff a cert-manager wildcard cert. Individual apps attach either as an extra listener on that shared Gateway (offsite's pattern βdave,sonarr,radarr,prowlarr,bazarr,bittorrentare all listeners on onecluster-gateway, one shared VIP) or as their own dedicated Gateway with its own VIP from the LB pool (folly's pattern βjellyfin,hermes,dump,tronbyt,netbencheach get their ownGateway+HTTPRoute).cert-manager (
clusters/*/networking/cert-manager/) runsletsencrypt-productionandletsencrypt-stagingClusterIssuers using ACME DNS-01 against Cloudflare (API token fromcloudflare-secret.sops.yaml), scoped to the cluster's secret domain and${GATEWAY_ZONE}.Each console obtains and renews its own Let's Encrypt certificate for its gateway domain (
fml.pulsifer.ca,nest.pulsifer.ca) through UniFi OS's built-in Let's Encrypt support (DNS-01 via Cloudflare), configured in the console UI β nothing in this repo mints or delivers console certificates.
external-dns
clusters/base/networking/external-dns/runs external-dns against providercloudflare, sourcing records fromcrd,ingress, andgateway-httproute, insyncpolicy withtxtOwnerId: ${CLUSTER_NAME}(so folly and offsite don't fight over the same zone's TXT ownership records) anddomainFiltersscoped to the cluster's secret domain,${GATEWAY_ZONE},${SPINDRIFT_DOMAIN},embarrassing.ca, andwishin.appβ the last four are the zones Spindrift mints App names in, which is what lets a route on any of them (oauth2-proxy's authenticated edge, a Spindrift App's own HTTPRoute) publish its name. Each cluster's overlay patches in--fqdn-template={{.Name}}.${SECRET_DOMAIN}.Each cluster also ships a static
DNSEndpointCRD (networking/external-dns/endpoints/gateway.yaml) publishing${GATEWAY_DOMAIN}as an A record targeting every local VLAN gateway IP β all four (10.1.0.1,10.2.0.1,10.3.0.1,10.13.37.1) on folly, just10.89.0.1on offsite.
Cloudflare Tunnel
The site tunnels run from the shared Deployment in
clusters/base/networking/cloudflare/, with each overlay supplying its SOPS token. Their remotely managed ingress tables live with the tunnel resources interraform/network/cloudflare/lolwtf.ca.tf.oauth2.lolwtf.devis oauth2-proxy's authenticated edge: its ownoauth2-tlslistener on offsite'scluster-gateway, a cert-manager DNS-01 certificate, and an unproxied A record external-dns writes from the route at the gateway's address. A Spindrift App publishes its name the same way β an unproxied A record at the Apps gateway forreach: private, a proxied CNAME at the Spindrift tunnel forreach: public; Architecture/Spindrift covers the reach and auth semantics.Other cluster apps exposed through Gateway API and external-dns retain their private Cilium LB records and LAN/tailnet reachability. The site offsite tunnel also routes Atlantis's webhook hostname.
Tailscale
terraform/network/tailscale/manages thepirate-musical.ts.nettailnet: devices, the ACL policy (policy.hujson), and a federated OIDC identity that lets thenixos-deployGitHub Actions workflow join astag:ci(scoped by the ACL to SSH intotag:pi4only, no long-lived secret).Forge enrolls with an independently revocable OAuth client restricted to
tag:lab-host. Terraform escrows the long-lived client secret in 1Password; Forge consumes an encrypted copy through its SSH-host-key-scoped SOPS file.In-cluster,
clusters/base/networking/tailscale/runs thetailscale-operatorHelmRelease; each cluster'stailscale-connectors/connector.yamldeploys a subnet-routerConnectoradvertising that site's LAN CIDRs plus${K8S_NODE_CIDR}and${LB_RANGE}β folly's connector also advertises10.1.0.0/24and10.2.0.0/24, offsite's advertises192.168.1.0/24.The k8s node hosts themselves run no Tailscale client β
nix/system/tailscale-disable.nixforce-disablesservices.tailscale, and it's imported by all five k8s hosts (optiplex,riptide,shale,oldschool,retrofit). Tailnet reachability into the clusters goes entirely through the Connector subnet router, not per-node clients.policy.hujson'sgrantsexplicitly permittag:follyβ offsite's k8s nodes/LB/LAN andtag:offsiteβ folly's k8s nodes/LB, plusautoApprovers.routesthat auto-accept the Connectors' advertised CIDRs without manual review.
DNS
capsuleandsporerun the shared CoreDNS sinkhole policy fromnix/services/coredns-sinkhole.nix, forwarding over TLS to Cloudflare's malware-filtering resolvers. Their machine records arecapsule.lolwtf.caandspore.lolwtf.ca;dns.lolwtf.capublishes both as the stable DNS service.The immutable hosts policy is loaded without polling, and CoreDNS keeps no query database or query log. Its Prometheus endpoint provides aggregate request, response-code, cache, latency, upstream-health, and hosts-entry metrics; it does not provide Pi-hole-style per-query blocked/client analytics.
capsuleandsporerun the redundant Chrony service described on their fleet pages.time.lolwtf.capublishes both hosts as the stable NTP service name.Cloudflare zones, records, Access policy, and tunnels are declared under
terraform/network/cloudflare/. The dedicatedlolwtf.devzone carries theoauth2authenticated edge and is the default zone Spindrift mints App names in;lolwtf.cacarries ordinary lab and cluster names and is also a zone Spindrift mints in, so a minted name and a hand-managed one share that flat space.embarrassing.caandwishin.appare public-only Spindrift zones.LAN and cluster hosts resolve as
<host>.lolwtf.caβ static A records come fromk8s.tf(k8s,optiplex,riptide,shale,nuc,erx) andlolwtf.ca.tf(everylab/rpisclient inclients.yaml), plus the per-cluster API-server and gateway records above. Reaching offsite hosts from off-net requires the tailnet.
Known gaps
The FRR
*.conffiles (bgp-folly.conf,bgp.conf) hold their prefix-lists as hardcoded CIDR literals, not${...}-interpolated from the topology JSON βunifi_bgp.configis a rawfile()read with no templating, so a CIDR change incluster-topology.jsonhas to be hand-copied into the matchingip prefix-listlines on both sites.terraform/network/tailscale/policy.hujsonhardcodes the same subnet and VIP-pool CIDRs inautoApprovers.routes,ipsets, andtestsβ HuJSON has no variable substitution, so this file can't reference the topology SSOT even in principle; it has to be hand-kept in sync.
Linked references 5
Network fabric β terraform/network/: UniFi at the two sites (unifi/folly/, unifi/offsite/), cloudflare/, tailscale/. Covered in Architecture/Networking β this page doesn't duplicate VLANs, BGP, or tunnel detail.
Cilium (CNI + BGP load balancing) and the Gateway API live under each cluster's networking/, built from shared Helm releases in clusters/base/networking/{cert-manager,cloudflare,external-dns,tailscale} plus per-cluster secrets and config. Full detail, including the cross-site firewall gating, is on Architecture/Networking.
Consumers beyond Flux: nix/services/k8s/networks.nix reads cluster topology with builtins.fromJSON; nix/lib/lab.nix projects lab topology. OpenTofu roots instantiate terraform/modules/cluster-topology, selecting the site and ConfigMap name. Full picture on Architecture/Networking.
Architecture/Networking β VLANs, BGP, Cilium load balancing, tunnels, and the cross-site fabric
Cloud and network β UniFi, Cloudflare, Tailscale, GCP and Google Workspace under Architecture/Terraform, with applies gated through Atlantis. The network fabric itself is Architecture/Networking.