SSD Nodes Learn
Guides Matt ConnorBy Matt Connor

Certbot on Ubuntu 24.04: Apache + Let's Encrypt

Get a Let's Encrypt cert for Apache on Ubuntu 24.04 with Certbot: snap vs apt, the ServerName trap, auto-renewal, and the exact errors that block issuance.

What you are building

An Apache site on Ubuntu 24.04 answering on HTTPS with a free, browser-trusted Let's Encrypt certificate — issued by Certbot, renewed automatically by a systemd timer you never think about again. The command that does the work is one line. Everything that goes wrong goes wrong before that line: a vhost with no ServerName, port 80 closed at the provider firewall, DNS still pointing at the old server. So this guide spends most of its time on the preconditions, and names the exact error string each mistake prints.

Two scope notes. If your web server is nginx, the flow is the same shape but the plugin and the configs differ — use the nginx version of this guide instead. And if the thing you are securing is internal-only — an admin panel on a private address, a staging box nobody else visits — you do not need a certificate authority at all; a self-signed certificate is less machinery and works offline.

Prerequisites, and the three ways this fails before Certbot even runs

  • Apache already serving your site over plain HTTP. Certbot's Apache plugin edits an existing site; it does not create one. If you are starting from a bare VPS, build the LAMP stack on Ubuntu 24.04 first and come back — this guide is its missing TLS chapter.
  • A public domain with an A record at your VPS address. Let's Encrypt's HTTP-01 challenge means their validation servers connect to your box from the internet: no NATed homelab without a port forward, no .local names, no bare IPs. dig +short example.com must return your VPS address, and if you changed DNS in the last hour, wait out the old record's TTL before issuing.
  • If an AAAA record exists, it must be correct. Let's Encrypt prefers IPv6 when a AAAA record is published, so a stale AAAA fails validation even while curl from your laptop — probably on IPv4 — works fine. Publish a correct AAAA or none at all.

Ports 80 and 443 need to be open in ufw and in your provider's network firewall — most hosting panels have a second firewall the OS never sees. HTTP-01 validates over port 80 specifically; you cannot run this 443-only.

sudo ufw allow "Apache Full"
sudo ufw status

With those in place the whole job is fifteen minutes, and ten of them are reading.

Snap or apt Certbot? On 24.04, apt is finally fine

Certbot moved to snap distribution years ago for a good reason: distro packages fossilized. Ubuntu 20.04 shipped Certbot 0.40 and never moved, and the project got tired of debugging five-year-old bugs. On 24.04 that reason is gone — the archive ships Certbot 2.9.0, a current-generation release, and unattended-upgrades keeps it patched. My recommendation for this OS: use apt. You skip the snapd daemon, the Apache plugin installs in the same transaction, and the renewal timer integrates with systemd the ordinary Debian way.

sudo apt update
sudo apt install -y certbot python3-certbot-apache
certbot --version

Correct result: certbot 2.9.0. The python3-certbot-apache package is the plugin that reads and edits your Apache configs — without it, certbot --apache fails with The requested apache plugin does not appear to be installed.

The snap is still the right call in two cases: you want the newest Certbot the day it ships, or you need a DNS plugin that is only distributed as a snap (several of the certbot-dns-* provider plugins are). If you go that way:

sudo apt remove -y certbot python3-certbot-apache
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Whichever you pick, never run both. Two installs mean two renewal schedulers fighting over /etc/letsencrypt, and the certbot your shell finds in PATH may not be the one that owns your certificates. The apt remove line above is not optional decoration.

The vhost Certbot edits must already exist — ServerName is the whole game

certbot --apache works by finding the port-80 virtual host whose ServerName or ServerAlias matches each -d domain you pass, proving control of the domain through it, then writing an SSL twin of that vhost. No matching ServerName, no match — and Ubuntu's default 000-default.conf ships with ServerName commented out. That single commented line is the most common reason this guide's one big command fails.

So before touching Certbot, give the site a proper name-based vhost. Create /etc/apache2/sites-available/example.com.conf:

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/example.com
    ErrorLog ${APACHE_LOG_DIR}/example.com-error.log
    CustomLog ${APACHE_LOG_DIR}/example.com-access.log combined
</VirtualHost>

Enable it and confirm Apache both parses it and routes the name to it:

sudo a2ensite example.com.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
sudo apache2ctl -S

configtest must print Syntax OK. If it also prints AH00558: apache2: Could not reliably determine the server's fully qualified domain name, that is a warning about the global ServerName, not your vhost — harmless here, and silenced by echo "ServerName $(hostname -f)" | sudo tee /etc/apache2/conf-available/servername.conf && sudo a2enconf servername && sudo systemctl reload apache2.

The -S output is the check that matters. You want a line like port 80 namevhost example.com (/etc/apache2/sites-enabled/example.com.conf:1) with alias www.example.com under it — Apache reports the sites-enabled symlink it actually read, not the file you edited in sites-available. If example.com is not listed against port 80, Certbot will not find it either.

Issue the certificate: certbot --apache

sudo certbot --apache -d example.com -d www.example.com

First run asks three things: an email address (expiry warnings go there — use a real one), agreement to the Let's Encrypt terms, and whether to share your email with the EFF. There is no redirect question anymore: since Certbot 2.0 the Apache installer redirects HTTP to HTTPS by default, which is what you want. Pass --no-redirect if you genuinely need plain HTTP to keep serving content.

Success looks like this, and you should read it rather than skim it:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/example.com/privkey.pem
This certificate expires on 2026-10-14.

Deploying certificate
Successfully deployed certificate for example.com to /etc/apache2/sites-available/example.com-le-ssl.conf
Successfully deployed certificate for www.example.com to /etc/apache2/sites-available/example.com-le-ssl.conf
Congratulations! You have successfully enabled HTTPS on https://example.com and https://www.example.com

Behind that message Certbot did four things: enabled Apache's ssl module if it was not already, wrote example.com-le-ssl.conf — a copy of your vhost on *:443 with SSLEngine on and the certificate paths — enabled it, and added a RewriteRule block to the original port-80 vhost that 301s everything to HTTPS. Your original vhost file is edited, not replaced, and the SSL twin sits beside it where you can read every line it added.

Where the certificate actually lives, and why you never copy it

Everything lands under /etc/letsencrypt/live/example.com/: fullchain.pem (the certificate plus intermediate chain — what servers should point at), privkey.pem (the private key, root-readable only), plus cert.pem and chain.pem for software that wants the pieces separately. These are symlinks into /etc/letsencrypt/archive/, and that indirection is the renewal mechanism: renewal writes new files into archive/ and repoints the symlinks. Point any other software at the live/ paths and it picks up renewals for free; copy the files somewhere and you have built yourself an outage 90 days out.

The other file worth knowing is /etc/letsencrypt/renewal/example.com.conf, which records how this certificate was issued — authenticator = apache, installer = apache, the domains — so renewal can repeat the process unattended, including reloading Apache afterward.

Renewal is already scheduled — verify it, do not build it

Let's Encrypt certificates last 90 days by design, and the apt package already installed the machinery: a systemd timer that runs Certbot twice a day at randomized times, renewing any certificate within 30 days of expiry. Do not add a cron job on top; a second scheduler adds nothing but log noise and rate-limit exposure.

systemctl list-timers certbot.timer
sudo certbot renew --dry-run

The first command shows the timer active, with a NEXT time somewhere in the coming 24 hours — the schedule is twice daily with a randomized delay, so the exact time is deliberately unpredictable (on the snap install, the timer is snap.certbot.renew.timer instead). The dry run performs a full renewal rehearsal against Let's Encrypt's staging environment — real challenge, no issued certificate, no rate-limit cost. Correct result ends with:

Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/example.com/fullchain.pem (success)

If the dry run fails, the real renewal in ~60 days will fail the same way — fix it now, while the current certificate still has its whole life ahead of it. The usual culprit is a firewall rule added after issuance that closed port 80 again.

Verify with curl, and what the padlock should say

curl -sI http://example.com | head -n 3
curl -I https://example.com
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -issuer -dates

The first should return HTTP/1.1 301 Moved Permanently with a Location: https://example.com/ header — that is the redirect Certbot installed. The second should return HTTP/1.1 200 OK with no TLS complaint from curl. The third prints the issuer — an O = Let's Encrypt line with a short CN like R12 or E7 — and notAfter roughly 90 days out. In a browser you get the padlock, and clicking it shows the same issuer. If curl works and the browser warns, you are almost certainly looking at a cached page or the wrong hostname, not a certificate problem.

Multiple sites: one SAN certificate or one cert per site

Both work; they renew the same way. For unrelated sites on the same box, run the issue command once per site — each gets its own directory under live/ and its own renewal config, and a problem with one domain never blocks renewing the others. That is my default.

For one site with several names, put them on one SAN certificate — a single cert can carry up to 100 names. You already did this above with example.com and www.example.com. To add a name to an existing certificate later, reissue naming the cert and the full new list:

sudo certbot --apache --cert-name example.com -d example.com -d www.example.com -d blog.example.com

Certbot notices the changed domain set, asks you to confirm the expansion, and replaces the certificate in place — same live/ path, so nothing else needs touching. Note that the list is a replacement, not an append: omit www from that command and the new certificate silently drops it.

Wildcards need DNS-01, and usually you do not need a wildcard

HTTP-01 cannot issue *.example.com — placing a file on a web server proves control of one hostname, not a whole namespace. Wildcards require the DNS-01 challenge: Certbot sets a TXT record at _acme-challenge.example.com, which in practice means a certbot-dns-* plugin with API credentials for your DNS provider, or hand-editing TXT records at every renewal with --manual (miserable — do not plan around it). The setup is provider-specific, so I will point you at the Certbot DNS plugin documentation rather than paraphrase it badly. Honest advice: if you have four known subdomains, a SAN certificate listing all four is simpler than a wildcard and needs no DNS API keys sitting on the server.

Failure modes, with the strings you will see

Certbot refuses to start because Apache's config is broken.

The apache plugin is not working; there may be problems with your existing configuration.
The error was: MisconfigurationError('Error while running apache2ctl configtest.\n\nAction \'configtest\' failed.\nThe Apache error log may have more information.\n\nAH00526: Syntax error on line 12 of /etc/apache2/sites-enabled/example.com.conf')

The plugin runs configtest before touching anything and bails if Apache itself is unhappy — the \ns are literal because Certbot prints the exception's repr. Run sudo apache2ctl configtest yourself: it names the file and line — typically a typo from hand-editing, a SSLCertificateFile pointing at a path that no longer exists, or a module referenced but not enabled. Fix until it prints Syntax OK, then rerun Certbot.

No vhost matches the domain.

Unable to find a virtual host listening on port 80 which is currently the only challenge port.

This is the missing-ServerName failure from earlier, caught at issue time. Certbot searched every enabled port-80 vhost for a ServerName/ServerAlias matching your -d and found nothing. sudo apache2ctl -S shows what Apache actually routes; add the ServerName line to the right vhost, reload, retry. A close cousin is validation reaching the wrong vhost — the challenge response comes back Invalid response ... 404 because another site caught the request. Same diagnosis, same tool: apache2ctl -S.

Validation times out.

Certbot failed to authenticate some domains (authenticator: apache).
...
Detail: ...: Timeout during connect (likely firewall problem)

Let's Encrypt could not open a TCP connection to port 80 at the address your DNS advertises. In order of likelihood: your provider's network firewall (separate from ufw, configured in the hosting panel), a ufw ruleset allowing only 443 or only SSH, DNS still pointing at a previous server, or the stale-AAAA problem — their servers tried IPv6, yours only answers on IPv4. Test from outside the VPS: curl -I http://example.com from your laptop reproduces what their validator sees.

You retried your way into a rate limit.

Error creating new order :: too many failed authorizations recently: see https://letsencrypt.org/docs/rate-limits/

Let's Encrypt allows 5 failed validations per hostname per account per hour — since their 2025 rate-limit rework it is a refilling bucket, earning back roughly one retry every 12 minutes — and hammering retry against a broken firewall burns through it fast. Waiting works, but the real fix is behavioral: after any failure, debug with the staging environment until it succeeds.

sudo certbot certonly --apache --dry-run -d example.com -d www.example.com

Note the certonly: --dry-run is only accepted by the certonly and renew subcommands, and the bare certbot --apache --dry-run form refuses to run at all, telling you --dry-run currently only works with the 'certonly' or 'renew' subcommands. The dry run validates against staging, which has its own generous limits and issues no real certificates, so you can fail there all afternoon. Only rerun the real command once staging passes. The other limits — 50 certificates per registered domain per week, 5 duplicates of the same name set per week — you will only meet if a script is reissuing in a loop.

Once HTTPS is up, remember the certificate secures the transport, not the server: port 22 is still taking password guesses all day. Pairing this with Fail2ban on Ubuntu 24.04 is the natural next thirty minutes.

FAQ

Should I install Certbot with snap or apt on Ubuntu 24.04?

Use apt. Ubuntu 24.04 ships Certbot 2.9.0, which is current enough for everything in this guide, gets security patches through unattended-upgrades, and does not require snapd. Choose the snap only if you need the newest release immediately or a DNS plugin distributed exclusively as a snap — and if you switch, apt remove certbot python3-certbot-apache first so two renewal schedulers never coexist.

Why does Certbot say "Unable to find a virtual host listening on port 80"?

Because no enabled port-80 vhost has a ServerName or ServerAlias matching the domain you passed with -d — Ubuntu's default vhost ships with ServerName commented out. Run sudo apache2ctl -S, find (or create) the vhost that should own the name, add ServerName example.com, reload Apache, and rerun Certbot.

How do I fix "Timeout during connect (likely firewall problem)"?

Let's Encrypt could not reach port 80 at the address your DNS publishes. Check your provider's panel-level network firewall as well as ufw, confirm dig +short example.com returns this VPS, and delete or correct any stale AAAA record — validation prefers IPv6 when one exists. Confirm the fix from outside the server with curl -I http://example.com, then rehearse with sudo certbot certonly --apache --dry-run -d example.com before real issuance.

Does Certbot renew certificates automatically on Ubuntu 24.04?

Yes. The apt package installs certbot.timer, a systemd timer that runs twice daily and renews any certificate within 30 days of expiry, reloading Apache afterward; the snap uses snap.certbot.renew.timer for the same job. Verify with systemctl list-timers certbot.timer and rehearse with sudo certbot renew --dry-run — do not add your own cron job on top.

How do I get a wildcard certificate with Certbot and Apache?

Wildcards require the DNS-01 challenge: Certbot must place a TXT record at _acme-challenge.example.com, which means a certbot-dns-* plugin with API credentials for your DNS provider (the --manual alternative needs hand-edited TXT records at every renewal). If you only have a handful of known subdomains, a SAN certificate listing them explicitly is simpler and keeps DNS API keys off the server.