The plain Kubernetes Ingress object maps cleanly to about half of Traefik’s features. The other half — middleware chains, TLS option references, weighted services, sticky sessions, TCP routes — needs the IngressRoute CRD. Once a cluster commits to IngressRoute as the canonical route surface, the operational benefits are obvious: every route is the same object shape, every middleware is a reference, every TLS option is named. This article is a reference for authoring IngressRoute, IngressRouteTCP, IngressRouteUDP, Middleware, and ServersTransport objects.
How to verify
kubectl get crd | grep traefik.io
# ingressroutes, ingressroutetcps, ingressrouteudps, middlewares, serverstransports, tlsoptions, tlsstores
kubectl get ingressroute -A
kubectl get middleware -A
# Status conditions show whether Traefik accepted each object
kubectl describe ingressroute -n my-app api | grep -A5 'Status:'
What’s happening
The Traefik CRD set is a strict 1:1 with the Traefik configuration model. An IngressRoute is the K8s shape of an HTTP router. A Middleware is the K8s shape of a middleware. A TraefikService is the K8s shape of weighted/mirrored services. A TLSOption is the K8s shape of tls.options. There is no semantic gap between writing a file-provider YAML and an IngressRoute — only the wrapper differs.
The CRD model has three properties worth understanding. First, Middleware objects live in a namespace and can be referenced cross-namespace only if allowCrossNamespace is enabled on the kubernetesCRD provider — which is off by default for security, and should stay off in multi-tenant clusters. Second, the same Middleware is shareable across many IngressRoutes — define secure-headers once in a platform namespace, reference it from every app namespace by namespace:name. Third, Status conditions on each CRD tell you whether Traefik accepted the object and why it didn’t.
The reference
IngressRoute (HTTP)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: api
namespace: my-app
spec:
entryPoints:
- websecure
routes:
- match: Host(`api.example.com`) && PathPrefix(`/v1`)
kind: Rule
priority: 200
middlewares:
- name: secure-headers
namespace: platform
- name: rate-limit
services:
- name: api
port: 8080
weight: 100
passHostHeader: true
responseForwarding:
flushInterval: 100ms
tls:
secretName: api-tls
options:
name: tls-strict
namespace: platform
services accepts either a K8s Service (name + port) or a TraefikService (for weighted/mirrored composition). middlewares references are local-namespace by default; cross-namespace requires the namespace: field plus allowCrossNamespace: true on the provider.
Middleware
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: secure-headers
namespace: platform
spec:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
contentTypeNosniff: true
referrerPolicy: strict-origin-when-cross-origin
customResponseHeaders:
Server: ""
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: rate-limit
namespace: my-app
spec:
rateLimit:
average: 100
burst: 50
sourceCriterion:
ipStrategy:
depth: 2
TraefikService for traffic splitting
apiVersion: traefik.io/v1alpha1
kind: TraefikService
metadata:
name: api-split
namespace: my-app
spec:
weighted:
services:
- name: api-v1
port: 8080
weight: 90
- name: api-v2
port: 8080
weight: 10
Reference this from an IngressRoute by setting services[].kind: TraefikService.
IngressRouteTCP
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: mqtt
namespace: my-app
spec:
entryPoints:
- websecure
routes:
- match: HostSNI(`mqtt.example.com`)
services:
- name: mqtt-broker
port: 8883
tls:
passthrough: true
IngressRouteUDP
apiVersion: traefik.io/v1alpha1
kind: IngressRouteUDP
metadata:
name: dns
namespace: dns
spec:
entryPoints:
- dns
routes:
- services:
- name: coredns
port: 53
TLSOption
apiVersion: traefik.io/v1alpha1
kind: TLSOption
metadata:
name: tls-strict
namespace: platform
spec:
minVersion: VersionTLS12
cipherSuites:
- TLS_AES_128_GCM_SHA256
- TLS_AES_256_GCM_SHA384
- TLS_CHACHA20_POLY1305_SHA256
curvePreferences:
- X25519
- secp384r1
sniStrict: true
ServersTransport — backend TLS
When the backend speaks HTTPS with its own cert, control the upstream TLS shape here.
apiVersion: traefik.io/v1alpha1
kind: ServersTransport
metadata:
name: backend-tls
namespace: my-app
spec:
serverName: api.internal
insecureSkipVerify: false
rootCAsSecrets:
- internal-ca
certificatesSecrets:
- client-cert
Reference from an IngressRoute:
services:
- name: api
port: 8443
serversTransport: backend-tls
scheme: https
Common pitfalls
- Forgetting that
tls.secretNameon an IngressRoute uses an existing Kubernetes TLS secret — it does not trigger ACME issuance; cert-manager or Traefik’s ACME store is what creates that secret. - Cross-namespace Middleware references silently fail when
allowCrossNamespace: false— the IngressRoute Status shows the error; teach teams to read it. - Putting two routes with the same
matchrule on the same entrypoint — both load, both compete on priority, debugging which one served the request needs the access log’sRouterNamefield. - IngressRouteTCP with
tls.passthrough: trueandtls.secretName— passthrough means Traefik never sees the cert, so the secretName is ignored, but the route loads silently with confusing behaviour. - ServersTransport requires the secret to live in the same namespace as the ServersTransport object; cross-namespace secret references are not supported.
The IngressRoute CRD is what makes a Kubernetes Traefik install operationally clean — every piece of routing surface is a named object in a Git-managed repo, statuses are observable, and Stack Harbor’s managed Kubernetes operations treat these CRDs the same way we treat any other cluster-critical state. For the install side of things see traefik-install-kubernetes.