Traefik¶
Traefik is the only ingress. It runs as three replicas in the traefik namespace, one per
hypervisor (required anti-affinity on topology.kubernetes.io/zone), from the Helm chart in
kustomization.yaml with
values.yaml. Routes are Gateway API
HTTPRoutes; the Kubernetes CRD provider is on for Middleware objects, with
allowCrossNamespace: true.
Gateways¶
Two Gateways share the same pods and are told apart by entrypoint port. Each has its own
LoadBalancer Service with a fixed VIP from the Cilium pool (see README.md).
| Gateway | Service and VIP | Listener | Port (Service to pod) | TLS secret in cert-manager |
|---|---|---|---|---|
traefik-gateway-public |
traefik-public, 10.3.10.101 |
web |
80 to 8000, redirects to https | none |
websecure |
443 to 8443, TCP and UDP | bigd-no-tls, nordbye-it-tls, logeverylift-com-tls, picked by SNI |
||
traefik-gateway-private |
traefik-private, 10.3.10.102 |
web |
80 to 8001, redirects to https | none |
local-bigd-no |
443 to 8444, hostname *.local.bigd.no |
local-bigd-no-tls |
Files: gateway-public.yaml,
gateway-private.yaml,
service-public.yaml,
service-private.yaml. The chart's
own Service and Gateway are disabled. The certificates are in
certificates.md.
The public Gateway carries the label external-dns: public and the target annotation
ddns.bigd.no, so external-dns publishes every bigd.no hostname routed through it (see
cloudflare.md). The private Gateway gets no DNS from the cluster;
local.bigd.no names are UniFi records (dns.md).
The two VIPs are announced from disjoint node sets, public from the workers and private
from the control planes (cilium.md).
The dashboard is at traefik.local.bigd.no
(httproute-dashboard.yaml).
HTTP/3¶
websecure serves HTTP/3 over UDP on the same port. It needs four pieces, and a missing
one makes h3 fail silently while HTTP/2 keeps working:
http3.advertisedPort: 443invalues.yaml. Traefik listens on 8443 and would otherwise advertiseAlt-Svc: h3=":8443", which is not reachable from outside.- The
websecure-h3UDP port ontraefik-public. - UDP 8443 in the Traefik
ciliumnetworkpolicy.yaml. - The
443 tcp/udpport forward to10.3.10.101on the UniFi gateway (unifi.md).
The private gateway serves TCP only.
Tracing and metrics¶
Every request is traced (sampleRate: 1.0) and exported over OTLP gRPC to
tempo.monitoring:4317, service name traefik. Prometheus metrics with entrypoint, router
and service labels are on port 9100, scraped through the chart's ServiceMonitor. Access
logs are on. See ../observability/README.md.
Middlewares¶
Middlewares are Traefik Middleware objects in the app's own namespace, attached to an
HTTPRoute rule as ExtensionRef filters. Traefik applies filters in the order listed, so
the allow list goes first and compression last.
| Middleware | Used by | What it does |
|---|---|---|
<app>-cloudflare-only |
every proxied public route | ipAllowList of the Cloudflare ranges, so the origin only answers the edge |
<app>-ratelimit |
blog, portfolio | rateLimit token bucket per client (blog 25/s burst 100, portfolio 50/s burst 200) |
<app>-inflight |
blog, portfolio | inFlightReq concurrency cap per client (blog 30, portfolio 60) |
<app>-compress |
blog, blog-stage, portfolio, portfolio-stage, headroom-demo | compress with br, zstd, gzip on text types over 1 KiB |
<app>-authentik |
homepage, reelsmith admin route | forwardAuth to authentik-server.identity |
Templates: blog/httproute.yaml and the
*-middleware.yaml files beside it, and
homepage/httproute.yaml for forward auth.
Rules that the files depend on:
ipAllowListhas noipStrategy. It checks the TCP peer, which is a Cloudflare edge; X-Forwarded-For can be spoofed.rateLimitandinFlightReqkey onipStrategy.depth: 1, the rightmost X-Forwarded-For entry, which is the client Cloudflare saw. That entry is only kept because the entrypoints trust the Cloudflare ranges (forwardedHeaders.trustedIPs).inFlightReqmust setsourceCriterionexplicitly. Its default keys on the request host, which caps the whole site rather than each client.- Each Traefik replica keeps its own bucket, so the effective limit is up to three times the configured value.
- A forward-auth route needs a separate
/outpost.goauthentik.io/rule without the auth filter that points atauthentik-serverinidentity, allowed by a ReferenceGrant inidentity(for examplereferencegrant-homepage.yaml). forwardAuth.trustForwardHeaderstaysfalseon public hosts.
audiobookshelf.bigd.no is the one public route without cloudflare-only: it is DNS-only
(see cloudflare.md), so its clients connect directly.
Cloudflare ranges¶
The Cloudflare IP ranges exist in fourteen places that must stay identical:
trustedIPs under ports.web.forwardedHeaders in values.yaml (reused by websecure
through the YAML anchor &cloudflareIPs), and the sourceRange of every
cloudflare-only-middleware.yaml. A range missing from a middleware returns 403 to
visitors served by that edge; a range missing from trustedIPs makes those visitors share
one rate-limit bucket.
List every copy:
git ls-files 'k8s/**/cloudflare-only-middleware.yaml'
grep -rln 'ranges mirror trustedIPs' k8s/
To update, fetch https://www.cloudflare.com/ips-v4 and https://www.cloudflare.com/ips-v6,
edit the list under trustedIPs: &cloudflareIPs in values.yaml, then write the same list
into every middleware in the same PR. Check that each copy matches:
sed -n '/trustedIPs: &cloudflareIPs/,/expose:/p' k8s/talos/infra/traefik/values.yaml \
| grep -oE '[0-9a-f:.]+/[0-9]+' > /tmp/cf-ranges
for f in $(git ls-files 'k8s/**/cloudflare-only-middleware.yaml'); do
grep -oE '[0-9a-f:.]+/[0-9]+' "$f" | diff -q /tmp/cf-ranges - >/dev/null || echo "differs: $f"
done
No output means every copy matches. A new public app adds its own copy.
Exposing an app¶
- Pick the gateway. Internal apps use
traefik-gateway-privatewith a<name>.local.bigd.nohostname, and need a UniFi DNS record (dns.md). Public apps usetraefik-gateway-publicwith a hostname underbigd.no,nordbye.itorlogeverylift.com, the zones that have a certificate onwebsecure. - Write an
HTTPRoutein the app's namespace withparentRefsto that Gateway in namespacetraefik.verksted/httproute.yamlis the private template. - For a public app, copy a
cloudflare-only-middleware.yamlinto the app, renamed<app>-cloudflare-only, and add it as the first filter. Abigd.nohostname also setsexternal-dns.kubernetes.io/cloudflare-proxied: "true"on the route;it-tools/httproute.yamlis the template.nordbye.itandlogeverylift.comhostnames are Terraform records, seecloudflare.md. - Add rate-limit, in-flight or compress middlewares as the site needs, in the order above.
- Allow ingress from Traefik in the app's CiliumNetworkPolicy
(
cilium.md). - An app that scales to zero routes to
keda-add-ons-http-interceptor-proxyinkedainstead of its own Service, and its namespace must be added toreferencegrant.yaml.
Check the route was accepted:
kubectl -n <ns> get httproute <name> -o jsonpath='{.status.parents[*].conditions}'