The Traefik complete guide covers Docker integration, Let’s Encrypt, the middleware ecosystem, and touches on Kubernetes. This post goes much deeper on the Kubernetes-specific story. It assumes you are already comfortable with Kubernetes fundamentals — pods, services, namespaces, RBAC, Helm — and want to understand how to run Traefik in a cluster properly, at production quality.
By the end you will have a clear mental model of how Traefik integrates with the Kubernetes API, why its CRDs are more expressive than standard Ingress resources, how to handle TLS with cert-manager, how to lock down RBAC, how to run multiple replicas without certificate-storage conflicts, and how to wire up real observability.
A full working production example ties everything together in the final section.
Part 1: Why Traefik for Kubernetes Ingress?
The ingress controller landscape
Several controllers compete for the same role. Choosing well saves significant operational pain later.
nginx-ingress (ingress-nginx) is the incumbent. It is battle-tested, has the largest community, and every Kubernetes tutorial reaches for it by default. Its configuration surface is wide — you can tune almost anything through annotations — but those annotations are stringly-typed, not validated by the API server, and invisible to kubectl explain. Reloading nginx requires restarting worker processes; under heavy load this causes a brief traffic disruption. Complex routing logic (header-based splits, traffic weights) requires NGINX Plus or community hacks.
HAProxy Ingress delivers raw performance that matters at very high request rates. Configuration is annotation-driven like nginx-ingress, with the same ergonomic downsides. It is an excellent choice when throughput is the dominant constraint and you want a familiar proxy underneath.
Contour pairs with Envoy and introduced its own CRD — HTTPProxy — that solves the annotation problem with real API objects. It has solid multi-team namespace isolation through delegation. The trade-off is that you now have two components to manage (Contour + Envoy DaemonSet/Deployment).
Envoy Gateway is the most ambitious entrant: a full implementation of the Kubernetes Gateway API backed by Envoy. It is production-ready as of v1.0 and will likely become the canonical implementation of the Gateway API spec. If you are starting a new cluster today and want to be on the standards track, Envoy Gateway deserves serious consideration.
Traefik sits in a different place from all of these. It was built for dynamic environments from the ground up. Its key differentiators for Kubernetes:
| Feature |
Traefik |
nginx-ingress |
Contour |
Envoy Gateway |
| Live reconfiguration |
Yes, zero reload |
Partial (Lua, but limited) |
Yes |
Yes |
| CRDs for routing |
IngressRoute (rich) |
Annotations only |
HTTPProxy |
Gateway API |
| TCP routing via CRD |
Yes (IngressRouteTCP) |
Limited (SNI passthrough) |
No |
TCPRoute (alpha) |
| UDP routing via CRD |
Yes (IngressRouteUDP) |
No |
No |
No |
| Middleware as first-class objects |
Yes (Middleware CRD) |
Annotations |
No |
Policy attachments |
| Built-in dashboard |
Yes |
No |
No |
No |
| ACME built-in |
Yes |
No |
No |
No |
| Weighted traffic splitting |
Yes |
nginx-ingress-plus / canary annotations |
Yes |
Yes |
| Gateway API support |
Experimental/v3 |
v1.0 (ingress-nginx v4+) |
v1.0 |
v1.0 |
Where Traefik wins
Live reconfiguration without disruption. Traefik watches the Kubernetes API server and updates its routing table in memory. Adding a new IngressRoute is instantaneous — no process restart, no dropped connections. This matters for high-volume clusters where nginx’s reload-on-change causes measurable tail latency spikes.
CRDs as the configuration surface. Instead of opaque nginx.ingress.kubernetes.io/... annotations scattered across dozens of Ingress resources, Traefik configuration lives in versioned, validatable CRD objects. kubectl explain ingressroute.spec works. Admission webhooks can validate them. GitOps tools treat them as first-class resources.
Middleware reuse. Define a Middleware resource once (e.g., an rateLimit policy or a forwardAuth configuration pointing at Authentik) and reference it by name from any number of IngressRoute objects — even across namespaces with cross-namespace references.
TCP and UDP routing. Traefik can proxy raw TCP and UDP traffic through the same binary, the same Helm release, the same LoadBalancer IP. Routing a PostgreSQL port and a DNS port alongside HTTP routes is a first-class workflow. Other controllers require separate infrastructure for this.
The dashboard. It is a real-time view of every router, service, middleware, and TLS certificate in the cluster. Invaluable for debugging routing problems.
Where nginx-ingress still wins
Honest assessment: nginx-ingress has a larger ecosystem of battle-tested annotations, a bigger community, and more production case studies at very large scale. If you are joining an existing cluster that runs nginx-ingress and your team knows it well, the switching cost is rarely justified. nginx-ingress also has more mature support for some edge cases in the Gateway API implementation as of early 2026.
The Gateway API question
The Kubernetes Gateway API (gateway.networking.k8s.io) is the community’s answer to the fragmentation of controller-specific annotations and CRDs. It provides standard, portable resource types: GatewayClass, Gateway, HTTPRoute, TCPRoute, TLSRoute, UDPRoute, GRPCRoute.
Traefik v3 ships experimental Gateway API support. It covers HTTPRoute (v1, GA status in the spec) well. TCPRoute and UDPRoute remain alpha in both the spec and Traefik’s implementation.
The pragmatic position for 2026: use Traefik’s native IngressRoute CRDs for everything except cases where portability across controllers is a hard requirement. IngressRoute is more expressive today. If you are building infrastructure for an organization that wants to avoid controller lock-in and is willing to live at the cutting edge, use the Gateway API — but test carefully.
Both approaches are covered in this guide.
Part 2: Installation with Helm
Adding the Helm repository
1
2
|
helm repo add traefik https://traefik.github.io/charts
helm repo update
|
Anatomy of a production values.yaml
The official traefik/traefik chart is well-structured. A production values.yaml needs to address: replica count, resource limits, entrypoints, persistence for ACME state, the LoadBalancer service, RBAC, and observability hooks.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
|
# values.yaml — production Traefik Helm configuration
# Chart: traefik/traefik v32.x (Traefik v3.x)
# ─── Deployment ────────────────────────────────────────────────────────────────
deployment:
replicas: 2 # see Part 9 for HA considerations
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9100"
# ─── Resource limits ───────────────────────────────────────────────────────────
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
# ─── Entrypoints ───────────────────────────────────────────────────────────────
ports:
web:
port: 8000 # internal container port
expose:
default: true
exposedPort: 80 # port on the LoadBalancer
protocol: TCP
redirectTo:
port: websecure # global HTTP→HTTPS redirect
websecure:
port: 8443
expose:
default: true
exposedPort: 443
protocol: TCP
tls:
enabled: true
# Custom TCP entrypoint for PostgreSQL passthrough
postgres:
port: 5432
expose:
default: true
exposedPort: 5432
protocol: TCP
# Custom UDP entrypoint for DNS
dns-udp:
port: 5353
expose:
default: true
exposedPort: 53
protocol: UDP
# Traefik metrics endpoint (internal only, not exposed via LoadBalancer)
metrics:
port: 9100
expose:
default: false
# ─── Service (LoadBalancer) ─────────────────────────────────────────────────────
service:
type: LoadBalancer
annotations:
# AWS NLB example — swap for your cloud provider's annotation
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
externalTrafficPolicy: Local # preserves client source IP
# Use NodePort instead for bare-metal / MetalLB:
# service:
# type: NodePort
# ─── RBAC ──────────────────────────────────────────────────────────────────────
rbac:
enabled: true
# namespaced: false → ClusterRole (can watch all namespaces)
# namespaced: true → Role per namespace (more restrictive)
namespaced: false
serviceAccount:
name: traefik
# ─── Persistence for built-in ACME ─────────────────────────────────────────────
# Only needed when using Traefik's built-in ACME (not cert-manager).
# With cert-manager this section can be omitted.
persistence:
enabled: true
storageClass: "standard"
accessMode: ReadWriteOnce
size: 128Mi
path: /data
# Disable built-in ACME when using cert-manager (recommended):
# certResolvers: {}
# ─── Providers ─────────────────────────────────────────────────────────────────
providers:
kubernetesCRD:
enabled: true
allowCrossNamespace: true # required for cross-namespace middleware refs
allowExternalNameServices: true
kubernetesIngress:
enabled: true # keep enabled for legacy Ingress resources
allowExternalNameServices: true
publishedService:
enabled: true # writes LoadBalancer IP back to Ingress status
# ─── Logs ──────────────────────────────────────────────────────────────────────
logs:
general:
level: INFO
access:
enabled: true
format: json
fields:
general:
defaultMode: keep
headers:
defaultMode: drop # drop all headers by default
names:
User-Agent: keep
X-Forwarded-For: keep
X-Request-Id: keep
# ─── Metrics ───────────────────────────────────────────────────────────────────
metrics:
prometheus:
entryPoint: metrics
addEntryPointsLabels: true
addRoutersLabels: true
addServicesLabels: true
# ─── Tracing ───────────────────────────────────────────────────────────────────
tracing:
otlp:
grpc:
endpoint: "otel-collector.observability.svc.cluster.local:4317"
insecure: true
# ─── Dashboard ─────────────────────────────────────────────────────────────────
ingressRoute:
dashboard:
enabled: false # we create our own IngressRoute with auth (see Part 10)
# ─── API & Dashboard internal activation ───────────────────────────────────────
api:
dashboard: true
insecure: false # never expose the API without auth
# ─── Global arguments ──────────────────────────────────────────────────────────
globalArguments:
- "--global.checknewversion=false"
- "--global.sendanonymoususage=false"
# ─── Additional arguments ──────────────────────────────────────────────────────
additionalArguments:
- "--api.dashboard=true"
- "--serversTransport.insecureSkipVerify=false"
# ─── Pod anti-affinity (see Part 9) ────────────────────────────────────────────
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- traefik
topologyKey: kubernetes.io/hostname
# ─── PodDisruptionBudget ───────────────────────────────────────────────────────
podDisruptionBudget:
enabled: true
minAvailable: 1
|
Installing into a dedicated namespace
1
2
3
4
5
6
7
|
kubectl create namespace traefik
helm upgrade --install traefik traefik/traefik \
--namespace traefik \
--values values.yaml \
--version "32.1.0" \
--wait
|
Verify:
1
2
3
|
kubectl -n traefik get pods
kubectl -n traefik get svc
# The LoadBalancer service should have an EXTERNAL-IP assigned within a minute
|
GitOps with Flux HelmRelease
If you manage your cluster with Flux, declare Traefik as a HelmRelease:
1
2
3
4
5
6
7
|
# infrastructure/traefik/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: traefik
labels:
pod-security.kubernetes.io/enforce: privileged # Traefik needs hostPort or privileged for port < 1024 in some setups
|
1
2
3
4
5
6
7
8
9
|
# infrastructure/traefik/helmrepository.yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: traefik
namespace: flux-system
spec:
interval: 1h
url: https://traefik.github.io/charts
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
# infrastructure/traefik/helmrelease.yaml
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: traefik
namespace: traefik
spec:
interval: 30m
chart:
spec:
chart: traefik
version: "32.x"
sourceRef:
kind: HelmRepository
name: traefik
namespace: flux-system
interval: 12h
install:
remediation:
retries: 3
upgrade:
cleanupOnFail: true
remediation:
strategy: rollback
retries: 3
values:
deployment:
replicas: 2
# ... rest of values inline, or use valuesFrom:
valuesFrom:
- kind: ConfigMap
name: traefik-values
valuesKey: values.yaml
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# infrastructure/traefik/values-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: traefik-values
namespace: traefik
data:
values.yaml: |
# paste production values.yaml content here
deployment:
replicas: 2
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
|
Part 3: The Two Routing APIs
Traefik in Kubernetes supports two parallel routing mechanisms. Understanding the difference and when to use each avoids confusion when reading cluster configurations.
Kubernetes Ingress (classic)
The standard networking.k8s.io/v1 Ingress resource is supported by all ingress controllers. It handles the common case: HTTP/HTTPS routing by hostname and path prefix.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
namespace: my-app
annotations:
# Traefik-specific behavior via annotations
traefik.ingress.kubernetes.io/router.middlewares: my-app-secure-headers@kubernetescrd
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
ingressClassName: traefik
tls:
- hosts:
- app.example.com
secretName: app-example-com-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-svc
port:
number: 8080
|
Limitations of Ingress:
- Path matching is limited to
Exact and Prefix (no regex, no header matching)
- Middleware configuration requires annotations (stringly-typed, not validated)
- No TCP or UDP routing
- No traffic splitting for canary (requires separate Ingress resources with weights via annotations)
- No native cross-namespace service references
Traefik IngressRoute CRD (recommended)
The IngressRoute CRD is Traefik’s native routing API. It expresses everything the Ingress resource can, and much more:
- Full rule language:
Host(), PathPrefix(), Path(), Headers(), Query(), Method(), all composable with && and ||
- Middleware references are typed, validated objects
- TLS configuration inline
- Traffic weights for canary
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: my-app
namespace: my-app
spec:
entryPoints:
- websecure
routes:
- match: Host(`app.example.com`)
kind: Rule
services:
- name: my-app-svc
port: 8080
middlewares:
- name: secure-headers
namespace: my-app
tls:
secretName: app-example-com-tls
|
When to use which
Use Ingress when:
- You are migrating an existing cluster incrementally and want to minimize changes
- Tools in your stack generate
Ingress resources and do not support custom CRDs (e.g., some Helm charts)
- You need to remain portable across ingress controllers without customization
Use IngressRoute when:
- You control the routing configuration directly
- You need header-based routing, traffic splitting, TCP/UDP routing, or complex middleware chains
- You want the configuration validated by the Kubernetes API server
Migrating from Ingress to IngressRoute
The migration is straightforward — map each Ingress rule to an IngressRoute route, convert annotations to Middleware resources, update TLS references, then delete the old Ingress. Run both in parallel during migration by giving them different names; Traefik applies all routing rules simultaneously.
1
2
3
4
5
|
# Verify both route to the same backend before deleting the Ingress
kubectl -n my-app get ingress,ingressroute
# Once IngressRoute is confirmed working:
kubectl -n my-app delete ingress my-app
|
Part 4: IngressRoute CRDs In Depth
IngressRoute — HTTP and HTTPS
The core routing object for HTTP traffic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: api-gateway
namespace: api
spec:
entryPoints:
- websecure
routes:
# Route by hostname
- match: Host(`api.example.com`)
kind: Rule
priority: 10
services:
- name: api-svc
port: 8080
middlewares:
- name: api-rate-limit
namespace: api
- name: secure-headers
namespace: shared-middleware
# Route by hostname AND path prefix
- match: Host(`api.example.com`) && PathPrefix(`/v2`)
kind: Rule
priority: 20 # higher priority wins when rules overlap
services:
- name: api-v2-svc
port: 8080
middlewares:
- name: api-rate-limit
namespace: api
# Route by header — useful for internal admin endpoints
- match: Host(`api.example.com`) && Headers(`X-Internal`, `true`)
kind: Rule
priority: 30
services:
- name: api-admin-svc
port: 9090
middlewares:
- name: admin-ip-allowlist
namespace: api
# Weighted canary split — 90% stable, 10% canary
- match: Host(`api.example.com`) && PathPrefix(`/feature-x`)
kind: Rule
services:
- name: api-svc
port: 8080
weight: 90
- name: api-canary-svc
port: 8080
weight: 10
tls:
secretName: api-example-com-tls # cert-manager-managed Secret
|
IngressRouteTCP — TCP Routing
IngressRouteTCP routes raw TCP connections. The primary matching mechanism is SNI (Server Name Indication), which Traefik reads from the TLS ClientHello without terminating TLS.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: postgres-route
namespace: databases
spec:
entryPoints:
- postgres # the custom TCP entrypoint defined in values.yaml
routes:
# SNI-based routing — the client connects with TLS and indicates the hostname
- match: HostSNI(`db.example.com`)
services:
- name: postgresql-svc
port: 5432
# TLS passthrough — Traefik does NOT terminate TLS, passes it to the backend
tls:
passthrough: true
|
TCP routing without TLS (no SNI available — Traefik must use the entrypoint alone):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: redis-route
namespace: databases
spec:
entryPoints:
- redis-port # dedicated entrypoint on port 6379
routes:
- match: HostSNI(`*`) # match all connections on this entrypoint
services:
- name: redis-svc
port: 6379
|
IngressRouteUDP — UDP Routing
UDP has no connection concept, so matching is purely by entrypoint. Each UDP entrypoint routes to exactly one service.
1
2
3
4
5
6
7
8
9
10
11
12
|
apiVersion: traefik.io/v1alpha1
kind: IngressRouteUDP
metadata:
name: dns-route
namespace: dns
spec:
entryPoints:
- dns-udp # the UDP entrypoint defined in values.yaml
routes:
- services:
- name: coredns-external-svc
port: 53
|
Game server UDP example:
1
2
3
4
5
6
7
8
9
10
11
12
|
apiVersion: traefik.io/v1alpha1
kind: IngressRouteUDP
metadata:
name: game-server-udp
namespace: game
spec:
entryPoints:
- game-udp # custom UDP entrypoint on port 7777
routes:
- services:
- name: game-server-svc
port: 7777
|
TLSOption — Cipher Suites and TLS Versions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
apiVersion: traefik.io/v1alpha1
kind: TLSOption
metadata:
name: modern-tls
namespace: traefik
spec:
minVersion: VersionTLS12
maxVersion: VersionTLS13
cipherSuites:
# TLS 1.2 ciphers (TLS 1.3 ciphers are fixed, not configurable)
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
curvePreferences:
- CurveP521
- CurveP384
sniStrict: true # reject connections with no SNI or wrong SNI
|
Reference from an IngressRoute:
1
2
3
4
5
6
|
spec:
tls:
secretName: my-cert-tls
options:
name: modern-tls
namespace: traefik
|
For mTLS (client certificate authentication):
1
2
3
4
5
6
7
8
9
10
11
|
apiVersion: traefik.io/v1alpha1
kind: TLSOption
metadata:
name: mtls-required
namespace: traefik
spec:
minVersion: VersionTLS12
clientAuth:
secretNames:
- client-ca-cert # Secret containing the CA cert that signed client certs
clientAuthType: RequireAndVerifyClientCert
|
TLSStore — Default Certificates
The TLSStore named default in any namespace sets the default certificate returned when no SNI-matched certificate exists:
1
2
3
4
5
6
7
8
|
apiVersion: traefik.io/v1alpha1
kind: TLSStore
metadata:
name: default
namespace: traefik
spec:
defaultCertificate:
secretName: wildcard-example-com-tls # *.example.com wildcard cert
|
ServersTransport — Backend mTLS
ServersTransport configures how Traefik connects to backend services — useful when backends require mTLS or use self-signed certificates:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
apiVersion: traefik.io/v1alpha1
kind: ServersTransport
metadata:
name: backend-mtls
namespace: my-app
spec:
serverName: "backend.internal"
# Present a client certificate to the backend
certificates:
- secretName: traefik-client-cert # Secret with tls.crt and tls.key
# Trust a specific CA for the backend's certificate
rootCAsSecrets:
- backend-ca-cert
insecureSkipVerify: false
# Connection pool settings
maxIdleConnsPerHost: 200
dialTimeout: 5s
responseHeaderTimeout: 30s
|
Reference from an IngressRoute service:
1
2
3
4
5
6
7
|
spec:
routes:
- match: Host(`secure-app.example.com`)
services:
- name: secure-backend-svc
port: 8443
serversTransport: backend-mtls # references ServersTransport name
|
Part 5: Middleware CRDs
Middleware resources are the operational heart of Traefik. Define them once, reuse them everywhere.
basicAuth
1
2
3
4
5
6
7
8
9
10
11
12
|
# First, create the Secret with htpasswd-encoded credentials
# Generate with: htpasswd -nB admin | kubectl create secret generic basic-auth-secret \
# --from-file=users=/dev/stdin -n traefik
apiVersion: v1
kind: Secret
metadata:
name: basic-auth-secret
namespace: traefik
type: Opaque
stringData:
users: |
admin:$2y$10$WdOA4BYkuVMDieGL6OteneORi7uEP5lPgB8OJZm0z3GCvIoMqQ7Ly
|
1
2
3
4
5
6
7
8
9
10
|
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: basic-auth
namespace: traefik
spec:
basicAuth:
secret: basic-auth-secret
realm: "Restricted Area"
removeHeader: true # strip Authorization header before forwarding to backend
|
forwardAuth — Delegating Authentication to Authelia or Authentik
forwardAuth is the key middleware for single sign-on integration. Traefik forwards every request to an external authentication service. If the auth service returns 2xx, the request proceeds; otherwise the response from the auth service (typically a redirect to the login page) is returned to the client.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
# ForwardAuth pointing at Authentik
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: authentik-forward-auth
namespace: traefik
spec:
forwardAuth:
address: "http://authentik-server.authentik.svc.cluster.local/outpost.goauthentik.io/auth/traefik"
trustForwardHeader: true
authResponseHeaders:
# Headers the auth service sends back that should be forwarded to the backend
- X-authentik-username
- X-authentik-groups
- X-authentik-email
- X-authentik-name
- X-authentik-uid
- X-authentik-jwt
- Authorization
authRequestHeaders:
# Headers from the original request to include in the auth request
- "Accept"
- "Cookie"
- "X-Forwarded-For"
- "X-Forwarded-Host"
- "X-Forwarded-Proto"
- "X-Real-Ip"
- "X-Request-Id"
# TLS config for the auth service if it uses HTTPS
tls:
insecureSkipVerify: false
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# ForwardAuth pointing at Authelia
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: authelia-forward-auth
namespace: traefik
spec:
forwardAuth:
address: "http://authelia.authelia.svc.cluster.local:9091/api/verify?rd=https://auth.example.com"
trustForwardHeader: true
authResponseHeaders:
- Remote-User
- Remote-Groups
- Remote-Name
- Remote-Email
|
rateLimit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: api-rate-limit
namespace: api
spec:
rateLimit:
average: 100 # requests per period
period: 1m # period duration
burst: 200 # burst allowance above average
sourceCriterion:
ipStrategy:
depth: 1 # use client IP (1 = direct IP, 2 = first X-Forwarded-For entry)
excludedIPs:
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
|
Per-user rate limiting using a header:
1
2
3
4
5
6
7
8
9
10
11
12
|
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: per-user-rate-limit
namespace: api
spec:
rateLimit:
average: 60
period: 1m
burst: 100
sourceCriterion:
requestHeaderName: "X-User-ID" # rate limit per authenticated user identity
|
redirectScheme — HTTP to HTTPS
1
2
3
4
5
6
7
8
9
|
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-to-https
namespace: traefik
spec:
redirectScheme:
scheme: https
permanent: true # 301 redirect
|
Apply this to the HTTP (web) entrypoint’s IngressRoute to force HTTPS globally. Alternatively, the entrypoint-level redirect in values.yaml handles this without needing a Middleware at all — prefer the entrypoint-level approach for cluster-wide enforcement.
headers — Security Headers and CORS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: secure-headers
namespace: traefik
spec:
headers:
# HSTS — tell browsers to always use HTTPS
stsSeconds: 63072000 # 2 years
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
# Content security
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: "strict-origin-when-cross-origin"
permissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=()"
# Frame options — prevent clickjacking
frameDeny: true
# OR: customFrameOptionsValue: "SAMEORIGIN"
# Custom headers to add
customResponseHeaders:
X-Robots-Tag: "noindex, nofollow" # for internal apps
X-Content-Type-Options: "nosniff"
# Remove headers that leak server info
customRequestHeaders:
X-Powered-By: "" # removes the header
Server: ""
# CORS configuration
accessControlAllowMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
accessControlAllowHeaders:
- "Content-Type"
- "Authorization"
- "X-Request-Id"
accessControlAllowOriginList:
- "https://app.example.com"
- "https://admin.example.com"
accessControlMaxAge: 3600
addVaryHeader: true
|
stripPrefix and addPrefix
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# Strip /api/v1 from the path before forwarding to the backend
# Client requests /api/v1/users → backend receives /users
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: strip-api-prefix
namespace: api
spec:
stripPrefix:
prefixes:
- /api/v1
- /api/v2
forceSlash: true # ensure a leading / after stripping
|
1
2
3
4
5
6
7
8
9
|
# Add a prefix — useful when an internal service expects a specific path root
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: add-app-prefix
namespace: my-app
spec:
addPrefix:
prefix: /app
|
retry
1
2
3
4
5
6
7
8
9
|
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: retry-policy
namespace: my-app
spec:
retry:
attempts: 3
initialInterval: 100ms # wait before first retry; doubles each attempt
|
circuitBreaker
1
2
3
4
5
6
7
8
9
10
11
12
|
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: circuit-breaker
namespace: my-app
spec:
circuitBreaker:
# Open the circuit when error rate exceeds 50% or latency exceeds 1s at p99
expression: "ResponseCodeRatio(500, 600, 0, 600) > 0.50 || LatencyAtQuantileMS(99.0) > 1000"
checkPeriod: 10s # how often to evaluate the expression
fallbackDuration: 30s # how long to keep the circuit open
recoveryDuration: 10s # how long the half-open state lasts
|
ipAllowList
1
2
3
4
5
6
7
8
9
10
11
12
13
|
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: internal-only
namespace: traefik
spec:
ipAllowList:
sourceRange:
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
ipStrategy:
depth: 1
|
Reusing Middleware Across Namespaces
When allowCrossNamespace: true is set in the provider configuration, you can reference middleware from any namespace using the namespace field:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
# In namespace "app-team-a"
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: team-a-app
namespace: app-team-a
spec:
entryPoints:
- websecure
routes:
- match: Host(`team-a.example.com`)
kind: Rule
services:
- name: team-a-svc
port: 8080
middlewares:
- name: secure-headers # defined in traefik namespace
namespace: traefik
- name: authentik-forward-auth # defined in traefik namespace
namespace: traefik
- name: team-a-rate-limit # defined locally
namespace: app-team-a
tls:
secretName: team-a-example-com-tls
|
Part 6: TLS in Kubernetes
cert-manager: The Recommended Approach
Running Traefik’s built-in ACME solver works well for single-replica deployments. For multi-replica HA deployments, the built-in ACME needs a shared storage backend (covered in Part 9). The simpler and more flexible solution is to delegate all certificate lifecycle management to cert-manager.
With cert-manager, Traefik becomes a pure proxy that consumes TLS secrets. cert-manager handles provisioning, renewal, and storage.
Install cert-manager:
1
2
3
4
5
6
7
8
|
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm upgrade --install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true \
--version "v1.17.x"
|
ClusterIssuers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# ACME HTTP-01 challenge via Let's Encrypt production
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
ingressClassName: traefik
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Staging issuer for testing (avoids rate limits)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
- http01:
ingress:
ingressClassName: traefik
|
Certificate Resources
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# Per-domain certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: app-example-com
namespace: my-app
spec:
secretName: app-example-com-tls # this Secret is what IngressRoute references
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
commonName: app.example.com
dnsNames:
- app.example.com
duration: 2160h # 90 days
renewBefore: 360h # renew 15 days before expiry
|
Wildcard Certificates with DNS-01
HTTP-01 challenges cannot issue wildcard certificates. For *.example.com you need DNS-01, which requires your DNS provider to have an API. cert-manager supports AWS Route 53, Cloudflare, Google Cloud DNS, Azure DNS, and many others.
1
2
3
4
5
6
7
8
9
|
# Cloudflare DNS-01 ClusterIssuer
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token
namespace: cert-manager
type: Opaque
stringData:
api-token: "your-cloudflare-api-token-here"
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-dns-prod-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# Wildcard certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example-com
namespace: traefik # in traefik namespace for use as default cert
spec:
secretName: wildcard-example-com-tls
issuerRef:
name: letsencrypt-dns-prod
kind: ClusterIssuer
commonName: "*.example.com"
dnsNames:
- "*.example.com"
- "example.com" # also cover the apex
duration: 2160h
renewBefore: 360h
|
Reference this as the default certificate in a TLSStore:
1
2
3
4
5
6
7
8
|
apiVersion: traefik.io/v1alpha1
kind: TLSStore
metadata:
name: default
namespace: traefik
spec:
defaultCertificate:
secretName: wildcard-example-com-tls
|
With the wildcard as the default certificate, any IngressRoute using websecure without specifying a secretName will automatically use the wildcard. This simplifies configuration dramatically.
Using Traefik’s Built-in ACME (single replica only)
When you do not want to install cert-manager and only run one Traefik replica:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# In values.yaml
certResolvers:
letsencrypt:
email: admin@example.com
storage: /data/acme.json # requires persistence.enabled: true
httpChallenge:
entryPoint: web
# OR for DNS-01:
# dnsChallenge:
# provider: cloudflare
# delayBeforeCheck: 0
# resolvers:
# - "1.1.1.1:53"
# - "8.8.8.8:53"
|
Then reference in IngressRoute:
1
2
3
|
spec:
tls:
certResolver: letsencrypt
|
TLS Passthrough
When the backend handles its own TLS — common for databases, some gRPC services, and hardware-backed PKI — use TCP routing with TLS passthrough:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: tls-passthrough-route
namespace: secure-app
spec:
entryPoints:
- websecure
routes:
- match: HostSNI(`secure.example.com`)
services:
- name: secure-app-svc
port: 8443
tls:
passthrough: true # Traefik does NOT decrypt; SNI is read from ClientHello
|
Part 7: Gateway API Support
The Kubernetes Gateway API provides a portable, standardized alternative to both the classic Ingress resource and controller-specific CRDs like IngressRoute. Traefik v3 implements the core Gateway API resources.
Enabling Gateway API in Traefik
1
2
3
4
|
# In values.yaml
providers:
kubernetesGateway:
enabled: true
|
Install the Gateway API CRDs first (they are not bundled with Traefik):
1
2
3
4
|
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml
# For experimental resources (TCPRoute, UDPRoute, TLSRoute):
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/experimental-install.yaml
|
GatewayClass and Gateway
1
2
3
4
5
6
7
|
# GatewayClass — tells Kubernetes that Traefik is the implementation
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: traefik
spec:
controllerName: traefik.io/gateway-controller
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
# Gateway — the actual listener configuration
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: main-gateway
namespace: traefik
spec:
gatewayClassName: traefik
listeners:
- name: web
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All # accept HTTPRoutes from any namespace
- name: websecure
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: wildcard-example-com-tls
namespace: traefik
allowedRoutes:
namespaces:
from: All
- name: postgres
protocol: TCP
port: 5432
allowedRoutes:
namespaces:
from: All
|
HTTPRoute
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app
namespace: my-app
spec:
parentRefs:
- name: main-gateway
namespace: traefik
sectionName: websecure # bind to the HTTPS listener
hostnames:
- "app.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: my-app-svc
port: 8080
weight: 100
|
Traffic splitting with HTTPRoute:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: canary-split
namespace: my-app
spec:
parentRefs:
- name: main-gateway
namespace: traefik
sectionName: websecure
hostnames:
- "app.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: my-app-stable-svc
port: 8080
weight: 90
- name: my-app-canary-svc
port: 8080
weight: 10
|
TCPRoute (experimental)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TCPRoute
metadata:
name: postgres-tcp
namespace: databases
spec:
parentRefs:
- name: main-gateway
namespace: traefik
sectionName: postgres
rules:
- backendRefs:
- name: postgresql-svc
port: 5432
|
Gateway API vs IngressRoute: Practical Guidance
Use Gateway API when:
- You need portability — the ability to swap Traefik for another controller without rewriting all routing config
- Your organization standardizes on Gateway API across multiple clusters with different controllers
- You are on a greenfield cluster and want to be on the standards track
Use IngressRoute when:
- You want the full Traefik feature set today (Middleware reuse, all matching rules, ServersTransport)
- You need TCP/UDP routing that is stable (Gateway API TCP/UDP is still experimental in both the spec and Traefik)
- You want the simplest, most documented path
Mixing both is fine. Traefik processes all routing sources simultaneously.
Part 8: RBAC and Security
What Traefik Needs
Traefik watches the Kubernetes API to discover routes, middleware, secrets, and services. It needs read access to these resource types across the namespaces it manages.
The Helm chart generates the correct RBAC resources by default. Understanding what it creates helps you audit and restrict it.
Generated ClusterRole (annotated)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: traefik
rules:
# Watch services and endpoints to build the upstream pool
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get", "list", "watch"]
# Watch Secrets for TLS certificates
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
# Watch classic Ingress resources
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingressclasses"]
verbs: ["get", "list", "watch"]
# Update Ingress status (writes the LoadBalancer IP to Ingress.status)
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses/status"]
verbs: ["update"]
# Watch Traefik CRDs
- apiGroups: ["traefik.io"]
resources:
- "ingressroutes"
- "ingressroutetcps"
- "ingressrouteudps"
- "middlewares"
- "middlewaretcps"
- "serverstransports"
- "serverstransporttcps"
- "tlsoptions"
- "tlsstores"
- "traefikservices"
verbs: ["get", "list", "watch"]
# Gateway API resources (if enabled)
- apiGroups: ["gateway.networking.k8s.io"]
resources:
- "gatewayclasses"
- "gateways"
- "httproutes"
- "tcproutes"
- "tlsroutes"
verbs: ["get", "list", "watch"]
# Update Gateway and HTTPRoute status
- apiGroups: ["gateway.networking.k8s.io"]
resources:
- "gatewayclasses/status"
- "gateways/status"
- "httproutes/status"
verbs: ["update"]
|
Namespace-Scoped RBAC (more restrictive)
For clusters where Traefik should only manage specific namespaces, use namespaced: true in values.yaml. This generates a Role + RoleBinding per namespace rather than a cluster-wide ClusterRole. You must then list the watched namespaces explicitly.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# values.yaml
rbac:
enabled: true
namespaced: true
providers:
kubernetesCRD:
enabled: true
namespaces:
- traefik
- my-app
- api
kubernetesIngress:
enabled: true
namespaces:
- traefik
- my-app
- api
|
Least-Privilege Secrets Access
The broadest attack surface in Traefik’s RBAC is the secrets read permission — it needs it to read TLS certificates. One mitigation is to keep TLS secrets in the same namespace as the workloads and let namespace-scoped RBAC naturally limit which secrets Traefik can reach. Another is to use ExternalSecret (External Secrets Operator) to mirror only the needed secrets into namespaces Traefik watches.
Running Traefik in a Dedicated Namespace
All examples in this guide deploy Traefik into the traefik namespace. This is important for:
- Isolating Traefik’s ServiceAccount permissions
- Making network policies easier to reason about
- Separating operational concerns from workload namespaces
Network Policies
By default, pods in a Kubernetes cluster can communicate freely. Network policies restrict this. Apply a policy that allows:
- Inbound traffic from everywhere on ports 80/443 (the entrypoints)
- Outbound traffic to all namespaces on arbitrary backend ports
- Inbound traffic from Prometheus on port 9100 (metrics)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
# Allow inbound traffic to Traefik from the outside world and from Prometheus
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: traefik-ingress-policy
namespace: traefik
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: traefik
policyTypes:
- Ingress
- Egress
ingress:
# Allow all inbound (external traffic hits the LoadBalancer first)
- ports:
- protocol: TCP
port: 8000 # web entrypoint
- protocol: TCP
port: 8443 # websecure entrypoint
- protocol: TCP
port: 5432 # postgres entrypoint
- protocol: UDP
port: 5353 # dns-udp entrypoint
# Allow Prometheus scraping
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- protocol: TCP
port: 9100
egress:
# Allow outbound to all namespaces — Traefik needs to reach backend services
- {}
# More restrictive: enumerate specific backend namespaces
# - to:
# - namespaceSelector:
# matchLabels:
# traefik-backend: "true"
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# Allow backend pods to receive traffic from Traefik
# Apply this in each workload namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-traefik-ingress
namespace: my-app
spec:
podSelector: {} # applies to all pods in the namespace
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik
|
Part 9: High Availability
Running a single Traefik pod is fine for development. Production requires multiple replicas and attention to a few key concerns.
Replica Count and Anti-Affinity
Two replicas is the minimum for HA. Three is better for rolling updates (Kubernetes terminates one pod at a time during rollout, keeping two alive).
The anti-affinity rule in values.yaml (shown in Part 2) ensures Traefik pods land on different nodes. If one node fails, the other replica continues serving traffic.
1
2
3
4
5
6
7
8
9
10
11
|
# values.yaml — force pods onto different nodes
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- traefik
topologyKey: kubernetes.io/hostname
|
Use preferredDuringSchedulingIgnoredDuringExecution instead of required if you are on a small cluster where strict anti-affinity might cause unschedulable pods.
PodDisruptionBudget
1
2
3
4
5
6
7
8
9
10
|
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: traefik-pdb
namespace: traefik
spec:
minAvailable: 1
selector:
matchLabels:
app.kubernetes.io/name: traefik
|
With two replicas and minAvailable: 1, node drains during cluster upgrades are safe — Kubernetes evicts one Traefik pod at a time, only when one replacement is running.
HPA — Horizontal Pod Autoscaling
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: traefik-hpa
namespace: traefik
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: traefik
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # avoid thrashing
policies:
- type: Pods
value: 1
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 2
periodSeconds: 60
|
Shared ACME Storage with Redis
When using Traefik’s built-in ACME resolver across multiple replicas, each replica independently tries to solve ACME challenges and store the resulting certificates. This causes certificate duplication, race conditions, and rate-limit exhaustion. The solution: a shared storage backend.
Option 1: cert-manager (recommended). Use cert-manager as covered in Part 6. Each replica reads the TLS Secret from Kubernetes; cert-manager handles renewal. No shared ACME state needed.
Option 2: Traefik with Redis. If you must use the built-in ACME:
1
2
3
4
5
|
# values.yaml
additionalArguments:
- "--certificatesresolvers.letsencrypt.acme.storage=redis://redis-master.redis.svc.cluster.local:6379/0"
- "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
|
Deploy a Redis instance in the cluster (a single-replica Redis is fine for ACME storage — the data is reproducible by re-running the ACME challenge):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: traefik
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
name: redis-master
namespace: traefik
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
|
Recommendation: Use cert-manager. It is simpler, more robust, and separates concerns cleanly.
Topology Spread Constraints
For clusters with multiple availability zones:
1
2
3
4
5
6
7
8
|
# values.yaml
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/name: traefik
|
Part 10: Observability in Kubernetes
Access Logs
Traefik produces structured JSON access logs when configured as shown in values.yaml. In Kubernetes these flow to stdout/stderr and are collected by your log aggregation stack (Loki, Elasticsearch, Splunk).
A Loki PodLogs resource to scrape Traefik logs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
apiVersion: monitoring.grafana.com/v1alpha2
kind: PodLogs
metadata:
name: traefik-logs
namespace: traefik
spec:
selector:
matchLabels:
app.kubernetes.io/name: traefik
pipelineStages:
- json:
expressions:
status: ClientHost
method: RequestMethod
host: RequestHost
path: RequestPath
duration: Duration
status_code: DownstreamStatus
- labels:
status_code:
method:
host:
|
Prometheus Metrics with ServiceMonitor
The Helm chart generates a Service for the metrics port. Create a ServiceMonitor to tell Prometheus to scrape it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: traefik-metrics
namespace: traefik
labels:
release: kube-prometheus-stack # must match your Prometheus operator selector
spec:
selector:
matchLabels:
app.kubernetes.io/name: traefik
namespaceSelector:
matchNames:
- traefik
endpoints:
- port: metrics
path: /metrics
interval: 30s
scrapeTimeout: 10s
|
PrometheusRule for Alerting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: traefik-alerts
namespace: traefik
labels:
release: kube-prometheus-stack
spec:
groups:
- name: traefik.rules
interval: 30s
rules:
# Alert when Traefik error rate exceeds 5% over 5 minutes
- alert: TraefikHighErrorRate
expr: |
sum(rate(traefik_router_requests_total{code=~"5.."}[5m])) by (router)
/
sum(rate(traefik_router_requests_total[5m])) by (router)
> 0.05
for: 2m
labels:
severity: warning
annotations:
summary: "Traefik router {{ $labels.router }} has high error rate"
description: "Error rate is {{ $value | humanizePercentage }} over the last 5 minutes."
# Alert when a backend is entirely down (100% 5xx for 2 minutes)
- alert: TraefikBackendDown
expr: |
sum(rate(traefik_service_requests_total{code=~"5.."}[2m])) by (service)
/
sum(rate(traefik_service_requests_total[2m])) by (service)
== 1
for: 2m
labels:
severity: critical
annotations:
summary: "Traefik service {{ $labels.service }} is returning 100% errors"
description: "All requests to {{ $labels.service }} are failing."
# Alert when P99 latency exceeds 2 seconds
- alert: TraefikHighLatency
expr: |
histogram_quantile(0.99,
sum(rate(traefik_router_request_duration_seconds_bucket[5m])) by (router, le)
) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "Traefik router {{ $labels.router }} has high P99 latency"
description: "P99 latency is {{ $value | humanizeDuration }}."
# Alert when TLS certificate is expiring within 14 days
- alert: TraefikTLSCertExpiringSoon
expr: |
traefik_tls_certs_not_after - time() < 14 * 24 * 3600
for: 1h
labels:
severity: warning
annotations:
summary: "TLS certificate expiring soon"
description: "Certificate for {{ $labels.sans }} expires in less than 14 days."
# Alert when no Traefik pods are ready
- alert: TraefikDown
expr: |
kube_deployment_status_replicas_ready{namespace="traefik", deployment="traefik"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Traefik is down — no ready replicas"
description: "The Traefik deployment in namespace traefik has no ready replicas."
|
Securing the Dashboard
Never expose the Traefik dashboard without authentication. Create a dedicated IngressRoute with forwardAuth or basicAuth:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
# Secret for dashboard basic auth
apiVersion: v1
kind: Secret
metadata:
name: dashboard-auth-secret
namespace: traefik
type: Opaque
stringData:
# Generate: htpasswd -nB admin
users: "admin:$2y$10$WdOA4BYkuVMDieGL6OteneORi7uEP5lPgB8OJZm0z3GCvIoMqQ7Ly"
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: dashboard-auth
namespace: traefik
spec:
basicAuth:
secret: dashboard-auth-secret
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: dashboard-ip-allowlist
namespace: traefik
spec:
ipAllowList:
sourceRange:
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: traefik-dashboard
namespace: traefik
spec:
entryPoints:
- websecure
routes:
- match: Host(`traefik.internal.example.com`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`))
kind: Rule
services:
- name: api@internal
kind: TraefikService
middlewares:
- name: dashboard-ip-allowlist
namespace: traefik
- name: dashboard-auth
namespace: traefik
tls:
secretName: wildcard-example-com-tls
|
Distributed Tracing with OpenTelemetry
Traefik v3 ships native OpenTelemetry support. Configure it in values.yaml:
1
2
3
4
5
6
7
|
tracing:
otlp:
grpc:
endpoint: "otel-collector.observability.svc.cluster.local:4317"
insecure: true # set false and configure TLS if the collector uses TLS
http:
endpoint: "http://otel-collector.observability.svc.cluster.local:4318"
|
With this configuration, Traefik adds spans to every request processed and propagates trace context (traceparent header) to backends that support it. Traces appear in Grafana Tempo, Jaeger, or any OTLP-compatible backend.
Part 11: Advanced Patterns
Weighted Traffic Splitting for Canary Deployments
The pattern: two Kubernetes Deployments (stable and canary) behind separate Services, with an IngressRoute splitting traffic by weight.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
# Stable deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app-stable
namespace: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
track: stable
template:
metadata:
labels:
app: my-app
track: stable
spec:
containers:
- name: my-app
image: my-app:v1.5.0
ports:
- containerPort: 8080
---
# Canary deployment (smaller replica count)
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app-canary
namespace: my-app
spec:
replicas: 1
selector:
matchLabels:
app: my-app
track: canary
template:
metadata:
labels:
app: my-app
track: canary
spec:
containers:
- name: my-app
image: my-app:v1.6.0-rc1
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: my-app-stable-svc
namespace: my-app
spec:
selector:
app: my-app
track: stable
ports:
- port: 8080
targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: my-app-canary-svc
namespace: my-app
spec:
selector:
app: my-app
track: canary
ports:
- port: 8080
targetPort: 8080
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# IngressRoute with weighted split
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: my-app-canary-split
namespace: my-app
spec:
entryPoints:
- websecure
routes:
- match: Host(`app.example.com`)
kind: Rule
services:
- name: my-app-stable-svc
port: 8080
weight: 90 # 90% of traffic to stable
- name: my-app-canary-svc
port: 8080
weight: 10 # 10% to canary
tls:
secretName: wildcard-example-com-tls
|
Promote the canary by updating weights: 90/10 → 50/50 → 0/100, then point everything at the (now promoted) stable service.
For internal testing before exposing the canary to real traffic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: my-app-header-canary
namespace: my-app
spec:
entryPoints:
- websecure
routes:
# Requests with X-Canary: true header go to canary
- match: Host(`app.example.com`) && Headers(`X-Canary`, `true`)
kind: Rule
priority: 20
services:
- name: my-app-canary-svc
port: 8080
# All other requests go to stable
- match: Host(`app.example.com`)
kind: Rule
priority: 10
services:
- name: my-app-stable-svc
port: 8080
tls:
secretName: wildcard-example-com-tls
|
TCP Routing for Databases
Running databases inside a Kubernetes cluster and needing to access them from outside (e.g., a developer connecting via a database GUI, or a legacy application not yet in the cluster):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# Entrypoint in values.yaml
ports:
mysql:
port: 3306
expose:
default: true
exposedPort: 3306
protocol: TCP
postgresql:
port: 5432
expose:
default: true
exposedPort: 5432
protocol: TCP
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# MySQL routing (no TLS, relies on network security)
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: mysql-route
namespace: databases
spec:
entryPoints:
- mysql
routes:
- match: HostSNI(`*`)
services:
- name: mysql-svc
port: 3306
terminationDelay: 400 # ms to wait after FIN before forcing close
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# PostgreSQL routing with TLS passthrough (PostgreSQL handles its own TLS)
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: postgres-route
namespace: databases
spec:
entryPoints:
- postgresql
routes:
- match: HostSNI(`db.example.com`)
services:
- name: postgresql-svc
port: 5432
tls:
passthrough: true
|
UDP Routing for DNS and Game Servers
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# External DNS resolver — expose an in-cluster CoreDNS or Pi-hole externally
apiVersion: traefik.io/v1alpha1
kind: IngressRouteUDP
metadata:
name: external-dns
namespace: dns
spec:
entryPoints:
- dns-udp
routes:
- services:
- name: pihole-dns-svc
port: 53
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# Game server with multiple ports
apiVersion: traefik.io/v1alpha1
kind: IngressRouteUDP
metadata:
name: game-server-1
namespace: games
spec:
entryPoints:
- game-udp-7777
routes:
- services:
- name: game-server-1-svc
port: 7777
weight: 1
|
Using Traefik as an Internal Cluster Ingress
Not all traffic originates externally. For internal service-to-service routing where you want middleware (auth, rate limiting, observability) applied, create a separate Traefik deployment configured as an internal cluster ingress — only accessible within the cluster.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Internal Traefik instance — ClusterIP service, not LoadBalancer
service:
type: ClusterIP
ports:
web:
port: 80
expose:
default: true
exposedPort: 80
websecure:
port: 443
expose:
default: true
exposedPort: 443
|
Internal services use the ClusterIP service’s DNS name (traefik-internal.traefik.svc.cluster.local) as their target, routing through Traefik’s middleware pipeline without leaving the cluster.
Cross-Namespace IngressRoutes
When Traefik has allowCrossNamespace: true, an IngressRoute in namespace A can route to a Service in namespace B:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: cross-ns-route
namespace: frontend
spec:
entryPoints:
- websecure
routes:
- match: Host(`app.example.com`) && PathPrefix(`/api`)
kind: Rule
services:
# Service is in a different namespace
- name: api-svc
namespace: backend # cross-namespace reference
port: 8080
middlewares:
- name: strip-api-prefix
namespace: frontend
tls:
secretName: wildcard-example-com-tls
|
Use cross-namespace routing deliberately. It can make it harder to reason about what is hitting a service. Consider whether the backends should instead be exposed via their own IngressRoute in their own namespace.
Part 12: Complete Production Example
This section assembles everything into a single working deployment: Traefik via Helm, cert-manager for TLS, a sample application, rate limiting, ForwardAuth with Authentik, Prometheus metrics, and alerting rules.
Directory Layout (GitOps structure)
infrastructure/
cert-manager/
helmrelease.yaml
clusterissuer-prod.yaml
clusterissuer-staging.yaml
wildcard-cert.yaml
traefik/
namespace.yaml
helmrepository.yaml
helmrelease.yaml
values-configmap.yaml
middleware/
authentik-forward-auth.yaml
secure-headers.yaml
rate-limit-api.yaml
redirect-https.yaml
monitoring/
servicemonitor-traefik.yaml
prometheusrule-traefik.yaml
apps/
sample-app/
namespace.yaml
deployment.yaml
service.yaml
ingressroute.yaml
middleware-rate-limit.yaml
certificate.yaml
Step 1: Install cert-manager
1
2
3
4
5
6
|
helm upgrade --install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true \
--version "v1.17.x" \
--wait
|
Step 2: Create ClusterIssuers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# infrastructure/cert-manager/clusterissuer-prod.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
|
Step 3: Issue Wildcard Certificate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# infrastructure/cert-manager/wildcard-cert.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example-com
namespace: traefik
spec:
secretName: wildcard-example-com-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
commonName: "*.example.com"
dnsNames:
- "*.example.com"
- "example.com"
duration: 2160h
renewBefore: 360h
|
Step 4: Install Traefik
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
# infrastructure/traefik/values-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: traefik-values
namespace: traefik
data:
values.yaml: |
deployment:
replicas: 2
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9100"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
ports:
web:
port: 8000
expose:
default: true
exposedPort: 80
redirectTo:
port: websecure
websecure:
port: 8443
expose:
default: true
exposedPort: 443
tls:
enabled: true
metrics:
port: 9100
expose:
default: false
service:
type: LoadBalancer
spec:
externalTrafficPolicy: Local
rbac:
enabled: true
namespaced: false
providers:
kubernetesCRD:
enabled: true
allowCrossNamespace: true
allowExternalNameServices: true
kubernetesIngress:
enabled: true
publishedService:
enabled: true
logs:
general:
level: INFO
access:
enabled: true
format: json
metrics:
prometheus:
entryPoint: metrics
addEntryPointsLabels: true
addRoutersLabels: true
addServicesLabels: true
tracing:
otlp:
grpc:
endpoint: "otel-collector.observability.svc.cluster.local:4317"
insecure: true
ingressRoute:
dashboard:
enabled: false
api:
dashboard: true
insecure: false
globalArguments:
- "--global.checknewversion=false"
- "--global.sendanonymoususage=false"
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- traefik
topologyKey: kubernetes.io/hostname
podDisruptionBudget:
enabled: true
minAvailable: 1
|
1
2
3
4
5
6
7
|
kubectl create namespace traefik
helm upgrade --install traefik traefik/traefik \
--namespace traefik \
--values infrastructure/traefik/values-configmap.yaml \
--version "32.1.0" \
--wait
|
Step 5: Set TLSStore Default Certificate
1
2
3
4
5
6
7
8
9
|
# infrastructure/traefik/tlsstore-default.yaml
apiVersion: traefik.io/v1alpha1
kind: TLSStore
metadata:
name: default
namespace: traefik
spec:
defaultCertificate:
secretName: wildcard-example-com-tls
|
Step 6: Shared Middleware
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# infrastructure/traefik/middleware/secure-headers.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: secure-headers
namespace: traefik
spec:
headers:
stsSeconds: 63072000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: "strict-origin-when-cross-origin"
frameDeny: true
permissionsPolicy: "camera=(), microphone=(), geolocation=()"
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
# infrastructure/traefik/middleware/authentik-forward-auth.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: authentik-forward-auth
namespace: traefik
spec:
forwardAuth:
address: "http://authentik-server.authentik.svc.cluster.local/outpost.goauthentik.io/auth/traefik"
trustForwardHeader: true
authResponseHeaders:
- X-authentik-username
- X-authentik-groups
- X-authentik-email
- X-authentik-name
- X-authentik-uid
- Authorization
authRequestHeaders:
- "Accept"
- "Cookie"
- "X-Forwarded-For"
- "X-Forwarded-Host"
- "X-Forwarded-Proto"
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# infrastructure/traefik/middleware/rate-limit-api.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: rate-limit-api
namespace: traefik
spec:
rateLimit:
average: 100
period: 1m
burst: 200
sourceCriterion:
ipStrategy:
depth: 1
excludedIPs:
- "10.0.0.0/8"
|
Step 7: Deploy the Sample Application
1
2
3
4
5
6
7
|
# apps/sample-app/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: sample-app
labels:
kubernetes.io/metadata.name: sample-app
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
# apps/sample-app/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-app
namespace: sample-app
spec:
replicas: 2
selector:
matchLabels:
app: sample-app
template:
metadata:
labels:
app: sample-app
spec:
containers:
- name: sample-app
image: nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 2
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: sample-app-svc
namespace: sample-app
spec:
selector:
app: sample-app
ports:
- port: 80
targetPort: 80
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
# apps/sample-app/ingressroute.yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: sample-app
namespace: sample-app
spec:
entryPoints:
- websecure
routes:
- match: Host(`app.example.com`)
kind: Rule
services:
- name: sample-app-svc
port: 80
# Health check: remove unhealthy backends from rotation
healthCheck:
path: /healthz
interval: 30s
timeout: 5s
middlewares:
# Auth — all traffic requires Authentik login
- name: authentik-forward-auth
namespace: traefik
# Rate limiting
- name: rate-limit-api
namespace: traefik
# Security headers
- name: secure-headers
namespace: traefik
tls:
# Using the default wildcard cert from TLSStore — no secretName needed here
{}
|
Step 8: ServiceMonitor and PrometheusRule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# infrastructure/monitoring/servicemonitor-traefik.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: traefik-metrics
namespace: traefik
labels:
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app.kubernetes.io/name: traefik
namespaceSelector:
matchNames:
- traefik
endpoints:
- port: metrics
path: /metrics
interval: 30s
scrapeTimeout: 10s
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
# infrastructure/monitoring/prometheusrule-traefik.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: traefik-alerts
namespace: traefik
labels:
release: kube-prometheus-stack
spec:
groups:
- name: traefik.critical
rules:
- alert: TraefikDown
expr: kube_deployment_status_replicas_ready{namespace="traefik",deployment="traefik"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Traefik has no ready replicas"
runbook: "https://wiki.example.com/runbooks/traefik-down"
- alert: TraefikBackendDown
expr: |
sum(rate(traefik_service_requests_total{code=~"5.."}[2m])) by (service)
/
sum(rate(traefik_service_requests_total[2m])) by (service) == 1
for: 2m
labels:
severity: critical
annotations:
summary: "Traefik service {{ $labels.service }} is 100% failing"
- name: traefik.warnings
rules:
- alert: TraefikHighErrorRate
expr: |
sum(rate(traefik_router_requests_total{code=~"5.."}[5m])) by (router)
/
sum(rate(traefik_router_requests_total[5m])) by (router) > 0.05
for: 2m
labels:
severity: warning
annotations:
summary: "High error rate on router {{ $labels.router }}"
- alert: TraefikCertExpiringSoon
expr: traefik_tls_certs_not_after - time() < 14 * 24 * 3600
for: 1h
labels:
severity: warning
annotations:
summary: "TLS cert expiring in < 14 days"
description: "Certificate {{ $labels.sans }} expires soon."
|
Step 9: Dashboard IngressRoute
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
# infrastructure/traefik/dashboard-ingressroute.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: dashboard-auth
namespace: traefik
spec:
basicAuth:
secret: dashboard-auth-secret
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: traefik-dashboard
namespace: traefik
spec:
entryPoints:
- websecure
routes:
- match: Host(`traefik.internal.example.com`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`))
kind: Rule
services:
- name: api@internal
kind: TraefikService
middlewares:
- name: dashboard-auth
namespace: traefik
- name: secure-headers
namespace: traefik
tls: {}
|
Verification
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
# Check all Traefik pods are running
kubectl -n traefik get pods -o wide
# Verify the LoadBalancer got an external IP
kubectl -n traefik get svc traefik
# Check cert-manager issued the wildcard cert
kubectl -n traefik get certificate wildcard-example-com
kubectl -n traefik describe certificate wildcard-example-com
# Verify IngressRoute is registered
kubectl -n sample-app get ingressroute sample-app -o yaml
# Watch Traefik logs for any routing errors
kubectl -n traefik logs -l app.kubernetes.io/name=traefik -f
# Test the endpoint
curl -I https://app.example.com
# Check Prometheus scraping
kubectl -n traefik get servicemonitor traefik-metrics
# Port-forward to the dashboard (secured with basic auth in the IngressRoute)
kubectl -n traefik port-forward svc/traefik 9000:9000 &
# Then open: http://localhost:9000/dashboard/
|
Operational Tips and Common Pitfalls
IngressRoute not routing? The most common causes:
- The
entryPoints field names must match the names defined in values.yaml exactly. websecure not web-secure.
- Middleware referenced in an
IngressRoute must exist before the IngressRoute is applied. Traefik will skip a route whose middleware it cannot find.
- Cross-namespace middleware references require
allowCrossNamespace: true in the provider config and the namespace field on the middleware ref.
- Check the Traefik dashboard — it shows route status and any errors in the “Routers” and “Middlewares” panels.
Certificate not being served?
- Verify the
Certificate resource is in Ready state: kubectl get certificate -A
- Verify the TLS Secret exists:
kubectl -n <ns> get secret <secretName>
- If using a
TLSStore for the default cert, confirm Traefik has read access to the Secret’s namespace.
ForwardAuth redirect loop? The Authentik or Authelia auth endpoint itself must not be protected by the same forwardAuth middleware. Create a separate IngressRoute for the auth service with no forwardAuth middleware.
TCP routing with HostSNI(*) sends all connections on that entrypoint to the single service. If you want to multiplex multiple backends on one TCP port, each must use TLS with SNI-based routing — there is no other information in a plain TCP stream to distinguish clients.
Wildcard cert not being used as default? Confirm the TLSStore resource is named exactly default. Only the TLSStore named default is treated as the default; any other name has no effect on fallback behavior.
Prometheus alerts not firing? Verify the release label on ServiceMonitor and PrometheusRule matches your kube-prometheus-stack Helm release name: kubectl -n monitoring get prometheus -o yaml | grep serviceMonitorSelector.
Summary
Traefik earns its place as the Kubernetes ingress controller of choice for teams that want expressive, maintainable routing configuration. The IngressRoute CRD system replaces annotation-driven configuration with validated, reusable, composable API objects. The Middleware CRD separates cross-cutting concerns cleanly. TCP and UDP routing from the same binary eliminates the need for separate infrastructure for non-HTTP workloads.
The full production setup — Helm with HA values, cert-manager for certificate lifecycle, shared middleware in the traefik namespace, per-application IngressRoute resources with ForwardAuth and rate limiting, Prometheus ServiceMonitor with alerting rules — gives you a production-grade ingress layer that can grow with the cluster.
The operational investment is low once the initial configuration is in place. Adding a new service means writing a single IngressRoute and referencing existing middleware — no nginx config to edit, no certificate cron jobs to maintain, and no reload disruptions to worry about.
Comments