Starting from version 8.0, the thingsHub application allows Traefik v3 as its ingress controller for routing external traffic to the internal Kubernetes services. Traefik is deployed using the official Traefik Helm chart.
Recommended chart 41.0.2, image v3.7.9, with traefik-crds 1.18.0. Install Traefik as its own Helm release. These values do not map one-to-one onto a distribution addon such as RKE2 bundled Traefik.
This guide provides step-by-step instructions for deploying and configuring a production-ready Traefik ingress controller.
Prerequisites
Before deploying the Traefik ingress controller, ensure the following requirements are met:
-
Kubernetes Cluster: A running Kubernetes cluster (GKE, EKS, or RKE).
-
Helm: Helm v3.10+ must be installed. (Install Helm)
-
kubectl:
kubectlmust be installed and configured to connect to the target cluster. -
External IP Address: A static external IP address is required for the ingress load balancer. This IP will be the public entry point for all traffic to thingsHub tenants.
Google Cloud (GKE): Reserve a regional static IP:
gcloud compute addresses create traefik-ingress-ip --region <YOUR_REGION>
AWS (EKS): Allocate an Elastic IP via the AWS Console or CLI.
Heads-up notice
The Traefik configuration is split into two logical files:
-
traefik-values.yaml— The Shared Production Core. This configuration contains the recommended settings for high availability, resource allocation, Traefik providers, observability, and logging. These settings should remain the same across all deployments. -
cloud-environment-config.yaml— The Cloud-Specific Networking layer. Operators should use this file as a reference and adapt it for their own GitOps pipeline and specific cloud environment.
IMPORTANT: Do not copy and paste the example configurations directly. Use them as a reference for your own GitOps or Helm value overrides.
Example configuration files
Shared Production Core (traefik-values.yaml)
This file contains the recommended base configuration. These settings should remain the same.
image:
tag: v3.7.9
additionalArguments:
- "--global.checknewversion=false"
- "--global.sendanonymoususage=false"
podDisruptionBudget:
enabled: true
minAvailable: 1
deployment:
replicas: 3 # Minimum 2 for high availability
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app.kubernetes.io/name: '{{ template "traefik.name" . }}'
app.kubernetes.io/instance: '{{ .Release.Name }}-{{ include "traefik.namespace" . }}'
topologyKey: kubernetes.io/hostname
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "2Gi"
providers:
kubernetesIngress:
enabled: true
allowExternalNameServices: true
allowEmptyServices: true
publishedService:
enabled: true
kubernetesCRD:
enabled: true
allowExternalNameServices: true
allowCrossNamespace: true
experimental:
kubernetesGateway:
enabled: true
# HTTP to HTTPS. allowACMEByPass keeps Let's Encrypt HTTP-01 on port 80.
ports:
web:
allowACMEByPass: true
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
http:
middlewares:
- traefik-security-headers@kubernetescrd
tls:
enabled: true
metrics:
prometheus:
enabled: true
addRoutersLabels: true
addEntryPointsLabels: true
service:
enabled: true
serviceMonitor:
enabled: false # Set to true if Prometheus Operator is present
# Chart 40+ renamed logs.general / logs.access to log / accessLog.
log:
level: WARN
accessLog:
enabled: true
format: json
# Name must be "default". Do not set maxVersion (that disables TLS 1.3).
tlsOptions:
default:
minVersion: VersionTLS12
cipherSuites:
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
curvePreferences:
- X25519
- CurveP384
- CurveP256
extraObjects:
- apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: security-headers
spec:
headers:
contentTypeNosniff: true
referrerPolicy: strict-origin-when-cross-origin
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: false
forceSTSHeader: true
customResponseHeaders:
Server: ""
The middleware name in websecure assumes the release namespace is traefik (traefik-security-headers@kubernetescrd). Change the prefix if you install Traefik in another namespace. Chart 39.0.0 left the HTTP redirect off because it broke Let's Encrypt HTTP-01. Chart 41.0.2 can keep the redirect on when allowACMEByPass is set. After enabling it, confirm certificate renewal still succeeds.
Cloud-Specific Networking (cloud-environment-config.yaml)
Operators must adapt this file for their cloud provider and include it alongside the shared core during helm install or helm upgrade.
service:
enabled: true
type: LoadBalancer
spec:
externalTrafficPolicy: Local
## --- OPTION A: GOOGLE CLOUD (GKE) ---
annotations:
cloud.google.com/l4-rbs: "enabled"
networking.gke.io/weighted-load-balancing: "pods-per-node"
cloud.google.com/load-balancer-type: "External"
loadBalancerIP: "<YOUR_STATIC_IP>"
## --- OPTION B: AWS (EKS) ---
# annotations:
# service.beta.kubernetes.io/aws-load-balancer-type: "external"
# service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
# service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
# # service.beta.kubernetes.io/aws-load-balancer-eip-allocations: "eipalloc-xxxxxx"
## --- OPTION C: BARE METAL / RKE (MetalLB) ---
# annotations:
# metallb.universe.tf/address-pool: "production-ips"
# metallb.universe.tf/loadBalancerIPs: "<STATIC_IP>"
Deploying the Ingress Controller
Step 1: Verify Cluster Context
kubectl config current-context
If the context is not correct, switch to the appropriate one:
kubectl config use-context <YOUR_CONTEXT>
Step 2: Add the Helm Repository
helm repo add traefik https://traefik.github.io/charts
helm repo update
Step 3: Create the Namespace
kubectl create namespace traefik
Step 4: Install Traefik
Install Traefik CRDs first, then the controller. --skip-crds keeps Gateway API and Hub CRDs out of the controller chart.
helm upgrade --install traefik-crds traefik/traefik-crds \
--namespace traefik \
--version 1.18.0 \
--set traefik=true
helm install traefik traefik/traefik \
--namespace traefik \
--version 41.0.2 \
--skip-crds \
-f traefik-values.yaml \
-f cloud-environment-config.yaml
NOTE: The
-fflag can be specified multiple times. Helm merges the values files in order, with later files taking precedence. This allows the cloud-specific configuration to override the shared core where needed (e.g.,serviceblock).
Step 5: Verify the Deployment
-
Check that the Traefik pods are running:
kubectl get pods -n traefik -
Verify the LoadBalancer Service has the expected external IP:
kubectl get svc -n traefik traefik -
Confirm the
EXTERNAL-IPcolumn matches your reserved static IP.
Updating / Upgrading the Ingress Controller
To upgrade Traefik to a new chart version or apply configuration changes, update the Helm repository, upgrade traefik-crds first when the controller chart requires it, then upgrade the controller.
helm repo update
helm upgrade traefik-crds traefik/traefik-crds \
--namespace traefik \
--version 1.18.0 \
--set traefik=true
helm upgrade traefik traefik/traefik \
--namespace traefik \
--version 41.0.2 \
--skip-crds \
-f traefik-values.yaml \
-f cloud-environment-config.yaml
Helm Chart Configuration Options
The following table documents the key configuration values used in the thingsHub Traefik deployment:
|
Name |
Type |
Description |
|---|---|---|
|
|
integer |
Number of Traefik pods. Minimum 2 for HA; recommended 3. |
|
|
boolean |
Enables Pod Disruption Budgets for safe node drains. |
|
|
integer |
Minimum number of available pods during voluntary disruptions. |
|
|
object |
Ensures Traefik pods are scheduled on different nodes using topologyKey kubernetes.io/hostname. |
|
|
string |
Minimum CPU/Memory guaranteed for each Traefik pod. |
|
|
string |
Maximum CPU/Memory each Traefik pod can consume. |
|
|
boolean |
Enables the standard Kubernetes Ingress provider. |
|
|
boolean |
Allows Ingress resources to route to ExternalName services. |
|
|
boolean |
Returns HTTP 503 when a service has no endpoints instead of failing silently. |
|
|
boolean |
Enables Traefik CRDs (IngressRoute, Middleware, and so on). |
|
|
boolean |
Allows Traefik to reference Middlewares or TLSOptions defined in other namespaces. |
|
|
boolean |
Enables the Kubernetes Gateway API support (Traefik v3). |
|
|
string |
Set to LoadBalancer to expose Traefik externally. |
|
|
string |
Must be Local to preserve the source IP address of client requests. |
|
|
string |
Static IP assignment for the LoadBalancer (used in GKE). |
|
|
object |
Cloud provider-specific annotations for the LoadBalancer service. |
|
|
boolean |
Enables Prometheus metrics endpoint. |
|
|
string |
Log level for Traefik (recommended WARN). Chart 40+ renamed logs.general.level to log.level. |
|
|
boolean |
Enables access logging. Chart 40+ renamed logs.access.enabled to accessLog.enabled. |
|
|
string |
Format of access logs. Set to json for structured logging. |
|
|
object |
Permanent HTTP to HTTPS redirect. |
|
|
boolean |
Leaves /.well-known/acme-challenge/ on port 80 so HTTP-01 renewal still works. |
|
|
list |
Headers middleware applied to every HTTPS router. |
|
|
object |
TLS 1.2+ and Mozilla Intermediate ciphers for routers that do not name another TLSOption. |
|
|
list |
The security-headers Middleware shipped with the release. |
|
|
list |
CLI flags. --global.checknewversion=false and --global.sendanonymoususage=false disable telemetry. |
How to...
... configure for Google Cloud (GKE)
-
Reserve a Regional Static External IP in your GCP project.
-
In your
cloud-environment-config.yaml, setservice.loadBalancerIPto the reserved IP. -
Ensure the following annotations are present:
service:
annotations:
cloud.google.com/l4-rbs: "enabled"
networking.gke.io/weighted-load-balancing: "pods-per-node"
cloud.google.com/load-balancer-type: "External"
The l4-rbs annotation enables modern backend-service based load balancing, which is the recommended mode for GKE.
... configure for AWS (EKS)
-
Ensure the AWS Load Balancer Controller is installed in the cluster.
-
Allocate an Elastic IP in AWS.
-
Use the following annotations:
service:
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "external"
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
# service.beta.kubernetes.io/aws-load-balancer-eip-allocations: "eipalloc-xxxxxx"
AWS does not use loadBalancerIP. Static IP binding is done via the eip-allocations annotation.
... ensure Source IP preservation
Setting service.spec.externalTrafficPolicy: Local is critical for thingsHub. Without it:
-
All incoming requests appear to originate from cluster-internal node IPs.
-
IP-based access controls (e.g.,
system_user_ip_whitelist) will not function correctly. -
Access logs will not reflect real client IP addresses.
With externalTrafficPolicy: Local, Kubernetes forwards traffic directly to the node running the Traefik pod, preserving the original client IP address.
WARNING: This setting requires that each node running a Traefik pod can receive traffic from the load balancer. The Pod Anti-Affinity configuration ensures pods are distributed across nodes, which supports this requirement.
... enable the hardened controller settings
TLS terminates at Traefik, so redirect, HSTS, ciphers, and the extra response headers belong on the controller, not in the thingsHub application. X-Frame-Options stays on the tenant chart (DENY for the application, SAMEORIGIN for Visualizer). Do not set it on this middleware.
The security-headers middleware in the shared values sets:
-
HSTS for one year (
stsSeconds: 31536000), including subdomains of the requested host. Preload stays off. -
X-Content-Type-Options: nosniff -
Referrer-Policy: strict-origin-when-cross-origin(full URL on same-origin requests, origin only when the browser calls another site) -
removal of the
Serverheader
tlsOptions.default is Mozilla Intermediate: TLS 1.2 or newer, AEAD ciphers only. TLS 1.3 stays enabled.
curl -sI https://<tenant-host>/ui
... enable the Traefik Dashboard (Optional)
The Traefik dashboard can be enabled for debugging and observability, but it must be secured.
-
Generate htpasswd credentials:
htpasswd -nb admin <your-secure-password> -
Base64 encode the output:
echo 'admin:<hash>' | base64 -
Create the following manifests in the
traefiknamespace and apply them.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: dashboard-auth
namespace: traefik
spec:
basicAuth:
secret: dashboard-users-secret
---
apiVersion: v1
kind: Secret
metadata:
name: dashboard-users-secret
namespace: traefik
type: Opaque
data:
users: "<BASE64_ENCODED_HTPASSWD>"
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: traefik-dashboard
namespace: traefik
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.middlewares: traefik-dashboard-auth@kubernetescrd
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
ingressClassName: traefik
rules:
- host: "traefik.<YOUR_DOMAIN>"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api@internal
port:
name: traefik
tls:
- hosts:
- "traefik.<YOUR_DOMAIN>"
secretName: traefik-dashboard-tls
Apply with kubectl apply -f traefik-dashboard-security.yaml and open https://traefik.<YOUR_DOMAIN>.