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.