#!/usr/bin/env sh set -eu # Add certificates for new domains on an already running OpenResty gateway. # This flow is separate from init-certs-core.sh and never restarts containers. # Order matters: # 1. Ensure nginx configs exist for the new domains. # 2. Create temporary dummy certificates for SSL configs that do not have certs yet. # 3. Reload OpenResty so the new HTTP-01 challenge routes are active. # 4. Probe the challenge routes, request real certificates, then reload again. CERT_ROOT="./certs/live" ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" CONF_DIR="${CONF_DIR:-./conf/conf.d}" SKIP_PUBLIC_HTTP_CHECK="${SKIP_PUBLIC_HTTP_CHECK:-0}" DUMMY_DOMAINS="" HTTP_PROBE_DIR="./www/.well-known/acme-challenge" HTTP_PROBE_TOKEN="add-domain-certs-probe-$$" HTTP_PROBE_PATH=".well-known/acme-challenge/$HTTP_PROBE_TOKEN" HTTP_PROBE_VALUE="openresty-gateway-add-domain-probe-$$" cd "$ROOT_DIR" usage() { cat </dev/null 2>&1; then echo "Warning: curl is missing; skipping HTTP-01 reachability probe." >&2 return 0 fi mkdir -p "$HTTP_PROBE_DIR" printf '%s' "$HTTP_PROBE_VALUE" > "$HTTP_PROBE_DIR/$HTTP_PROBE_TOKEN" trap cleanup_http_probe EXIT HUP INT TERM for domain in $DOMAINS; do url="http://$domain/$HTTP_PROBE_PATH" local_url="http://127.0.0.1/$HTTP_PROBE_PATH" echo "Checking local HTTP-01 route for $domain..." if ! body="$(curl -fsS --noproxy '*' --max-time 5 -H "Host: $domain" "$local_url" 2>&1)"; then echo "Error: OpenResty is not serving the challenge path for $domain on local port 80." >&2 echo "Tried: $local_url with Host: $domain" >&2 echo "$body" >&2 echo "Hint: an empty reply usually means the request hit the default deny server, so verify OpenResty loaded the domain config." >&2 return 1 fi if [ "$body" != "$HTTP_PROBE_VALUE" ]; then echo "Error: local challenge response mismatch for $domain." >&2 echo "Expected: $HTTP_PROBE_VALUE" >&2 echo "Got: $body" >&2 return 1 fi if is_enabled "$SKIP_PUBLIC_HTTP_CHECK"; then echo "Skipping public HTTP-01 route check for $domain." continue fi echo "Checking public HTTP-01 route for $domain..." if ! body="$(curl -fsS --noproxy '*' --max-time 10 "$url" 2>&1)"; then echo "Error: $domain is not reachable on public HTTP port 80." >&2 echo "Tried: $url" >&2 echo "$body" >&2 echo "If this host cannot hairpin to its public IP but external clients can, rerun with SKIP_PUBLIC_HTTP_CHECK=1." >&2 return 1 fi if [ "$body" != "$HTTP_PROBE_VALUE" ]; then echo "Error: public challenge response mismatch for $domain." >&2 echo "Expected: $HTTP_PROBE_VALUE" >&2 echo "Got: $body" >&2 return 1 fi done } reload_openresty() { echo "Validating OpenResty configuration..." compose exec -T openresty openresty -t echo "Reloading OpenResty without restart..." compose exec -T openresty openresty -s reload } remove_dummy_certs() { for domain in $DUMMY_DOMAINS; do cert_dir="$CERT_ROOT/$domain" rm -f "$cert_dir/fullchain.pem" "$cert_dir/privkey.pem" "$cert_dir/.dummy-init-certs" rmdir "$cert_dir" 2>/dev/null || true done } while [ "$#" -gt 0 ]; do case "$1" in --skip-public-http-check) SKIP_PUBLIC_HTTP_CHECK=1 ;; -h|--help) usage exit 0 ;; *) echo "Error: unknown argument: $1" >&2 usage >&2 exit 1 ;; esac shift done domain_count=0 for domain in $DOMAINS; do domain_count=$((domain_count + 1)) done if [ "$domain_count" -eq 0 ]; then echo "Error: DOMAINS is required. Set it in add-domain-certs.sh." >&2 exit 1 fi if [ -z "${CERT_EMAIL:-}" ]; then echo "Error: CERT_EMAIL is required. Set it in add-domain-certs.sh." >&2 exit 1 fi . "$ROOT_DIR/scripts/lib-compose.sh" echo "Validating Docker Compose configuration..." compose config >/dev/null echo "Checking that OpenResty is already running..." compose exec -T openresty openresty -v >/dev/null conf_error=0 for domain in $DOMAINS; do if ! conf_file="$(find_domain_conf "$domain")"; then CONF_DIR="$CONF_DIR" sh "$ROOT_DIR/scripts/ensure-domain-conf.sh" "$domain" conf_file="$CONF_DIR/$domain.conf" fi if ! grep -Fq "/.well-known/acme-challenge/" "$conf_file"; then echo "Error: nginx config lacks ACME challenge location: $conf_file" >&2 conf_error=1 fi done if [ "$conf_error" -ne 0 ]; then exit 1 fi export DOMAINS CERT_ROOT echo "Ensuring temporary certificates exist before loading any new SSL configs..." DUMMY_DOMAINS="$(sh "$ROOT_DIR/scripts/ensure-dummy-certs.sh")" echo "Reloading OpenResty so new domain configs can serve HTTP-01 challenges..." reload_openresty check_http_challenge if [ -n "$DUMMY_DOMAINS" ]; then echo "Removing dummy certificate files before requesting real certificates..." remove_dummy_certs fi if ! sh "$ROOT_DIR/scripts/request-certs.sh"; then echo "Certificate request failed; restoring dummy certificates." >&2 sh "$ROOT_DIR/scripts/ensure-dummy-certs.sh" >/dev/null || true reload_openresty >/dev/null 2>&1 || true exit 1 fi reload_openresty echo "Done."