Set up Cert-Manager, DNS-01, and Gateway API on K3s

Updated on Sep 1, 2026
Mila H
6 MINS READ
Table of Contents
cert-manager on k3s

Automating TLS on Kubernetes gets tricky once you go beyond a single Ingress certificate. This guide shows how to set up cert-manager on K3s with Let's Encrypt and DNS-01, starting from a fresh Ubuntu server to a working wildcard certificate attached to a Gateway API resource. It also covers checking that renewal works and storing your DNS credentials safely.

What You'll Need

Before starting with cert-manager on K3s with Let's Encrypt and DNS-01, make sure to have: 

  • A Linux server running Ubuntu 24.04 with sudo access. A reliable dedicated server gives you the stability K3s and ACME challenges need.
  • A domain with API-manageable DNS. If you need one, register a domain with DNS you can control via API.
  • Ports 80 and 443 are open on your firewall.

Step 1: Create a Working Directory for All Manifests

Everything you create in this guide, such as YAML files and Helm values, should live in one folder so nothing gets lost. On your server, run:

Bash
mkdir -p ~/k3s-manifestscd ~/k3s-manifests

Step 2: Install K3s

Install K3s with a single command:

Bash
curl -sfL https://get.k3s.io | sh -

Export the kubeconfig so kubectl works without the k3s prefix:

Bash
mkdir -p ~/.kubesudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/configsudo chown $(id -u):$(id -g) ~/.kube/configexport KUBECONFIG=~/.kube/configkubectl get nodes

Add this line to ~/.bashrc so KUBECONFIG persists across reboots:

Bash
echo 'export KUBECONFIG=~/.kube/config' >> ~/.bashrc

Step 3: Prepare K3s for cert-manager

K3s bundles Traefik v3 as its default ingress controller, and Traefik v3 already supports Gateway API v1.4; it just needs to be switched on.

Enable Gateway API in Traefik

Create the HelmChartConfig file directly in the K3s manifests folder. This is a K3s-specific system path, not your ~/k3s-manifests working folder:

Bash
sudo mkdir -p /var/lib/rancher/k3s/server/manifestssudo nano /var/lib/rancher/k3s/server/manifests/traefik-config.yaml

Paste this content into the file:

YAML
apiVersion: helm.cattle.io/v1kind: HelmChartConfigmetadata:  name: traefik  namespace: kube-systemspec:  valuesContent: |-    providers:      kubernetesGateway:        enabled: true

K3s picks this up automatically within a minute; restart K3s if you want it immediately:

Bash
sudo systemctl restart k3s

Install the Gateway API CRDs

K3s's bundled Traefik already installs some Gateway API CRDs via Helm, so a plain kubectl apply will hit field-ownership conflicts. Use --force-conflicts to take ownership cleanly:

Bash
kubectl apply --server-side --force-conflicts -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/standard-install.yamlkubectl get crd | grep gateway

You must see these are listed in your output:

Bash
gatewayclasses.gateway.networking.k8s.iogateways.gateway.networking.k8s.io httproutes.gateway.networking.k8s.io

Step 4: Install cert-manager 1.21 via Helm

This is the core of any cert-manager on K3s with Let's Encrypt and DNS-01 setup. cert-manager provides the CRDs, including Certificate, Issuer, and ClusterIssuer, and controllers that talk to Let's Encrypt on your behalf.

If Helm isn't installed, use the commands below:

Bash
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3chmod +x get_helm.sh./get_helm.shhelm version

Now install cert-manager 1.21.1 with Gateway API support enabled:

Bash
kubectl create namespace cert-manager helm install cert-manager oci://quay.io/jetstack/charts/cert-manager \  --version v1.21.1 \  --namespace cert-manager \  --create-namespace \  --set crds.enabled=true \  --set config.apiVersion="controller.config.cert-manager.io/v1alpha1" \  --set config.kind="ControllerConfiguration" \  --set config.enableGatewayAPI=true

Confirm every pod comes up:

Bash
kubectl get pods -n cert-manager

You should see cert-manager, cert-manager-cainjector, and cert-manager-webhook all in Running state within about 30 seconds.

Step 5: Store DNS Credentials Securely

Never hardcode API tokens in a ClusterIssuer manifest or commit them to Git. You must create a scoped Cloudflare API Token first. From the Cloudflare dashboard > My Profile > API Tokens > Create Token, use the Edit zone DNS template, scoped to your specific zone only.

Copy the token, then create the secret directly on the command line:

Bash
kubectl create secret generic cloudflare-api-token-secret \  --namespace cert-manager \  --from-literal=api-token=<paste-your-real-token-here>
If you're working in a team, encrypt this secret before it ever goes into a Git repo, using a tool like Sealed Secrets or SOPS. Also use RBAC to limit who can run kubectl get secret in the cert-manager namespace.

Step 6: Create the ClusterIssuers

From the ~/k3s-manifests directory, you must create the DNS-01 issuer file:

Bash
nano letsencrypt-dns.yaml

Add:

YAML
apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata:  name: letsencrypt-dnsspec:  acme:    server: https://acme-v02.api.letsencrypt.org/directory    email: you@yourdomain.com    privateKeySecretRef:      name: letsencrypt-dns-key    solvers:    - dns01:        cloudflare:          apiTokenSecretRef:            name: cloudflare-api-token-secret            key: api-token      selector:        dnsZones:        - "yourdomain.com"

Save and exit, then create the HTTP-01 issuer file:

Bash
nano letsencrypt-http.yaml

Add:

YAML
apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata:  name: letsencrypt-httpspec:  acme:    server: https://acme-v02.api.letsencrypt.org/directory    email: you@yourdomain.com    privateKeySecretRef:      name: letsencrypt-http-key    solvers:    - http01:        gatewayHTTPRoute:          parentRefs:          - name: cert-manager-gateway            namespace: default            kind: Gateway

Apply both with the commands below:

Bash
kubectl apply -f letsencrypt-dns.yamlkubectl apply -f letsencrypt-http.yamlkubectl get clusterissuer

Step 7: Request the Wildcard Certificate

At this point, you must define a Certificate resource that requests both the root domain and the wildcard subdomain.

Create the file with:

Bash
nano wildcard-certificate.yaml

Add:

YAML
apiVersion: cert-manager.io/v1kind: Certificatemetadata:  name: wildcard-yourdomain-com  namespace: defaultspec:  secretName: wildcard-yourdomain-com-tls  issuerRef:    name: letsencrypt-dns    kind: ClusterIssuer  dnsNames:  - "yourdomain.com"  - "*.yourdomain.com"

Apply the Certificate resource and check the status and details of the certificate:

Bash
kubectl apply -f wildcard-certificate.yamlkubectl describe certificate wildcard-yourdomain-com -n default

Step 8: Attach the Certificate to Gateway API

Gateway API doesn't use an Ingress annotation. Instead, it points straight to the TLS secret on a Gateway listener. This is the main upgrade that makes a modern cert-manager on K3s with Let's Encrypt and DNS-01 setup different from a basic Ingress-only setup.

Create the gateway file:

Bash
nano gateway.yaml

Add:

Bash
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:  name: cert-manager-gateway  namespace: defaultspec:  gatewayClassName: traefik  listeners:  - name: https    protocol: HTTPS    port: 443    hostname: "*.yourdomain.com"    tls:      mode: Terminate      certificateRefs:      - kind: Secret        name: wildcard-yourdomain-com-tls    allowedRoutes:      namespaces:        from: All

Save and exit, then create the HTTP route file:

Bash
nano httproute.yaml

Add:

YAML
apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata:  name: app-route  namespace: defaultspec:  parentRefs:  - name: cert-manager-gateway  hostnames:  - "app.yourdomain.com"  rules:  - backendRefs:    - name: app-service      port: 80

Apply both files:

Bash
kubectl apply -f gateway.yamlkubectl apply -f httproute.yamlkubectl get gateway,httproute -n default

Step 9: Verify Issuance and Renewal

Check that the certificate is Ready:

Bash
kubectl get certificate -n defaultkubectl describe secret wildcard-yourdomain-com-tls -n default

Check the certificate details directly instead of just trusting the Kubernetes status:

Bash
kubectl get secret wildcard-yourdomain-com-tls -n default -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -dates -subject -ext subjectAltName

Let's Encrypt certificates last 90 days. Cert-manager renews them automatically before they expire, usually once about two-thirds of that time has passed. You can also run a renewal manually to check that the automation actually works:

Bash
kubectl annotate certificate wildcard-yourdomain-com -n default cert-manager.io/issue-temporary-certificate="true" --overwritekubectl delete secret wildcard-yourdomain-com-tls -n defaultkubectl get certificate wildcard-yourdomain-com -n default -w

If you delete the secret, cert-manager will immediately request a new certificate. If a fresh one shows up within a minute or two, that confirms automatic renewal is working correctly in your cert-manager on K3s with Let's Encrypt and DNS-01 setup.

Conclusion

At this point, you have a cert-manager on K3s pipeline using Let's Encrypt and DNS-01. It issues both regular and wildcard certificates, handles TLS through a Gateway API listener instead of an old-style Ingress, and renews certificates automatically with no manual work needed.

With cert-manager 1.21.1, Traefik's Gateway API support in K3s, and a safely stored DNS token, this setup can scale from one app to a whole wildcard-covered domain.

We hope you enjoy this guide.

For deeper information on the Gateway API integration, you can check the official cert-manager Gateway API documentation.

HTTP-01 proves domain ownership by serving a file over HTTP; DNS-01 proves it by creating a TXT record. Only DNS-01 supports wildcard certificates.

Ingress still works fine for single-host certificates. Gateway API is the newer, more flexible standard, especially useful for wildcard listeners and multi-namespace routing.

Cert-manager 1.21 officially supports Kubernetes 1.33 through 1.36; older clusters should use cert-manager 1.20 or earlier instead.

Cloudflare, Route53, Google Cloud DNS, Azure DNS, DigitalOcean, RFC2136, and several others are natively supported.