Kubernetes SSL Certificate Automation using Certmanager - Part 2

In the previous part, we set up cert-manager on a Kubernetes cluster and issued SSL certificates using the HTTP01 challenge method. It works great for individual subdomains, but it comes with some limitations — you can't issue wildcard certificates, it doesn't work with NodePort ingress controllers, and you can't issue certificates for root domains like example.com.
In this part, we'll overcome all of these limitations using the DNS-01 challenge method. We'll issue a wildcard certificate (*.example.com) along with a certificate for the root domain, and directly integrate it with the ingress controller so that every ingress automatically gets HTTPS — no annotations needed.
Limitations of HTTP01 Challenge (Part 1 Recap)
Just to recap why we need a different approach:
- Host must be mapped to LoadBalancer before issuance — the ACME solver verifies by hitting your domain on port 80/443
- Certificates are host-specific — a cert for
app.example.comcan't be reused forapi.example.com - No wildcard certificates — HTTP01 has no way to verify ownership of
*.example.com - No root domain certificates — can't issue a cert for
example.comdirectly - Doesn't work with NodePort — ACME only verifies on ports 80 and 443, so NodePort-based ingress controllers are out
The DNS-01 challenge solves all of this. Instead of verifying domain ownership by serving a file over HTTP, it verifies by creating a DNS TXT record in your domain's DNS zone. If you can write a DNS record, you prove you own the domain — and that works for wildcards and root domains too.
How DNS-01 Challenge Works
When cert-manager needs to verify domain ownership, it:
- Asks Let's Encrypt for a challenge token
- Creates a
_acme-challenge.example.comTXT record in your DNS with that token - Let's Encrypt queries the DNS record to verify
- If verified, the certificate is issued
- The TXT record is cleaned up automatically
For this to work, cert-manager needs write access to your DNS provider. We'll use AWS Route53 for this, which means we need to create an IAM user with limited Route53 permissions.
Prerequisites
- Kubernetes cluster with cert-manager already installed (covered in Part 1)
- Domain hosted on AWS Route53
- AWS CLI configured with sufficient IAM permissions to create users and policies
Step 1: Create IAM Policy for Route53 Access
We'll create a policy that only allows the actions cert-manager needs — creating, reading, and deleting DNS records, plus checking change status.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "route53:GetChange",
"Resource": "arn:aws:route53:::change/*"
},
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets"
],
"Resource": "arn:aws:route53:::hostedzone/*"
},
{
"Effect": "Allow",
"Action": "route53:ListHostedZonesByName",
"Resource": "*"
}
]
}
Save this as certmanager-route53.json and create the policy:
aws iam create-policy \
--policy-name certmanager-route53 \
--policy-document file://certmanager-route53.json
Note the Policy ARN from the output — you'll need it in the next step.
Step 2: Create IAM User and Attach Policy
# Create the user
aws iam create-user --user-name certmanager
# Attach the policy (replace with your policy ARN)
aws iam attach-user-policy \
--policy-arn arn:aws:iam::123456789012:policy/certmanager-route53 \
--user-name certmanager
Now generate access keys for this user:
aws iam create-access-key --user-name certmanager
This returns an AccessKeyId and SecretAccessKey. Store the secret key securely — you'll use it in the next step.
Step 3: Store AWS Credentials as a Kubernetes Secret
Cert-manager needs the AWS credentials to create DNS records during verification. We'll store them as a Kubernetes secret in the cert-manager namespace.
# Save the secret key to a file
echo "YOUR_AWS_SECRET_ACCESS_KEY" > aws-secret-key.txt
# Create the Kubernetes secret
kubectl create secret generic aws-route53-creds \
--from-file=aws-secret-key.txt \
-n cert-manager
# Clean up
rm -f aws-secret-key.txt
Replace YOUR_AWS_SECRET_ACCESS_KEY with the actual secret from Step 2. The file name aws-secret-key.txt matters — we'll reference it by name in the ClusterIssuer.
Step 4: Update ClusterIssuer for DNS-01 Challenge
We'll modify the existing ClusterIssuer from Part 1 to add a DNS-01 solver alongside the HTTP01 solver. This way, cert-manager uses HTTP01 for regular subdomains and DNS-01 for wildcards and root domains.
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
email: user@example.com
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: example-issuer-account-key
solvers:
- http01:
ingress:
class: nginx
- dns01:
route53:
accessKeyID: YOUR_AWS_ACCESS_KEY_ID
region: us-east-1
secretAccessKeySecretRef:
name: aws-route53-creds
key: aws-secret-key.txt
selector:
dnsZones:
- example.com
- '*.example.com'
EOF
What changed:
- Kept the HTTP01 solver — it still works for individual subdomains
- Added DNS01 solver — Route53-based, for
example.comand*.example.com selector.dnsZones— tells cert-manager to use DNS-01 only for these zonesregion— use the region where your Route53 hosted zone lives
Replace YOUR_AWS_ACCESS_KEY_ID with the access key from Step 2 and example.com with your actual domain.
Step 5: Issue the Wildcard Certificate
Now comes the fun part. We'll create a Certificate resource that requests both the root domain and wildcard certificate in one go.
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-com-cert
namespace: cert-manager
spec:
secretName: example-com-tls
dnsNames:
- example.com
- '*.example.com'
issuerRef:
name: letsencrypt-staging
kind: ClusterIssuer
group: cert-manager.io
EOF
What's happening:
secretName— the TLS certificate and key will be stored in this secretdnsNames— we're requesting bothexample.com(root) and*.example.com(wildcard) in a single certificatenamespace: cert-manager— the certificate is created in cert-manager's namespace since it's a shared resource
Important: Before applying this, make sure there are no existing _acme-challenge DNS records in your Route53 hosted zone. Leftover records from previous attempts can cause verification to fail.
Step 6: Verify Certificate Issuance
The verification process takes about 5-7 minutes. Cert-manager will create a _acme-challenge TXT record in Route53, Let's Encrypt will verify it, and then issue the certificate.
Check the status:
kubectl describe certificate example-com-cert -n cert-manager | egrep "Message|Status|Type"
Expected output:
Status:
Message: Certificate is up to date and has not expired
Status: True
Type: Ready
If it's not ready, follow the debugging chain to find the issue:
# Check certificate request
kubectl describe certificaterequest -n cert-manager
# Check ACME order
kubectl describe order -n cert-manager
# Check challenge status
kubectl describe challenge -n cert-manager
The challenge resource will tell you exactly what's happening — whether it's waiting for DNS propagation, a permissions issue, or a mismatch in credentials.
Step 7: Configure Ingress Controller with the Wildcard Certificate
This is where the magic happens. Instead of adding TLS annotations to every ingress, we'll configure the ingress controller itself to use the wildcard certificate as the default SSL certificate.
For Helm-installed ingress controllers:
Add this to your values:
extraArgs:
default-ssl-certificate: "cert-manager/example-com-tls"
For directly managed deployments/daemonsets:
Add this argument to the container spec:
--default-ssl-certificate=cert-manager/example-com-tls
The format is namespace/secret-name. Since we created the certificate in the cert-manager namespace and the secret is named example-com-tls, it becomes cert-manager/example-com-tls.
What this does:
Once configured, the ingress controller automatically serves HTTPS for any ingress using the wildcard certificate. You don't need:
- TLS sections in ingress manifests
cert-manager.io/cluster-issuerannotations- Individual certificates per host
Any new service you expose through ingress gets HTTPS automatically. The wildcard covers all subdomains.
Using the Wildcard Certificate in Individual Ingresses
If you prefer not to set the default SSL certificate on the ingress controller, you can still reference the wildcard secret directly in individual ingress resources:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
namespace: my-namespace
spec:
ingressClassName: nginx
tls:
- hosts:
- myapp.example.com
secretName: example-com-tls
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-service
port:
number: 80
Important: Do NOT add the cert-manager.io/cluster-issuer annotation here. If you do, cert-manager will try to re-issue a certificate specifically for myapp.example.com and overwrite the wildcard certificate in the secret.
If the ingress is in a different namespace than cert-manager where the secret lives, you'll need to copy the secret over:
kubectl get secret example-com-tls -n cert-manager -o yaml \
| kubectl apply -n my-namespace -f -
Switching from Staging to Production
Throughout this guide we've been using letsencrypt-staging. Staging is great for testing — it has higher rate limits but browsers won't trust the certificates. Once everything is working, switch to the production server:
Just update the server field in your ClusterIssuer:
# Staging
server: https://acme-staging-v02.api.letsencrypt.org/directory
# Production
server: https://acme-v02.api.letsencrypt.org/directory
Delete the old certificate and re-issue — cert-manager will get a trusted certificate from the production server.
Summary
Here's what we achieved compared to Part 1:
| Feature | Part 1 (HTTP01) | Part 2 (DNS-01) |
|---|---|---|
| Wildcard certificates | ❌ | ✅ *.example.com |
| Root domain certificate | ❌ | ✅ example.com |
| NodePort support | ❌ | ✅ |
| Per-ingress annotation | Required | Not needed |
| DNS provider dependency | None | Route53 (or other) |
| Setup complexity | Low | Medium (IAM + DNS) |
The DNS-01 method requires a bit more setup with IAM users and DNS credentials, but the payoff is massive — one wildcard certificate that covers everything, automatic renewal by cert-manager, and no more per-ingress certificate management.
If you're running on AWS with Route53, this is the way to go. Set it up once, and you'll never think about SSL certificates again.
If you have any questions or run into issues, drop a comment below. Happy securing! 🔒
Enjoyed this post?
Get AI + DevOps insights delivered to your inbox. No spam, unsubscribe anytime.