DNS webhook solver for cert-manager using etcd as DNS backend. This webhook allows cert-manager to create DNS TXT records in etcd for ACME DNS-01 validation.
- ✅ DNS-01 validation support for Let's Encrypt (and other ACME CAs)
- ✅ DNS records storage in etcd using SkyDNS/CoreDNS format
- ✅ Compatible with CoreDNS using the etcd plugin
- ✅ Deployment via Helm or Kubernetes manifests
- ✅ etcd authentication support
- ✅ TLS/mTLS support for secure etcd connections
- ✅ Wildcard certificates supported
- Kubernetes 1.20+
- cert-manager 1.0+
- etcd cluster accessible from Kubernetes
- CoreDNS (or other DNS server) configured with etcd backend
┌─────────────────┐ ┌──────────────────────┐ ┌─────────┐
│ cert-manager │────▶│ webhook-etcd │────▶│ etcd │
│ │ │ (this project) │ │ │
└─────────────────┘ └──────────────────────┘ └────┬────┘
│
┌──────────────────────┐ │
│ CoreDNS │◀─────────┘
│ (etcd plugin) │
└──────────────────────┘
# Install the Helm chart
helm install cert-manager-webhook-etcd \
./charts/cert-manager-webhook-etcd \
--namespace cert-manager \
--set groupName=acme.example.com# Apply the manifests
kubectl apply -f deploy/rbac.yaml
kubectl apply -f deploy/deployment.yaml
kubectl apply -f deploy/apiservice.yaml
⚠️ Important: Make sure thegroupNamein your Helm values/deployment matches exactly thegroupNamein your ClusterIssuer. This is a common source of configuration errors.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- dns01:
webhook:
groupName: acme.example.com # Must match GROUP_NAME
solverName: etcd
config:
endpoints:
- "http://etcd-0.etcd:2379"
- "http://etcd-1.etcd:2379"
- "http://etcd-2.etcd:2379"
prefix: "/skydns"
dialTimeout: 10
# Optional: authentication
# username: "root"
# password: "password"apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: my-certificate
namespace: default
spec:
secretName: my-tls-secret
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- example.com
- "*.example.com" # Wildcard supported| Parameter | Description | Needed | Default |
|---|---|---|---|
endpoints |
List of etcd endpoints | yes | |
prefix |
Prefix for DNS records | yes | /skydns |
username |
etcd username (inline, use credentialsSecretRef for production) | no | - |
password |
etcd password (inline, use credentialsSecretRef for production) | no | - |
credentialsSecretRef |
Name of Kubernetes secret containing etcd credentials | no | - |
credentialsSecretNamespace |
Namespace of the credentials secret | no | challenge namespace |
dialTimeout |
Connection timeout (seconds) | no | 10 |
tlsSecretRef |
Name of Kubernetes secret containing TLS certs | no | - |
tlsSecretNamespace |
Namespace of the TLS secret | no | challenge namespace |
tlsCAKey |
Key name for CA certificate in the secret | no | ca.crt |
tlsCertKey |
Key name for client certificate in the secret | no | tls.crt |
tlsKeyKey |
Key name for client private key in the secret | no | tls.key |
tlsServerName |
Server name for TLS verification (when connecting via IP) | no | - |
tlsInsecureSkipVerify |
Skip TLS verification (not recommended) | no | false |
For production environments, it's recommended to store etcd credentials in a Kubernetes secret instead of inline in the Issuer configuration:
apiVersion: v1
kind: Secret
metadata:
name: etcd-credentials
namespace: cert-manager
type: Opaque
data:
# echo -n 'root' | base64
username: cm9vdA==
# echo -n 'your-password' | base64
password: eW91ci1wYXNzd29yZA==Then reference it in your Issuer:
config:
endpoints:
- "http://etcd:2379"
credentialsSecretRef: "etcd-credentials"
credentialsSecretNamespace: "cert-manager" # Optional, defaults to challenge namespaceTo connect to a TLS-secured etcd cluster, create a Kubernetes secret containing the certificates:
apiVersion: v1
kind: Secret
metadata:
name: etcd-tls-certs
namespace: cert-manager
type: Opaque
data:
# CA certificate to verify the etcd server
ca.crt: <base64-encoded-ca-cert>
# Client certificate (optional, for mTLS)
tls.crt: <base64-encoded-client-cert>
# Client private key (optional, for mTLS)
tls.key: <base64-encoded-client-key>Then reference it in your Issuer:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- dns01:
webhook:
groupName: acme.example.com
solverName: etcd
config:
endpoints:
- "https://etcd-0.etcd:2379"
- "https://etcd-1.etcd:2379"
- "https://etcd-2.etcd:2379"
prefix: "/skydns"
tlsSecretRef: "etcd-tls-certs"
tlsSecretNamespace: "cert-manager"- CA only: Provide only
ca.crtto verify the etcd server - mTLS (mutual TLS): Provide
ca.crt,tls.crtandtls.keyfor client authentication - Insecure: Use
tlsInsecureSkipVerify: true(not recommended for production)
DNS records are stored in etcd using the SkyDNS format, compatible with CoreDNS:
/skydns/com/example/_acme-challenge
The content is JSON:
{
"text": "challenge-token-value",
"ttl": 60
}To verify that the webhook is working:
# Check the pod
kubectl get pods -n cert-manager -l app.kubernetes.io/name=cert-manager-webhook-etcd
# Check the logs
kubectl logs -n cert-manager -l app.kubernetes.io/name=cert-manager-webhook-etcd
# Check the APIService
kubectl get apiservice v1alpha1.acme.example.com# Download dependencies
make deps
# Build
make build
# Tests
make test# Build the image
make docker-build
# Push to a registry
REGISTRY=ghcr.io/your-org make docker-pushgroupName must match exactly in:
- Helm values (or deployment ENV)
- ClusterIssuer
webhook.groupNamefield
Common error: "failed to load config: config is required" - See TROUBLESHOOTING.md for detailed debugging steps.
# Verify groupName matches everywhere
kubectl get deployment cert-manager-webhook-etcd -n cert-manager -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="GROUP_NAME")].value}'
kubectl get clusterissuer letsencrypt-prod -o yaml | grep groupName
# Check APIService status
kubectl get apiservice | grep acme
# Check webhook logs
kubectl logs -n cert-manager -l app.kubernetes.io/name=cert-manager-webhook-etcdFor detailed troubleshooting, see TROUBLESHOOTING.md.
Apache License 2.0
Contributions are welcome! Feel free to open an issue or a pull request.