fix: replace staging certificate on production switch

This commit is contained in:
bisco
2026-06-25 12:39:43 +02:00
parent 5bdf628a2e
commit a835900418
4 changed files with 42 additions and 4 deletions
+17 -2
View File
@@ -109,12 +109,18 @@ Use this checklist for the first run of a new environment.
LETSENCRYPT_STAGING=0 LETSENCRYPT_STAGING=0
``` ```
Then restart the affected services: Then recreate Certbot so it sees the new environment and restart the proxy:
```bash ```bash
docker compose up -d proxy certbot docker compose up -d --force-recreate certbot
docker compose up -d proxy
``` ```
If a staging certificate already exists, the Certbot entrypoint removes only that
staging certificate before requesting the production one. During the replacement
window, NGINX may briefly return `503` for application traffic and will recover as
soon as the production certificate is issued.
7. Final sanity checks: 7. Final sanity checks:
```bash ```bash
@@ -181,6 +187,15 @@ TRUST_PROXY_HEADERS=0
Before the first certificate exists, NGINX serves ACME challenges and returns 503 for Before the first certificate exists, NGINX serves ACME challenges and returns 503 for
application traffic. After issuance, HTTP redirects to HTTPS automatically. application traffic. After issuance, HTTP redirects to HTTPS automatically.
After validating the staging certificate flow, set `LETSENCRYPT_STAGING=0` and recreate
the Certbot container. Existing staging certificates are removed automatically before a
production certificate is requested:
```bash
docker compose up -d --force-recreate certbot
docker compose up -d proxy
```
Run the real bootstrap without the test override: Run the real bootstrap without the test override:
```bash ```bash
+6
View File
@@ -6,6 +6,7 @@ email="${LETSENCRYPT_EMAIL:-}"
staging="${LETSENCRYPT_STAGING:-1}" staging="${LETSENCRYPT_STAGING:-1}"
renew_interval="${LETSENCRYPT_RENEW_INTERVAL_SECONDS:-43200}" renew_interval="${LETSENCRYPT_RENEW_INTERVAL_SECONDS:-43200}"
retry_interval="${LETSENCRYPT_RETRY_SECONDS:-300}" retry_interval="${LETSENCRYPT_RETRY_SECONDS:-300}"
renewal_config="/etc/letsencrypt/renewal/${domain}.conf"
case "$domain" in "" | *[!A-Za-z0-9.-]* | .* | *. | *..*) echo "Invalid LETSENCRYPT_DOMAIN." >&2; exit 1 ;; esac case "$domain" in "" | *[!A-Za-z0-9.-]* | .* | *. | *..*) echo "Invalid LETSENCRYPT_DOMAIN." >&2; exit 1 ;; esac
[ -n "$email" ] || { echo "LETSENCRYPT_EMAIL is required when Let's Encrypt is enabled." >&2; exit 1; } [ -n "$email" ] || { echo "LETSENCRYPT_EMAIL is required when Let's Encrypt is enabled." >&2; exit 1; }
@@ -17,6 +18,11 @@ staging_argument=""
[ "$staging" = 0 ] || staging_argument="--staging" [ "$staging" = 0 ] || staging_argument="--staging"
trap 'exit 0' TERM INT trap 'exit 0' TERM INT
if [ "$staging" = 0 ] && [ -f "$renewal_config" ] && grep -qi 'acme-staging' "$renewal_config"; then
echo "Removing existing Let's Encrypt staging certificate for ${domain} before requesting a production certificate."
certbot delete --cert-name "$domain" --non-interactive
fi
while :; do while :; do
if certbot certonly --webroot --webroot-path /var/www/certbot --preferred-challenges http \ if certbot certonly --webroot --webroot-path /var/www/certbot --preferred-challenges http \
--cert-name "$domain" --domain "$domain" --email "$email" --agree-tos --non-interactive \ --cert-name "$domain" --domain "$domain" --email "$email" --agree-tos --non-interactive \
+4 -2
View File
@@ -44,8 +44,10 @@ caller and it overwrites `X-Forwarded-*` headers.
For direct TLS, public DNS and inbound ports 80/443 must reach NGINX. Start with For direct TLS, public DNS and inbound ports 80/443 must reach NGINX. Start with
`LETSENCRYPT_STAGING=1` and set `WP_URL` to the final HTTPS URL. Application HTTP `LETSENCRYPT_STAGING=1` and set `WP_URL` to the final HTTPS URL. Application HTTP
returns 503 until a certificate exists, then redirects to HTTPS. Switch to the returns 503 until a certificate exists, then redirects to HTTPS. Switch to the
production CA only after validating DNS and firewall behavior, removing only the production CA only after validating DNS and firewall behavior. When
staging certificate directory when necessary. `LETSENCRYPT_STAGING` changes from `1` to `0`, recreate the Certbot container; it
removes an existing staging certificate for the configured domain before requesting the
production certificate.
## State and rollback ## State and rollback
+15
View File
@@ -35,6 +35,21 @@ When Let's Encrypt is enabled, the proxy intentionally returns 503 for normal
application paths until a certificate exists. ACME challenge paths and proxy health application paths until a certificate exists. ACME challenge paths and proxy health
paths must still work in that pending state, otherwise Certbot will never start. paths must still work in that pending state, otherwise Certbot will never start.
## Staging certificate remains after switching to production
Certbot does not replace a still-valid staging certificate just because
`LETSENCRYPT_STAGING` changed. Recreate the Certbot container so it reads the updated
environment:
```bash
docker compose up -d --force-recreate certbot
docker compose up -d proxy
```
The entrypoint removes an existing staging certificate for `LETSENCRYPT_DOMAIN` before
requesting the production certificate. If issuance fails, inspect `docker compose logs
certbot proxy`, verify DNS and port 80, and avoid repeated production-CA retries.
## Bootstrap writes to the wrong data volume ## Bootstrap writes to the wrong data volume
Do not combine the test override with production bootstrap commands. This command writes Do not combine the test override with production bootstrap commands. This command writes