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
sudoprivileges. - 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 DNSArecord (andAAAAif 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:Editpermission.
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:
| Stage | What happens |
|---|---|
| Account | The client generates an account key pair and registers it with the CA. |
| New order | The client lists the names it wants (for example your_domain and www.your_domain). |
| Authorizations | For each name, the CA returns one or more challenges the client can complete. |
| Validation | The client provisions the challenge response and tells the CA to check it. |
| Finalize | Once every name is valid, the client sends a CSR signed with the certificate key. |
| Download | The 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:
| Challenge | How it is proven | Port / access needed | Wildcards | Typical use |
|---|---|---|---|---|
| HTTP-01 | CA fetches http://<name>/.well-known/acme-challenge/<token> | Inbound port 80 from the Internet | No | Public web servers |
| DNS-01 | CA looks up a TXT record at _acme-challenge.<name> | API access to your DNS provider | Yes | Wildcards, internal hosts, servers without port 80 |
| TLS-ALPN-01 | CA connects to port 443 using the acme-tls/1 ALPN protocol | Inbound port 443 | No | Servers 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.
NoteIf you only want the certificate files and prefer to edit the web server configuration yourself, use
sudo certbot certonly --nginx ...instead. For software without a Certbot plugin,--webroot -w /var/www/htmlwrites the challenge files into a directory your server already exposes.
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.
TipIf your DNS provider has no plugin, you can delegate
_acme-challenge.your_domainwith a CNAME to a zone you control and automate that zone instead. This also keeps a powerful DNS API token off the web server.
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:
| Client | Language | Strengths | When to choose it |
|---|---|---|---|
| Certbot | Python | Packaged by most distros, Nginx/Apache installers, many DNS plugins | General purpose servers |
| acme.sh | POSIX shell | No dependencies, over 150 DNS APIs, runs without root | Minimal systems, routers, shared hosting |
| lego | Go | Single static binary, many DNS providers | Containers, CI pipelines, scripted issuance |
| Built-in ACME (Caddy, Traefik, cert-manager) | Varies | The proxy or cluster manages certificates itself | When 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.
