From 237aa913f0e7940349634cb1e612de3c0ba832ab Mon Sep 17 00:00:00 2001 From: William Gill Date: Wed, 16 Sep 2026 14:21:35 -0500 Subject: [PATCH] fix(webfinger): apply values passed to a re-run instead of discarding them .env was written only on the first run. All four values reach Caddy solely through compose interpolating .env into the container environment -- the Caddyfile is installed verbatim every run and carries Caddy's own {$ISSUER_URL} / {$BASE_DOMAIN} / {$REDIRECT_URL} / {$ACME_EMAIL} placeholders, resolved at config load. So .env is the only thing that decides what is served. Both paths were wrong. Interactively, the prompts are required and have no default, so a re-run made the operator retype all four -- and then discarded every one, because a value read into a nameref is not exported and .env was not rewritten. Via automations.sh the values ARE exported, so compose preferred them for that run only and the documented later `docker compose up -d` reverted to stale .env. Either way the run exits 0 and the DEPLOYED banner prints this shell's values, not what was deployed. Moving the IdP from auth.example.com to id.example.com leaves the endpoint serving "href": the old issuer, pointing every discovering client at a decommissioned issuer, with the deploy that was meant to fix it reporting success. Same shape as d654299: record which keys arrived in the environment before the ":=" defaults, pre-load .env into keys not passed so the prompts stop demanding a full retype (and SKIP_PROMPTS=1 no longer dies over values .env already has), then write only the passed keys through with set_env. Verified: a corrected ISSUER_URL containing a & query string lands in .env intact (the old escaping would have corrupted it); a re-run passing nothing leaves .env byte-identical and does not die. Co-Authored-By: Claude Opus 5 --- deployments/webfinger/deploy.sh | 67 ++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/deployments/webfinger/deploy.sh b/deployments/webfinger/deploy.sh index 550259a..d0261f3 100644 --- a/deployments/webfinger/deploy.sh +++ b/deployments/webfinger/deploy.sh @@ -9,7 +9,9 @@ # What this does: # 1. Installs docker + docker-cli-compose if missing. # 2. Lays down docker-compose.yml, Caddyfile, .env.example in $STACK_DIR. -# 3. Generates .env on first run; existing .env is never overwritten. +# 3. Generates .env on first run. On a re-run an existing .env is kept, +# except for values passed explicitly that run, which are written +# through -- .env is what compose interpolates into the container. # 4. Prompts for required values not preset (BASE_DOMAIN, ISSUER_URL, # REDIRECT_URL, ACME_EMAIL). # 5. Opens TCP 80/443 in UFW if active. @@ -36,6 +38,18 @@ set -euo pipefail : "${FORCE:=0}" : "${SKIP_PROMPTS:=0}" # non-interactive: require values via env, no prompts [[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1 +ENV_FILE="$STACK_DIR/.env" + +# All four values reach Caddy only by compose interpolating .env, so .env has to absorb +# what this run passed or the run deploys the old values while reporting the +# new ones. Which keys arrived in the ENVIRONMENT must be recorded BEFORE the +# ":=" defaults below, which make an unset variable look like an empty one. +RUNTIME_KEYS=(BASE_DOMAIN ISSUER_URL REDIRECT_URL ACME_EMAIL) +KEYS_FROM_ENV=() +for _k in "${RUNTIME_KEYS[@]}"; do + if [[ -n "${!_k+x}" ]]; then KEYS_FROM_ENV+=("$_k"); fi +done + : "${BASE_DOMAIN:=}" : "${ISSUER_URL:=}" : "${REDIRECT_URL:=}" @@ -148,6 +162,24 @@ prompt() { fi } +# On a re-run the deployed values live in .env. Load them into any key NOT +# passed this run, so the prompts below do not force the operator to retype +# everything (and so SKIP_PROMPTS=1 does not die demanding values .env has). +if [[ -f "$ENV_FILE" ]]; then + _preloaded=() + for _k in "${RUNTIME_KEYS[@]}"; do + if [[ " ${KEYS_FROM_ENV[*]-} " == *" ${_k} "* ]]; then continue; fi + _v=$(sed -n "s/^${_k}=//p" "$ENV_FILE" | tail -n1) + if [[ -z "$_v" ]]; then continue; fi + printf -v "$_k" '%s' "$_v" + _preloaded+=("$_k") + done + if (( ${#_preloaded[@]} > 0 )); then + log "Reusing ${ENV_FILE} values for: ${_preloaded[*]}" + log " (pass VAR=... to change one; it is then written back to .env)" + fi +fi + prompt BASE_DOMAIN "Apex domain to serve from (e.g. example.com)" prompt ISSUER_URL "OIDC issuer URL (e.g. https://auth.example.com)" prompt REDIRECT_URL "Where to redirect non-webfinger traffic (e.g. https://example.org)" @@ -171,6 +203,28 @@ install -d -m 0750 "$STACK_DIR" install -m 0640 "$SCRIPT_DIR/docker-compose.yml" "$STACK_DIR/docker-compose.yml" install -m 0640 "$SCRIPT_DIR/Caddyfile" "$STACK_DIR/Caddyfile" +set_env() { # : update KEY in .env, or append if absent + # The value goes through the ENVIRONMENT, never interpolated into a sed + # script. Interpolating it corrupts any value containing & (sed expands it to + # the whole match) and aborts the run on one containing the s||| delimiter -- + # which is reachable for an OIDC issuer URL or a redirect query string. + local key="$1" val="$2" tmp + if [[ ! -f "$ENV_FILE" ]]; then + printf '%s=%s +' "$key" "$val" >> "$ENV_FILE" + return 0 + fi + tmp="$(mktemp)" + _SE_KEY="$key" _SE_VAL="$val" awk ' + BEGIN { k = ENVIRON["_SE_KEY"]; v = ENVIRON["_SE_VAL"]; seen = 0 } + !seen && index($0, k "=") == 1 { print k "=" v; seen = 1; next } + { print } + END { if (!seen) print k "=" v } + ' "$ENV_FILE" > "$tmp" + cat "$tmp" > "$ENV_FILE" # rewrite in place: keeps the original mode/owner + rm -f "$tmp" +} + ENV_FILE="$STACK_DIR/.env" if [[ ! -f "$ENV_FILE" ]]; then log "Seeding $ENV_FILE..." @@ -182,7 +236,16 @@ if [[ ! -f "$ENV_FILE" ]]; then -e "s|^ACME_EMAIL=.*|ACME_EMAIL=${ACME_EMAIL}|" \ "$ENV_FILE" else - log ".env exists; leaving it alone." + # Keep the file, but absorb what was passed this run: compose reads .env, + # so letting it drift means deploying the old values while the banner + # prints the new ones. Keys not passed are left untouched. + log ".env exists; keeping it (only values passed this run are updated)." + for _k in ${KEYS_FROM_ENV[@]+"${KEYS_FROM_ENV[@]}"}; do + _cur=$(sed -n "s/^${_k}=//p" "$ENV_FILE" | tail -n1) + if [[ "$_cur" == "${!_k}" ]]; then continue; fi + set_env "$_k" "${!_k}" + log " ${_k}: ${_cur:-} -> ${!_k:-}" + done fi # Validate required values are present.