A Helm install of Traefik on Kubernetes is the quickest path to a working ingress controller, but a wide-open default install creates a dashboard on the public LoadBalancer, gives the controller cluster-wide read on every secret, and lets a developer in any namespace claim any hostname. This article walks through a Traefik v3 install with the CRD provider, RBAC trimmed to what the controller actually needs, the dashboard fenced behind an IngressRoute with auth, and Prometheus metrics scraped through a separate ServiceMonitor.
How to verify
kubectl get pods -n traefik -l app.kubernetes.io/name=traefik
kubectl get crd | grep traefik.io
kubectl get svc -n traefik traefik
kubectl logs -n traefik -l app.kubernetes.io/name=traefik --tail=30 | grep -iE 'configuration loaded|cert'
kubectl get ingressroute -A
The service should be a LoadBalancer with an external IP and the dashboard router should NOT appear in any namespace except traefik. The CRDs ingressroutes.traefik.io, middlewares.traefik.io, tlsstores.traefik.io, serverstransports.traefik.io are present.
What’s happening
The Traefik Helm chart deploys three things together: a Deployment (or DaemonSet for hostNetwork patterns), a Service of type LoadBalancer that fronts the pod, and the CRDs and RBAC that let Traefik watch IngressRoute and Middleware objects across the cluster. By default the controller reads ingresses cluster-wide, which is fine for shared infrastructure but a problem for multi-tenant clusters — there a per-namespace kubernetesIngress.namespaces or a label selector is the right answer.
The CRD path is the way to use Traefik on Kubernetes. The plain Ingress resource maps to a subset of Traefik’s features, and almost every operator-grade pattern (TLS options, middleware chains, TCP routes, sticky sessions) needs the CRD. Once teams adopt IngressRoute, they rarely go back, but mixed installs that accept both Ingress and IngressRoute drift quickly — pick one as the canonical surface and treat the other as deprecated.
The procedure
-
Add the Helm repo and pull the chart values.
helm repo add traefik https://traefik.github.io/charts helm repo update helm show values traefik/traefik > /tmp/traefik-defaults.yaml -
Write a tight
values.yaml. NoteadditionalArguments, the metrics + dashboard layout, andingressClassset so the controller does not silently grab every old Ingress on the cluster.# values.yaml image: tag: v3.1.6 deployment: replicas: 2 service: type: LoadBalancer annotations: service.beta.kubernetes.io/aws-load-balancer-type: nlb ingressClass: enabled: true isDefaultClass: false name: traefik providers: kubernetesIngress: enabled: true allowExternalNameServices: false ingressClass: traefik kubernetesCRD: enabled: true allowCrossNamespace: false ports: web: redirectTo: port: websecure priority: 10 websecure: tls: enabled: true metrics: prometheus: service: enabled: true serviceMonitor: enabled: true logs: general: level: INFO format: json access: enabled: true format: json additionalArguments: - "--api.dashboard=true" - "--api.insecure=false" - "--global.checknewversion=false" - "--global.sendanonymoususage=false" resources: requests: cpu: 200m memory: 256Mi limits: memory: 512Mi podSecurityContext: fsGroup: 65532 securityContext: readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 65532 -
Install into a dedicated namespace.
kubectl create namespace traefik helm install traefik traefik/traefik -n traefik -f values.yaml kubectl rollout status -n traefik deploy/traefik -
Expose the dashboard through an IngressRoute with basic-auth — never via the chart’s
dashboard.ingressRoutequickstart, which has no auth.apiVersion: v1 kind: Secret metadata: name: traefik-dashboard-auth namespace: traefik stringData: users: | admin:$2y$05$REPLACE_WITH_HASH --- apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: dashboard-auth namespace: traefik spec: basicAuth: secret: traefik-dashboard-auth --- apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: traefik-dashboard namespace: traefik spec: entryPoints: [websecure] routes: - match: Host(`traefik.example.com`) kind: Rule services: - name: api@internal kind: TraefikService middlewares: - name: dashboard-auth tls: secretName: traefik-dashboard-tls -
Test with a sample IngressRoute in another namespace.
apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: whoami namespace: default spec: entryPoints: [websecure] routes: - match: Host(`whoami.example.com`) kind: Rule services: - name: whoami port: 80 tls: secretName: whoami-tls
Common pitfalls
- Leaving
allowCrossNamespace: true— any IngressRoute can reference a Service in any namespace, which lets one team’s app proxy another team’s backend. - Installing without
ingressClass.isDefaultClass: falseon a cluster that already has another controller — both controllers grab the same Ingress object and you get races on cert issuance. - Trusting the chart’s default
dashboard.enabledIngressRoute — recent versions disable it, but if you flip it back on you get an unauthenticated dashboard publicly. - Not pinning
image.tag— Helm upgrades that flip the appVersion can roll a new major mid-incident; pin the tag and bump it intentionally. - Forgetting the
ServiceMonitoris in a namespace that Prometheus actually selects — by default Prometheus Operator only picks up monitors in namespaces it watches.
For Kubernetes-native ACME, see traefik-letsencrypt-acme; cert-manager is the more common production choice and the IngressRoute references it the same way. In the clusters Stack Harbor operates we run managed Kubernetes operations end-to-end — Traefik upgrades, RBAC drift, IngressRoute audits, and ACME rotation are tracked the same way we track any other cluster-critical component.