1
This commit is contained in:
246
scripts/init-certs-core.sh
Normal file
246
scripts/init-certs-core.sh
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/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 "完成。"
|
||||
Reference in New Issue
Block a user