#!/usr/bin/env sh set -eu # First deployment flow: # 1. Validate nginx configs and ACME HTTP-01 challenge locations. # 2. Create temporary dummy certificates for missing certificate files. # 3. Start OpenResty so HTTP-01 challenge files can be served. # 4. Verify that challenge files are reachable locally and via domain HTTP. # 5. Remove only the dummy certificates created or reused by this run. # 6. Request real Let's Encrypt certificates. # 7. Restart OpenResty so it loads the real certificates. # 8. Optionally install the renewal cron job. CERT_ROOT="./certs/live" ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" CONF_ROOT="${CONF_ROOT:-./conf}" CONF_DIR="${CONF_DIR:-./conf/conf.d}" CREATE_MISSING_CONF="${CREATE_MISSING_CONF:-0}" INSTALL_RENEW_CRON="${INSTALL_RENEW_CRON:-1}" SKIP_PUBLIC_HTTP_CHECK="${SKIP_PUBLIC_HTTP_CHECK:-0}" DUMMY_DOMAINS="" HTTP_PROBE_DIR="./www/.well-known/acme-challenge" HTTP_PROBE_TOKEN="init-certs-probe-$$" HTTP_PROBE_PATH=".well-known/acme-challenge/$HTTP_PROBE_TOKEN" HTTP_PROBE_VALUE="openresty-gateway-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" echo "Checking local HTTP-01 route for $domain..." if ! body="$(curl -fsS --max-time 5 --resolve "$domain:80:127.0.0.1" "$url" 2>&1)"; then echo "Error: OpenResty is not serving the challenge path for $domain on local port 80." >&2 echo "Tried: $url via 127.0.0.1" >&2 echo "$body" >&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 --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 "Check DNS, cloud security group, host firewall, and whether OpenResty is listening on 0.0.0.0:80." >&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 } while [ "$#" -gt 0 ]; do case "$1" in --create-missing-conf) CREATE_MISSING_CONF=1 ;; --skip-renew-cron) INSTALL_RENEW_CRON=0 ;; -h|--help) usage exit 0 ;; *) echo "Error: unknown argument: $1" >&2 usage >&2 exit 1 ;; esac shift done if [ -z "${DOMAINS:-}" ]; then echo "Error: DOMAINS is required. Set it in init-certs.sh before calling init-certs-core.sh." >&2 exit 1 fi if [ -z "${CERT_EMAIL:-}" ]; then echo "Error: CERT_EMAIL is required. Set it in init-certs.sh before calling init-certs-core.sh." >&2 exit 1 fi conf_error=0 for domain in $DOMAINS; do if ! conf_file="$(find_domain_conf "$domain")"; then if is_enabled "$CREATE_MISSING_CONF"; then CONF_DIR="$CONF_DIR" sh "$ROOT_DIR/scripts/ensure-domain-conf.sh" "$domain" conf_file="$CONF_DIR/$domain.conf" else echo "Error: missing nginx config: $CONF_DIR/$domain.conf" >&2 echo "Hint: create the config or run sh init-certs.sh --create-missing-conf." >&2 conf_error=1 continue fi fi if ! grep -Fq "/.well-known/acme-challenge/" "$conf_file"; then echo "Error: nginx config lacks ACME challenge location: $conf_file" >&2 echo "Hint: add location ^~ /.well-known/acme-challenge/ with root /var/www." >&2 conf_error=1 fi done if [ -d "$CONF_ROOT" ]; then ssl_domains="$( find "$CONF_ROOT" -type f -name '*.conf' | while IFS= read -r conf_file; do [ -f "$conf_file" ] || continue sed -n 's#^[[:space:]]*ssl_certificate[[:space:]][[:space:]]*/etc/letsencrypt/live/\([^/][^/]*\)/fullchain\.pem;.*#\1#p' "$conf_file" done | sort -u )" else echo "Error: nginx config scan directory does not exist: $CONF_ROOT" >&2 ssl_domains="" conf_error=1 fi for domain in $ssl_domains; do if ! domain_in_list "$domain"; then echo "Error: nginx config references a certificate domain not listed in DOMAINS: $domain" >&2 echo "Hint: add $domain to init-certs.sh DOMAINS or remove the ssl_certificate config." >&2 conf_error=1 fi done if [ "$conf_error" -ne 0 ]; then exit 1 fi . "$ROOT_DIR/scripts/lib-compose.sh" export DOMAINS CERT_ROOT DUMMY_DOMAINS="$(sh "$ROOT_DIR/scripts/ensure-dummy-certs.sh")" echo "Starting OpenResty with available certificates..." compose up -d openresty echo "Validating OpenResty configuration inside the container..." compose exec -T openresty openresty -t check_http_challenge if [ -n "$DUMMY_DOMAINS" ]; then echo "Removing dummy certificate files before requesting real certificates..." 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 fi if ! sh "$ROOT_DIR/scripts/request-certs.sh"; then echo "Certificate request failed; restoring dummy certificates for missing domains." >&2 if ! sh "$ROOT_DIR/scripts/ensure-dummy-certs.sh" >/dev/null; then echo "Warning: failed to restore dummy certificates. Check $CERT_ROOT manually." >&2 fi exit 1 fi echo "Restarting OpenResty to load real certificates..." compose restart openresty if is_enabled "$INSTALL_RENEW_CRON"; then if command -v crontab >/dev/null 2>&1; then echo "Installing certificate renewal cron..." sh "$ROOT_DIR/scripts/install-renew-cron.sh" else echo "Warning: crontab is missing; skipped renewal cron installation." >&2 echo "Install crontab and run sh scripts/install-renew-cron.sh later if needed." >&2 fi else echo "Skipped certificate renewal cron installation." fi echo "Done."