Certbot

Posted on 7 2026

The Dehydrated article covered the dns-01 challenge path: wildcard certificates, multi-domain certificates, services without port 80. Certbot covers the other case. For a service like a mail server that has a single public hostname, accepts SMTP connections, and does not run a web server of its own, certbot is the more straightforward tool.

The distinction in this series is:

  • Dehydrated manages wildcard and multi-domain certificates via dns-01 challenge, and handles reload hooks across multiple services.
  • Certbot manages individual certificates for specific services, using HTTP-01 or the standalone authenticator when no web server is present.

Both produce Let’s Encrypt certificates. The certificates are identical in format and can be used interchangeably.

Installation

The EFF recommends installing certbot via snap rather than apt. The reason is straightforward: the Ubuntu apt repositories carry a version of certbot that lags behind the upstream releases, sometimes significantly. The snap version stays current automatically.

Remove any previously installed apt version first to avoid conflicts:

sudo apt remove certbot python3-certbot-nginx python3-certbot-apache

Install via snap:

sudo snap install core
sudo snap refresh core
sudo snap install --classic certbot

Create the symlink so certbot is available as a standard command:

sudo ln -sf /snap/bin/certbot /usr/bin/certbot

Confirm the installation:

certbot --version

The two authenticator modes

Certbot needs to prove to Let’s Encrypt that you control the domain before it will issue a certificate. It does this via a challenge, and the challenge method depends on what is running on the server.

Webroot authenticator — if nginx or another web server is already running on port 80, certbot can place a challenge file in a directory that nginx serves. This is the cleanest approach when a web server is present. The nginx plugin (python3-certbot-nginx) can handle this automatically, but since this series keeps nginx configuration under manual control, the webroot mode is preferable.

Standalone authenticator — if no web server is running on port 80, certbot briefly starts its own minimal listener to answer the challenge. This is the mode used for the mail server, which does not run nginx.

Webroot mode (for services with nginx)

Before certbot can use webroot mode, nginx must serve the challenge directory. Add this location block to the nginx HTTP server block for each domain:

server {
    listen 80;
    server_name nextcloud.yourdomain.net;

    location /.well-known/acme-challenge/ {
        root /var/www/acme-challenge;
    }

    location / {
        return 301 https://$server_name$request_uri;
    }
}

Create the challenge directory:

sudo mkdir -p /var/www/acme-challenge
sudo chown www-data:www-data /var/www/acme-challenge

Obtain a certificate using webroot:

sudo certbot certonly \
    --webroot \
    --webroot-path /var/www/acme-challenge \
    --domain nextcloud.yourdomain.net \
    --email you@yourdomain.net \
    --agree-tos \
    --no-eff-email

The --no-eff-email flag opts out of the EFF newsletter. The --agree-tos flag accepts the Let’s Encrypt terms of service.

Standalone mode (for services without nginx)

Standalone mode requires port 80 to be free. If a firewall is running, open port 80 temporarily before the request and close it after.

If using ufw:

sudo ufw allow 80/tcp

Obtain the certificate:

sudo certbot certonly \
    --standalone \
    --domain mail.yourdomain.net \
    --email you@yourdomain.net \
    --agree-tos \
    --no-eff-email

Close the port again:

sudo ufw deny 80/tcp

For a mail server that never opens port 80 except for certificate renewal, automate this with certbot’s pre and post hooks.

Firewall hooks for standalone mode

Certbot supports hooks that run before and after the renewal process. Create pre and post hook scripts to manage the firewall automatically:

sudo tee /etc/letsencrypt/renewal-hooks/pre/open-port-80.sh << 'EOF'
#!/usr/bin/env bash
# Open port 80 for ACME challenge during certificate renewal
echo "Opening port 80 for certificate renewal"
ufw allow 80/tcp
EOF

sudo tee /etc/letsencrypt/renewal-hooks/post/close-port-80.sh << 'EOF'
#!/usr/bin/env bash
# Close port 80 after ACME challenge
echo "Closing port 80 after certificate renewal"
ufw deny 80/tcp
EOF

sudo chmod 0755 /etc/letsencrypt/renewal-hooks/pre/open-port-80.sh
sudo chmod 0755 /etc/letsencrypt/renewal-hooks/post/close-port-80.sh

These hooks run for every certificate renewal, whether the certificate needed renewing or not. The deny in the post hook adds a deny rule rather than removing the allow rule; run ufw status periodically to confirm the ruleset remains tidy.

Reload hooks

After renewal, services using the certificate need to reload. Add a deploy hook that fires only when a certificate is actually renewed (not on every run):

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh << 'EOF'
#!/usr/bin/env bash
# Reload services after certificate renewal

# Reload nginx if running
if systemctl is-active --quiet nginx; then
    echo "Reloading nginx"
    systemctl reload nginx
fi

# Reload Postfix if running
if systemctl is-active --quiet postfix; then
    echo "Reloading postfix"
    systemctl reload postfix
fi

# Reload Dovecot if running
if systemctl is-active --quiet dovecot; then
    echo "Reloading dovecot"
    systemctl reload dovecot
fi
EOF

sudo chmod 0755 /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh

The deploy hook only runs when certbot actually issues or renews a certificate. The pre and post hooks run on every renewal attempt, whether a new certificate was issued or not.


Certificate paths

Certbot stores certificates at /etc/letsencrypt/live/{domain}/:

/etc/letsencrypt/live/mail.yourdomain.net/
├── cert.pem        # The certificate
├── chain.pem       # The intermediate CA chain
├── fullchain.pem   # cert.pem + chain.pem (use this in service config)
└── privkey.pem     # The private key

These are symlinks to the current version in /etc/letsencrypt/archive/. Do not reference the archive paths directly; always use the live symlinks.

In Postfix:

smtpd_tls_chain_files = /etc/letsencrypt/live/mail.yourdomain.net/fullchain.pem

In Dovecot:

ssl_cert = </etc/letsencrypt/live/mail.yourdomain.net/fullchain.pem
ssl_key  = </etc/letsencrypt/live/mail.yourdomain.net/privkey.pem

In nginx:

ssl_certificate     /etc/letsencrypt/live/nextcloud.yourdomain.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/nextcloud.yourdomain.net/privkey.pem;

Automatic renewal

The snap installation registers a systemd timer that runs certbot twice daily. Check it:

sudo systemctl status snap.certbot.renew.timer

Certificates are only renewed when they have fewer than 30 days remaining. The twice-daily cadence means certificates renew well before expiry even if one run fails.

Test that renewal would succeed:

sudo certbot renew --dry-run

A successful dry run confirms the authenticator, firewall hooks, and reload hooks are all working without actually obtaining a new certificate.

Listing certificates

See all certificates certbot is managing:

sudo certbot certificates

Output shows each certificate’s domain, expiry date, and the path to its files. Check this periodically to confirm renewal is happening and no certificate has quietly expired.

Key type

Certbot defaults to ECDSA keys (P-256) for new certificates since version 2.0. This is the correct default. If for some reason an RSA certificate is needed (older mail servers that do not support ECDSA), specify it explicitly:

sudo certbot certonly \
    --standalone \
    --key-type rsa \
    --domain mail.yourdomain.net \
    --email you@yourdomain.net \
    --agree-tos \
    --no-eff-email

The source material for this series requested both RSA and ECDSA certificates for mail servers to maximise compatibility. In 2026 this is no longer necessary: ECDSA support is universal among mail servers still in active use. A single ECDSA certificate is sufficient.

Coexisting with dehydrated

Certbot and dehydrated manage separate certificates and do not conflict. The split to maintain:

  • Certbot manages certificates for single services where the domain has a public DNS record and port 80 is available (even briefly) for HTTP-01 challenge.
  • Dehydrated manages wildcard certificates and any domain where dns-01 is preferable or necessary.

Do not let both tools manage the same domain. If a domain is in dehydrated’s domains.txt, do not also request it via certbot.