Skip to content

Network Reference

Network configuration and addressing for the fzymgc-house cluster.

NetworkCIDRPurpose
LAN Supernet192.168.20.0/22Physical LAN — one flat L2 segment (.20.0-.23.255); general client devices (laptops, phones) are DHCP-assigned anywhere in this range, not just .20.0/24
Node Network192.168.20.0/24Physical node IPs (a subset of the LAN supernet above)
Pod Network10.42.0.0/16Kubernetes pods
Service Network10.43.0.0/16Kubernetes services
MetalLB Pool 1192.168.20.145-149LoadBalancer VIPs
MetalLB Pool 2192.168.20.155-159LoadBalancer VIPs

Unique Local Addresses (ULA) provide stable internal IPv6 addressing that works without internet connectivity.

PrefixCIDRPurpose
ULA Basefddb:f665:73f7::/48Site-local IPv6 prefix

Each VLAN receives a /64 subnet from the ULA prefix:

VLANInterfaceIPv6 SubnetRouter Address
Main (3000)bond0.3000fddb:f665:73f7:1::/64::1
Guest (3100)bond0.3100fddb:f665:73f7:2::/64::1
IoT (3020)bond0.3020fddb:f665:73f7:3::/64::1
Lab (1000)bond0.1000fddb:f665:73f7:4::/64::1
Telework (3200)bond0.3200fddb:f665:73f7:5::/64::1

ULA addresses are configured by the router-ipv6-ula Ansible role:

  • Assigns ULA addresses to router interfaces at boot
  • Configures dnsmasq for Router Advertisement (RA)
  • Clients auto-configure via SLAAC

See: ansible/roles/router-ipv6-ula/defaults/main.yml

NodeIP AddressRole
tpi-alpha-1192.168.20.101Control plane (etcd)
tpi-alpha-2192.168.20.102Control plane (etcd)
tpi-alpha-3192.168.20.103Control plane (etcd)
NodeIP AddressBoard
tpi-alpha-4192.168.20.104alpha
tpi-beta-1192.168.20.111beta
tpi-beta-2192.168.20.112beta
tpi-beta-3192.168.20.113beta
tpi-beta-4192.168.20.114beta
AddressPurposeProvider
192.168.20.140Kubernetes API VIPkube-vip
192.168.20.145-149Service LoadBalancersMetalLB
192.168.20.155-159Service LoadBalancersMetalLB

Domain: fzymgc.house

RecordTypeTargetProxy
vaultCNAMECloudflare TunnelNo (TCP passthrough)
authCNAMECloudflare TunnelYes
grafanaCNAMECloudflare TunnelYes
argocdCNAMECloudflare TunnelYes
mealieCNAMECloudflare TunnelYes
longhornCNAMECloudflare TunnelYes
statusCNAMECloudflare TunnelYes

k8s.fzymgc.house - Direct cluster access (not via Cloudflare)

RecordTypeTarget
*.k8sAMetalLB Traefik VIP
doltdbAMetalLB Dolt VIP
PatternResolution
<svc>.<ns>.svc.cluster.localService ClusterIP
<pod-ip-dashed>.<ns>.pod.cluster.localPod IP
<svc>.<ns>.svcShort form (within cluster)

Client DNS for fzymgc.house follows a fixed chain, not a flat forward:

Client -> dnsmasq (192.168.20.1:53, interceptor only) -> unbound (127.0.0.1:8953, authoritative for .house) -> Control D over DoT (everything else)
  • unbound is authoritative for every .house name and answers its configured RR types; any unlisted type (including type 65/HTTPS) gets NODATA, closing the split-horizon ECH leak that previously let clients see a public HTTPS record for an internal-only name.
  • dnsmasq is a pure interceptor: it forwards .house queries to unbound and carries no zone records of its own (the earlier per-name local=/address= dnsmasq overlay has been retired).
  • router-hosts (the Firewalla-side gRPC daemon managed from tf/router-hosts) regenerates the unbound zone (unbound_local/10-fzymgc-authoritative.conf) from the host inventory on startup and on every host-record change — the zone is generated, not hand-maintained.
  • A systemd .path unit watches unbound_local/ and reloads unbound whenever the file changes, so zone updates take effect without manual intervention.

See docs/operations/dns.md for operational commands and the verification runbook, and docs/engineering/specs/2026-07-07-authoritative-house-resolver-design.md for the full design.

Internet
|
v
Cloudflare Edge (WAF, DDoS protection)
|
v
Cloudflare Tunnel (encrypted)
|
v
cloudflared pod (namespace: cloudflared)
|
v
Traefik (namespace: traefik)
|
v
Backend Services
Internal Client
|
v
DNS: *.k8s.fzymgc.house -> MetalLB VIP
|
v
Traefik (namespace: traefik)
|
v
Backend Services
NamePortProtocolPurpose
web80HTTPRedirect to HTTPS
websecure443HTTPSTLS termination
NameNamespaceTypePurpose
oauth-auth-<app> (one per dashboard: agentgateway, temporal, hubble, traefik-dashboard, longhorn)oauth2-proxyForwardAuthPer-route Keycloak-backed SSO, gated by ?allowed_groups=<app>-users (ADR hl-1fia)
oauth-errorsoauth2-proxyErrorsConverts oauth2-proxy’s 401/403 into a redirect to /oauth2/sign_in
redirect-httpstraefikRedirectSchemeHTTP to HTTPS
admin-allowlistkeycloakIPAllowListRestricts id.fzymgc.house /admin + /realms/master to 192.168.20.0/22 (LAN supernet, not just the node /24) + 100.64.0.0/10 (Tailscale CGNAT) for traffic that reaches Traefik directly. Uses default RemoteAddrStrategy; Traefik’s Service runs externalTrafficPolicy: Local so this Middleware sees real client IPs, not a SNAT’d address (hl-ba3a). The Cloudflare-tunnel path never reaches this Middleware at all — it’s gated separately by a Cloudflare Access Application (tf/cloudflare/access.tf, keycloak_admin) — see argocd/app-configs/keycloak/ingress-route.yaml
CertificateIssuerDomains
WildcardLet’s Encrypt*.fzymgc.house
Per-serviceLet’s EncryptIndividual hostnames

MetalLB runs in the metallb namespace (argocd/app-configs/metallb/kustomization.yaml).

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default
spec:
addresses:
- "192.168.20.145-192.168.20.149"
- "192.168.20.155-192.168.20.159"

All pools use L2 mode (ARP) for service advertisement.

apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default
spec:
ipAddressPools:
- default

The cluster uses Cilium for policy enforcement (policyEnforcementMode: default):

  • Standard Kubernetes NetworkPolicy (networking.k8s.io/v1) — Cilium honors these as L3/L4 rules
  • CiliumNetworkPolicy (cilium.io/v2) — used when identity-aware rules (FQDN, ServiceAccount, etc.) or L7 awareness are needed
  • CiliumClusterwideNetworkPolicy (cilium.io/v2) — cluster-scoped policies (not currently in use)

Cilium’s enforcer evaluates policies per pod identity. Pods with NO selecting policy default to allow-all (standard K8s NetworkPolicy semantics).

No CiliumNetworkPolicies are currently in force. Crown-jewel namespace coverage is tracked under the post-migration hardening initiative (see docs/engineering/specs/2026-05-12-cilium-post-migration-hardening-design.md).

The cluster previously used Calico for both CNI and policy enforcement. Migration to Cilium completed 2026-05-12 (PR3 #1017 + PR4). See docs/operations/cilium-migration.md for the full migration log.

Production-mode posture (since 2026-05-13)

Section titled “Production-mode posture (since 2026-05-13)”

The cluster runs Cilium kube-proxy replacement in Strict mode. kube-proxy is no longer running on any node.

SettingValueSource
kube-proxy-replacementtrue (Strict mode + Socket LB)cilium-config ConfigMap
enable-host-legacy-routingfalse (eBPF native routing for host net ns)cilium-config
enable-bpf-masqueradetrue (preserves source-NAT for pod-to-external)cilium-config
KUBERNETES_SERVICE_HOST / _PORT192.168.20.140 (kube-vip VIP) / 6443Cilium agent pod env
disable-kube-proxy: trueon all 3 control-plane nodes/etc/rancher/k3s/config.yaml
  1. Pod opens a connection to a Service ClusterIP
  2. Cilium socketLB intercepts at the socket layer (BEFORE iptables) and rewrites the destination to a backend pod IP via the eBPF Service map
  3. Packet flows directly to the backend pod (across nodes via the existing tunnel/VXLAN datapath if needed)

There are no KUBE-SVC-* iptables chains. The only KUBE-* chain on each node is KUBE-KUBELET-CANARY, which kubelet manages for its own iptables health-check.

  • Service routing issues: see docs/operations/cilium-kpr.md
  • Service map state: cilium-dbg service list from any agent
  • Real-time policy verdicts: cilium-dbg monitor filtered by endpoint ID

Pre-cutover state had kube-proxy-replacement: false with k3s embedded kube-proxy programming iptables KUBE-SVC-* chains for Service routing. Cutover landed via PR #1076 (KPR enable + bpf flips) and PR #1078 (correct k3s disable-kube-proxy flag). Spec at docs/engineering/specs/2026-05-13-cilium-production-mode-hardening-design.md; plan at docs/engineering/plans/2026-05-13-cilium-production-mode-hardening.md.

SettingValue
Tunnel Namefzymgc-house-main
Namespacecloudflared
Replicas2
ProtocolQUIC (primary), HTTP/2 (fallback)

Configured in Cloudflare Zero Trust dashboard, routes to:

  • http://traefik.traefik.svc:80 (most services)
  • tcp://vault-active.vault.svc:8200 (Vault - TCP passthrough)
PortProtocolService
443HTTPSAll web services (via Cloudflare)
6443HTTPSKubernetes API (internal only)
ServicePortProtocol
Traefik80/443HTTP/HTTPS
Vault8200HTTPS
Keycloak443HTTPS
Grafana3000HTTP
ArgoCD80/443HTTP/HTTPS
VictoriaMetrics8428HTTP
Loki3100HTTP
Dolt3306MySQL
Terminal window
kubectl get ipaddresspools -n metallb
kubectl get l2advertisements -n metallb
kubectl get svc -A | grep LoadBalancer
Terminal window
kubectl get ingressroutes -A
kubectl get ingressroutetcps -A
kubectl get middlewares -A
Terminal window
# From within cluster
kubectl run -it --rm debug --image=busybox -- nslookup vault.vault.svc
# External resolution
dig vault.fzymgc.house
Terminal window
# Standard Kubernetes NetworkPolicies
kubectl get networkpolicies -A
kubectl describe networkpolicy <name> -n <namespace>
# CiliumNetworkPolicies (cilium.io/v2)
kubectl get ciliumnetworkpolicies -A
kubectl describe ciliumnetworkpolicy <name> -n <namespace>
# Check policy verdicts via Hubble Relay (cilium-cli)
hubble observe --verdict DROPPED
hubble observe --pod <namespace>/<pod>
# Check enforcement state on a specific node's cilium agent
kubectl exec -n kube-system <cilium-pod> -c cilium-agent -- \
cilium-dbg endpoint list