1
This commit is contained in:
45
README.md
45
README.md
@@ -15,6 +15,8 @@
|
||||
## 脚本结构
|
||||
|
||||
- `init-certs.sh`:首次初始化入口,内部调用 `scripts/init-certs-core.sh`。
|
||||
- `add-domain-certs.sh`:在 OpenResty 已运行时追加新域名证书,内部调用 `scripts/add-domain-certs-core.sh`。
|
||||
- `reload.sh`:修改 nginx 配置后校验并重载 OpenResty。
|
||||
- `uninstall.sh`:卸载入口,目前用于卸载证书自动续期 cron。
|
||||
- `scripts/`:放具体实现脚本,包括 nginx 域名模板生成、证书初始化核心逻辑、手动续期、安装和卸载续期 cron。
|
||||
|
||||
@@ -65,6 +67,48 @@ sh init-certs.sh --skip-renew-cron
|
||||
重复执行 `init-certs.sh` 不会强制重签已有证书;Certbot 会保留还没到期的证书。
|
||||
重复执行时也不会重复添加 cron;安装脚本会先删除旧任务块,再写入新任务。
|
||||
|
||||
## 追加域名证书
|
||||
|
||||
当 OpenResty 已经在线运行,只需要给新域名补证书时,使用 `add-domain-certs.sh`。
|
||||
|
||||
先把要新增的域名写入 `add-domain-certs.sh` 的 `DOMAINS`,这里只放本次要新增的域名:
|
||||
|
||||
```sh
|
||||
DOMAINS="
|
||||
proxy.sggai.site
|
||||
gitea.sggai.site
|
||||
"
|
||||
```
|
||||
|
||||
然后执行:
|
||||
|
||||
```bash
|
||||
sh add-domain-certs.sh
|
||||
```
|
||||
|
||||
这个脚本会:
|
||||
|
||||
1. 校验 Docker Compose 配置,并确认 `openresty` 容器已经在运行。
|
||||
2. 检查每个新增域名是否已有 nginx 配置。
|
||||
3. 如果缺少 `conf/conf.d/<domain>.conf`,自动调用 `scripts/ensure-domain-conf.sh` 生成基础模板。
|
||||
4. 为缺失证书的新域名生成临时 dummy 证书。
|
||||
5. 校验并重载 OpenResty,让新增配置和 dummy 证书生效。
|
||||
6. 检查本机和公网 HTTP-01 challenge 路径是否可访问。
|
||||
7. 删除本次创建的 dummy 证书,调用 Certbot 申请正式证书。
|
||||
8. 再次重载 OpenResty,让正式证书生效。
|
||||
|
||||
如果服务器无法从本机访问自己的公网域名,但外部访问是正常的,可以跳过公网探测:
|
||||
|
||||
```bash
|
||||
sh add-domain-certs.sh --skip-public-http-check
|
||||
```
|
||||
|
||||
自动生成的 nginx 配置是静态站点基础模板,只保证证书申请和 HTTPS 站点能启动。如果新域名需要反向代理到后端服务,请按业务需要修改 `conf/conf.d/<domain>.conf`,再执行:
|
||||
|
||||
```bash
|
||||
sh reload.sh
|
||||
```
|
||||
|
||||
## 日常启动和停止
|
||||
|
||||
启动:
|
||||
@@ -182,6 +226,7 @@ http://10.1.0.1:3001
|
||||
## 注意事项
|
||||
|
||||
- `certs/`、`logs/`、运行时 webroot 文件默认不提交到 Git。
|
||||
- `conf/conf.d/00-default-deny.conf` 是默认拒绝站点,用于丢弃没有匹配到具体 `server_name` 的 HTTP 请求,并拒绝未知 SNI 的 HTTPS 握手。
|
||||
- 如果证书文件不存在,OpenResty 的 HTTPS 配置会启动失败;首次部署请先运行 `init-certs.sh`。
|
||||
- `ai.sggai.site` 当前通过 `000-` 前缀配置文件覆盖原配置,`openresty -t` 可能出现同名 server 被忽略的 warning。
|
||||
- 如果需要透传带下划线的请求头,例如 `Session_id`,需要确认 Nginx 的 `underscores_in_headers` 策略是否符合预期。
|
||||
|
||||
18
add-domain-certs.sh
Normal file
18
add-domain-certs.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
|
||||
# Domains to add to an already running gateway. Put only new domains here,
|
||||
# one per line.
|
||||
DOMAINS="
|
||||
proxy.sggai.site
|
||||
gitea.sggai.site
|
||||
"
|
||||
|
||||
# Let's Encrypt registration email.
|
||||
CERT_EMAIL="243823965@qq.com"
|
||||
|
||||
export DOMAINS CERT_EMAIL
|
||||
|
||||
sh "$SCRIPT_DIR/scripts/add-domain-certs-core.sh" "$@"
|
||||
11
conf/conf.d/00-default-deny.conf
Normal file
11
conf/conf.d/00-default-deny.conf
Normal file
@@ -0,0 +1,11 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
return 444;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
server_name _;
|
||||
ssl_reject_handshake on;
|
||||
}
|
||||
233
scripts/add-domain-certs-core.sh
Normal file
233
scripts/add-domain-certs-core.sh
Normal file
@@ -0,0 +1,233 @@
|
||||
#!/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.
|
||||
|
||||
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 <<EOF
|
||||
Usage: sh add-domain-certs.sh [options]
|
||||
|
||||
Options:
|
||||
--skip-public-http-check Skip public domain HTTP challenge probe
|
||||
-h, --help Show this help
|
||||
|
||||
Environment:
|
||||
CONF_DIR=./conf/conf.d Directory for per-domain nginx configs
|
||||
SKIP_PUBLIC_HTTP_CHECK=1 Same as --skip-public-http-check
|
||||
EOF
|
||||
}
|
||||
|
||||
is_enabled() {
|
||||
case "${1:-}" in
|
||||
1|true|TRUE|yes|YES|on|ON)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
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 "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
|
||||
DUMMY_DOMAINS="$(sh "$ROOT_DIR/scripts/ensure-dummy-certs.sh")"
|
||||
|
||||
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."
|
||||
Reference in New Issue
Block a user