k3s ships with traefik that works out of the box, but for certificate generation while running on Kubernetes you should consider using cert-manager which is apparently a go-to tool for certs management in the k8s world.

For the simplicity of this example let’s assume you have a single node k3s cluster running which doesn’t have any external load balance in front of it (otherwise you might need a DNS challenge and here we will use HTTP-01).

The official way of installing cert-manager seems to be just kubectl apply -f, which reminds me of piping response from curl directly to bash with a sudo on top. Anyway, I shall talk about ArgoCD in some other blog post. So:

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.1/cert-manager.yaml

Should spin up 3 new pods in cert-manager namespace and create a bunch of other resources.

Now let’s configure ACME HTTP-01 using Terraform.

First, configure let’s encrypt ClusterIssuer - we’re going to use ClusterIssuer and not Issuer so we can issue certificates in any namespace. Is that somehow less secure? I have no idea, but for the sake of let’s just go with it.

resource "kubernetes_manifest" "letsencrypt_issuer" {
  manifest = {
    "apiVersion" = "cert-manager.io/v1"
    "kind"       = "ClusterIssuer"
    "metadata"   = {
      "name" = "letsencrypt-prod"
    }
    "spec" = {
      "acme" = {
        # NOTE: remember to update email here
        "email"               = "[email protected]"
        "server"              = "https://acme-v02.api.letsencrypt.org/directory"
        "privateKeySecretRef" = {
          "name" = "letsencrypt-prod"
        }
        "solvers" = [
          {
            "http01" = {
              "ingress" = {
                "class" = "traefik"
              }
            }
          }
        ]
      }
    }
  }
}

Applying the plan should create a new resource, but nothing interesting will happen yet. Now let’s try to generate a new certificate; at this point, you should have a domain (see traefik.mydomain.com part) pointing to your k3s node.

resource "kubernetes_manifest" "traefik_cert" {
  manifest = {
    "apiVersion" = "cert-manager.io/v1"
    "kind"       = "Certificate"
    "metadata"   = {
      "name" = "traefik-cert"
      "namespace" = "kube-system"
    }
    "spec" = {
      "secretName" = "traefik-cert"
      "issuerRef"  = {
        "name" = "letsencrypt-prod"
        "kind" = "ClusterIssuer"
      }
      "dnsNames" = [
        # NOTE: remember to change this
        "traefik.mydomain.com"
      ]
    }
  }
}

When you apply the plan, the cert-manager should create a new resource - cert-manager.io/v1/certificaterequests - and will spin a new temporary pod that will serve an HTTP-01 request. Everything should work automagically with the default k3s traefik configuration (as we specified ingress class in the ClusterIssuer definition).

The new certificate should be stored in secrets as kubernetes.io/tls. Now we can expose traefik dashboard to the world.

resource "kubernetes_manifest" "traefik_route" {
  manifest = {
    "apiVersion" = "traefik.containo.us/v1alpha1"
    "kind"       = "IngressRoute"
    "metadata"   = {
      "name"        = "dashboard-ingressroute"
      "namespace"   = "kube-system"
    }
    "spec" = {
      "entryPoints" = [
        "websecure"
      ]
      "routes" = [
        {
          "kind"        = "Rule"
          # NOTE: remember to change the domain
          "match"       = "Host(`traefik.mydomain.com`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`))"
          "services" = [
            {
              "name" = "[email protected]"
              "kind" = "TraefikService"
            }
          ]
        }
      ]
      "tls" = {
        "secretName" = "traefik-cert"
      }
    }
  }
}

After applying plan once you go to traefik.mydomain.com/dashboard you should see good, old traefik web ui dashboard.

Hello, old friend.

Is there a simpler way?

If prefer achieving same result using more automagical approach you can try using annotations - look for cert-manager.io/cluster-issuer.

What about auto https redirect?

You might be tempted to redirect all non-http traffic to https automatically by configuring a traefik helm chart shipped with k3s by setting chart values to something like:

ports:
  web:
    redirectTo: websecure

That seems to be breaking the cert-manager HTTP-01 challenge so I would recommend doing http->https redirect explicitly by specifying redirect middleware. That said allowCrossNamespace might interest you in case you don’t want to define same middleware in every namespace.