DNS and TLS certificates are foundational infrastructure. Manual management doesn’t scale and creates security gaps. This guide covers DNS and certificate automation using modern tools and infrastructure-as-code patterns, with the latest developments through mid-2026.
Certificate Lifetime Landscape Is Changing Rapidly
The CA/Browser Forum ballot SC-088v3 passed in October 2025, establishing a phased reduction in TLS certificate validity. This timeline directly affects every automation strategy:
| Effective Date | Max Certificate Validity | Impact |
|---|---|---|
| Until Mar 15, 2026 | 398 days | Current baseline |
| Mar 15, 2026 – Mar 15, 2027 | 200 days | Renewals every ~6 months |
| Mar 15, 2027 – Mar 15, 2029 | 100 days | Renewals every ~3 months |
| Mar 15, 2029+ | 45 days | Renewals every ~6 weeks |
Shorter lifetimes make manual certificate management untenable. Automation is no longer optional — it is the only viable approach at scale.
DNS Architecture
DNS Record Types
| Type | Purpose | Example |
|---|---|---|
| A | IPv4 address | example.com → 1.2.3.4 |
| AAAA | IPv6 address | example.com → 2001:db8::1 |
| CNAME | Canonical name | www → @ |
| MX | Mail exchange | @ → mail.example.com |
| TXT | Text records | @ → "v=spf1 include:_spf.example.com ~all" |
| NS | Name servers | @ → ns1.example.com |
| SOA | Start of Authority | Administrative info |
| CAA | Certificate Authority | example.com → letsencrypt.org |
Route53 DNS Configuration
# Terraform - Route53 hosted zone and records
resource "aws_route53_zone" "main" {
name = "example.com"
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
resource "aws_route53_record" "api" {
zone_id = aws_route53_zone.main.zone_id
name = "api.example.com"
type = "A"
alias {
name = "alb-example-123456789.us-east-1.elb.amazonaws.com"
zone_id = "Z35SXDOWRQ4VI"
evaluate_target_health = true
}
}
resource "aws_route53_record" "www" {
zone_id = aws_route53_zone.main.zone_id
name = "www.example.com"
type = "CNAME"
ttl = 300
records = ["example.com"]
}
resource "aws_route53_record" "mx" {
zone_id = aws_route53_zone.main.zone_id
name = "example.com"
type = "MX"
ttl = 3600
records = [
"10 mail1.example.com",
"20 mail2.example.com"
]
}
resource "aws_route53_record" "spf" {
zone_id = aws_route53_zone.main.zone_id
name = "example.com"
type = "TXT"
ttl = 3600
records = ["v=spf1 include:_spf.example.com ~all"]
}
Cloudflare DNS
# Cloudflare Terraform provider
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
resource "cloudflare_record" "api" {
zone_id = cloudflare_zone.example.id
name = "api"
value = "1.2.3.4"
type = "A"
proxied = true
}
resource "cloudflare_record" "cdn" {
zone_id = cloudflare_zone.example.id
name = "cdn"
value = "cdn.example.com"
type = "CNAME"
proxied = true
}
Certificate Management with cert-manager
Installation
cert-manager is the most widely adopted Kubernetes certificate controller. As of May 2026, the latest stable release is v1.20.2 (released April 2026), with v1.21.0-alpha.0 already available for testing.
# Install cert-manager v1.20.2 (latest stable)
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.20.2/cert-manager.yaml
# Verify installation
kubectl get pods -n cert-manager
Supported releases as of this writing:
| Release | Release Date | End of Life | Kubernetes Versions |
|---|---|---|---|
| 1.20 | Mar 10, 2026 | Release of 1.22 | 1.32 → 1.35 |
| 1.19 | Oct 07, 2025 | Release of 1.21 | 1.31 → 1.35 |
| 1.18 | Jun 10, 2025 | EOL (Mar 10, 2026) | 1.29 → 1.33 |
Important: cert-manager v1.18 reached end of life. Users still on v1.14 (referenced in older documentation) should upgrade to v1.20 immediately — several CVEs have been patched between v1.14 and v1.20.
Breaking Changes in Recent Releases
cert-manager v1.18 (June 2025) introduced two breaking changes that affect existing deployments:
Private Key Rotation Policy now defaults to Always — previously Never. This means every certificate renewal now generates a new private key by default. To preserve the old behavior, set rotationPolicy: Never explicitly on Certificate resources:
spec:
privateKey:
rotationPolicy: Never
Revision History Limit now defaults to 1 — old CertificateRequest resources are automatically garbage collected. If you need to retain more history, set revisionHistoryLimit explicitly.
Let’s Encrypt 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]
solvers:
- dns01:
route53:
region: us-east-1
hostedZoneID: Z1234567890ABC
ACME Certificate Profiles
cert-manager v1.18 introduced support for ACME certificate profiles, extending the ACME protocol to request different certificate categories. Let’s Encrypt offers two profiles:
tlsserver— Standard server certificates (the default, suitable for most use cases)shortlived— Short-lived certificates valid for six days, for environments that prioritize rapid revocation
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-com
namespace: production
spec:
secretName: example-com-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- example.com
- www.example.com
- api.example.com
# ACME certificate profile
privateKey:
rotationPolicy: Always
# Use short-lived profile for rapid rotation
# omit for default tlsserver profile
# extraConfig:
# acme:
# profile: shortlived
Ingress with TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: production-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- example.com
- www.example.com
secretName: example-com-tls
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
DNS-01 Challenge
The DNS-01 challenge proves you control the domain by creating a TXT record at _acme-challenge.<domain>. cert-manager automates this through its DNS provider integrations.
# Manual DNS-01 challenge with Cloudflare
import cloudflare
def create_dns_challenge(domain, token):
"""Create DNS TXT record for Let's Encrypt challenge"""
client = cloudflare.Cloudflare(api_token=token)
zone_id = client.zones.get(params={'name': domain})[0]['id']
client.zones.dns_records.post(
zone_id,
data={
'type': 'TXT',
'name': f'_acme-challenge.{domain}',
'content': token,
'ttl': 60
}
)
import time
time.sleep(30)
return True
def cleanup_dns_challenge(domain, token, record_id):
"""Clean up DNS TXT record"""
client = cloudflare.Cloudflare(api_token=token)
zone_id = client.zones.get(params={'name': domain})[0]['id']
client.zones.dns_records.delete(zone_id, record_id)
CNAME Delegation for DNS-01 Security
Putting full DNS API credentials on every web server increases the blast radius if any server is compromised. A safer pattern is to delegate the _acme-challenge subdomain to a less-privileged zone:
- Create a separate DNS zone with limited scope (e.g.,
less-privileged.example.org) - Add a CNAME record in the primary zone pointing the challenge subdomain to the limited zone
- Grant cert-manager credentials only to the limited zone
# Primary zone CNAME delegation
_acme-challenge.example.com IN CNAME _acme-challenge.less-privileged.example.org.
# cert-manager ClusterIssuer with CNAME delegation
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-delegated
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
solvers:
- selector:
dnsZones:
- "example.com"
dns01:
cnameStrategy: Follow
route53:
region: us-east-1
hostedZoneID: ZDELEGATEDZONEID
This approach limits the credential scope to a single TXT record subdomain rather than the entire DNS zone.
Azure DNS Support
cert-manager v1.20 added support for Azure Private DNS zones, extending the DNS-01 provider list. The full set of supported DNS providers includes: ACMEDNS, Akamai, AzureDNS, CloudFlare, Google CloudDNS, Route53, DigitalOcean, and RFC2136 (generic DNS update protocol).
DNS-01 Recursive Nameservers
For environments with split-horizon DNS or multiple authoritative nameservers, cert-manager provides two flags for DNS01 self-check control:
# Helm values for cert-manager
extraArgs:
- --dns01-recursive-nameservers-only
- --dns01-recursive-nameservers=8.8.8.8:53,1.1.1.1:53
--dns01-recursive-nameservers-only forces cert-manager to skip direct authoritative server queries and rely solely on the specified recursive resolvers, which is useful when the pod cannot reach external authoritative servers directly.
Multiple DNS Providers
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: multi-dns-issuer
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
solvers:
- dns01:
route53:
region: us-east-1
hostedZoneID: Z1234567890ABC
selector:
dnsZones:
- "example.com"
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- "*.example.com"
DNS-PERSIST-01: The Future of DNS Validation
On February 18, 2026, Let’s Encrypt announced DNS-PERSIST-01, a new ACME challenge type that eliminates recurring DNS updates at renewal time. Instead of publishing a one-time token for each certificate issuance, you publish a standing TXT record that identifies both the CA and the specific ACME account authorized to issue for the domain.
How It Works
For the hostname example.com, the record lives at _validation-persist.example.com:
_validation-persist.example.com. IN TXT (
"letsencrypt.org;"
"accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234567890"
)
Once this record exists, it is reused for all future renewals. No DNS changes are needed in the issuance path — only the initial setup and any scope changes require DNS updates.
Security Tradeoffs
DNS-PERSIST-01 shifts the security boundary from DNS write access to ACME account key protection:
- DNS-01: The sensitive asset is DNS write access. Every renewal requires API credentials that can modify DNS records.
- DNS-PERSIST-01: The sensitive asset is the ACME account key. DNS credentials are needed only during initial setup.
This reduces the number of systems that need DNS API access but increases the importance of ACME account key security.
Scope Controls
| Parameter | Description | Example |
|---|---|---|
policy=wildcard |
Extends authorization to wildcard and subdomains | Allows *.example.com |
persistUntil=<timestamp> |
Sets an expiration for the authorization | persistUntil=1767225600 |
Wildcard example:
_validation-persist.example.com. IN TXT (
"letsencrypt.org;"
"accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234567890;"
"policy=wildcard"
)
With expiration:
_validation-persist.example.com. IN TXT (
"letsencrypt.org;"
"accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234567890;"
"persistUntil=1767225600"
)
Rollout Timeline
| Milestone | Date |
|---|---|
| CA/B Forum SC-088v3 passed | October 2025 |
| Pebble support available | Now |
| Staging rollout | Late Q1 2026 |
| Production rollout | Q2 2026 |
As of this writing, DNS-PERSIST-01 is available in Pebble (Let’s Encrypt’s test CA) for experimentation. A lego-cli implementation is in progress. cert-manager support is expected to follow once the spec stabilizes.
IRSA and Workload Identity for AWS
For cert-manager running on EKS, the recommended pattern is IAM Roles for Service Accounts (IRSA), which eliminates static AWS credentials:
# Terraform - IRSA for cert-manager
module "cert_manager_irsa" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
version = "~> 5.0"
role_name = "cert-manager"
oidc_providers = {
main = {
provider_arn = var.oidc_provider_arn
namespace_service_accounts = ["cert-manager:cert-manager"]
}
}
}
resource "aws_iam_policy" "cert_manager" {
name = "cert-manager-route53"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"route53:GetChange",
"route53:ListHostedZonesByName",
"route53:ChangeResourceRecordSets"
]
Resource = ["*"]
}
]
})
}
resource "aws_iam_role_policy_attachment" "cert_manager" {
role = module.cert_manager_irsa.iam_role_name
policy_arn = aws_iam_policy.cert_manager.arn
}
Then annotate the cert-manager service account:
apiVersion: v1
kind: ServiceAccount
metadata:
name: cert-manager
namespace: cert-manager
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/cert-manager
ACM (AWS Certificate Manager)
Request Certificate
# Terraform - ACM certificate
resource "aws_acm_certificate" "main" {
domain_name = "example.com"
subject_alternative_names = ["*.example.com"]
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
resource "aws_route53_record" "cert_validation" {
for_each = {
for val in aws_acm_certificate.main.domain_validation_options :
val.domain_name => val
}
zone_id = aws_route53_zone.main.zone_id
name = each.value.resource_record_name
type = each.value.resource_record_type
ttl = 60
records = [each.value.resource_record_value]
}
ALB with HTTPS
# Terraform - ALB with HTTPS
resource "aws_lb" "main" {
name = "main-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public[*].id
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = aws_acm_certificate.main.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.main.arn
}
}
Automated Renewal Scripts
Renew Certificates Script
#!/usr/bin/env python3
"""Certificate renewal monitoring and automation."""
import boto3
import datetime
import logging
from dataclasses import dataclass
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class Certificate:
arn: str
domain: str
expires: datetime.datetime
def get_expiring_certificates(days=30):
"""Find certificates expiring within specified days"""
client = boto3.client('acm')
certs = client.list_certificates()['CertificateSummaryList']
expiring = []
for cert in certs:
detail = client.describe_certificate(CertificateArn=cert['CertificateArn'])
if detail['Certificate']['Status'] != 'ISSUED':
continue
not_after = detail['Certificate']['NotAfter']
days_until_expiry = (not_after - datetime.datetime.now(not_after.tzinfo)).days
if days_until_expiry <= days:
expiring.append(Certificate(
arn=cert['CertificateArn'],
domain=cert['DomainName'],
expires=not_after
))
return expiring
def send_alert(certs):
"""Send alert about expiring certificates"""
if not certs:
return
message = "Expiring certificates:\n"
for cert in certs:
message += f"- {cert.domain} expires {cert.expires}\n"
sns = boto3.client('sns')
sns.publish(
TopicArn='arn:aws:sns:us-east-1:123456789:alerts',
Subject='Certificate Expiry Alert',
Message=message
)
def main():
expiring = get_expiring_certificates(days=30)
if expiring:
logger.warning(f"Found {len(expiring)} expiring certificates")
send_alert(expiring)
else:
logger.info("No certificates expiring soon")
if __name__ == "__main__":
main()
cert-manager Renewal Monitor
# Prometheus alerts for cert-manager
- name: certificate-expiry
rules:
- alert: CertManagerCertificateExpiry
expr: |
certmanager_certificate_expiration_timestamp_seconds - time() < 604800
for: 1h
labels:
severity: warning
annotations:
summary: "Certificate expiring in less than 7 days"
- alert: CertManagerCertificateExpired
expr: |
certmanager_certificate_expiration_timestamp_seconds - time() < 0
for: 1m
labels:
severity: critical
annotations:
summary: "Certificate has expired"
Metric note: The Prometheus metric name changed in cert-manager v1.18 from certmanager_certificate_expiration_timestamp to certmanager_certificate_expiration_timestamp_seconds to follow OpenMetrics naming conventions.
Security Advisories
Staying current with cert-manager releases is critical. The following CVEs have been patched in recent versions:
| CVE | Severity | Affected Versions | Fixed In |
|---|---|---|---|
| CVE-2025-68121 | High | < 1.18.6, < 1.19.4 | 1.18.6, 1.19.4, 1.20.0 |
| CVE-2026-24051 | Medium (macOS only) | < 1.19.4 | 1.19.4, 1.20.0 |
| GHSA-gx3x-vq4p-mhhv | Moderate | 1.18.0–1.18.4, 1.19.0–1.19.2 | 1.18.5, 1.19.3 |
| CVE-2026-25518 | Moderate | 1.18.0–1.18.4, 1.19.0–1.19.2 | 1.18.5, 1.19.3 |
Upgrade to cert-manager v1.20.2 to receive all fixes.
DNSSEC
Enable DNSSEC on Route53
# Terraform - DNSSEC configuration
resource "aws_route53_key_signing_key" "main" {
name = "example-com-key"
zone_id = aws_route53_zone.main.zone_id
key_management_service_arn = aws_kms_key.dnssec.arn
}
resource "aws_route53_dnssec" "main" {
hosted_zone_id = aws_route53_zone.main.zone_id
}
Cloudflare DNSSEC
# Enable DNSSEC via Cloudflare API
import cloudflare
def enable_dnssec(zone_id, zone_name):
client = cloudflare.Cloudflare()
dnssec = client.zones.dnssec.post(zone_id, data={
'type': 'DS'
})
print(f"DS Record: {dnssec['ds']}")
Traffic Routing Patterns
Weighted Routing
# Terraform - Weighted routing for canary deployments
resource "aws_route53_record" "api-v1" {
zone_id = aws_route53_zone.main.zone_id
name = "api.example.com"
type = "A"
set_identifier = "v1"
health_check_id = aws_route53_health_check.v1.id
alias {
name = "v1-alb.example.com"
zone_id = "ZONE_ID"
evaluate_target_health = true
}
weight = 90
}
resource "aws_route53_record" "api-v2" {
zone_id = aws_route53_zone.main.zone_id
name = "api.example.com"
type = "A"
set_identifier = "v2"
health_check_id = aws_route53_health_check.v2.id
alias {
name = "v2-alb.example.com"
zone_id = "ZONE_ID"
evaluate_target_health = true
}
weight = 10
}
Latency-Based Routing
resource "aws_route53_record" "us-east" {
zone_id = aws_route53_zone.main.zone_id
name = "api.example.com"
type = "A"
set_identifier = "us-east"
region = "us-east-1"
alias {
name = "alb-us-east.example.com"
zone_id = "ZONE_ID"
evaluate_target_health = true
}
}
resource "aws_route53_record" "eu-west" {
zone_id = aws_route53_zone.main.zone_id
name = "api.example.com"
type = "A"
set_identifier = "eu-west"
region = "eu-west-1"
alias {
name = "alb-eu-west.example.com"
zone_id = "ZONE_ID"
evaluate_target_health = true
}
}
Best Practices
DNS Best Practices
- Use multiple nameservers for redundancy
- Enable DNSSEC for security
- Set appropriate TTLs (shorter for dynamic records — 60s for challenge records, longer for stable records)
- Use ALIAS records instead of CNAMEs at apex
- Monitor DNS resolution latency
- Implement rate limiting protection
- Use CNAME delegation for
_acme-challengeto limit credential scope
Certificate Best Practices
- Use short certificate validity — Let’s Encrypt already enforces 90-day maximum, with further reductions to 200 days (2026), 100 days (2027), and 45 days (2029)
- Automate renewal at least 14 days before expiry (wider margin as cert lifetimes shrink)
- Use DNS-01 challenge for wildcard certificates
- Monitor certificate expiration proactively with Prometheus alerts
- Store certificates in secrets, not configmaps
- Use dedicated certificates per service
- Set
privateKey.rotationPolicy: Always(now the default in cert-manager v1.18+) - Use IRSA or Workload Identity instead of static credentials on Kubernetes
- Stay on supported cert-manager releases and patch promptly for CVEs
Security
# CAA record - restrict certificate authorities
resource "aws_route53_record" "caa" {
zone_id = aws_route53_zone.main.zone_id
name = "example.com"
type = "CAA"
ttl = 3600
records = [
"0 issue \"letsencrypt.org\"",
"0 issuewild \";\"",
"0 iodef \"mailto:[email protected]\""
]
}
# DNSSEC signing and DMARC
resource "cloudflare_record" "dmarc" {
zone_id = cloudflare_zone.example.id
name = "_dmarc"
value = "v=DMARC1; p=quarantine; rua=mailto:[email protected]"
type = "TXT"
}
Monitoring
# DNS monitoring
- name: dns
rules:
- alert: HighDNSLatency
expr: histogram_quantile(0.95, rate(dns_query_duration_seconds_bucket[5m])) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "High DNS latency"
- alert: DNSErrors
expr: rate(dns_query_errors_total[5m]) > 10
for: 5m
labels:
severity: critical
annotations:
summary: "High DNS error rate"
Conclusion
Automated DNS and certificate management is essential:
- Use cert-manager for Kubernetes certificate automation (v1.20.2+)
- Use Route53 or Cloudflare for DNS management as code
- Enable DNS-01 challenges for wildcard certificates
- Evaluate DNS-PERSIST-01 once production rollout completes (Q2 2026)
- Implement DNSSEC for domain security
- Monitor certificate expiration proactively
- Use traffic routing features for blue-green and canary
- Plan for the 45-day certificate future — shorter lifetimes are coming
Start with automated certificates, then add DNS automation.
External Resources
- cert-manager Documentation
- Route53 Documentation
- Let’s Encrypt Documentation
- cert-manager Supported Releases
- DNS01 Configuration
- Let’s Encrypt DNS-PERSIST-01 Announcement
- Let’s Encrypt Challenge Types
- EFF: Securing ACME DNS Challenge Validation
Related Articles
- Zero Trust Security — TLS everywhere
- Secrets Management — Certificate storage
- Edge Computing — Edge TLS termination
Comments