Networking¶
External Traffic¶
All external traffic enters through Traefik on the NAS. Traefik is the single TLS termination point and routes to Kubernetes, NAS services, and Proxmox.
graph LR
Internet -->|1. DNS lookup| CF[Cloudflare\nDDNS]
CF -->|2. Port 80/443| Router[Home Router\nPort Forwarding]
Router -->|3. Forward| Traefik[Traefik\nNAS]
Traefik -->|4. Route| GW[K8s Gateway API]
Traefik -->|4. Route| Ingress[K8s Ingress]
Traefik -->|4. Route| LB[K8s LoadBalancers\nEmby · Jellyfin\nMinecraft · CS2]
Traefik -->|4. Route| NAS[NAS Services\nGitea · Vault · Portainer ···]
Traefik -->|4. Route| PVE[Proxmox\npve1–pve4]
GW --> Apps[Apps\nvia HTTPRoute]
Ingress --> Apps2[Apps\nvia Ingress]
Internal Traffic¶
Home network devices have both Pi-hole instances configured as DNS resolvers — the primary is the in-cluster instance, the secondary is the NAS replica. DNS records for K8s services resolve directly to the K8s Gateway or Ingress — traffic goes straight to the cluster without passing through Traefik. Only NAS services and Proxmox are routed through Traefik.
graph LR
Device[Home Device] -->|1. DNS query| PH[Pi-hole\nCluster · NAS]
PH -->|2a. K8s app → K8s IP| Device
Device -->|3a. Direct| GW[K8s Gateway API]
Device -->|3a. Direct| Ingress[K8s Ingress]
Device -->|3a. Direct| LB[K8s LoadBalancers\nEmby · Jellyfin\nMinecraft · CS2]
PH -->|2b. NAS/Proxmox → Traefik IP| Device
Device -->|3b. Via Traefik| Traefik[Traefik\nNAS]
Traefik -->|4. Route| NAS[NAS Services\nGitea · Vault · Portainer ···]
Traefik -->|4. Route| PVE[Proxmox\npve1–pve4]
Cloudflare & DDNS¶
hdhomelab.com is hosted on Cloudflare. DNS records are not proxied (DNS-only mode) — Cloudflare is used purely for domain hosting and the Let's Encrypt DNS challenge.
A cloudflare-ddns container runs alongside Traefik and keeps hdhomelab.com and *.hdhomelab.com pointing at the current public IP, updating automatically when the ISP changes it.
The home router forwards ports 80 and 443 to Traefik on the NAS, making all homelab services reachable from the internet under *.hdhomelab.com.
Traefik¶
Traefik runs on the NAS as a Docker container and acts as the edge reverse proxy for the entire homelab. It sits on two networks:
- macvlan — receives traffic from the router
- bridge (
172.27.0.0/16) — reaches NAS Docker containers
Entrypoints¶
| Entrypoint | Port | Protocol | Purpose |
|---|---|---|---|
web |
80 | HTTP | Redirect to HTTPS |
websecure |
443 | HTTPS | TLS termination |
minecraft |
25565 | TCP | Minecraft passthrough |
cs2-tcp |
27015 | TCP | CS2 game server |
cs2-udp |
27015 | UDP | CS2 game server |
cs2-tv-udp |
27020 | UDP | CS2 SourceTV |
TLS¶
All *.hdhomelab.com services are covered by a wildcard Let's Encrypt certificate obtained via the Cloudflare DNS challenge. The certificate is stored in acme.json on the NAS and renewed automatically by Traefik.
*.synology.me domains use a manually imported certificate from Synology DSM, mounted into the Traefik container.
TLS termination at Traefik
All traffic from Traefik to backends is plain HTTP. Backends receive requests with X-Forwarded-Proto: http unless corrected — some apps use this header to determine whether to redirect to HTTPS or construct absolute URLs, and will misbehave if it reports http. See X-Forwarded-Proto for the fix.
Backends running HTTPS with self-signed certificates (Proxmox, Synology DSM) use the skip-verify servers transport.
Routing¶
Dynamic routing config lives in the dockers git repository under traefik/dynamic/ and is synced to the NAS every 60 seconds via a git-sync sidecar. Config changes hot-reload without restarting Traefik.
Routes are split into files by destination:
| File | Destinations |
|---|---|
k8s-gateway.yml |
K8s Gateway API — most apps |
k8s-ingress.yml |
K8s Ingress Controller |
k8s-lb.yml |
K8s dedicated LoadBalancers (Emby, Jellyfin) |
nas-services.yml |
NAS Docker containers + Synology native services |
infra.yml |
Proxmox hypervisors (pve1–pve4) |
minecraft.yml |
Minecraft TCP passthrough |
cs2.yml |
CS2 TCP/UDP passthrough |
Middlewares¶
| Middleware | Purpose |
|---|---|
local-only |
IP allowlist — restricts to home network only (Proxmox, MinIO, etc.) |
authentik |
Forward auth — proxies auth decisions to Authentik |
security-headers |
HSTS and security response headers |
dashboard-auth |
Digest auth for the Traefik dashboard |
skip-verify |
Disables TLS verification for self-signed backends |
Cilium¶
Cilium is the CNI for the cluster. It replaces kube-proxy entirely (kubeProxyReplacement: true) and handles pod networking, load balancing, Gateway API, and network policy enforcement via eBPF.
| Feature | Details |
|---|---|
| Gateway API | Ingress and HTTP routing via standard Gateway API resources |
| L2 Announcements | Advertises LoadBalancer IPs over L2 (ARP) on worker nodes |
| Load Balancer DSR | Direct Server Return via Geneve encapsulation |
| Hubble | Observability with relay, UI, and metrics (DNS, HTTP, TCP, drop, flow) |
| BPF masquerade | eBPF-based NAT masquerade |
Cilium is installed during cluster bootstrap via the taloser-k8s OpenTofu module — see Provisioning.
Gateway API¶
A single shared Gateway (shared in the infra namespace) handles all inbound HTTP/HTTPS traffic for *.hdhomelab.com arriving from Traefik. It has a fixed IP from the L2 LoadBalancer pool.
Gateway: shared (infra)
├── HTTP :80 → *.hdhomelab.com (all namespaces)
└── HTTPS :443 → *.hdhomelab.com (TLS terminated, Let's Encrypt cert)
Apps expose themselves by creating an HTTPRoute in their own namespace that references the shared gateway:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app
namespace: my-namespace
spec:
parentRefs:
- name: shared
namespace: infra
hostnames:
- my-app.hdhomelab.com
rules:
- backendRefs:
- name: my-app
port: 8080
Tip
TLS is handled centrally at the gateway — apps only need to define their route and backend service. No TLS config needed in the HTTPRoute.
L2 LoadBalancer¶
Cilium manages an L2 LoadBalancer IP pool within the 192.168.71.0/24 subnet:
| Resource | Value |
|---|---|
| IP Pool | 11 IPs within 192.168.71.0/24 |
| Announcement | ARP on worker nodes only (control planes excluded) |
Services that request a LoadBalancer type get an IP from this pool automatically. Multiple services can share a single IP using Cilium's IP sharing key (port multiplexing), which is used by Pi-hole's DNS and web services.
DNS¶
Two Pi-hole instances serve different scopes and stay in sync automatically:
| Instance | Location | Serves |
|---|---|---|
| Primary | In-cluster | Home network (primary DNS) |
| Secondary | NAS | Home network (secondary DNS) + cluster nodes |
Config Sync¶
nebula-sync runs in the cluster and pushes a full sync from the primary to the secondary every 15 minutes. This means DNS records created by ExternalDNS (e.g. when a new app is deployed) propagate automatically.
Unbound¶
Unbound runs as a sidecar in the primary Pi-hole pod and acts as a recursive DNS resolver. Pi-hole uses it as the sole upstream (127.0.0.1#5335) instead of a public DNS like 8.8.8.8.
Client → Pi-hole (blocking + caching) → Unbound (recursive resolution)
↓
Root servers → TLD → Authoritative
This means DNS queries never leave the homelab — Unbound walks the DNS tree from the root servers itself, with no dependency on a third-party resolver.
Secondary Pi-hole
The NAS replica does not run Unbound. It continues to use a public upstream (1.1.1.1) as a fallback resolver. This is acceptable since the secondary is a last-resort failover.
ExternalDNS¶
ExternalDNS watches Kubernetes resources and automatically creates DNS records in the primary Pi-hole:
| Setting | Value |
|---|---|
| Sources | Ingress, Service, HTTPRoute |
| Policy | upsert-only — records created and updated, never deleted |
Tip
Deploying an app with a hostname makes it resolvable cluster-wide immediately and home-network-wide within 15 minutes — no manual DNS management required.