Update ssl
This commit is contained in:
239
ssl
239
ssl
@@ -1,21 +1,17 @@
|
|||||||
cat >/usr/local/bin/issue_cert.sh <<'EOF'
|
cat >/usr/local/bin/issue_cert.sh <<'EOF'
|
||||||
#!/usr/bin/env bash
|
#!/bin/sh
|
||||||
set -euo pipefail
|
# POSIX sh script: compatible with Debian/Ubuntu/Alpine/busybox
|
||||||
|
set -eu
|
||||||
|
|
||||||
# ===== 可调参数 =====
|
INSTALL_BASE="/data"
|
||||||
INSTALL_BASE="/data" # 证书安装目录根路径
|
|
||||||
ACME="${HOME}/.acme.sh/acme.sh" # acme.sh 路径(默认安装位置)
|
|
||||||
CA_SERVER="letsencrypt" # 默认使用 Let's Encrypt
|
|
||||||
KEY_NAME="key.pem"
|
KEY_NAME="key.pem"
|
||||||
FULLCHAIN_NAME="fullchain.pem"
|
FULLCHAIN_NAME="fullchain.pem"
|
||||||
|
CA_SERVER="letsencrypt"
|
||||||
|
DEBUG_LEVEL="${DEBUG_LEVEL:-0}" # 0=off, 1=--debug, 2=--debug 2
|
||||||
|
ACME_SH="${ACME_SH:-}" # allow override: ACME_SH=/path/to/acme.sh issue_cert.sh xxx
|
||||||
|
|
||||||
# ===== 工具函数 =====
|
log() { printf '\n[%s] %s\n' "$(date '+%F %T')" "$*"; }
|
||||||
log() { printf "\n[%s] %s\n" "$(date '+%F %T')" "$*"; }
|
err() { printf '\n[ERROR] %s\n' "$*" >&2; }
|
||||||
die() { printf "\n[ERROR] %s\n" "$*" >&2; exit 1; }
|
|
||||||
|
|
||||||
need_cmd() {
|
|
||||||
command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1,请先安装。"
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<USAGE
|
cat <<USAGE
|
||||||
@@ -23,165 +19,184 @@ usage() {
|
|||||||
issue_cert.sh <domain> [--email you@example.com] [--force] [--wildcard]
|
issue_cert.sh <domain> [--email you@example.com] [--force] [--wildcard]
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
- 自动尝试多种签发机制(Cloudflare DNS API -> standalone 80 -> 手动DNS保底)
|
1) 优先 Cloudflare DNS API(检测到 CF_Token 时自动启用,支持自动续期)
|
||||||
- 证书安装到: ${INSTALL_BASE}/<domain>/${KEY_NAME} 与 ${FULLCHAIN_NAME}
|
2) 再尝试 standalone 80(需要公网80可达,支持自动续期但续期仍需80)
|
||||||
- 自动续期: DNS API/standalone 都会被 acme.sh cron 管理(手动DNS无法自动续期)
|
3) 最后保底手动 DNS(可签发但无法自动续期)
|
||||||
|
|
||||||
参数:
|
输出:
|
||||||
--email 指定邮箱(首次安装 acme.sh 时用过也可不填)
|
/data/<domain>/key.pem
|
||||||
--force 强制重签
|
/data/<domain>/fullchain.pem
|
||||||
--wildcard 同时签发通配符 *.domain(仅DNS方式支持;standalone不支持)
|
|
||||||
|
可选环境变量:
|
||||||
|
ACME_SH=/root/.acme.sh/acme.sh # 指定 acme.sh 路径
|
||||||
|
DEBUG_LEVEL=1 或 2 # 打开调试输出(对应 acme.sh --debug 或 --debug 2)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
issue_cert.sh ui.shanghi.net
|
||||||
|
CF_Token=xxx issue_cert.sh ui.shanghi.net
|
||||||
|
DEBUG_LEVEL=2 issue_cert.sh ui.shanghi.net --force
|
||||||
USAGE
|
USAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
# ===== 解析参数 =====
|
# ---- parse args ----
|
||||||
if [[ $# -lt 1 ]]; then usage; exit 1; fi
|
[ $# -ge 1 ] || { usage; exit 1; }
|
||||||
|
DOMAIN="$1"; shift
|
||||||
|
|
||||||
DOMAIN=""
|
|
||||||
EMAIL=""
|
EMAIL=""
|
||||||
FORCE="0"
|
FORCE="0"
|
||||||
WILDCARD="0"
|
WILDCARD="0"
|
||||||
|
|
||||||
DOMAIN="$1"; shift || true
|
while [ $# -gt 0 ]; do
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--email) EMAIL="${2:-}"; shift 2;;
|
--email) EMAIL="${2:-}"; [ -n "$EMAIL" ] || { err "--email 缺少参数"; exit 1; }; shift 2;;
|
||||||
--force) FORCE="1"; shift;;
|
--force) FORCE="1"; shift;;
|
||||||
--wildcard) WILDCARD="1"; shift;;
|
--wildcard) WILDCARD="1"; shift;;
|
||||||
-h|--help) usage; exit 0;;
|
-h|--help) usage; exit 0;;
|
||||||
*) die "未知参数: $1";;
|
*) err "未知参数: $1"; usage; exit 1;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# ===== 前置检查 =====
|
# ---- find acme.sh ----
|
||||||
need_cmd curl
|
find_acme() {
|
||||||
need_cmd mkdir
|
if [ -n "${ACME_SH}" ] && [ -x "${ACME_SH}" ]; then
|
||||||
need_cmd awk
|
echo "${ACME_SH}"; return 0
|
||||||
need_cmd sed
|
fi
|
||||||
|
|
||||||
if [[ ! -x "$ACME" ]]; then
|
# Common locations
|
||||||
|
if [ -x "$HOME/.acme.sh/acme.sh" ]; then echo "$HOME/.acme.sh/acme.sh"; return 0; fi
|
||||||
|
if [ -x "/root/.acme.sh/acme.sh" ]; then echo "/root/.acme.sh/acme.sh"; return 0; fi
|
||||||
|
|
||||||
|
# Try to locate in PATH
|
||||||
|
if command -v acme.sh >/dev/null 2>&1; then
|
||||||
|
command -v acme.sh; return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
ACME="$(find_acme || true)"
|
||||||
|
|
||||||
|
# ---- install acme.sh if missing ----
|
||||||
|
if [ -z "$ACME" ]; then
|
||||||
log "未发现 acme.sh,开始安装..."
|
log "未发现 acme.sh,开始安装..."
|
||||||
if [[ -n "${EMAIL}" ]]; then
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
curl -fsSL https://get.acme.sh | sh -s email="${EMAIL}"
|
err "系统没有 curl,请先安装 curl。"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$EMAIL" ]; then
|
||||||
|
curl -fsSL https://get.acme.sh | sh -s email="$EMAIL"
|
||||||
else
|
else
|
||||||
curl -fsSL https://get.acme.sh | sh
|
curl -fsSL https://get.acme.sh | sh
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
ACME="$(find_acme || true)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 兼容 alias 未生效情况
|
[ -n "$ACME" ] || { err "仍未找到 acme.sh。请确认安装用户与执行用户一致,或用 ACME_SH 指定路径。"; exit 1; }
|
||||||
if [[ ! -x "$ACME" ]]; then
|
|
||||||
die "acme.sh 安装后仍未找到: $ACME。请确认是否安装在 ${HOME}/.acme.sh/ 下。"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 设定默认 CA
|
log "使用 acme.sh: $ACME"
|
||||||
log "设置默认 CA 为: ${CA_SERVER}"
|
|
||||||
"$ACME" --set-default-ca --server "${CA_SERVER}" >/dev/null
|
|
||||||
|
|
||||||
|
# ---- prepare debug flags ----
|
||||||
|
DBG=""
|
||||||
|
if [ "$DEBUG_LEVEL" = "1" ]; then DBG="--debug"; fi
|
||||||
|
if [ "$DEBUG_LEVEL" = "2" ]; then DBG="--debug 2"; fi
|
||||||
|
|
||||||
|
# ---- set CA ----
|
||||||
|
log "设置默认 CA 为: $CA_SERVER"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
$ACME --set-default-ca --server "$CA_SERVER" $DBG >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# ---- prepare install dir ----
|
||||||
INSTALL_DIR="${INSTALL_BASE}/${DOMAIN}"
|
INSTALL_DIR="${INSTALL_BASE}/${DOMAIN}"
|
||||||
mkdir -p "${INSTALL_DIR}"
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
KEY_PATH="${INSTALL_DIR}/${KEY_NAME}"
|
KEY_PATH="${INSTALL_DIR}/${KEY_NAME}"
|
||||||
FULLCHAIN_PATH="${INSTALL_DIR}/${FULLCHAIN_NAME}"
|
FULLCHAIN_PATH="${INSTALL_DIR}/${FULLCHAIN_NAME}"
|
||||||
|
|
||||||
ISSUE_ARGS=(--issue -d "${DOMAIN}")
|
# ---- build issue args ----
|
||||||
if [[ "${WILDCARD}" == "1" ]]; then
|
ISSUE_DOMAINS="-d $DOMAIN"
|
||||||
# 通配符只适用于 DNS 验证
|
if [ "$WILDCARD" = "1" ]; then
|
||||||
ISSUE_ARGS=(--issue -d "${DOMAIN}" -d "*.${DOMAIN}")
|
# 注意:通配符只适用于 DNS 验证。并且对 ui.shanghi.net 这样的子域,
|
||||||
fi
|
# 通配符 *.ui.shanghi.net 通常没意义。一般通配符用于 shanghi.net -> *.shanghi.net
|
||||||
if [[ "${FORCE}" == "1" ]]; then
|
ISSUE_DOMAINS="-d $DOMAIN -d *.$DOMAIN"
|
||||||
ISSUE_ARGS+=("--force")
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
FORCE_FLAG=""
|
||||||
|
[ "$FORCE" = "1" ] && FORCE_FLAG="--force"
|
||||||
|
|
||||||
install_cert() {
|
install_cert() {
|
||||||
log "安装证书到: ${INSTALL_DIR}"
|
log "安装证书到: $INSTALL_DIR"
|
||||||
"$ACME" --install-cert -d "${DOMAIN}" \
|
# shellcheck disable=SC2086
|
||||||
--key-file "${KEY_PATH}" \
|
$ACME --install-cert -d "$DOMAIN" \
|
||||||
--fullchain-file "${FULLCHAIN_PATH}" \
|
--key-file "$KEY_PATH" \
|
||||||
--reloadcmd "echo cert_installed_for_${DOMAIN}" >/dev/null
|
--fullchain-file "$FULLCHAIN_PATH" \
|
||||||
|
--reloadcmd "echo cert_installed_for_$DOMAIN" $DBG
|
||||||
|
|
||||||
log "完成。证书文件:"
|
log "证书文件已生成:"
|
||||||
ls -l "${KEY_PATH}" "${FULLCHAIN_PATH}" || true
|
ls -l "$KEY_PATH" "$FULLCHAIN_PATH" 2>/dev/null || true
|
||||||
|
|
||||||
if command -v openssl >/dev/null 2>&1; then
|
if command -v openssl >/dev/null 2>&1; then
|
||||||
log "证书信息:"
|
log "证书信息:"
|
||||||
openssl x509 -in "${FULLCHAIN_PATH}" -noout -subject -issuer -dates || true
|
openssl x509 -in "$FULLCHAIN_PATH" -noout -subject -issuer -dates 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
try_cloudflare_dns() {
|
try_cf_dns() {
|
||||||
# 若 WILDCARD=1 必须 DNS,且 Cloudflare 是最稳之一
|
if [ -n "${CF_Token:-}" ]; then
|
||||||
if [[ -n "${CF_Token:-}" ]]; then
|
log "检测到 CF_Token,尝试 Cloudflare DNS API(dns_cf)..."
|
||||||
log "检测到 CF_Token,尝试 Cloudflare DNS API(dns_cf)签发..."
|
# shellcheck disable=SC2086
|
||||||
"$ACME" "${ISSUE_ARGS[@]}" --dns dns_cf && return 0
|
$ACME --issue $ISSUE_DOMAINS --dns dns_cf $FORCE_FLAG $DBG && return 0
|
||||||
log "Cloudflare DNS API 签发失败,将尝试其他方式。"
|
log "Cloudflare DNS API 失败,继续尝试其他方式。"
|
||||||
return 1
|
|
||||||
fi
|
fi
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
try_standalone_80() {
|
try_standalone() {
|
||||||
if [[ "${WILDCARD}" == "1" ]]; then
|
if [ "$WILDCARD" = "1" ]; then
|
||||||
log "通配符证书不支持 standalone 80 验证,跳过。"
|
log "通配符不支持 standalone 80,跳过。"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 仅做基础检查:80 端口本地可监听(不等于公网可达,但能过滤一部分情况)
|
log "尝试 standalone 80(需要公网80可达,且本机可绑定80)..."
|
||||||
if command -v ss >/dev/null 2>&1; then
|
# shellcheck disable=SC2086
|
||||||
if ss -lnt 2>/dev/null | awk '{print $4}' | grep -qE '(:80$)|(\[::\]:80$)'; then
|
$ACME --issue $ISSUE_DOMAINS --standalone $FORCE_FLAG $DBG && return 0
|
||||||
log "检测到本机已有服务占用 80 端口,standalone 可能失败(仍会尝试)。"
|
log "standalone 80 失败。"
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "尝试 standalone 80(需要公网 80 可达)..."
|
|
||||||
"$ACME" "${ISSUE_ARGS[@]}" --standalone && return 0
|
|
||||||
log "standalone 80 签发失败。"
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
try_manual_dns() {
|
try_manual_dns() {
|
||||||
log "进入保底:手动 DNS TXT 验证(--dns)。"
|
log "进入保底:手动 DNS TXT 验证(--dns)。"
|
||||||
log "注意:这种模式无法自动续期;续期时仍需要你手动添加 TXT。"
|
log "提示:手动 DNS 无法自动续期(续期仍需手动加 TXT)。"
|
||||||
"$ACME" "${ISSUE_ARGS[@]}" --dns
|
# shellcheck disable=SC2086
|
||||||
log "上面输出了 TXT 记录,请去 DNS 面板添加后,等待生效。"
|
$ACME --issue $ISSUE_DOMAINS --dns $FORCE_FLAG $DBG || return 1
|
||||||
log "添加完成后,执行下面命令完成签发/续期:"
|
|
||||||
echo " ${ACME} --renew -d ${DOMAIN} --force"
|
|
||||||
echo
|
|
||||||
log "当你确认 TXT 已生效后,也可以直接回车继续(脚本会尝试 renew)。"
|
|
||||||
read -r -p "按回车继续(或 Ctrl+C 退出): " _
|
|
||||||
|
|
||||||
"$ACME" --renew -d "${DOMAIN}" --force
|
log "上面已输出 TXT 记录,请去 DNS 面板添加,等待生效后再执行:"
|
||||||
|
echo " $ACME --renew -d $DOMAIN --force $DBG"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
log "开始签发:$DOMAIN"
|
||||||
log "开始签发域名证书:${DOMAIN}"
|
log "输出目录:$INSTALL_DIR"
|
||||||
log "安装目录:${INSTALL_DIR}"
|
|
||||||
|
|
||||||
# 1) Cloudflare DNS API(最稳)
|
if try_cf_dns; then
|
||||||
if try_cloudflare_dns; then
|
install_cert
|
||||||
install_cert
|
log "完成:DNS API 模式支持自动续期(acme.sh 自带 cronjob 管理)。"
|
||||||
log "提示:DNS API 模式支持自动续期(acme.sh cronjob 已自动管理)。"
|
exit 0
|
||||||
exit 0
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
# 2) standalone 80
|
if try_standalone; then
|
||||||
if try_standalone_80; then
|
install_cert
|
||||||
install_cert
|
log "完成:standalone 模式支持自动续期,但续期时仍需要 80 可用且公网可达。"
|
||||||
log "提示:standalone 模式支持自动续期,但续期时仍需要 80 可用且公网可达。"
|
exit 0
|
||||||
exit 0
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
# 3) 手动 DNS 保底
|
if try_manual_dns; then
|
||||||
if try_manual_dns; then
|
log "已进入手动 DNS 模式:按提示添加 TXT 后续期/完成。"
|
||||||
install_cert
|
exit 0
|
||||||
log "提示:手动 DNS 模式不支持自动续期。建议尽快切换到 DNS API(如 Cloudflare Token)。"
|
fi
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
die "所有方式都失败了。建议使用:${ACME} --issue -d ${DOMAIN} --debug 2 查看详细日志。"
|
err "全部方式失败。建议:DEBUG_LEVEL=2 issue_cert.sh $DOMAIN --force 观察详细日志。"
|
||||||
}
|
exit 1
|
||||||
|
|
||||||
main
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod +x /usr/local/bin/issue_cert.sh
|
chmod +x /usr/local/bin/issue_cert.sh
|
||||||
|
|||||||
Reference in New Issue
Block a user