thingsHub Public Documentation

Ingress Controller for Thingshub Deployments (Traefik)

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:

  1. Kubernetes Cluster: A running Kubernetes cluster (GKE, EKS, or RKE).

  2. Helm: Helm v3.10+ must be installed. (Install Helm)

  3. kubectl: kubectl must be installed and configured to connect to the target cluster.

  4. 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 -f flag 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., service block).

Step 5: Verify the Deployment

  1. Check that the Traefik pods are running: kubectl get pods -n traefik

  2. Verify the LoadBalancer Service has the expected external IP: kubectl get svc -n traefik traefik

  3. Confirm the EXTERNAL-IP column 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

deployment.replicas

integer

Number of Traefik pods. Minimum 2 for HA; recommended 3.

podDisruptionBudget.enabled

boolean

Enables Pod Disruption Budgets for safe node drains.

podDisruptionBudget.minAvailable

integer

Minimum number of available pods during voluntary disruptions.

affinity.podAntiAffinity

object

Ensures Traefik pods are scheduled on different nodes using topologyKey kubernetes.io/hostname.

resources.requests.cpu / memory

string

Minimum CPU/Memory guaranteed for each Traefik pod.

resources.limits.cpu / memory

string

Maximum CPU/Memory each Traefik pod can consume.

providers.kubernetesIngress.enabled

boolean

Enables the standard Kubernetes Ingress provider.

providers.kubernetesIngress.allowExternalNameServices

boolean

Allows Ingress resources to route to ExternalName services.

providers.kubernetesIngress.allowEmptyServices

boolean

Returns HTTP 503 when a service has no endpoints instead of failing silently.

providers.kubernetesCRD.enabled

boolean

Enables Traefik CRDs (IngressRoute, Middleware, and so on).

providers.kubernetesCRD.allowCrossNamespace

boolean

Allows Traefik to reference Middlewares or TLSOptions defined in other namespaces.

experimental.kubernetesGateway.enabled

boolean

Enables the Kubernetes Gateway API support (Traefik v3).

service.type

string

Set to LoadBalancer to expose Traefik externally.

service.spec.externalTrafficPolicy

string

Must be Local to preserve the source IP address of client requests.

service.loadBalancerIP

string

Static IP assignment for the LoadBalancer (used in GKE).

service.annotations

object

Cloud provider-specific annotations for the LoadBalancer service.

metrics.prometheus.enabled

boolean

Enables Prometheus metrics endpoint.

log.level

string

Log level for Traefik (recommended WARN). Chart 40+ renamed logs.general.level to log.level.

accessLog.enabled

boolean

Enables access logging. Chart 40+ renamed logs.access.enabled to accessLog.enabled.

accessLog.format

string

Format of access logs. Set to json for structured logging.

ports.web.http.redirections

object

Permanent HTTP to HTTPS redirect.

ports.web.allowACMEByPass

boolean

Leaves /.well-known/acme-challenge/ on port 80 so HTTP-01 renewal still works.

ports.websecure.http.middlewares

list

Headers middleware applied to every HTTPS router.

tlsOptions.default

object

TLS 1.2+ and Mozilla Intermediate ciphers for routers that do not name another TLSOption.

extraObjects

list

The security-headers Middleware shipped with the release.

additionalArguments

list

CLI flags. --global.checknewversion=false and --global.sendanonymoususage=false disable telemetry.

How to...

... configure for Google Cloud (GKE)

  1. Reserve a Regional Static External IP in your GCP project.

  2. In your cloud-environment-config.yaml, set service.loadBalancerIP to the reserved IP.

  3. 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)

  1. Ensure the AWS Load Balancer Controller is installed in the cluster.

  2. Allocate an Elastic IP in AWS.

  3. 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 Server header

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.

  1. Generate htpasswd credentials: htpasswd -nb admin <your-secure-password>

  2. Base64 encode the output: echo 'admin:<hash>' | base64

  3. Create the following manifests in the traefik namespace 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>.