Security
SSL Renewal Strategies: Comparing Let's Encrypt, ZeroSSL, and Caddy Auto-Renewal
Pragmatic comparison of ACME providers, deploy hooks, container realities, and OCSP stapling — plus the patterns that actually work in production in 2026.
Let's Encrypt issues roughly half a billion certificates a year and is now part of the air we breathe. Most teams set up certbot once five years ago, watched it auto-renew without incident, and treat TLS as a solved problem. It mostly is — until it is not. The certificate that quietly auto-renewed for four years stops renewing because someone changed the firewall rule that allowed inbound port 80, or the deploy hook silently failed because the script's bash assumption broke under dash, or the renewal succeeded but Nginx never reloaded because the SIGHUP was masked.
This post is about the renewal patterns that hold up in production, the differences between the major ACME providers (Let's Encrypt, ZeroSSL, Buypass, and the new entrants like Google Trust Services and Sectigo's free tier), and the architectural choices that make TLS boring again. We will cover certbot, Caddy, acme.sh, Nginx with native ACME, container deploys, and the OCSP stapling and certificate transparency considerations that actually matter at scale.
If you are running CheckFast SSL checks weekly and everything has been green for years, you are probably doing this right and can skip to the OCSP and CT sections. If you have ever seen a renewal silently fail and realized it from a Twitter mention, this is your refresher.
ACMEv2 in five minutes
ACME (RFC 8555) is the protocol every modern free CA speaks. The flow is the same regardless of provider: you register an account by sending a JWS-signed POST to the CA's directory; you submit an order naming the domains you want a certificate for; the CA returns one or more authorization challenges; you complete the challenges by serving a specific token at a known URL or by publishing a TXT record; the CA validates and issues; you finalize by uploading a CSR.
There are three challenge types. HTTP-01 requires the CA to fetch /.well-known/acme-challenge/ over plain HTTP from your domain. DNS-01 requires you to publish _acme-challenge. as a TXT record. TLS-ALPN-01 is similar to HTTP-01 but operates over TLS on port 443 and is mostly used by Caddy and acme.sh in specific edge cases. HTTP-01 is the default for almost every common deployment; DNS-01 is required for wildcard certificates and is the right choice for any host that does not expose port 80 to the public internet.
Certificate validity is 90 days for Let's Encrypt and ZeroSSL, 6 days for Let's Encrypt's experimental short-lived profile (introduced in 2026 — opt-in), and 365 days for some commercial CAs (those are not free). The CAB Forum is moving the industry toward shorter validities — by 2027 the public-trust max is expected to drop to 47 days, which makes automated renewal mandatory and manual cert management impossible.
Renewal is conventionally triggered when the certificate has 30 days or less remaining. Certbot, acme.sh, and Caddy all use this default. If something has been broken for a month and renewal has been failing silently the whole time, you will have ~7 days of breathing room before the cert actually expires — assuming you notice the renewal failure at all.
Let's Encrypt: still the default, with caveats
Let's Encrypt is free, well-funded, and the right default for almost every project. Their rate limits in 2026 are 50 certificates per registered domain per week, 300 new orders per account per 3 hours, and 5 duplicate certificates per week. For all but the largest hosting providers, these limits are invisible.
Where Let's Encrypt occasionally falls down is in their staged rollout of new validation infrastructure — they have had a few incidents over the years where MultiVA validation (the requirement that multiple geographic validators see your challenge response) flagged perfectly legitimate sites because of asymmetric routing or geo-fenced firewalls. The fix is usually opening port 80 from a wider range of source IPs or running DNS-01 instead of HTTP-01. CheckFast's SSL checker will flag this if it sees evidence of a recent renewal failure.
Let's Encrypt also runs ISRG Root X2 (ECDSA) alongside ISRG Root X1 (RSA). If you specify a key type when generating your CSR, you get a cert chained accordingly. ECDSA certs are smaller, faster to validate, and supported by every browser shipped in the last decade — there is no reason to default to RSA in 2026 unless you have a specific compatibility constraint.
The cross-signed chain story is settled: ISRG Root X1 is now in every major trust store (Apple, Microsoft, Mozilla, Android), and the cross-sign from IdenTrust DST Root CA X3 expired in September 2024. Modern certbot versions default to the shorter ISRG-only chain. If you are still serving the long cross-signed chain, you are wasting ~1.4KB on every TLS handshake.
ZeroSSL, Buypass, Google Trust Services, and the alternatives
ZeroSSL is the second most common free ACME provider. They started with a web UI and added ACME support later; the API is fully compatible with certbot and acme.sh. The differentiating features are 90-day certs as the default (same as Let's Encrypt), unlimited certificates on the free tier (no rate limits), and a paid tier that adds wildcard support without requiring DNS-01 (they do it via a hosted DNS plugin). For high-volume hosting providers chafing against Let's Encrypt's rate limits, ZeroSSL is the obvious second choice.
Buypass is a Norwegian CA that offers ACME-issued 180-day certificates on their free tier. The longer validity is unusual and useful for systems where renewal automation is genuinely brittle (some embedded IoT, some legacy systems). The trade-off is that Buypass is less well-known outside Europe and some older browsers occasionally show warnings — verify the cert chain in your target geographies before relying on Buypass as primary.
Google Trust Services launched ACME in 2023 and is now production-stable. The pitch is enterprise-grade reliability, integration with Google Cloud, and free issuance for any domain. Their rate limits are higher than Let's Encrypt (300 certificates per registered domain per week vs 50) which makes them attractive for SaaS platforms that issue lots of subdomain certs. The catch is that you authenticate via a Google Cloud service account, which adds operational complexity if your stack is not already in GCP.
Sectigo's CertManager has a free ACME tier introduced in 2025. Functionally similar to ZeroSSL with slightly more conservative rate limits and the option to upgrade to OV/EV paid tiers without changing your client. Worth considering if you anticipate ever needing organization-validated certs (rare in 2026 — DV certs win the battle for browser UI).
For most teams, the answer is: use Let's Encrypt as primary, fall back to ZeroSSL if you hit rate limits or have a specific need. Both are mature, both are free, both speak ACMEv2.
Verify your cert chain, validity, expiry, and OCSP stapling — instantly, free, no signup.
Run an SSL checkCaddy: the cheat code for TLS automation
Caddy automates TLS so completely that most users do not realize it is happening. You write a Caddyfile listing your domains, run caddy run, and Caddy obtains certs from Let's Encrypt (with ZeroSSL fallback by default), renews them, and reloads itself — without you writing a single line of cron, deploy hook, or systemd unit. It is the single best argument for using Caddy as your edge proxy if you have no strong reason to be on Nginx or Envoy.
Behind the scenes, Caddy uses a library called CertMagic. The same library is embedded in Traefik, in Coredns's auto cert plugin, and in a handful of other Go-native tools. Caddy's storage backend defaults to local disk, but you can configure Redis, Consul, S3, or PostgreSQL — which means a Caddy fleet can share a single cert store and renew once across N nodes, not N times. This is the operational pattern that high-traffic sites use to avoid hammering the ACME provider.
Caddy 2 also handles OCSP stapling automatically and supports HTTP/3 (QUIC) out of the box. The combination of zero-config TLS, native HTTP/3, and reasonable defaults makes it the right answer for greenfield projects. The downside is that it is less common in shops that already have Nginx expertise, and the configuration syntax (Caddyfile or JSON) is not what most ops teams know.
For a basic deployment, the entire Caddyfile is example.com { reverse_proxy localhost:3000 }. Start Caddy, point DNS at the box, and TLS works in 30 seconds. Renewals happen at ~30 days remaining, in the background, with retry on failure. This is the path of least resistance.
# /etc/caddy/Caddyfile
# Three domains, one config, automatic TLS via Let's Encrypt with ZeroSSL fallback.
example.com {
reverse_proxy localhost:3000
}
api.example.com {
reverse_proxy localhost:8080
encode gzip zstd
}
# wildcard for *.staging.example.com via DNS-01 (Cloudflare token)
*.staging.example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
reverse_proxy localhost:9000
}Certbot in 2026: still works, with sharp edges
Certbot is the canonical ACME client and the right choice when you cannot use Caddy. The basic invocation has not changed in years: certbot --nginx -d example.com -d www.example.com. The Nginx and Apache plugins handle config rewriting; the standalone plugin runs its own HTTP server on port 80; the webroot plugin places challenge files in a directory your existing web server already serves.
Renewals are handled by a systemd timer (certbot.timer on every modern Linux distro. The timer runs certbot renew twice a day, which is a no-op for any cert with more than 30 days remaining and a real renewal otherwise. The configuration lives in /etc/letsencrypt/renewal/.conf — that file is the source of truth for how each cert was originally issued and how it will be renewed.
The classic certbot footgun is the deploy hook. After renewal, you typically need to reload your web server. Certbot's --deploy-hook flag accepts a script to run after successful renewal. It is non-obvious that this script must be in the renewal config of the cert, not on the command line of the renew invocation — running certbot renew --deploy-hook 'systemctl reload nginx' does not persist the hook between runs. Use certbot reconfigure --deploy-hook to make the hook permanent or write it directly into the renewal config.
Certbot's --keep-until-expiring flag is the right default for unattended renewal — it skips work if the cert is already valid for more than 30 days. Combined with the systemd timer, this makes renewal a no-op 99% of the time and an issue only in the rare case it fails.
Monitoring renewal is the gap most teams miss. Certbot writes to /var/log/letsencrypt/letsencrypt.log but the log rotates and is rarely watched. The cleanest pattern is to run a daily check that confirms cert expiry is more than 14 days out — if it is not, alert. CheckFast's SSL monitoring does exactly this; rolling your own is a 10-line bash script that runs openssl x509 -enddate on each cert.
#!/usr/bin/env bash
# /etc/cron.daily/cert-expiry-check
set -euo pipefail
for cert in /etc/letsencrypt/live/*/fullchain.pem; do
days_left=$(( ( $(date -d "$(openssl x509 -enddate -noout -in "$cert" | cut -d= -f2)" +%s) - $(date +%s) ) / 86400 ))
if [[ $days_left -lt 14 ]]; then
domain=$(basename "$(dirname "$cert")")
echo "ALERT: $domain has $days_left days left" | mail -s "Cert renewal stuck" ops@example.com
fi
doneContainers and Kubernetes: the renewal patterns that work
TLS in container environments is fundamentally different. The container is ephemeral; the certificate is not. The pattern that works is to keep certs out of the container image and mount them as volumes, secrets, or fetch them from a shared store at startup.
On Docker Compose with Caddy, you mount Caddy's data volume (/data as a named volume so that certificates survive container restarts. Renewals happen inside the running Caddy container; the volume persists. This is the simplest production pattern for single-host deployments and what we use for CheckFast's own infrastructure.
On Kubernetes, the canonical answer is cert-manager (cert-manager.io). It runs as a controller, watches Certificate and Issuer custom resources, talks to ACME on your behalf, and stores the resulting certificates as Kubernetes secrets. Pair with an Ingress controller that consumes those secrets (Nginx Ingress, Traefik, Contour, Istio Gateway). The DNS-01 challenge integrations cover all major DNS providers (Cloudflare, Route 53, Google Cloud DNS, DigitalOcean, etc.) so wildcard certificates work cleanly.
On AWS specifically, ACM (AWS Certificate Manager) is the simplest path if you are using ALB, CloudFront, or API Gateway — ACM-issued certs cannot be exported but renew automatically and integrate with the AWS load balancer ecosystem. ACM is not free for non-AWS use; the certs are free but only work on AWS-terminated traffic. For Kubernetes on EKS with a self-managed Ingress, cert-manager is still the answer.
On Cloudflare in front of your origin, Cloudflare's Universal SSL handles the public-facing TLS automatically — you do not need ACME at all for the edge. You may still want a cert on your origin for end-to-end encryption (Cloudflare's "Full (strict)" mode), and cert-manager or Caddy on the origin handles that.
OCSP stapling, must-staple, and the post-OCSP world
OCSP (Online Certificate Status Protocol) is how clients check whether a cert has been revoked. Without OCSP stapling, the browser makes a separate request to the CA's OCSP responder during TLS handshake, which adds latency and leaks the user's browsing to the CA. With stapling, your server fetches the OCSP response, caches it, and serves it as part of the TLS handshake — faster and more private.
Caddy enables OCSP stapling by default. Nginx requires ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; — without ssl_trusted_certificate, stapling silently fails because Nginx cannot verify the OCSP response. Apache, HAProxy, and Envoy all have similar config requirements.
Let's Encrypt announced in 2024 that they will be deprecating OCSP entirely by 2025-2026 in favor of CRLite and short-lived certificates. The replacement model is: 6-day certs with no OCSP, refreshed continuously. The browser ecosystem (Chrome, Firefox, Safari) has been quietly moving away from OCSP for years — Chrome stopped doing live OCSP queries in 2012 and relies on CRLSets pushed via Chrome Update.
The practical consequence in 2026: OCSP stapling is still worth configuring because it covers the gap before the short-lived-cert model is fully rolled out, but it is no longer the must-have it was a decade ago. The OCSP must-staple flag (RFC 7633), which made stapling mandatory for browsers, is also being deprecated as part of the same shift.
For verification, run openssl s_client -connect example.com:443 -status -servername example.com < /dev/null and look for the OCSP response block. Empty response means stapling is not working; "Cert Status: good" means it is.
Certificate Transparency: the silent monitor
Every publicly-trusted certificate issued in the last decade has been logged to Certificate Transparency (CT) logs. The browser ecosystem requires CT for trust — a cert not in two CT logs will be untrusted by Chrome, Safari, and Firefox. This means every cert ever issued for your domain is publicly searchable.
Use crt.sh to search for your domain. The search returns every cert ever issued, the CA, the validity dates, and the SAN list. It is the single best tool for catching shadow IT — when marketing onboards a new SaaS that issues a marketing.example.com cert without your DNS team's knowledge, it shows up in CT within hours.
Set up CT monitoring. Free options include crt.sh email alerts and Certstream's open WebSocket feed; CheckFast can schedule a paid CT-log monitor at the cadence allowed by your plan. When an unexpected certificate appears, investigate promptly and follow your certificate-authority incident process.
CT also makes wildcard certs more interesting from a security posture perspective. A *.example.com cert covers anything; a per-subdomain cert lets you see in CT logs exactly which subdomains exist. Some security teams deliberately avoid wildcards for this reason — the operational cost of per-subdomain renewal is low when ACME is automated.
Frequently asked
ECDSA. They are smaller (a P-256 cert is ~600 bytes vs ~1500 for RSA-2048) faster to validate and supported by every browser shipped since 2013. There is no compatibility argument left for RSA on public-facing services. Internal services with legacy clients (older Java ancient Windows Server versions) may still need RSA; check your client compatibility matrix.
Run Caddy on a different port (8443) initially with the same domains. Verify Caddy obtains certs successfully. Then either flip the port via firewall/load balancer or stop Nginx and restart Caddy on 443. Caddy will use its own newly-obtained certs. There is no need to migrate the existing Let's Encrypt account or certs — Caddy will request new ones and Let's Encrypt does not penalize this.
The certificate expires. Browsers will show a hard error and your site is effectively offline for HTTPS. Most teams do not notice until customers report it. The defense is monitoring: alert at 14 days remaining page at 7 days remaining. CheckFast does this automatically but a 10-line cron script is enough.
Drop everything below 1.2. TLS 1.0 and 1.1 are deprecated since 2020 and disabled in every modern browser. TLS 1.3 is the default for new connections but TLS 1.2 still gets meaningful traffic from older Android and embedded clients (POS terminals IoT devices). For B2C web traffic in 2026 TLS 1.3 only is reasonable; for any API consumed by older devices keep 1.2.
Yes and you should if you are issuing many certs. The account is just a key pair; the rate limits are per registered domain not per account. Sharing an account across a fleet means coordinated rate limit accounting which is what shared cert storage (CertMagic with Redis backend) gives you out of the box.
If your traffic always goes through Cloudflare yes — Origin CA certs are 15-year validity and cannot be used elsewhere which is a reasonable security boundary. They are not publicly trusted so the cert is invalid if accessed directly. Use them for origin-side TLS in Full (strict) mode and let Cloudflare handle public TLS via Universal SSL.
Related reading
Security
Setting Security Headers in 2026: CSP, HSTS, COEP, and What Actually Matters
12 min read
Email deliverability
DNS Deep Dive: How SPF, DMARC, MX, and DNSSEC Fit Together
12 min read
Monitoring
Monitoring Cron Jobs Without Cronhub: Healthchecks, Heartbeats, and CheckFast's Approach
11 min read
Ops & infrastructure
Building a Status Page: When to Buy vs Build vs Use a Bundled Tool
11 min read