Traffic Flow
A request reaches an application through one of two independent paths, depending on whether the service is meant to be public or private. Every app picks one.
flowchart TB
req["Incoming request"] --> pick{"Public or private?"}
pick -->|Public| gw["Gateway API, served by Envoy Gateway"]
pick -->|Private| ts["Tailscale Operator"]
gw --> svc["Service in the cluster"]
ts --> svc
Public traffic is routed with the Gateway API, which Envoy Gateway implements and where TLS is terminated. Private traffic is published onto a Tailscale network with Tailscale Operator, so it stays reachable only from devices on that network. The rest of this page walks through each path in turn.
Public services
Section titled “Public services”Public services on ${FQDN_01} are routed with the Gateway API, using Gateway and HTTPRoute resources. Envoy Gateway is the controller that implements those resources and runs the Envoy proxies that sit in the request path.
flowchart TB user["Client"] --> dns["Cloudflare DNS"] dns --> router["Home router"] router --> gw["Envoy Gateway"] gw --> waf["Coraza WAF"] waf --> route["HTTPRoute"] route --> svc["Service"] --> pod["Pod"]
- DNS. Cloudflare hosts the zone.
external-dnsmanages the records, and a dynamic-DNS updater keeps them pointed at the home’s changing public IP. The records are DNS-only, not proxied through Cloudflare. - Router. The home router forwards port 443 to the gateway’s LoadBalancer IP, which Cilium assigns from a pool and announces on the LAN over L2.
- TLS. The gateway terminates TLS with a wildcard certificate (see below). Port 80 returns a 301 redirect to HTTPS.
- WAF. Coraza runs as an Envoy extension with the OWASP Core Rule Set. Per-app exclusions handle known false positives without disabling the rules globally.
- Routing. Each public app owns an
HTTPRoutethat matches its hostname and forwards to the app’s Service, which load-balances across the pods.
A service is public when it has an HTTPRoute attached to the http-gateway-api gateway, with a hostname under ${FQDN_01}.
Private services
Section titled “Private services”Anything on ${FQDN_02} is published onto the Tailnet with Tailscale Operator.
flowchart TB user["Client on the Tailnet"] --> ts["Tailscale Operator"] ts --> svc["Service"] --> pod["Pod"]
A service is private when it uses a Kubernetes Ingress with ingressClassName: tailscale and the tailscale.com/proxy-group: ingress-proxies annotation. The operator exposes it on the Tailnet at a hostname under ${FQDN_02}, with TLS handled by Tailscale. Nothing about this path touches the public internet, so only devices on the Tailnet can reach it.
TLS certificates
Section titled “TLS certificates”cert-manager obtains a wildcard certificate for *.${FQDN_01} from Let’s Encrypt using the DNS-01 challenge through Cloudflare, then stores it in the tls-wildcard secret that the gateway serves. Renewal is automatic, and because the challenge is DNS-based it works without exposing anything to the internet.