Ingress is Traefik¶
Traefik is the cluster's front door for HTTPS. It is a Flux controller in
flux/20_controllers/traefik/, and every public hostname is an IngressRoute
served by it.
Reached on a LoadBalancer IP¶
Traefik's Service is a LoadBalancer pinned to 10.82.50.200 with the
lbipam.cilium.io/ips annotation. Cilium hands it out and answers ARP for it:
| Piece | Where |
|---|---|
l2announcements.enabled, raised k8sClientRateLimit |
Cilium values in terraform/20_talos-bootstrap/main.tf |
CiliumLoadBalancerIPPool default, 10.82.50.200–229 |
terraform/20_talos-bootstrap/load-balancer.tf |
CiliumL2AnnouncementPolicy default, LoadBalancer IPs on every node |
same file |
The pool and policy live in OpenTofu next to Cilium rather than in Flux. Flux
installs the Traefik chart and waits for it, and a LoadBalancer Service is not
ready until it has an address; a pool in a later Flux layer would never arrive,
because that layer waits for this one. The pool sits outside the router's DHCP
range and next to the old cluster's MetalLB pool (.150–.199), which stays in
use until that cluster is gone.
Every announced IP holds a Kubernetes lease that its node renews every few seconds, which is what the raised client rate limit is for. Which node holds it, and how the router reaches it, is in Ingress.
rollOutCiliumPods and operator.rollOutPods are on, so a change to the Cilium
values restarts the agents and the operator. Cilium reads cilium-config only at
startup; without them a values change such as enabling L2 announcements lands in
the ConfigMap and does nothing until the pods are restarted by hand.
Traefik is a DaemonSet with externalTrafficPolicy: Local, so the client address
survives into Traefik. With Local, Cilium only announces from a node that runs a
Traefik pod; the DaemonSet puts one on every node, so any node can take over the
IP when another dies.
router02 forwards public :8443 to 10.82.50.200:443, which is why every public
URL in the OpenTofu roots and Grafana carries :8443.
Routes are IngressRoutes¶
Every published service has an IngressRoute on the websecure entry point,
next to the workload it serves: flux/30_openbao/, flux/50_apps/authentik/,
flux/50_apps/observability/grafana/, flux/50_apps/hubble/ and
flux/50_apps/flux-web/, each in an ingressroute.yaml. web (:80) only
redirects to HTTPS. The traefik IngressClass is the cluster default, so a plain
Ingress also lands here.
TLS comes from the default TLS store: wildcard-cbc-gewis-nl-tls in the
traefik namespace, issued by its own Certificate. A route
needs only tls: {}.
Each route carries external-dns.alpha.kubernetes.io/target:
router02.net.gewis.nl, the CNAME target for its hostname — see DNS.
allowCrossNamespace stays off: a route may only point at Services and
Middlewares in its own namespace, so an app namespace cannot publish another
namespace's Service.
Authentication is ForwardAuth to the authentik outpost¶
A protected route uses a forwardAuth Middleware in its own namespace, pointing
at the cbc proxy outpost by its cluster address:
forwardAuth:
address: http://ak-outpost-cbc.authentik.svc.cluster.local:9000/outpost.goauthentik.io/auth/traefik
trustForwardHeader: true
authResponseHeaders: [X-authentik-username, X-authentik-groups, X-authentik-email]
The login callback, /outpost.goauthentik.io/, has to reach the outpost on every
protected host. One IngressRoute in the authentik namespace matches that path
on any host, with priority: 10000 so it beats every app's Host(...) rule,
which Traefik would otherwise rank higher by rule length. A new protected app
needs only the Middleware on its route.
If the outpost is unreachable, the forwardAuth request fails and Traefik
refuses the request rather than serving it.
The outpost publishes nothing itself¶
authentik's outpost controller would create its own routing objects for the
protected hosts: an Ingress, an HTTPRoute and a Traefik Middleware. The
cbc outpost disables all three with kubernetes_disabled_components in
terraform/50_authentik-config/proxy.tf, so it runs only its Deployment and
Service. The Ingress in particular would land on the default class and make
Traefik log errors about its TLS secret, which does not exist; the
outpost-callback route already covers those paths.
Disabling a component stops authentik from reconciling it; an object it already created stays until it is deleted by hand.
The provider sends config as a whole, replacing what authentik stored, so
that block carries the outpost's complete configuration, not just the change.