Files
openresty-gateway/scripts/init-certs-core.sh
2026-05-18 22:32:08 +08:00

247 lines
8.2 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env sh
set -eu
# 首次部署用:
# 1. 校验每个域名都有 nginx 配置,且配置里放行 ACME HTTP-01 校验路径。
# 2. 为缺失的证书路径生成 1 天有效期的自签 dummy 证书,避免 OpenResty 因证书文件不存在而启动失败。
# 3. 启动 OpenResty让 certbot 的 webroot 校验可以访问 /.well-known/acme-challenge/。
# 4. 删除本次创建的 dummy 证书文件,避免挡住 certbot 创建正式证书目录。
# 5. 运行 Certbot为每个域名单独签发正式证书。
# 6. 重启 OpenResty让正式证书生效。
# 7. 按需安装证书自动续期 cron 任务,后续证书会定期检查续期。
# nginx 容器里挂载到 /etc/letsencrypt宿主机这里对应 ./certs。
# nginx 配置引用的是 /etc/letsencrypt/live/域名/fullchain.pem。
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}"
# 记录本次脚本创建或复用的 dummy 证书域名。
# 后面只删除这些 dummy 证书,不动已经存在的正式证书。
DUMMY_DOMAINS=""
cd "$ROOT_DIR"
usage() {
cat <<EOF
用法sh init-certs.sh [选项]
选项:
--create-missing-conf 为缺少 nginx 配置的域名生成基础模板
--skip-renew-cron 跳过证书自动续期 cron 安装
-h, --help 显示帮助
环境变量:
CREATE_MISSING_CONF=1 等同于 --create-missing-conf
INSTALL_RENEW_CRON=0 等同于 --skip-renew-cron
CONF_ROOT=./conf 指定 nginx 配置扫描根目录
CONF_DIR=./conf/conf.d 指定 nginx 域名配置目录
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
}
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 "错误:未知参数:$1" >&2
usage >&2
exit 1
;;
esac
shift
done
if [ -z "${DOMAINS:-}" ]; then
echo "错误:必须设置 DOMAINS。请先在 init-certs.sh 中配置,再调用 init-certs-core.sh。" >&2
exit 1
fi
if [ -z "${CERT_EMAIL:-}" ]; then
echo "错误:必须设置 CERT_EMAIL。请先在 init-certs.sh 中配置,再调用 init-certs-core.sh。" >&2
exit 1
fi
# 阶段 1校验每个域名都有 nginx 配置和 ACME 校验路径。
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 "错误:缺少 nginx 配置:$CONF_DIR/$domain.conf" >&2
echo "提示:确认配置文件存在,或执行 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 "错误nginx 配置缺少 ACME 校验路径:$conf_file" >&2
echo "提示:需要添加 location ^~ /.well-known/acme-challenge/ 并把 root 指向 /var/www。" >&2
conf_error=1
fi
done
# 反向检查 nginx 配置里引用的证书域名是否都被 DOMAINS 覆盖。
# 否则 OpenResty 可能因为某个额外 HTTPS 站点缺证书而启动失败。
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 "错误nginx 配置扫描目录不存在:$CONF_ROOT" >&2
ssl_domains=""
conf_error=1
fi
for domain in $ssl_domains; do
if ! domain_in_list "$domain"; then
echo "错误nginx 配置引用了未加入 DOMAINS 的证书域名:$domain" >&2
echo "提示:请把 $domain 加到 init-certs.sh 的 DOMAINS或移除对应 ssl_certificate 配置。" >&2
conf_error=1
fi
done
if [ "$conf_error" -ne 0 ]; then
exit 1
fi
# 阶段 2选择可用的 Docker Compose 命令。
. "$ROOT_DIR/scripts/lib-compose.sh"
# 阶段 3为缺失证书的域名生成 dummy 证书。
# 目的不是让浏览器信任,而是让 OpenResty 启动时能找到证书文件。
export DOMAINS CERT_ROOT
DUMMY_DOMAINS="$(sh "$ROOT_DIR/scripts/ensure-dummy-certs.sh")"
# 阶段 4启动 OpenResty。
# 这里能启动,是因为上面已经确保每个 HTTPS 域名都有证书文件。
# 启动后Let's Encrypt 才能通过 HTTP 访问 /.well-known/acme-challenge/ 校验文件。
echo "使用 dummy 证书启动 OpenResty..."
compose up -d openresty
if [ -n "$DUMMY_DOMAINS" ]; then
# 阶段 5删除本次创建或复用的 dummy 证书。
# 必须先删掉 dummy 文件,否则 certbot 可能认为 live/域名 目录已经被占用。
# 已存在的正式证书不会进入 DUMMY_DOMAINS所以不会被删除。
echo "申请正式证书前删除 dummy 证书文件..."
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
# 阶段 6申请正式 Let's Encrypt 证书。
# certbot 会把 challenge 文件写到 ./www然后由 OpenResty 对外提供访问。
if ! sh "$ROOT_DIR/scripts/request-certs.sh"; then
echo "证书申请失败,恢复缺失域名的 dummy 证书,避免 OpenResty 后续因证书文件缺失而无法启动..." >&2
if ! sh "$ROOT_DIR/scripts/ensure-dummy-certs.sh" >/dev/null; then
echo "警告dummy 证书恢复失败,请手动检查 $CERT_ROOT" >&2
fi
exit 1
fi
# 阶段 7重启 OpenResty。
# OpenResty 启动时已经加载过 dummy 证书,正式证书签发后需要重启才能加载新文件。
echo "重启 OpenResty 以加载正式证书..."
compose restart openresty
# 阶段 8安装证书自动续期任务。
# scripts/install-renew-cron.sh 是幂等的:重复执行会先删除旧任务块,再写入新任务。
# 这样首次部署只需要执行 init-certs.sh就能同时完成启动和自动续期安装。
if is_enabled "$INSTALL_RENEW_CRON"; then
if command -v crontab >/dev/null 2>&1; then
echo "安装证书自动续期 cron 任务..."
sh "$ROOT_DIR/scripts/install-renew-cron.sh"
else
echo "警告:缺少 crontab 命令,已跳过证书自动续期任务安装。" >&2
echo "提示:安装 crontab 后可手动执行 sh scripts/install-renew-cron.sh。" >&2
fi
else
echo "跳过证书自动续期 cron 安装。"
fi
echo "完成。"