Skip to content

Traefik multi-tenant patterns — namespace isolation, route ownership, and shared platform middleware

Run one Traefik to front many tenants — namespace isolation, route ownership boundaries, and a platform-middleware library every team consumes.

A single Traefik fronting many tenants is efficient but only safe if route ownership is bounded — one tenant cannot claim another’s hostname, reference another’s secrets, or smuggle their traffic through a shared middleware. This article covers the isolation patterns we use when multiple teams share a Traefik install: K8s RBAC scoped per namespace, a Host(...) rule lock, a platform-managed middleware library, and the audits that catch drift before it becomes an incident.

How to verify

# Confirm the controller only watches the namespaces it should
kubectl describe deploy -n traefik traefik | grep -A2 'kubernetes.namespaces\|kubernetesIngress.namespaces'
# Confirm allowCrossNamespace is off
kubectl describe deploy -n traefik traefik | grep allowCrossNamespace
# List all IngressRoutes by namespace and hostname — the audit surface
kubectl get ingressroute -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.spec.routes[*].match}{"\n"}{end}'
# Look for tenants stepping outside their assigned host space
kubectl get ingressroute -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.spec.routes[*].match}{"\n"}{end}' \
  | awk -F'\t' '$1=="tenant-a" && $2 !~ /tenant-a\./ {print}'

What’s happening

Multi-tenancy in Traefik comes from layering several controls. The first is provider scope: the kubernetesCRD provider can be told to watch a label selector or a namespace list, so a single Traefik fleet can serve only the namespaces explicitly granted to it. The second is cross-namespace deny: allowCrossNamespace: false keeps an IngressRoute in namespace A from referencing a Middleware, Service, or TLS secret in namespace B unless both sides opt in. The third is RBAC: the controller’s ServiceAccount is read-only on the resources it watches, and tenant ServiceAccounts have CRUD only on their own namespace’s IngressRoute / Middleware objects.

Those three controls cover everything except the hostname namespace. Traefik does not enforce “tenant A owns *.tenant-a.example.com” — any team can author an IngressRoute matching Host(tenant-b.example.com) and it works. The fix is a validating admission webhook (Kyverno, OPA Gatekeeper, kubewarden) that rejects IngressRoutes whose match rule mentions a host outside the tenant’s assigned space.

The pattern that keeps everyone honest: the platform namespace owns a curated middleware library — secure-headers, rate-limit-default, compress, cors-default — that tenants reference rather than redefine. That gives one place to update security headers when a CVE drops, one place to tighten rate limits when an abuser shows up, and one place to audit what every tenant is opting into.

The procedure

  1. Scope the controller to specific namespaces. On the kubernetesCRD provider:

    providers:
      kubernetesCRD:
        enabled: true
        namespaces:
          - tenant-a
          - tenant-b
          - platform
        allowCrossNamespace: false
        allowExternalNameServices: false
  2. Set the controller ServiceAccount’s ClusterRole to read-only on Traefik CRDs. Do not give it the cluster-wide secret read that the chart’s default RBAC grants — narrow to the namespaces it serves.

    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: traefik-secrets-reader
      namespace: tenant-a
    rules:
      - apiGroups: [""]
        resources: ["secrets"]
        verbs: ["get", "list", "watch"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: traefik-secrets-reader
      namespace: tenant-a
    roleRef:
      kind: Role
      name: traefik-secrets-reader
      apiGroup: rbac.authorization.k8s.io
    subjects:
      - kind: ServiceAccount
        name: traefik
        namespace: traefik

    Repeat per tenant namespace. The chart needs a values override to drop the default cluster-wide secret get.

  3. Publish the platform middleware library. Tenants reference by namespace:name.

    apiVersion: traefik.io/v1alpha1
    kind: Middleware
    metadata:
      name: secure-headers
      namespace: platform
    spec:
      headers:
        stsSeconds: 31536000
        contentTypeNosniff: true
        referrerPolicy: strict-origin-when-cross-origin
    ---
    apiVersion: traefik.io/v1alpha1
    kind: Middleware
    metadata:
      name: rate-limit-default
      namespace: platform
    spec:
      rateLimit:
        average: 100
        burst: 50

    Because allowCrossNamespace: false is on, tenants cannot use these by default — flip the flag to true and rely on the Kyverno guard below to scope which platform middlewares are referenceable, or add specific allow-list rules. Most teams find it cleaner to ship a tenant-namespace-scoped clone of each platform middleware via GitOps and treat the platform-namespace versions as the canonical source.

  4. Add a Kyverno (or OPA Gatekeeper) policy that enforces hostname ownership.

    apiVersion: kyverno.io/v1
    kind: ClusterPolicy
    metadata:
      name: tenant-hostname-scope
    spec:
      validationFailureAction: enforce
      rules:
        - name: tenant-a-must-own-tenant-a-hosts
          match:
            any:
              - resources:
                  kinds: [IngressRoute]
                  namespaces: [tenant-a]
          validate:
            message: "tenant-a IngressRoute must match a host within tenant-a.example.com"
            pattern:
              spec:
                routes:
                  - match: "*tenant-a.example.com*"
  5. Audit weekly. The audit surface is small and worth wiring into the same pipeline that emits other compliance signals.

    kubectl get ingressroute -A -o json \
      | jq -r '.items[] | [.metadata.namespace, .spec.routes[].match] | @tsv' \
      | sort

Operational notes

  • The chart’s default ClusterRole grants cluster-wide secret read; narrow this before going to a multi-tenant install or any tenant can craft a route that lets Traefik leak another tenant’s TLS secrets.
  • Tenants who insist on bringing their own custom middleware can have it — keep their middlewares in their namespace, and never accept a PR that adds a middleware to platform without platform-team review.
  • The Host rule lock is the most common gap. Without admission policy, the first team to author Host(other-tenant.example.com) gets the route and the legitimate owner’s traffic.
  • A shared Traefik fleet is a shared failure domain — a bug in a tenant-supplied custom plugin can bring down every tenant. Either disallow custom plugins in shared fleets or split per-tenant fleets at high blast-radius thresholds.
  • For shared TLS, store wildcard certs in the platform namespace and reference them via secretName from tenant IngressRoutes — but the wildcard now becomes a high-impact secret; rotate accordingly.

Multi-tenant Traefik is a balance: efficient enough to share, isolated enough that one tenant cannot hurt another. The runbook that captures who owns which namespace, which platform middleware they consume, and the admission policies that keep them honest is part of our managed Kubernetes operations. For the underlying CRD shape, see traefik-k8s-ingressroute-crd.