Kubernetes SSL Certificate Automation using Certmanager - Part 2

Published on 1 May 2026 07:49 AM
This post thumbnail

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:

  1. Host must be mapped to LoadBalancer before issuance — the ACME solver verifies by hitting your domain on port 80/443
  2. Certificates are host-specific — a cert for app.example.com can't be reused for api.example.com
  3. No wildcard certificates — HTTP01 has no way to verify ownership of *.example.com
  4. No root domain certificates — can't issue a cert for example.com directly
  5. 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:

  1. Asks Let's Encrypt for a challenge token
  2. Creates a _acme-challenge.example.com TXT record in your DNS with that token
  3. Let's Encrypt queries the DNS record to verify
  4. If verified, the certificate is issued
  5. 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.com and *.example.com
  • selector.dnsZones — tells cert-manager to use DNS-01 only for these zones
  • region — 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 secret
  • dnsNames — we're requesting both example.com (root) and *.example.com (wildcard) in a single certificate
  • namespace: 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-issuer annotations
  • 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:

FeaturePart 1 (HTTP01)Part 2 (DNS-01)
Wildcard certificates*.example.com
Root domain certificateexample.com
NodePort support
Per-ingress annotationRequiredNot needed
DNS provider dependencyNoneRoute53 (or other)
Setup complexityLowMedium (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.