Get Rewarded! We will reward you with up to €50 credit on your account for every tutorial that you write and we publish!

Automatic SSL Certificates with cert-manager and Hetzner Console DNS

profile picture
Author
Björn Ternes
Published
2026-02-09
Time to read
6 minutes reading time

Introduction

In this tutorial, you will learn how to automatically obtain and renew SSL/TLS certificates from Let's Encrypt using cert-manager in Kubernetes. We will use the DNS-01 challenge method with the official Hetzner Cloud DNS webhook.

The DNS-01 challenge is useful when you need certificates and for example your servers are not publicly accessible via HTTP. Instead of proving domain ownership through HTTP, cert-manager will create temporary DNS TXT records to verify that you control the domain.

By the end of this tutorial, you will have an automated certificate management system that handles issuance and renewal without manual intervention.

Prerequisites

  • A running Kubernetes cluster
  • kubectl configured to access your cluster
  • Helm v3.16 or later (specifically we need the toYamlPretty attribute support)
  • A Hetzner Console account with DNS zones managed in the Hetzner Console
  • A Hetzner Cloud API token with Read & Write permissions - I recommend separating all your zones in different projects, as an API key represents Read & Write for the project. Key word here: Least Privilege.

Important: Hetzner has two DNS APIs. The old DNS Console API (dns.hetzner.com) will be discontinued in May 2026. This tutorial uses the new Cloud API (api.hetzner.cloud). Make sure your DNS zones are managed in the Hetzner Console, not the old DNS Console.

Step 1 - Install cert-manager

cert-manager is a Kubernetes add-on that automates the management and issuance of TLS certificates. First, add the Jetstack Helm repository and install cert-manager:

helm repo add jetstack https://charts.jetstack.io
helm repo update

Now install cert-manager with the Custom Resource Definitions (CRDs) enabled:

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true

Wait for all cert-manager pods to be ready before proceeding:

kubectl get pods -n cert-manager

You should see three pods running: cert-manager, cert-manager-cainjector, and cert-manager-webhook.

NAME                                            READY   STATUS    RESTARTS       AGE
cert-manager-7ff7f97d55-p6scf                   1/1     Running   0              13s
cert-manager-cainjector-59bb669f8d-zddqq        1/1     Running   0              13s
cert-manager-webhook-59bbd786df-qjq8t           1/1     Running   0              13s

Step 2 - Install the Hetzner Cloud DNS Webhook

The webhook extends cert-manager to solve DNS-01 challenges using the Hetzner Cloud DNS API. You can install it from the official Helm repository or directly from the Git repository.

helm repo add hcloud https://charts.hetzner.cloud
helm repo update

helm install cert-manager-webhook-hetzner hcloud/cert-manager-webhook-hetzner \
  --namespace cert-manager

Step 2.2 - Option B: Install from Git Repository

If you prefer to install from source or need to modify the chart:

git clone https://github.com/hetzner/cert-manager-webhook-hetzner.git
cd cert-manager-webhook-hetzner

helm install cert-manager-webhook-hetzner ./chart \
  --namespace cert-manager

Step 3 - Create the API Token Secret

The webhook needs your Hetzner Cloud API token to create DNS records. Store it as a Kubernetes Secret:

Create a file named hetzner-secret.yaml:

apiVersion: v1
kind: Secret
metadata:
  name: hetzner-secret
  namespace: cert-manager
type: Opaque
stringData:
  api-token: "<your-hetzner-cloud-api-token>"

Replace <your-hetzner-cloud-api-token> with your actual token, then apply it:

kubectl apply -f hetzner-secret.yaml

Security tip: After applying, delete the YAML file as it contains sensitive credentials.

Step 4 - Create a ClusterIssuer

A ClusterIssuer is a cluster-wide resource that defines how certificates should be obtained. Create a file named clusterissuer-letsencrypt.yaml:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: mail@example.com
    privateKeySecretRef:
      name: letsencrypt-account-key
    solvers:
      - dns01:
          webhook:
            groupName: acme.hetzner.com
            solverName: hetzner
            config:
              tokenSecretKeyRef:
                name: hetzner-secret
                key: api-token

The cert-manager still expects spec.acme.email for an ACME Issuer/ClusterIssuer. Let's Encrypt stopped the expiration notifications service on June 4, 2025. See also https://letsencrypt.org/2025/01/22/ending-expiration-emails.

Apply the ClusterIssuer:

kubectl apply -f clusterissuer-letsencrypt.yaml

Step 4.1 - Create a Staging ClusterIssuer (Optional)

For testing, use Let's Encrypt's staging environment to avoid rate limits. In clusterissuer-letsencrypt.yaml, simply edit the values of:

Production value Testing value
metadata.name letsencrypt letsencrypt-staging
spec.acme.server https://acme-v02.api.letsencrypt.org/directory https://acme-staging-v02.api.letsencrypt.org/directory
spec.acme.privateKeySecretRef.name letsencrypt-account-key letsencrypt-staging-account-key
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: mail@example.com
    privateKeySecretRef:
      name: letsencrypt-staging-account-key
    solvers:
      - dns01:
          webhook:
            groupName: acme.hetzner.com
            solverName: hetzner
            config:
              tokenSecretKeyRef:
                name: hetzner-secret
                key: api-token

Staging certificates are not trusted by browsers but allow you to test the entire flow without hitting production rate limits.

Step 5 - Request a Certificate

Now you can request certificates. Create a file named certificate.yaml:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com-tls
  namespace: default
spec:
  secretName: example-com-tls
  issuerRef:
    name: letsencrypt # or for staging letsencrypt-staging
    kind: ClusterIssuer
  dnsNames:
    - <example.com>
    - "*.<example.com>"

Replace <example.com> with your actual domain. The wildcard entry (*.<example.com>) is optional but demonstrates one of the key benefits of DNS-01 challenges.

Apply the certificate request:

kubectl apply -f certificate.yaml

cert-manager will now create a DNS TXT record, wait for Let's Encrypt to verify it, obtain the certificate, and store it in the specified Secret.

It is also common to use an annotation on your ingress objects: cert-manager.io/cluster-issuer: letsencrypt

Step 6 - Verify

Check the status of your resources:

# Check ClusterIssuer status
kubectl get clusterissuer

# Check Certificate status
kubectl get certificates -A

# Check active challenges (should be empty when complete)
kubectl get challenges -A

A successfully issued certificate shows Ready: True (kubectl get certificates -A).

Conclusion

You have successfully set up automatic SSL/TLS certificate management using cert-manager with the Hetzner Cloud DNS webhook. Your certificates will now be automatically renewed before expiration.

Next steps:

  • Configure your Ingress resources to use the certificates
  • Set up monitoring and alerting for certificate expiration
  • Have fun with your secure TLS connections

Useful links:

License: MIT
Want to contribute?

Get Rewarded: Get up to €50 in credit! Be a part of the community and contribute. Do it for the money. Do it for the bragging rights. And do it to teach others!

Report Issue
Try Hetzner Cloud

Get €20/$20 free credit!

Valid until: 31 December 2026 Valid for: 3 months and only for new customers
Get started
Want to contribute?

Get Rewarded: Get up to €50 credit on your account for every tutorial you write and we publish!

Find out more