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
kubectlconfigured to access your cluster- Helm v3.16 or later (specifically we need the
toYamlPrettyattribute 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 updateNow 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=trueWait for all cert-manager pods to be ready before proceeding:
kubectl get pods -n cert-managerYou 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 13sStep 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.
Step 2.1 - Option A: Install from Helm Repository (Recommended)
helm repo add hcloud https://charts.hetzner.cloud
helm repo update
helm install cert-manager-webhook-hetzner hcloud/cert-manager-webhook-hetzner \
--namespace cert-managerStep 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-managerStep 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.yamlSecurity 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-tokenThe 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.yamlStep 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-tokenStaging 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.yamlcert-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 -AA 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:
- Official Hetzner webhook repository
- Hetzner DNS Migration Documentation
- cert-manager Documentation
- Hetzner Cloud API Reference