背景
ssl证书通过acme自动申请,约3个月自动续签一次证书,将申请下来的证书自动同步给其他需要使用的主机。
环境介绍
系统:CentOS 7.8
要求证书所在的主机与其他主机之间可以免密访问,否则脚本无法自动进行同步。
正文
脚本内容
#!/bin/bash
set -eo pipefail # 遇到错误立即退出
# 目标目录和文件配置
target_dir="/data/etc/nginx-proxy-manager/letsencrypt/archive/npm-4"
state_file="/root/scripts/file_state.txt"
log_file="/root/scripts/cert_sync.log"
# 初始化变量
old_pri_key=''
old_pub_crt=''
pri_key=''
pub_crt=''
# 日志函数 - 同时输出到控制台和日志文件
log() {
local timestamp=$(date +'%Y-%m-%d %H:%M:%S')
local message="[$timestamp] $1"
echo "$message" # 控制台输出
echo "$message" >> "$log_file" # 日志文件输出
}
# 从状态文件读取历史记录
loadState() {
if [ -f "$state_file" ]; then
old_pri_key=$(sed -n '1p' "$state_file" 2>/dev/null || true)
old_pub_crt=$(sed -n '2p' "$state_file" 2>/dev/null || true)
log "加载历史状态: 私钥=$old_pri_key, 证书=$old_pub_crt"
else
log "状态文件不存在,将创建新文件: $state_file"
fi
}
# 保存当前状态到文件
saveState() {
mkdir -p "$(dirname "$state_file")"
echo "$pri_key" > "$state_file"
echo "$pub_crt" >> "$state_file"
log "已保存当前状态到文件: 私钥=$pri_key, 证书=$pub_crt"
}
# 同步单个文件到目标主机
sync_file() {
local source_path=$1
local dest_host=$2
local dest_port=$3
local dest_path=$4
local file_type=$5
log "正在同步$file_type到$dest_host"
if scp -P "$dest_port" "$source_path" "root@$dest_host:$dest_path"; then
log "$file_type同步到$dest_host成功"
return 0
else
log "ERROR: $file_type同步到$dest_host失败"
return 1
fi
}
# 同步证书到所有目标主机
sync_to_all_hosts() {
local pri_key_path=$1
local pub_crt_path=$2
# 目标主机配置数组: 主机名 端口 私钥目标路径及文件名称 证书目标路径及文件名称 描述
local hosts=(
"192.168.215.247 2122 /home/dev.alexromeo.net/dev.alexromeo.net.key /home/dev.alexromeo.net/dev.alexromeo.net.crt 'hs中继服务器'"
"172.22.5.178 2318 /data/nginx-proxy-manager/data/custom_ssl/npm-2/privkey.pem /data/nginx-proxy-manager/data/custom_ssl/npm-2/fullchain.pem '阿里服务器'"
)
log "开始执行证书同步操作"
# 遍历所有主机并同步文件
local success=1
for host in "${hosts[@]}"; do
# 解析主机配置
local host_info=($host)
local dest_host=${host_info[0]}
local dest_port=${host_info[1]}
local priv_key_dest=${host_info[2]}
local cert_dest=${host_info[3]}
local desc=${host_info[4]}
# 同步私钥和证书
if ! sync_file "$pri_key_path" "$dest_host" "$dest_port" "$priv_key_dest" "私钥到$desc"; then
success=0
fi
if ! sync_file "$pub_crt_path" "$dest_host" "$dest_port" "$cert_dest" "证书到$desc"; then
success=0
fi
done
return $success
}
# 检查证书状态并同步
checkSSL() {
log "开始执行证书检测流程"
# 文件前缀定义
local pri_key_prefix='privkey'
local pub_crt_prefix='fullchain'
# 检查目标目录
if [ ! -d "$target_dir" ]; then
log "ERROR: 目标证书目录不存在 - $target_dir"
return 1
fi
log "证书目录检查通过: $target_dir"
# 获取最新证书文件
log "正在查找最新的证书文件..."
local new_pri_key=$(ls -tr "$target_dir"/${pri_key_prefix}* 2>/dev/null | tail -n 1)
local new_pub_crt=$(ls -tr "$target_dir"/${pub_crt_prefix}* 2>/dev/null | tail -n 1)
# 验证文件存在性
if [ -z "$new_pri_key" ] || [ ! -f "$new_pri_key" ]; then
log "ERROR: 未找到私钥文件 (前缀: $pri_key_prefix) 在 $target_dir"
return 1
fi
log "找到最新私钥文件: $new_pri_key"
if [ -z "$new_pub_crt" ] || [ ! -f "$new_pub_crt" ]; then
log "ERROR: 未找到证书文件 (前缀: $pub_crt_prefix) 在 $target_dir"
return 1
fi
log "找到最新证书文件: $new_pub_crt"
# 检查文件可读性
if [ ! -r "$new_pri_key" ] || [ ! -r "$new_pub_crt" ]; then
log "ERROR: 没有证书文件的读取权限"
return 1
fi
# 比较证书是否更新
log "开始比较证书是否有更新..."
log "历史私钥: $old_pri_key"
log "当前私钥: $new_pri_key"
log "历史证书: $old_pub_crt"
log "当前证书: $new_pub_crt"
if [ "$old_pri_key" != "$new_pri_key" ] && [ "$old_pub_crt" != "$new_pub_crt" ]; then
log "检测到证书有更新!"
# 更新变量
pri_key="$new_pri_key"
pub_crt="$new_pub_crt"
# 保存状态
saveState
# 同步文件
if sync_to_all_hosts "$pri_key" "$pub_crt"; then
log "所有主机证书同步操作已完成"
else
log "部分主机证书同步过程中出现错误"
fi
else
log "证书未发生变化,无需同步"
fi
log "证书检测流程执行完毕"
log "======================================"
return 0
}
# 初始化
log "===== SSL证书自动同步脚本启动 ====="
log "===== Version 1.2 ====="
log "目标证书目录: $target_dir"
# 加载历史状态
loadState
# 主循环
while true; do
current_time=$(date +'%Y-%m-%d %H:%M:%S')
log "===== 开始证书检测 (${current_time}) ====="
checkSSL
log "本轮检测结束,将在1小时后进行下一次检测"
log "======================================"
sleep 3600
echo -e "\n"
done