Skip to content

Traefik IngressRoute CRD on Kubernetes — the canonical way to express routes

Author Traefik IngressRoute objects on Kubernetes — match rules, middleware references, TLS, services — and the differences from plain Ingress.

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.secretName on 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 match rule on the same entrypoint — both load, both compete on priority, debugging which one served the request needs the access log’s RouterName field.
  • IngressRouteTCP with tls.passthrough: true and tls.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.