289 lines
8.6 KiB
Bash
289 lines
8.6 KiB
Bash
#!/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 <<EOF
|
|
Usage: sh init-certs.sh [options]
|
|
|
|
Options:
|
|
--create-missing-conf Generate a minimal nginx config for missing domains
|
|
--skip-renew-cron Do not install the certificate renewal cron job
|
|
-h, --help Show this help
|
|
|
|
Environment:
|
|
CREATE_MISSING_CONF=1 Same as --create-missing-conf
|
|
INSTALL_RENEW_CRON=0 Same as --skip-renew-cron
|
|
CONF_ROOT=./conf Directory tree to scan for nginx configs
|
|
CONF_DIR=./conf/conf.d Directory for per-domain nginx configs
|
|
SKIP_PUBLIC_HTTP_CHECK=1 Skip public domain HTTP challenge probe
|
|
EOF
|
|
}
|
|
|
|
is_enabled() {
|
|
case "${1:-}" in
|
|
1|true|TRUE|yes|YES|on|ON)
|
|
return 0
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
domain_in_list() {
|
|
needle="$1"
|
|
|
|
for item in $DOMAINS; do
|
|
if [ "$item" = "$needle" ]; then
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
domain_conf_matches() {
|
|
domain="$1"
|
|
conf_file="$2"
|
|
|
|
awk -v domain="$domain" '
|
|
/^[[:space:]]*server_name[[:space:]]/ {
|
|
for (i = 2; i <= NF; i++) {
|
|
name = $i
|
|
sub(/;$/, "", name)
|
|
if (name == domain) {
|
|
found = 1
|
|
}
|
|
}
|
|
}
|
|
END {
|
|
exit found ? 0 : 1
|
|
}
|
|
' "$conf_file"
|
|
}
|
|
|
|
find_domain_conf() {
|
|
domain="$1"
|
|
exact_conf_file="$CONF_DIR/$domain.conf"
|
|
|
|
if [ -f "$exact_conf_file" ]; then
|
|
echo "$exact_conf_file"
|
|
return 0
|
|
fi
|
|
|
|
for conf_file in "$CONF_DIR"/*.conf; do
|
|
[ -f "$conf_file" ] || continue
|
|
if domain_conf_matches "$domain" "$conf_file"; then
|
|
echo "$conf_file"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
cleanup_http_probe() {
|
|
rm -f "$HTTP_PROBE_DIR/$HTTP_PROBE_TOKEN"
|
|
}
|
|
|
|
check_http_challenge() {
|
|
if ! command -v curl >/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."
|