1、获取阿里云的AccessKey
2、当前的ssl证书存放在/etc/ssl/apache路径下
3、将下面的脚本放在某个目录下,命名为ssl_auto_renew.sh,新的服务器用./ssl_auto_renew.sh setup,如果已经有配置过了,则只需要将新加的域名添加到DOMAINS里面,新增一行即可。
#!/bin/bash #============================================================================= # SSL证书自动检测与续期脚本 (Apache2版, 支持任意多个域名) # 环境: Ubuntu 24.04 + Apache 2.4.58 + 阿里云DNS + acme.sh + Let's Encrypt # # 功能: # 1. 检测所有SSL证书的到期时间 # 2. 临近到期(默认30天)自动通过acme.sh + DNS-01申请免费证书 # 3. 自动部署到对应目录并重载Apache2 # 4. 完整日志记录 + 可选钉钉/邮件通知 # # 用法: # ./ssl_auto_renew.sh setup # 首次安装配置(安装acme.sh+签发证书+配置cron) # ./ssl_auto_renew.sh check # 检查并续期(cron定时调用) # ./ssl_auto_renew.sh status # 查看所有证书状态 # ./ssl_auto_renew.sh force # 强制续期所有证书 # # 新增域名: 在 DOMAINS 数组中加一行即可, 证书可放在任意目录 #============================================================================= set -o pipefail # ========================== 配置区域(请修改) ========================== # 阿里云AccessKey (需要AliyunDNSFullAccess权限) # 注意: 要求 wangyan.asia 和 timeless.wang 两个域名的DNS解析都托管在阿里云 # (timeless.wang 在Nginx服务器上已用阿里云DNS验证成功, 一般无需改动) # 获取: 阿里云控制台 -> 右上角头像 -> AccessKey管理 -> 创建AccessKey ALI_KEY="" ALI_SECRET="" # 续期阈值(距到期多少天开始自动续期) RENEW_DAYS=3 # 日志文件 LOG_FILE="/var/log/ssl_renew.log" # ACME CA: letsencrypt 或 zerossl ACME_CA="letsencrypt" # 通知配置(可选, 留空则不通知) # 钉钉/企业微信机器人webhook WEBHOOK_URL="" # 通知邮箱 NOTIFY_EMAIL="wy8817399@vip.qq.com" # 域名配置(支持任意多个域名, 新增域名加一行即可) # 格式: "主域名|附加SAN域名(逗号分隔,可空)|证书目录(绝对路径)|pem文件名|key文件名" # 注意: 文件名/路径与Apache配置中 SSLCertificateFile / SSLCertificateKeyFile 保持一致 DOMAINS=( "timelesscake.wangyan.asia|wangyan.asia|/etc/ssl/apache/timelesscake|timelesscake.wangyan.asia.pem|timelesscake.wangyan.asia.key" ) # 示例: 新增一个完全不同根域名、不同目录的证书, 直接加一行: # "blog.example.com||/etc/ssl/apache/blog|blog.example.com.pem|blog.example.com.key" # ========================== 配置结束(以下无需修改) ========================== # ACME账户邮箱: 优先使用用户填写的 NOTIFY_EMAIL; 若为空, 使用域名下的默认邮箱 ACME_EMAIL="${NOTIFY_EMAIL:-ssl@wangyan.asia}" ACME_SH="$HOME/.acme.sh/acme.sh" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # --- 日志函数 --- log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"; } info() { echo -e "${BLUE}[INFO]${NC} $1" | tee -a "$LOG_FILE"; } success() { echo -e "${GREEN}[ OK ]${NC} $1" | tee -a "$LOG_FILE"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "$LOG_FILE"; } error() { echo -e "${RED}[FAIL]${NC} $1" | tee -a "$LOG_FILE"; } # --- 通知函数 --- notify() { local title="$1" content="$2" if [[ -n "$WEBHOOK_URL" ]]; then curl -s -X POST "$WEBHOOK_URL" \ -H "Content-Type: application/json" \ -d "{\"msgtype\":\"markdown\",\"markdown\":{\"title\":\"$title\",\"text\":\"**$title**\n\n$content\n\n$(date '+%Y-%m-%d %H:%M:%S')\"}}" \ > /dev/null 2>&1 || true fi if [[ -n "$NOTIFY_EMAIL" ]]; then echo -e "$content" | mail -s "$title" "$NOTIFY_EMAIL" 2>/dev/null || true fi } # --- Apache配置检测与重载 --- apache_test() { apache2ctl configtest 2>&1 return ${PIPESTATUS[0]} } apache_reload() { if apache_test | tee -a "$LOG_FILE"; then systemctl reload apache2 success "Apache2 已重载" else error "Apache2 配置测试失败, 请检查配置" return 1 fi } # --- 前置检查 --- check_root() { if [[ $EUID -ne 0 ]]; then error "请以root用户运行此脚本" exit 1 fi } check_config() { if [[ -z "$ALI_KEY" || -z "$ALI_SECRET" ]]; then error "请先编辑本脚本, 填入 ALI_KEY 和 ALI_SECRET" echo "" echo " 获取方式:" echo " 1. 登录阿里云控制台 -> 右上角头像 -> AccessKey管理" echo " 2. 创建AccessKey (建议仅授予 AliyunDNSFullAccess 权限)" echo " 3. 将 AccessKey ID 填入 ALI_KEY" echo " 将 AccessKey Secret 填入 ALI_SECRET" exit 1 fi # 检查apache2ctl是否存在 if ! command -v apache2ctl >/dev/null 2>&1; then error "未找到 apache2ctl 命令, 请确认已安装 Apache2" exit 1 fi # 如果用户填写了通知邮箱, 校验基本格式 if [[ -n "$NOTIFY_EMAIL" && ! "$NOTIFY_EMAIL" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; then error "通知邮箱 NOTIFY_EMAIL 格式无效: $NOTIFY_EMAIL" error "留空则使用默认 ACME 邮箱, 或填写形如 admin@example.com 的有效地址" exit 1 fi } # --- 安装acme.sh --- install_acme() { if [[ -f "$ACME_SH" ]]; then info "acme.sh 已安装" else info "正在安装 acme.sh (优先官方源, 失败则切换Gitee国内镜像) ..." # 方式1: 官方安装脚本(境外服务器可用) # 注意: get.acme.sh 只是引导脚本, 内部会 git clone github, 大陆服务器会无限挂起, # 必须用 timeout 强制限时, 超时后自动降级到Gitee镜像 timeout 90 bash -c "curl -m 30 https://get.acme.sh | sh -s email='${ACME_EMAIL}'" >/dev/null 2>&1 if [[ ! -f "$ACME_SH" ]]; then warn "官方源安装失败/超时(大陆服务器常见), 切换Gitee镜像 ..." # 清理官方源安装可能残留的半成品目录 [[ -d "$HOME/.acme.sh" ]] && rm -rf "$HOME/.acme.sh" # 方式2: Gitee国内镜像安装 local tmp_dir="/tmp/acme_sh_install_$$" if timeout 180 git clone --depth 1 https://gitee.com/neilpang/acme.sh.git "$tmp_dir" >/dev/null 2>&1; then (cd "$tmp_dir" && sh ./acme.sh --install -m "${ACME_EMAIL}") >>"$LOG_FILE" 2>&1 rm -rf "$tmp_dir" fi fi if [[ ! -f "$ACME_SH" ]]; then error "acme.sh 安装失败(官方源和Gitee镜像均不可用)" error "请检查服务器网络后重试, 或参考: https://github.com/acmesh-official/acme.sh/wiki/Install-in-China" exit 1 fi success "acme.sh 安装完成" fi # 设置默认CA "$ACME_SH" --set-default-ca --server "$ACME_CA" 2>/dev/null || true # 关闭自动升级(避免大陆服务器反复访问GitHub导致续期报错) # --auto-upgrade 是 --upgrade 的子参数, 不能单独使用 "$ACME_SH" --upgrade --auto-upgrade 0 >/dev/null 2>&1 || true } # --- 确保ACME账户邮箱有效 --- ensure_acme_account() { local conf="$HOME/.acme.sh/account.conf" if [[ -f "$conf" ]]; then # 修正 account.conf 中可能残留的无效邮箱 sed -i "s/^SAVED_EMAIL=.*/SAVED_EMAIL='${ACME_EMAIL}'/" "$conf" 2>/dev/null || true sed -i "s/^ACCOUNT_EMAIL=.*/ACCOUNT_EMAIL='${ACME_EMAIL}'/" "$conf" 2>/dev/null || true fi info "确保 ACME 账户邮箱正确: ${ACME_EMAIL}" if ! "$ACME_SH" --register-account -m "$ACME_EMAIL" --server "$ACME_CA" 2>&1 | tee -a "$LOG_FILE"; then warn "ACME 账户注册/更新未成功, 将在签发时重试" fi } # --- 获取证书剩余天数(返回-1表示文件不存在) --- get_days() { local cert_file="$1" if [[ ! -f "$cert_file" ]]; then echo -1 return fi local expiry_date expiry_date=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2) if [[ -z "$expiry_date" ]]; then echo -1 return fi local expiry_epoch now_epoch expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null) now_epoch=$(date +%s) echo $(( (expiry_epoch - now_epoch) / 86400 )) } # --- 签发/续期并部署证书 --- # 参数: 主域名 附加SAN域名 证书目录(绝对路径) pem文件名 key文件名 deploy_cert() { local domain="$1" san="$2" cert_dir="$3" pem_name="$4" key_name="$5" local cert_file="$cert_dir/$pem_name" local key_file="$cert_dir/$key_name" # 构建域名参数 local domain_args="-d $domain" if [[ -n "$san" ]]; then IFS=',' read -ra san_array <<< "$san" for s in "${san_array[@]}"; do domain_args="$domain_args -d $s" done fi # 设置阿里云DNS API密钥 export Ali_Key="$ALI_KEY" export Ali_Secret="$ALI_SECRET" # 签发证书(--force确保每次都重新签发) info "正在签发证书: $domain ..." if "$ACME_SH" --issue --dns dns_ali $domain_args --force 2>&1 | tee -a "$LOG_FILE"; then success "证书签发成功: $domain" else error "证书签发失败: $domain" # 清理可能残留的半成品目录, 避免下次误判为已有证书 rm -rf "$HOME/.acme.sh/${domain}" "$HOME/.acme.sh/${domain}_ecc" return 1 fi # 确保证书目录存在 mkdir -p "$cert_dir" # 备份现有证书(如有) if [[ -f "$cert_file" ]]; then cp -f "$cert_file" "${cert_file}.bak.$(date +%Y%m%d%H%M%S)" cp -f "$key_file" "${key_file}.bak.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true fi # 安装证书到目标目录(acme.sh会记住安装位置, 后续renew时自动部署) # 注意: --reloadcmd 在批量部署时可能因其他域名尚未部署而configtest失败, # 但证书文件会先于reloadcmd写入, 因此以文件是否生成为准, reloadcmd失败仅警告 info "部署证书: $domain -> $cert_dir/" local install_rc=0 "$ACME_SH" --install-cert -d "$domain" \ --key-file "$key_file" \ --fullchain-file "$cert_file" \ --reloadcmd "apache2ctl configtest && systemctl reload apache2" \ 2>&1 | tee -a "$LOG_FILE" || install_rc=1 if [[ -f "$cert_file" && -f "$key_file" ]]; then chmod 644 "$cert_file" chmod 600 "$key_file" if [[ "$install_rc" -ne 0 ]]; then warn "证书文件已写入, 但 reloadcmd 失败(批量部署时正常, 将在最后统一重载): $domain" fi success "证书部署完成: $domain" return 0 else error "证书部署失败, 文件未生成: $domain" return 1 fi } # ========================== 主逻辑 ========================== case "${1:-help}" in # ---------- 首次安装配置 ---------- setup) check_root check_config mkdir -p "$(dirname "$LOG_FILE")" touch "$LOG_FILE" info "==========================================" info " SSL证书自动续期(Apache2版) - 初始配置" info "==========================================" # 安装acme.sh install_acme # 确保ACME账户邮箱正确有效 ensure_acme_account # 逐个签发并部署证书 for line in "${DOMAINS[@]}"; do IFS='|' read -r d_main d_san d_dir d_pem d_key <<< "$line" deploy_cert "$d_main" "$d_san" "$d_dir" "$d_pem" "$d_key" done # 重载Apache2 info "重载 Apache2 ..." apache_reload # 配置cron定时任务(每天凌晨3点检查) script_path=$(readlink -f "$0") cron_entry="0 3 * * * ${script_path} check >> ${LOG_FILE} 2>&1" if ! crontab -l 2>/dev/null | grep -qF "${script_path} check"; then (crontab -l 2>/dev/null; echo "$cron_entry") | crontab - success "已添加cron定时任务: 每天 03:00 自动检查证书" else info "cron定时任务已存在, 跳过" fi # 配置日志轮转 cat > /etc/logrotate.d/ssl_renew <<LOGROTATE_EOF ${LOG_FILE} { weekly rotate 8 compress missingok notifempty create 0644 root root } LOGROTATE_EOF success "已配置日志轮转: /etc/logrotate.d/ssl_renew" success "==========================================" success " 配置完成!" success "==========================================" echo "" echo " 常用命令:" echo " $0 status # 查看所有证书状态" echo " $0 check # 手动检查并续期" echo " $0 force # 强制续期所有证书" echo "" echo " 日志文件: $LOG_FILE" echo " cron任务: 每天 03:00 自动检查" echo " 新增域名: 编辑 DOMAINS 数组加一行, 再执行 $0 check 即可" echo "" ;; # ---------- 检查并续期(cron调用) ---------- check) check_root # 确保acme.sh已安装且ACME账户邮箱正确 install_acme ensure_acme_account info "==========================================" info " 开始检查SSL证书 (共 ${#DOMAINS[@]} 个)" info "==========================================" failed_count=0 renewed_count=0 for line in "${DOMAINS[@]}"; do IFS='|' read -r d_main d_san d_dir d_pem d_key <<< "$line" cert_file="$d_dir/$d_pem" days_left=$(get_days "$cert_file") if [[ "$days_left" -eq -1 ]]; then warn "证书文件不存在, 开始签发: $d_main" if deploy_cert "$d_main" "$d_san" "$d_dir" "$d_pem" "$d_key"; then ((renewed_count++)) else ((failed_count++)) fi elif [[ "$days_left" -le 0 ]]; then warn "证书已过期(${days_left}天), 紧急续期: $d_main" if deploy_cert "$d_main" "$d_san" "$d_dir" "$d_pem" "$d_key"; then ((renewed_count++)) else ((failed_count++)) fi elif [[ "$days_left" -le "$RENEW_DAYS" ]]; then warn "证书即将到期(剩余${days_left}天), 开始续期: $d_main" if deploy_cert "$d_main" "$d_san" "$d_dir" "$d_pem" "$d_key"; then ((renewed_count++)) else ((failed_count++)) fi else success "证书正常(剩余${days_left}天): $d_main" fi done # 如有证书被更新, 统一重载Apache2一次(确保所有域名使用最新证书) if [[ "$renewed_count" -gt 0 ]]; then info "本次有证书更新, 统一重载 Apache2 ..." apache_reload fi # 运行acme.sh内置续期检查(作为双保险) if [[ -f "$ACME_SH" ]]; then info "运行acme.sh内置续期检查 ..." "$ACME_SH" --cron 2>&1 | tail -5 | tee -a "$LOG_FILE" || true fi # 汇总 info "------------------------------------------" info " 检查完毕: 续期 ${renewed_count} 个, 失败 ${failed_count} 个" info "==========================================" if [[ "$failed_count" -gt 0 ]]; then error "有 ${failed_count} 个证书处理失败!" notify "SSL证书续期告警" "有 ${failed_count} 个证书续期失败, 请检查日志: ${LOG_FILE}" elif [[ "$renewed_count" -gt 0 ]]; then success "成功续期 ${renewed_count} 个证书" notify "SSL证书续期成功" "成功续期 ${renewed_count} 个证书" fi ;; # ---------- 查看证书状态 ---------- status) echo "==============================================================================" echo " SSL证书状态报告 $(date '+%Y-%m-%d %H:%M:%S') 共 ${#DOMAINS[@]} 个域名" echo "==============================================================================" printf " %-40s %-8s %-28s %s\n" "域名" "剩余" "到期时间" "状态" echo "------------------------------------------------------------------------------" for line in "${DOMAINS[@]}"; do IFS='|' read -r d_main d_san d_dir d_pem d_key <<< "$line" cert_file="$d_dir/$d_pem" days_left=$(get_days "$cert_file") if [[ "$days_left" -eq -1 ]]; then printf " %-40s %-8s %-28s %b\n" "$d_main" "N/A" "N/A" "${RED}不存在${NC}" else expiry_str=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2) if [[ "$days_left" -le 0 ]]; then status_str="${RED}已过期${NC}" elif [[ "$days_left" -le "$RENEW_DAYS" ]]; then status_str="${YELLOW}需续期${NC}" else status_str="${GREEN}正常${NC}" fi printf " %-40s %-8s %-28s %b\n" "$d_main" "${days_left}天" "$expiry_str" "$status_str" fi done echo "==============================================================================" echo " 续期阈值: ${RENEW_DAYS}天 | 日志: ${LOG_FILE}" echo "==============================================================================" ;; # ---------- 强制续期所有证书 ---------- force) check_root info "==========================================" info " 强制续期所有证书 (共 ${#DOMAINS[@]} 个)" info "==========================================" for line in "${DOMAINS[@]}"; do IFS='|' read -r d_main d_san d_dir d_pem d_key <<< "$line" deploy_cert "$d_main" "$d_san" "$d_dir" "$d_pem" "$d_key" done info "重载 Apache2 ..." apache_reload success "==========================================" success " 强制续期完成!" success "==========================================" ;; # ---------- 帮助 ---------- *) echo "" echo " SSL证书自动检测与续期脚本 (Apache2版)" echo " 环境: Ubuntu 24.04 + Apache 2.4.58 + 阿里云DNS + acme.sh + Let's Encrypt" echo "" echo " 用法: $0 {setup|check|status|force}" echo "" echo " setup 首次安装: 安装acme.sh + 签发所有证书 + 配置cron定时任务" echo " check 检查证书有效期, 到期自动续期 (cron定时调用)" echo " status 查看所有证书的当前状态(只读, 不做任何修改)" echo " force 强制续期所有证书(忽略到期时间)" echo "" echo " 首次使用:" echo " 1. 编辑本脚本, 确认 ALI_KEY / ALI_SECRET, 且域名DNS托管在阿里云" echo " 2. 运行: $0 setup" echo " 3. 查看状态: $0 status" echo "" echo " 新增域名:" echo " 在 DOMAINS 数组中按格式加一行, 再执行 $0 check 即自动签发部署" echo " 格式: \"主域名|附加SAN域名|证书目录(绝对路径)|pem文件名|key文件名\"" echo "" exit 1 ;; esac
Ubuntu24.04自动更新SSL证书并重启Apache
| Title | Ubuntu24.04自动更新SSL证书并重启Apache |
|---|---|
| Framework | Ubuntu |
| User | wy8817399@vip.qq.com |
| Id | 92 |
| Created | 8/21/26, 9:36 PM |
| Modified | 8/21/26, 9:36 PM |
| Published | Yes |
Content