Categories
Uncategorized

Deploying cert-manager for k3s using DNS-based ACME

Beware! This is still work in progress – I’ll need to re-run all steps on a clean install to verify everything really works as expected!

I also assume, that you’re familiar with DNS-based ACME and that the required infrastructure is already in place.

Installation of cert-manager

Based on this description.

linux # kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.20.1/cert-manager.yaml
namespace/cert-manager created
customresourcedefinition.apiextensions.k8s.io/challenges.acme.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/orders.acme.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/certificaterequests.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/certificates.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/clusterissuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/issuers.cert-manager.io created
serviceaccount/cert-manager-cainjector created
serviceaccount/cert-manager created
serviceaccount/cert-manager-webhook created
clusterrole.rbac.authorization.k8s.io/cert-manager-cainjector created
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-issuers created
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-clusterissuers created
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-certificates created
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-orders created
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-challenges created
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-ingress-shim created
clusterrole.rbac.authorization.k8s.io/cert-manager-cluster-view created
clusterrole.rbac.authorization.k8s.io/cert-manager-view created
clusterrole.rbac.authorization.k8s.io/cert-manager-edit created
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-approve:cert-manager-io created
clusterrole.rbac.authorization.k8s.io/cert-manager-controller-certificatesigningrequests created
clusterrole.rbac.authorization.k8s.io/cert-manager-webhook:subjectaccessreviews created
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-cainjector created
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-issuers created
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-clusterissuers created
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-certificates created
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-orders created
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-challenges created
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-ingress-shim created
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-approve:cert-manager-io created
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-certificatesigningrequests created
clusterrolebinding.rbac.authorization.k8s.io/cert-manager-webhook:subjectaccessreviews created
role.rbac.authorization.k8s.io/cert-manager-cainjector:leaderelection created
role.rbac.authorization.k8s.io/cert-manager:leaderelection created
role.rbac.authorization.k8s.io/cert-manager-tokenrequest created
role.rbac.authorization.k8s.io/cert-manager-webhook:dynamic-serving created
rolebinding.rbac.authorization.k8s.io/cert-manager-cainjector:leaderelection created
rolebinding.rbac.authorization.k8s.io/cert-manager:leaderelection created
rolebinding.rbac.authorization.k8s.io/cert-manager-tokenrequest created
rolebinding.rbac.authorization.k8s.io/cert-manager-webhook:dynamic-serving created
service/cert-manager-cainjector created
service/cert-manager created
service/cert-manager-webhook created
deployment.apps/cert-manager-cainjector created
deployment.apps/cert-manager created
deployment.apps/cert-manager-webhook created
mutatingwebhookconfiguration.admissionregistration.k8s.io/cert-manager-webhook created
validatingwebhookconfiguration.admissionregistration.k8s.io/cert-manager-webhook created

Check installation:

linux # kubectl get pods --namespace cert-manager
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-684886b7df-bdjgd              1/1     Running   0          110s
cert-manager-cainjector-7b86575979-k52tk   1/1     Running   0          110s
cert-manager-webhook-7d8878ddb-z5ns2       1/1     Running   0          110s
linux # kubectl get services --namespace cert-manager
NAME                      TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)            AGE
cert-manager              ClusterIP   10.43.61.185    <none>        9402/TCP           66s
cert-manager-cainjector   ClusterIP   10.43.175.196   <none>        9402/TCP           66s
cert-manager-webhook      ClusterIP   10.43.158.172   <none>        443/TCP,9402/TCP   66s

Creating first ACME config (using DNS-01/RFC2136 based protocol):

linux # cat letsencrypt-prod-rfc2136.yml 
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-rfc2136-prod-issuer
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-rfc2136-prod-issuer

    solvers:
    - dns01:
        rfc2136:
          nameserver: 192.168.1.1
          tsigKeyName: update-external.
          tsigAlgorithm: HMACSHA256
          tsigSecretSecretRef:
            name: letsencrypt-rfc2136-update-external
            key: letsencrypt-rfc2136-update-external-key

Applying it:

linux # kubectl apply -f letsencrypt-prod-rfc2136.yml 
clusterissuer.cert-manager.io/letsencrypt-rfc2136-prod-issuer created

Adding secret (make sure the namespace is “cert-manager“):

linux # kubectl -n cert-manager create secret generic letsencrypt-rfc2136-update-external --from-literal=letsencrypt-rfc2136-update-external-key=RandomKeyHere
secret/letsencrypt-rfc2136-update-external created

Now get a certificate for a specific service (in my case “whoami.mydomain.de“):

linux # cat whoami-certificate.yaml 
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: whoami-mydomain
  namespace: default
spec:
  secretName: whoami-tls
  issuerRef:
    name: letsencrypt-rfc2136-prod-issuer
  dnsNames:
  - whoami.mydomain.de

linux # kubectl apply -f whoami-certificate.yaml 
certificate.cert-manager.io/whoami-mydomain created

linux # kubectl describe certificate whoami-mydomain
Name:         whoami-mydomain
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  cert-manager.io/v1
Kind:         Certificate
Metadata:
  Creation Timestamp:  2026-04-07T15:07:51Z
  Generation:          1
  Resource Version:    35518
  UID:                 84878eb6-2b24-4317-8021-46c3115b04ea
Spec:
  Dns Names:
    whoami.mydomain.de
  Issuer Ref:
    Name:       letsencrypt-rfc2136-prod-issuer
  Secret Name:  whoami-tls
Status:
  Conditions:
    Last Transition Time:  2026-04-07T15:07:51Z
    Message:               Issuing certificate as Secret does not exist
    Observed Generation:   1
    Reason:                DoesNotExist
    Status:                False
    Type:                  Ready
Events:                    <none>

FIXME: Add intermediate steps required to setup a fresh deployment/pod.

Connect it to (pre-existing) pod using ingress controller:

linux # cat whoami-ingress-tls.yaml 
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: whoami-tls-ingress
  annotations:
    spec.ingressClassName: traefik
    cert-manager.io/cluster-issuer: letsencrypt-rfc2136-prod-issuer
spec:
  rules:
    - host: whoami.mydomain.de
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: whoami
                port:
                  number: 5678
  tls:
    - secretName: whoami-tls
      hosts:
        - whoami.mydomain.de

linux # kubectl apply -f whoami-ingress-tls.yaml 
ingress.networking.k8s.io/whoami-tls-ingress created

Cert-manager troubleshooting guide

Detailed instructions can be found here.

What helped me most was to check the different states of certificate creation:

  • certificates
  • certificaterequests
  • orders
  • challenges
linux # kubectl get certificates
linux # kubectl get certificaterequests
linux # kubectl get orders
linux # kubectl get challenges

If you require more details use “describe” instead of “get” (maybe also add the specific entry you want described):

linux # kubectl describe orders
<...>

Or take a look at the logs of cert-manager itself:

linux # kubectl logs -n cert-manager -lapp.kubernetes.io/name=cert-manager -f

Special case: split DNS

In case of split DNS (or other special DNS configurations) you may want to force using specific DNS servers for ACME.

You can edit the cert-manager options (--dns01-recursive-nameservers-only and --dns01-recursive-nameservers) by looking for spec/template/spec/containers/args:

linux # kubectl edit deployment cert-manager -n cert-manager
<...>
spec:
<...>
  template:
<...>
    spec:
      containers:
        args:
        - --v=2
        - --cluster-resource-namespace=$(POD_NAMESPACE)
        - --leader-election-namespace=kube-system
        - --acme-http01-solver-image=quay.io/jetstack/cert-manager-acmesolver:v1.20.1
        - --max-concurrent-challenges=60
        - --dns01-recursive-nameservers-only
        - --dns01-recursive-nameservers=8.8.8.8:53,1.1.1.1:53
<...>

Install kubectl extension

See here:

linux # curl -L -o kubectl-cert-manager.tar.gz https://github.com/cert-manager/cert-manager/releases/download/v1.2.0/kubectl-cert_manager-linux-amd64.tar.gz
linux # tar xzf kubectl-cert-manager.tar.gz
linux # sudo mv kubectl-cert_manager /usr/local/bin

Run it like:

linux # kubectl cert-manager renew --all
Manually triggered issuance of Certificate default/whoami-tls

Leave a Reply

Your email address will not be published. Required fields are marked *