ACME (Automated Certificate Management Environment, RFC 8555) is the protocol that lets a server prove it controls a domain and obtain a TLS certificate without any human involvement. Let's Encrypt made it popular, and today most public CAs (ZeroSSL, Google Trust Services, Sectigo, and others) speak it too. In this tutorial you will learn how an ACME order and its challenges work, then use Certbot on Ubuntu 24.04 to issue a certificate for Nginx, get a wildcard certificate through DNS-01, and make sure renewals and service reloads happen on their own.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, such as a CubePath VPS, with a non-root user that has sudo privileges.
  • Nginx installed and serving a site for your domain (sudo apt install nginx).
  • A registered domain. This guide uses your_domain; replace it with yours. Its DNS A record (and AAAA if you use IPv6) must point to your server's public IP.
  • Ports 80 and 443 open. With UFW: sudo ufw allow 'Nginx Full'.
  • For the wildcard section only: a domain whose DNS is hosted at Cloudflare and an API token with the Zone:DNS:Edit permission.

How ACME works

An ACME client talks to the CA over HTTPS and signs every request with an account key (a JSON Web Signature), so the CA always knows which account is acting. A certificate is obtained through an order:

StageWhat happens
AccountThe client generates an account key pair and registers it with the CA.
New orderThe client lists the names it wants (for example your_domain and www.your_domain).
AuthorizationsFor each name, the CA returns one or more challenges the client can complete.
ValidationThe client provisions the challenge response and tells the CA to check it.
FinalizeOnce every name is valid, the client sends a CSR signed with the certificate key.
DownloadThe CA issues the certificate and the client downloads the full chain.

Renewal is just a new order for the same names. Nothing is "extended": a fresh certificate replaces the old one.

Challenge types

The challenge is how you prove control of a name. There are three standard types:

ChallengeHow it is provenPort / access neededWildcardsTypical use
HTTP-01CA fetches http://<name>/.well-known/acme-challenge/<token>Inbound port 80 from the InternetNoPublic web servers
DNS-01CA looks up a TXT record at _acme-challenge.<name>API access to your DNS providerYesWildcards, internal hosts, servers without port 80
TLS-ALPN-01CA connects to port 443 using the acme-tls/1 ALPN protocolInbound port 443NoServers and proxies that handle TLS themselves (Caddy, Traefik)

HTTP-01 is the simplest when the server is reachable on port 80. DNS-01 is the only option for wildcard names, and it also works for machines that are not reachable from the Internet at all.

Certificate lifetimes are getting shorter

Let's Encrypt certificates are valid for 90 days, and the CA/Browser Forum has approved a schedule that cuts the maximum lifetime of all public TLS certificates step by step, down to 47 days by 2029. Manual renewal is no longer realistic, which is exactly the problem ACME solves. Also note that Let's Encrypt no longer sends expiration reminder emails, so your renewal automation has to be correct and monitored.

Step 1 - Installing Certbot and the Nginx plugin

Certbot is the ACME client maintained by the EFF. Ubuntu 24.04 packages it together with the Nginx plugin, and the package installs a systemd timer that handles renewals:

sudo apt update
sudo apt install certbot python3-certbot-nginx

Confirm the version and that the renewal timer is active:

certbot --version
systemctl list-timers certbot.timer
certbot 2.9.0
NEXT                        LEFT     LAST PASSED UNIT          ACTIVATES
Fri 2026-09-25 22:41:00 UTC 9h left  -    -      certbot.timer certbot.service

The timer runs certbot renew twice a day. Certbot only renews certificates that are close to expiry, so frequent runs are harmless.

Step 2 - Testing against the staging environment

Let's Encrypt enforces rate limits in production, for example 5 certificates for the exact same set of names per week and 5 failed validations per name per account per hour. A misconfigured DNS record or firewall can burn through them quickly. Test first with --dry-run, which runs the full order against the staging CA and saves nothing:

sudo certbot certonly --nginx --dry-run -d your_domain -d www.your_domain
Simulating a certificate request for your_domain and www.your_domain
The dry run was successful.

If it fails, fix the cause (see the Troubleshooting section) before touching production.

Step 3 - Issuing a certificate with HTTP-01

Run Certbot with the Nginx plugin. It answers the HTTP-01 challenge by temporarily adding a location block to your Nginx configuration, then installs the certificate in the matching server block:

sudo certbot --nginx -d your_domain -d www.your_domain

The first time, Certbot asks for an email address (used for account recovery and important service notices) and for acceptance of the terms of service. Then it validates each name:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/your_domain/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/your_domain/privkey.pem
This certificate expires on 2026-12-24.
Successfully deployed certificate for your_domain to /etc/nginx/sites-enabled/default
Successfully deployed certificate for www.your_domain to /etc/nginx/sites-enabled/default

Files under /etc/letsencrypt/live/your_domain/ are symlinks to the latest version in /etc/letsencrypt/archive/, so the paths in your web server configuration never change after a renewal.

Check what the server now presents:

echo | openssl s_client -connect your_domain:443 -servername your_domain 2>/dev/null | openssl x509 -noout -issuer -enddate
issuer=C = US, O = Let's Encrypt, CN = R12
notAfter=Dec 24 09:12:33 2026 GMT

The intermediate name (R12, E7, and so on) varies; what matters is that the issuer is Let's Encrypt and the date is about 90 days away.

Step 4 - Reloading services after each renewal

A renewed certificate on disk is useless until the service using it reloads. When the Nginx plugin installed the certificate, Certbot already reloads Nginx after each renewal. For anything else (a mail server, HAProxy, a Java application), use a deploy hook: an executable in /etc/letsencrypt/renewal-hooks/deploy/ runs once after every successful renewal, and never when nothing was renewed.

Create a hook that reloads Postfix and Dovecot as an example:

sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-mail.sh
#!/usr/bin/env bash
set -euo pipefail

# $RENEWED_LINEAGE is set by Certbot, e.g. /etc/letsencrypt/live/your_domain
echo "Renewed certificate in ${RENEWED_LINEAGE}"
systemctl reload postfix dovecot

Make it executable:

sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-mail.sh

Adjust the systemctl reload line to the services you actually run. Deploy hooks do not run during a dry run, so test the hook directly, passing the variable Certbot would set:

sudo RENEWED_LINEAGE=/etc/letsencrypt/live/your_domain /etc/letsencrypt/renewal-hooks/deploy/reload-mail.sh
Renewed certificate in /etc/letsencrypt/live/your_domain

Then simulate a renewal of every certificate to confirm the ACME side still works:

sudo certbot renew --dry-run
Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/your_domain/fullchain.pem (success)

Every certificate's renewal settings, including the challenge method and installer used, are stored in /etc/letsencrypt/renewal/your_domain.conf. You rarely need to edit it by hand.

Step 5 - Issuing a wildcard certificate with DNS-01

A wildcard such as *.your_domain can only be validated through DNS-01. Certbot needs an API credential to create the _acme-challenge TXT record automatically. This example uses the Cloudflare plugin, which Ubuntu also packages:

sudo apt install python3-certbot-dns-cloudflare

Store the API token in a file readable only by root:

sudo mkdir -p /root/.secrets
sudo nano /root/.secrets/cloudflare.ini
dns_cloudflare_api_token = your_cloudflare_api_token
sudo chmod 600 /root/.secrets/cloudflare.ini

Request the certificate. Include the bare domain as well, because *.your_domain does not cover your_domain itself:

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
  --dns-cloudflare-propagation-seconds 30 \
  -d your_domain -d '*.your_domain'
Waiting 30 seconds for DNS changes to propagate
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/your_domain-0001/fullchain.pem

Certbot appends -0001 because a certificate lineage named your_domain already exists from Step 3. Use --cert-name to choose a clearer name, and confirm the names covered with:

sudo certbot certificates
  Certificate Name: your_domain-0001
    Domains: your_domain *.your_domain
    Expiry Date: 2026-12-24 09:40:02+00:00 (VALID: 89 days)
    Certificate Path: /etc/letsencrypt/live/your_domain-0001/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/your_domain-0001/privkey.pem

Renewals reuse the same plugin and credentials file automatically. Plugins for other providers (Route 53, DigitalOcean, OVH, RFC 2136 for your own BIND or PowerDNS server) follow the same pattern; list what Ubuntu ships with apt search python3-certbot-dns.

Step 6 - Limiting which CAs can issue for your domain

A CAA DNS record tells every public CA which authorities may issue certificates for your domain. It is not part of ACME, but it pairs well with automation. At your DNS provider, add a record such as:

your_domain.  CAA  0 issue "letsencrypt.org"
your_domain.  CAA  0 issuewild "letsencrypt.org"

Verify it:

dig +short CAA your_domain
0 issue "letsencrypt.org"
0 issuewild "letsencrypt.org"

If you later move to another CA, add its identifier before switching, or issuance will fail with a CAA error.

Other ACME clients

Certbot is a solid default on a single server, but other clients fit other situations:

ClientLanguageStrengthsWhen to choose it
CertbotPythonPackaged by most distros, Nginx/Apache installers, many DNS pluginsGeneral purpose servers
acme.shPOSIX shellNo dependencies, over 150 DNS APIs, runs without rootMinimal systems, routers, shared hosting
legoGoSingle static binary, many DNS providersContainers, CI pipelines, scripted issuance
Built-in ACME (Caddy, Traefik, cert-manager)VariesThe proxy or cluster manages certificates itselfWhen that software already terminates TLS

Note that acme.sh uses ZeroSSL as its default CA; run acme.sh --set-default-ca --server letsencrypt if you want Let's Encrypt. Whichever client you pick, only run one per certificate, or they will fight over renewals.

Troubleshooting

HTTP-01 fails with "Timeout during connect" or "Connection refused". Port 80 is not reachable from the Internet. Check sudo ufw status, any provider-level firewall, and that Nginx listens on port 80 (sudo ss -tlnp | grep ':80'). Redirecting HTTP to HTTPS is fine; blocking port 80 is not.

"Invalid response" or 404 on the challenge URL. The A/AAAA record points somewhere else, or another server block answers for the name. Compare dig +short A your_domain and dig +short AAAA your_domain with your server's addresses; a stale AAAA record is a frequent cause, because the CA prefers IPv6.

DNS-01 fails with "No TXT record found". The record had not propagated when the CA checked. Increase --dns-cloudflare-propagation-seconds, and confirm the token can edit the right zone.

"too many certificates already issued" or "too many failed authorizations". You hit a rate limit. Wait for the window to pass, and use --dry-run until the setup works.

Renewal fails silently. Check the last runs with sudo journalctl -u certbot.service --since "-3 days" and the detailed log in /var/log/letsencrypt/letsencrypt.log.

Conclusion

You now know how an ACME order moves from account to issued certificate, when to use HTTP-01, DNS-01 or TLS-ALPN-01, and you have Certbot issuing, renewing and deploying certificates on Ubuntu 24.04 with a systemd timer and deploy hooks. Next, consider adding external expiry monitoring for your domains, tuning your TLS 1.3 configuration, and setting up a private CA for hosts that public CAs cannot validate.