SSL Certificate Renewal Notes

SSL Certificate Renewal Notes

This note records a simple SSL certificate workflow with certbot: acquire, verify, renew, and deploy.

The example domain is api.example.com. Replace it with your own domain.

Acquire A Certificate

For a standalone HTTP-01 challenge, port 80 must be reachable and not occupied by another process:

1
sudo certbot certonly --standalone -d api.example.com

If Caddy already owns port 80, first consider letting Caddy manage its own certificates, or using DNS-01 / a properly served webroot. The standalone alternative below causes downtime and is only for this port-conflict branch, not every service on the host.

Create certbot-caddy-stop.sh in your working directory:

1
2
3
4
5
6
7
#!/usr/bin/env bash
set -euo pipefail
if systemctl is-active --quiet caddy; then
install -d -o root -g root -m 0700 /run/certbot-caddy-renewal
touch /run/certbot-caddy-renewal/was-active
systemctl stop caddy
fi

Create certbot-caddy-start.sh:

1
2
3
4
5
6
#!/usr/bin/env bash
set -euo pipefail
if [[ -f /run/certbot-caddy-renewal/was-active ]]; then
systemctl start caddy
rm -f /run/certbot-caddy-renewal/was-active
fi

Install the inspected scripts as root-owned hooks, then request the certificate with those hooks:

1
2
3
4
5
6
sudo install -o root -g root -m 0755 certbot-caddy-stop.sh /usr/local/sbin/certbot-caddy-stop
sudo install -o root -g root -m 0755 certbot-caddy-start.sh /usr/local/sbin/certbot-caddy-start
sudo certbot certonly --standalone -d api.example.com \
--pre-hook /usr/local/sbin/certbot-caddy-stop \
--post-hook /usr/local/sbin/certbot-caddy-start
sudo systemctl status caddy

The marker restores Caddy only if it was active before the attempt. Certbot normally runs the post-hook after renewal attempts even when validation fails; a killed process or host failure can still prevent it. After an interrupted run, inspect Caddy and the marker and restore service manually if needed. Serialize renewal jobs using this pair; the shared marker is not designed for concurrent Certbot instances. Hook files and all ancestor directories must not be writable by unprivileged users.

Check The Certificate

1
2
3
4
sudo certbot certificates
sudo openssl x509 \
-in /etc/letsencrypt/live/api.example.com/fullchain.pem \
-noout -dates

Enable Renewal

1
2
sudo systemctl enable --now certbot.timer
systemctl list-timers | grep certbot

For the Caddy-conflict branch, check that the certificate’s /etc/letsencrypt/renewal/api.example.com.conf retains the standalone authenticator and the intended pre/post hook paths before relying on the timer. An existing certificate obtained without these hooks needs its renewal configuration updated using the installed Certbot version’s supported reconfiguration workflow. Keep the hooks off lineages that do not require this downtime.

Run a dry test (the pre/post hooks may stop and restart Caddy):

1
sudo certbot renew --dry-run

Deploy Hook

Some services cannot read directly from /etc/letsencrypt/live/..., or they need certificate files copied into a service-owned directory.

Example hook for a service called myapp. Confirm that the actual service identity is a member of group myapp and that all parent directories allow traversal by that group. Here the directory remains root-owned, with read/traverse access for the service group; the private key is never world-readable. Replace the domain/group/service names for your deployment. Save the following as deploy-myapp-cert.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env bash
set -euo pipefail

DOMAIN="api.example.com"
TARGET_DIR="/etc/myapp/cert"
SERVICE_GROUP="myapp"

# Deploy only the lineage that this service uses.
case " ${RENEWED_DOMAINS:-} " in
*" $DOMAIN "*) ;;
*) exit 0 ;;
esac
LINEAGE="${RENEWED_LINEAGE:?Certbot must provide RENEWED_LINEAGE}"
getent group "$SERVICE_GROUP" >/dev/null

install -d -o root -g "$SERVICE_GROUP" -m 0750 "$TARGET_DIR"
install -o root -g "$SERVICE_GROUP" -m 0644 "$LINEAGE/fullchain.pem" "$TARGET_DIR/fullchain.pem"
install -o root -g "$SERVICE_GROUP" -m 0640 "$LINEAGE/privkey.pem" "$TARGET_DIR/privkey.pem"

systemctl reload myapp

Install it as a deploy hook:

1
2
sudo install -o root -g root -m 0755 deploy-myapp-cert.sh \
/etc/letsencrypt/renewal-hooks/deploy/myapp.sh

Verify the directory/key owner, group and mode, then test certificate reads as the actual service identity. A successful copy does not prove the service can read the key or supports reload. Plain renew --dry-run does not normally run deploy hooks; use the installed version’s --run-deploy-hooks support deliberately (it can reload the real service). This example does not provide atomic replacement of the certificate/key pair; coordinate readers or use the service’s documented deployment mechanism when concurrent reads matter. No live issuance, renewal or myapp reload was tested here.