0908
This commit is contained in:
98
acme/README.md
Normal file
98
acme/README.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# 证书管理服务(acme.sh)
|
||||
|
||||
用宿主机上的 [acme.sh](https://github.com/acmesh-official/acme.sh) 管理 Let's Encrypt 证书:
|
||||
纯 Shell 实现,不依赖 Python / snap / Docker 容器。
|
||||
|
||||
## 为什么换成 acme.sh
|
||||
|
||||
之前用 certbot 容器时有个绕不开的别扭点:certbot 的 `--deploy-hook` 在**容器内**执行,
|
||||
而我们要 reload 的 nginx 是**宿主机上的容器**,只能在容器里挂 `docker.sock`(等于把宿主机
|
||||
root 交给容器),或者退而求其次每天无条件发一次 HUP。
|
||||
|
||||
acme.sh 直接跑在宿主机,`--reloadcmd` 天然在宿主机执行,**只在证书真正更新时才 reload**,
|
||||
顺带把 certbot 容器、Dockerfile、docker.sock、compose PATH 问题全部消掉。
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
宿主机 容器
|
||||
───────────────────────────────────── ──────────────────────────
|
||||
acme.sh(~/.acme.sh,自带 cron)
|
||||
├─ 写挑战文件 ──▶ /var/www/certbot ──bind mount(ro)──▶ nginx-proxy
|
||||
├─ 写证书 ──▶ /etc/letsencrypt ──bind mount(ro)──▶ nginx-proxy
|
||||
└─ 续签后执行 reloadcmd: docker kill -s HUP nginx-proxy
|
||||
|
||||
用户 ─▶ frps(80/443) ─▶ frpc ─▶ nginx-proxy(80/8443)
|
||||
```
|
||||
|
||||
## 目录约定
|
||||
|
||||
| 宿主机路径 | 用途 |
|
||||
|---|---|
|
||||
| `/var/www/certbot` | ACME HTTP-01 挑战文件 |
|
||||
| `/etc/letsencrypt/live/<CERT_NAME>/` | 证书(默认 CERT_NAME=stonelan.cn) |
|
||||
| `nginx/ssl.d/` | HTTPS server 块,证书就绪后由 `enable-ssl.sh` 写入 |
|
||||
|
||||
5 个域名合成**一张 SAN 证书**,放在同一个目录,nginx 的 5 个 server 块都引用它。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 安装
|
||||
|
||||
```bash
|
||||
cd common-server-public/acme
|
||||
chmod +x install.sh issue.sh renew.sh check-cert.sh
|
||||
./install.sh
|
||||
```
|
||||
|
||||
### 2. 申请证书(一键引导)
|
||||
|
||||
```bash
|
||||
sudo ./issue.sh
|
||||
```
|
||||
|
||||
依次完成:建目录 → 起 nginx(此时无 HTTPS 配置)→ 申请证书 → 安装证书 →
|
||||
写入 `ssl.d/` 并重载 nginx。
|
||||
|
||||
可用环境变量覆盖:
|
||||
|
||||
```bash
|
||||
EMAIL=you@example.com \
|
||||
CERT_NAME=stonelan.cn \
|
||||
DOMAINS="fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn" \
|
||||
sudo -E ./issue.sh
|
||||
```
|
||||
|
||||
### 3. 自动续签
|
||||
|
||||
`install.sh` 已让 acme.sh 自己写入 cron,**无需额外配置**。
|
||||
|
||||
建议再加一条兜底告警(`crontab.example`):
|
||||
|
||||
```cron
|
||||
0 8 * * * bash <绝对路径>/check-cert.sh >> /var/log/acme-check.log 2>&1
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
```bash
|
||||
acme.sh --list # 已申请的证书
|
||||
acme.sh --cron --debug 2 # 带调试跑一次续签
|
||||
./check-cert.sh # 剩余天数
|
||||
|
||||
# 确认 nginx 只加载了 HTTP 配置(冷启动时属正常)
|
||||
docker exec nginx-proxy nginx -T | grep -c "listen 8443"
|
||||
|
||||
# ACME 路径可达性
|
||||
echo probe | sudo tee /var/www/certbot/.well-known/acme-challenge/test
|
||||
curl -i http://fastgpt.stonelan.cn/.well-known/acme-challenge/test
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 需要 root(或 sudo)运行:要写 `/etc/letsencrypt` 和 `/var/www`
|
||||
2. nginx 容器 bind mount 的是**宿主机路径**,宿主机上这两个目录必须存在,
|
||||
否则 docker 会自动创建成 root 所有的空目录
|
||||
3. 挑战文件需要让容器内 nginx 可读,目录保持 755(`issue.sh` 已处理)
|
||||
4. CA 已显式固定为 Let's Encrypt(acme.sh 3.x 默认是 ZeroSSL)
|
||||
5. 证书有效期 90 天,acme.sh 默认在剩余 30 天内续签
|
||||
32
acme/check-cert.sh
Normal file
32
acme/check-cert.sh
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 检查证书剩余有效期,低于阈值时以非 0 退出,便于接入告警。
|
||||
# 自动续签有可能静默失败,必须有一个独立的检查点。
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
CERT_NAME="${CERT_NAME:-stonelan.cn}"
|
||||
CERT="${CERT:-/etc/letsencrypt/live/$CERT_NAME/cert.pem}"
|
||||
WARN_DAYS="${WARN_DAYS:-20}"
|
||||
|
||||
if [ ! -f "$CERT" ]; then
|
||||
echo "证书不存在:$CERT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
end="$(openssl x509 -enddate -noout -in "$CERT" | cut -d= -f2)"
|
||||
end_ts="$(date -d "$end" +%s)"
|
||||
days=$(( (end_ts - $(date +%s)) / 86400 ))
|
||||
|
||||
echo "证书文件:$CERT"
|
||||
echo "到期时间:$end(剩余 $days 天)"
|
||||
openssl x509 -subject -issuer -noout -in "$CERT" | sed 's/^/ /'
|
||||
openssl x509 -text -noout -in "$CERT" \
|
||||
| grep -A1 "Subject Alternative Name" | sed 's/^/ /'
|
||||
|
||||
if [ "$days" -lt "$WARN_DAYS" ]; then
|
||||
echo "警告:剩余 $days 天,低于阈值 $WARN_DAYS 天" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "证书状态正常"
|
||||
10
acme/crontab.example
Normal file
10
acme/crontab.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# acme.sh 安装时会自动写入一条 cron(每天执行 ~/.acme.sh/acme.sh --cron),
|
||||
# 续签成功后自动执行 --reloadcmd,所以续签本身不需要再配 cron。
|
||||
#
|
||||
# 这里只需要一个"有效期检查"作为兜底告警:
|
||||
# 自动续签可能静默失败,剩余天数低于 20 天时脚本以非 0 退出。
|
||||
|
||||
0 8 * * * bash /home/awei/workspace/common-server-public/acme/check-cert.sh >> /var/log/acme-check.log 2>&1
|
||||
|
||||
# 如需强制每天显式跑一次续签(一般不必),可加:
|
||||
# 0 3 * * * bash /home/awei/workspace/common-server-public/acme/renew.sh >> /var/log/acme-renew.log 2>&1
|
||||
25
acme/install.sh
Normal file
25
acme/install.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 安装 acme.sh(纯 Shell ACME 客户端,不依赖 Python / 容器 / snap)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
EMAIL="${EMAIL:-admin@stonelan.cn}"
|
||||
ACME="${ACME:-$HOME/.acme.sh/acme.sh}"
|
||||
|
||||
if [ -s "$ACME" ]; then
|
||||
echo "acme.sh 已安装:$ACME"
|
||||
else
|
||||
echo "==> 安装 acme.sh"
|
||||
curl https://get.acme.sh | sh -s email="$EMAIL"
|
||||
fi
|
||||
|
||||
# acme.sh 3.x 默认 CA 是 ZeroSSL,这里显式固定为 Let's Encrypt
|
||||
"$ACME" --set-default-ca --server letsencrypt
|
||||
|
||||
echo "==> acme.sh 就绪"
|
||||
"$ACME" --version
|
||||
|
||||
# 安装脚本会自动写入一条 cron(每天执行 acme.sh --cron),无需再手配续签
|
||||
echo "==> 已注册的 cron:"
|
||||
crontab -l 2>/dev/null | grep acme.sh || echo " (未找到,请手动添加,参考 crontab.example)"
|
||||
61
acme/issue.sh
Normal file
61
acme/issue.sh
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 首次申请证书并启用 HTTPS(完整引导)。
|
||||
#
|
||||
# 1. 准备宿主机目录:acme.sh 写挑战文件,nginx 容器 bind mount 只读
|
||||
# 2. 启动 nginx —— 此时 ssl.d/ 为空,只加载 80 端口配置,不依赖证书
|
||||
# 3. 申请 SAN 证书(webroot 验证,挑战文件由 nginx 直接对外提供)
|
||||
# 4. 把证书安装到 /etc/letsencrypt/live/<CERT_NAME>/
|
||||
# --reloadcmd 会被 acme.sh 记住,之后每次自动续签都执行
|
||||
# 5. 启用 HTTPS server 块并重载 nginx
|
||||
#
|
||||
# 需要 root 权限(要写 /etc/letsencrypt 与 /var/www):sudo bash issue.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
NGINX_DIR="$SCRIPT_DIR/../nginx"
|
||||
|
||||
EMAIL="${EMAIL:-admin@stonelan.cn}"
|
||||
CERT_NAME="${CERT_NAME:-stonelan.cn}"
|
||||
WEBROOT="${WEBROOT:-/var/www/certbot}"
|
||||
CERT_HOME="${CERT_HOME:-/etc/letsencrypt/live/$CERT_NAME}"
|
||||
DOMAINS="${DOMAINS:-fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn}"
|
||||
ACME="${ACME:-$HOME/.acme.sh/acme.sh}"
|
||||
|
||||
# acme.sh 的 Main_Domain 就是 -d 列表的第一个,--install-cert 要用它
|
||||
PRIMARY="${DOMAINS%% *}"
|
||||
|
||||
if [ ! -s "$ACME" ]; then
|
||||
echo "未找到 acme.sh,请先执行:$SCRIPT_DIR/install.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. 目录
|
||||
mkdir -p "$WEBROOT/.well-known/acme-challenge" "$CERT_HOME"
|
||||
chmod 755 "$WEBROOT" "$WEBROOT/.well-known" "$WEBROOT/.well-known/acme-challenge"
|
||||
|
||||
# 2. nginx(无证书也能起来)
|
||||
echo "==> 启动 nginx"
|
||||
docker compose -f "$NGINX_DIR/docker-compose.yml" up -d
|
||||
|
||||
# 3. 申请
|
||||
args=()
|
||||
for d in $DOMAINS; do
|
||||
args+=(-d "$d")
|
||||
done
|
||||
|
||||
echo "==> 申请证书(主域 $PRIMARY,共 $(echo $DOMAINS | wc -w) 个域名):$DOMAINS"
|
||||
"$ACME" --issue --webroot "$WEBROOT" --server letsencrypt "${args[@]}"
|
||||
|
||||
# 4. 安装证书
|
||||
echo "==> 安装证书到 $CERT_HOME"
|
||||
"$ACME" --install-cert -d "$PRIMARY" \
|
||||
--cert-file "$CERT_HOME/cert.pem" \
|
||||
--key-file "$CERT_HOME/privkey.pem" \
|
||||
--fullchain-file "$CERT_HOME/fullchain.pem" \
|
||||
--reloadcmd "docker kill -s HUP nginx-proxy"
|
||||
|
||||
# 5. 启用 HTTPS
|
||||
echo "==> 启用 HTTPS 配置"
|
||||
exec bash "$NGINX_DIR/enable-ssl.sh"
|
||||
20
acme/renew.sh
Normal file
20
acme/renew.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 手动触发一次续签检查。
|
||||
#
|
||||
# acme.sh 安装时已经自动写入了 cron(每天 ~/.acme.sh/acme.sh --cron),
|
||||
# 正常情况下不需要手动执行,这个脚本只是方便排查和立即验证。
|
||||
# 续签成功时 acme.sh 会自动执行 issue.sh 里登记的 --reloadcmd。
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ACME="${ACME:-$HOME/.acme.sh/acme.sh}"
|
||||
|
||||
if [ ! -s "$ACME" ]; then
|
||||
echo "未找到 acme.sh,请先执行本目录下的 install.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始续签检查"
|
||||
"$ACME" --cron
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 续签检查完成"
|
||||
Reference in New Issue
Block a user