Traefik on k3s: Let's Encrypt & Prometheus Monitoring


estimated read time: 4 minutes

Traefik v3 on K3s: Ingress Routing and TLS with cert-manager

One thing that caught me out early on with K3s is that it ships with Traefik already installed as the default ingress controller. You don’t need to install it — it’s running in kube-system from the moment your cluster comes up. This trips people up when they try to install their own ingress controller and end up with two fighting over the same ports.

kubectl get pods -n kube-system | grep traefik

This post covers what Traefik actually does in that context, how to expose services through it using IngressRoute (Traefik’s native CRD), and how to wire up TLS certificates via cert-manager using DNS01 validation.


IngressRoute vs Standard Ingress

Traefik supports both the standard Kubernetes Ingress resource and its own IngressRoute CRD. Standard Ingress works fine for basic routing, but IngressRoute gives you access to Traefik-specific features: middleware chains, priority rules, and more granular TLS configuration.

The tradeoff worth knowing upfront: cert-manager’s automatic certificate annotations only work on standard Ingress resources, not on IngressRoute. If you want IngressRoute, you manage certificates explicitly with a Certificate resource and reference the resulting secret directly. It’s a small amount of extra YAML, but you get more visibility into what’s happening.

Also worth noting — the API group changed in Traefik v3:

VersionAPI Group
v2traefik.containo.us/v1alpha1
v3traefik.io/v1alpha1

If you’re following older guides and your IngressRoute resources aren’t being picked up, this is the likely cause.


Exposing a Service with IngressRoute

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: my-service-route
  namespace: default
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`my-service.example.com`)
      kind: Rule
      services:
        - name: my-service
          port: 80
  tls:
    secretName: my-service-tls

The secretName here is where cert-manager will store the issued certificate — more on that below.


TLS with cert-manager

I use cert-manager with a Cloudflare DNS01 solver across my cluster — if you haven’t set that up yet, see this post which walks through the full configuration.

The pattern for issuing a certificate for a Traefik IngressRoute is:

  1. Create a Certificate resource pointing at your ClusterIssuer
  2. cert-manager handles the DNS01 challenge and stores the certificate in a Secret
  3. The IngressRoute references that Secret via tls.secretName

Certificate Resource

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: my-service-tls
  namespace: default
spec:
  secretName: my-service-tls
  dnsNames:
    - my-service.example.com
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer

Once cert-manager provisions the certificate, the secret my-service-tls will appear in the namespace and the IngressRoute will pick it up automatically. Renewals are handled by cert-manager without any manual intervention.


Redirecting HTTP to HTTPS

In Traefik v3, HTTP-to-HTTPS redirection is handled via Middleware. Create it once and reference it on any route that needs it:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: redirect-to-https
  namespace: default
spec:
  redirectScheme:
    scheme: https
    permanent: true

Then add a separate IngressRoute on the web entrypoint that applies the middleware:

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: my-service-http
  namespace: default
spec:
  entryPoints:
    - web
  routes:
    - match: Host(`my-service.example.com`)
      kind: Rule
      middlewares:
        - name: redirect-to-https
      services:
        - name: my-service
          port: 80

Putting It All Together

A full working setup for a service with HTTPS and HTTP redirect:

# cert-manager handles issuance and renewal automatically
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: my-service-tls
  namespace: default
spec:
  secretName: my-service-tls
  dnsNames:
    - my-service.example.com
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
---
# Redirect HTTP to HTTPS
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: redirect-to-https
  namespace: default
spec:
  redirectScheme:
    scheme: https
    permanent: true
---
# HTTP entrypoint — redirect only
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: my-service-http
  namespace: default
spec:
  entryPoints:
    - web
  routes:
    - match: Host(`my-service.example.com`)
      kind: Rule
      middlewares:
        - name: redirect-to-https
      services:
        - name: my-service
          port: 80
---
# HTTPS entrypoint — actual traffic
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: my-service-https
  namespace: default
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`my-service.example.com`)
      kind: Rule
      services:
        - name: my-service
          port: 80
  tls:
    secretName: my-service-tls

The DNS01 challenge means cert-manager doesn’t need inbound port 80 traffic to prove domain ownership — it creates a temporary DNS record via the Cloudflare API instead. This is particularly useful when your cluster is behind a firewall or NAT where HTTP01 challenges aren’t practical.


Pi Cluster Series