Skip to content

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]
Hold "Alt" / "Option" to enable pan & zoom

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]
Hold "Alt" / "Option" to enable pan & zoom

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.

ExternalDNS → Pi-hole (primary, in-cluster)
                    ↓ nebula-sync every 15m
             Pi-hole (secondary, NAS)

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.