Skip to content

How traffic flows

Every path ends at the same Cilium Gateway (192.168.1.220); they differ in how a packet reaches it. DNS decides which path a hostname takes.

Follow one request

Press Play or Step · click any box for details

Public request · internet → GrafanaInternet visitorpublic apps only1Cloudflare DNSDNS only2TraefikTLS + routing3WireGuard tunneledge → LAN5NetBird peertunnel end6Cilium Gatewaysingle entry point7Grafanadashboards8ForwardAuth · skipped for GrafanaAuthentiksingle sign-on4LAN or VPN request · straight to the gatewayLAN userhome network9Cloudflare DNSDNS only10Cilium Gatewaysingle entry point11Headlampcluster UI12wildcard *.madhan.app → 192.168.1.220 · skips the edgePress Play or Step to follow a request from the internet to Grafana, then one from the LAN to Headlamp.▶ PlayStep →

Cloudflare holds the records, all DNS-only (not proxied). Source: cloudflare.go.

RecordPoints toPath
*.madhan.app192.168.1.220 (Gateway LB IP)LAN or VPN only
auth, netbird, proxy, uptime, *.proxy178.156.199.250 (Bifrost)Served by containers on the VPS
each entry in publicServices178.156.199.250 (Bifrost)Public, tunnelled to the cluster

To make a cluster service public, add it to publicServices; see Service exposure.

Traefik on Bifrost terminates TLS and proxies plain HTTP to http://192.168.1.220. The host’s NetBird agent routes the LAN subnet into the WireGuard mesh, so that address is reachable from the VPS.

sequenceDiagram
    actor C as Client
    participant DNS as Cloudflare DNS
    participant TR as Traefik<br/>Bifrost :443
    participant BA as bifrost-agent<br/>wt0 (host network)
    participant WG as WireGuard<br/>(direct or relay)
    participant NP as k8s-routing-peer<br/>netbird-peer pod, wt0
    participant ETH as Gateway node<br/>eth0 + Cilium BPF
    participant EN as Envoy<br/>(Gateway)
    participant P as Backend pod

    C->>DNS: grafana.madhan.app?
    DNS-->>C: 178.156.199.250
    C->>TR: HTTPS
    Note over TR: TLS terminated<br/>ForwardAuth unless SkipAuth
    TR->>BA: HTTP to 192.168.1.220:80
    Note over BA: route 192.168.1.0/24 via wt0
    BA->>WG: encrypted
    WG->>NP: decapsulated on wt0
    Note over NP: kernel forward wt0 → eth0<br/>MASQUERADE: src → this node's IP
    NP->>ETH: LAN, dst 192.168.1.220
    Note over ETH: node holding the L2 lease answers ARP<br/>Cilium DNAT to Envoy
    ETH->>EN: HTTP
    EN->>P: HTTPRoute match on Host
    P-->>C: reply retraces the path<br/>(conntrack undoes NAT)
HopDetailSource
TraefikFile provider only; public routers are generated into public-services.yml from publicServiceshetzner.go
bifrost-agentnetbird-agent container, network_mode: host, peer name bifrost-agentdocker-compose.yml
k8s-routing-peernetbird-peer StatefulSet, hostNetwork, no nodeSelector: it runs on whichever worker the scheduler picksnetbird_peer.go
MASQUERADEInit container adds POSTROUTING ! -o wt0 -d 192.168.1.0/24 -j MASQUERADEnetbird_peer.go
CiliumDatapath on ens18/eth0 only; L2 announcement of the LB poolcilium.go

Replies need no route back to the overlay. MASQUERADE rewrites the source to the peer node’s LAN IP, so the backend answers that node, and conntrack on it reverses the NAT and returns the packet into wt0. Nodes carry only a default route (talos.go), which is why the peer can float between workers.

A 504 from a public URL means Traefik is up but cannot reach the Gateway. The runbook is on NetBird peer.