From a835900418fedb38fd053cd6fe58a648583aa34d Mon Sep 17 00:00:00 2001 From: bisco Date: Thu, 25 Jun 2026 12:39:43 +0200 Subject: [PATCH] fix: replace staging certificate on production switch --- README.md | 19 +++++++++++++++++-- certbot/renew.sh | 6 ++++++ docs/deployment.md | 6 ++++-- docs/runbook.md | 15 +++++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3fdd71c..7afca90 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,18 @@ Use this checklist for the first run of a new environment. LETSENCRYPT_STAGING=0 ``` - Then restart the affected services: + Then recreate Certbot so it sees the new environment and restart the proxy: ```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: ```bash @@ -181,6 +187,15 @@ TRUST_PROXY_HEADERS=0 Before the first certificate exists, NGINX serves ACME challenges and returns 503 for 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: ```bash diff --git a/certbot/renew.sh b/certbot/renew.sh index 67d8d86..354de4d 100644 --- a/certbot/renew.sh +++ b/certbot/renew.sh @@ -6,6 +6,7 @@ email="${LETSENCRYPT_EMAIL:-}" staging="${LETSENCRYPT_STAGING:-1}" renew_interval="${LETSENCRYPT_RENEW_INTERVAL_SECONDS:-43200}" 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 [ -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" 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 if certbot certonly --webroot --webroot-path /var/www/certbot --preferred-challenges http \ --cert-name "$domain" --domain "$domain" --email "$email" --agree-tos --non-interactive \ diff --git a/docs/deployment.md b/docs/deployment.md index cfa0b8e..48249d0 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -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 `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 -production CA only after validating DNS and firewall behavior, removing only the -staging certificate directory when necessary. +production CA only after validating DNS and firewall behavior. When +`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 diff --git a/docs/runbook.md b/docs/runbook.md index d8cea0b..bfc6e7c 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -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 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 Do not combine the test override with production bootstrap commands. This command writes