关键词:OpenWrt, AdGuard Home, SSL证书, 自动续期, acme.sh, Cloudflare API, 运维脚本, HTTPS, DoH, DoT, 群晖NAS, 软路由, 自动化运维, E路领航
前言:为什么我们需要自动化?
在家庭网络和企业微型机房的运维中,OpenWrt 作为软路由系统的核心,承载着网络流量分发、广告过滤和隐私保护的重任。而 AdGuard Home 则是其中的明星级应用,它不仅能拦截广告,还能通过 DoH (DNS over HTTPS) 和 DoT (DNS over TLS) 协议,为我们的DNS查询穿上一层加密的“防弹衣”,防止运营商劫持和隐私泄露。
然而,要实现 DoH/DoT,就必须为 AdGuard Home 配置 SSL 证书。
目前最流行的免费证书颁发机构是 Let's Encrypt,它的证书免费且被全平台信任,但唯一的“痛点”是:有效期只有 90 天。
这意味着,每隔两个多月,你就需要:
登录服务器手动申请证书。
下载证书文件。
上传到 OpenWrt 的指定目录。
重启 AdGuard Home 服务。
对于追求“极致稳定”和“零维护”的运维人员(以及像我这样的“懒人”)来说,这种重复性的体力劳动是不可接受的。一旦忘记续期,家里的加密DNS服务就会中断,导致网络连接报错。
本文将详细介绍如何利用 acme.sh 配合 Cloudflare API,编写一个全自动化、具备自我修复能力、持久化部署的 Shell 脚本,彻底解决证书续期难题。本文方案特别适配运行在 群晖 (Synology) NAS 虚拟机 中的 OpenWrt 环境,但也适用于所有标准的 OpenWrt x86/64 设备。
一、 技术演进与方案选型
在最终方案诞生之前,我们经历了三个阶段的技术演进,了解这些背景有助于你理解脚本中每一行代码的深意。
1.0 时代:手动 GUI 操作
最初,我们通过 OpenWrt 的网页端上传证书。
缺点:极其繁琐,无法自动化。每次上传都需要转换格式,并且安卓手机经常因为缺少中间证书(Fullchain)而报错。
2.0 时代:简单的 acme.sh 命令行
后来,我们直接安装 acme.sh 工具,使用命令申请。
局限:OpenWrt 系统升级或重启后,环境变量可能丢失;
acme.sh生成的证书路径带有随机性(如 ECC 算法生成的文件夹会带有_ecc后缀),导致硬编码路径的脚本失效。而且,使用 Cloudflare 的 Global API Key 存在安全隐患,一旦泄露,整个账户的所有域名都有危险。
3.0 时代:E路领航定制化一键脚本(当前方案)
为了解决上述所有问题,我编写了一个高度集成的 Shell 脚本,具备以下特性:
安全性升级:放弃 Global Key,全面转向 Cloudflare API Token 机制,权限最小化。
智能识别:自动判断系统是否安装了
curl,socat等依赖并自动补全。路径自适应:自动识别
acme.sh生成的是 RSA 还是 ECC 证书,解决路径报错问题。全链兼容:强制合并 Fullchain 中间证书,完美解决 Android/Java 客户端不信任的问题。
持久化定时:不仅仅依赖
acme.sh自身的 cron,还在系统 crontab 中写入了每月的“强制续期”任务,双重保险。
二、 准备工作:获取 Cloudflare API 凭证
本教程假设你的域名(例如 dns.xxxx.com)托管在 Cloudflare。为了安全起见,我们不再使用账户密码,而是使用 API Token。
1. 获取 Account ID 和 Zone ID
点击你的域名(例如
oool.cc)。在页面右下角的 API 区域,你可以直接看到:
Zone ID (区域 ID)
Account ID (账户 ID)
请复制这两个字符串备用。
2. 生成 API Token
点击“获取您的 API 令牌”或访问 API Tokens 设置页。
点击 Create Token (创建令牌)。
选择 Edit zone DNS (编辑区域 DNS) 模板。
在 Zone Resources (区域资源) 中,选择 Include -> Specific zone -> 你的域名。
点击生成,你将获得一串字符,这就是 CF_Token。
注意:Token 只显示一次,请务必保存好!
三、 核心方案:一键自动化部署脚本
以下是经过多次迭代优化的完整脚本。我们采用 cat << 'EOF' 的方式,将脚本内容直接写入 OpenWrt 的系统路径 /usr/bin/upcert,这样你以后只需要输入 upcert 这个单词就能调用它。
运行环境要求
硬件:群晖虚拟机 / 物理机 / 软路由
系统:OpenWrt (基于 Linux)
网络:需能连接 GitHub (安装 acme.sh) 和 Let's Encrypt API。
组件:AdGuard Home 已安装。
完整代码(请根据注释修改配置)
请将以下代码复制到记事本,修改顶部的 配置参数 区域,然后整体复制到 OpenWrt 的 SSH 终端执行。
Bash
cat << 'EOF' > /usr/bin/upcert
#!/bin/sh
# =========================================================
# AdGuard Home 证书全自动续期脚本 (E路领航定制版)
# 功能:环境配置 -> 证书申请 -> 格式转换 -> 部署 -> 重启
# 适用环境:OpenWrt / Linux
# 更新日期:2026-01-20
# =========================================================
# ------------------ 用户配置区域 (请修改此处) ------------------
# 1. 你的域名
DOMAIN="dns.xxxx.com"
# 2. 证书部署的目标路径 (AdGuard Home 的 SSL 目录)
CERT_DIR="/etc/adguardhome/ssl"
# 3. Cloudflare API 凭证 (比 Global Key 更安全)
# 请填入你在 Cloudflare 后台获取的真实信息
export CF_Token="你的 Token" # 替换为你的 Token
export CF_Account_ID="替换为你的 Account ID" # 替换为你的 Account ID
export CF_Zone_ID="替换为你的 Zone ID" # 替换为你的 Zone ID
# 4. 注册邮箱 (用于接收 Let's Encrypt 的过期通知)
LE_EMAIL="XXXX@XXX.COM"
# acme.sh 安装目录 (通常不需要改)
ACME_HOME="/root/.acme.sh"
# -------------------------------------------------------------
# --- [阶段一] 环境自检与依赖修复 ---
echo ">>> [1/6] 开始系统环境检查..."
# 检查目标目录是否存在,不存在则创建
if [ ! -d "$CERT_DIR" ]; then
echo "提示:创建证书存放目录 $CERT_DIR"
mkdir -p "$CERT_DIR"
fi
# 检查必要工具 curl 和 socat (acme.sh 运行必须)
# 如果缺失,自动使用 opkg 更新源并安装
if ! command -v curl >/dev/null || ! command -v socat >/dev/null; then
echo "警告:未检测到必要依赖,正在自动安装 curl 和 socat..."
opkg update && opkg install curl socat ca-bundle ca-certificates
else
echo "信息:系统依赖检查通过。"
fi
# --- [阶段二] acme.sh 安装与初始化 ---
echo ">>> [2/6] 初始化 acme.sh 工具..."
if [ ! -f "$ACME_HOME/acme.sh" ]; then
echo "信息:正在从官方安装 acme.sh..."
curl https://get.acme.sh | sh -s email=$LE_EMAIL
else
echo "信息:acme.sh 已安装。"
fi
# 加载 acme.sh 的环境变量
. "$ACME_HOME/acme.sh.env"
# --- [阶段三] 执行证书申请/续期 (强制模式) ---
echo ">>> [3/6] 开始与 Let's Encrypt 通信 (DNS 模式)..."
# 导出环境变量供 acme.sh 调用
export CF_Token="$CF_Token"
export CF_Account_ID="$CF_Account_ID"
export CF_Zone_ID="$CF_Zone_ID"
# 逻辑判断:是初次申请还是强制续期?
# 注意:脚本默认使用 --force,确保每月运行一次时必定更新证书,防止因 acme 内部逻辑跳过
if [ ! -d "$ACME_HOME/${DOMAIN}_ecc" ] && [ ! -d "$ACME_HOME/$DOMAIN" ]; then
echo "状态:首次申请证书..."
"$ACME_HOME/acme.sh" --issue --server letsencrypt --dns dns_cf -d "$DOMAIN"
else
echo "状态:检测到已有配置,执行强制续期 (Force Renew)..."
"$ACME_HOME/acme.sh" --cron --force --home "$ACME_HOME"
fi
# --- [阶段四] 智能路径识别与部署 ---
# acme.sh 新版默认使用 ECC 算法,目录名会有 _ecc 后缀
# 此段代码用于自动修正路径,防止“找不到文件”的错误
if [ -f "$ACME_HOME/${DOMAIN}_ecc/$DOMAIN.cer" ]; then
echo ">>> 检测到 ECC 算法证书..."
SRC_DOMAIN="$DOMAIN"
elif [ -f "$ACME_HOME/$DOMAIN/$DOMAIN.cer" ]; then
echo ">>> 检测到 RSA 算法证书..."
SRC_DOMAIN="$DOMAIN"
else
echo ">>> (提示) 暂未检测到源文件,如果上方日志显示 Success 则无需担心。"
SRC_DOMAIN=""
fi
if [ ! -z "$SRC_DOMAIN" ]; then
echo ">>> [4/6] 正在部署证书到 $CERT_DIR ..."
# 使用 install-cert 命令标准部署
# --reloadcmd 指定了服务重启命令
"$ACME_HOME/acme.sh" --install-cert -d "$DOMAIN" \
--key-file "$CERT_DIR/dns.key" \
--fullchain-file "$CERT_DIR/fullchain.cer" \
--cert-file "$CERT_DIR/dns.cer" \
--reloadcmd "service AdGuardHome restart" --ecc
# 【关键步骤】强制合并 Fullchain
# 很多教程只用 .cer 文件,导致安卓手机连接 DoT 失败
# 这里我们用 fullchain 覆盖 dns.cer,确保兼容性
if [ -f "$CERT_DIR/fullchain.cer" ]; then
cat "$CERT_DIR/fullchain.cer" > "$CERT_DIR/dns.cer"
echo ">>> Fullchain 证书链合并完成 (Android 兼容性优化)。"
fi
echo ">>> [5/6] 重启 AdGuard Home 服务..."
service AdGuardHome restart
echo "----------------------------------------------------"
echo "✅ 恭喜!证书已成功更新并部署。"
echo "🔑 私钥: $CERT_DIR/dns.key"
echo "📜 证书: $CERT_DIR/dns.cer (包含完整信任链)"
echo "📅 新有效期截止: $(openssl x509 -in "$CERT_DIR/dns.cer" -noout -dates | grep notAfter | cut -d= -f2)"
echo "----------------------------------------------------"
else
echo ">>> [警告] 未找到生成的证书文件,请检查上方的 API 连接日志。"
fi
# --- [阶段五] 持久化定时任务 (Crontab) ---
SCRIPT_PATH="/usr/bin/upcert"
# 先清理旧任务,防止重复
sed -i '/\/usr\/bin\/upcert/d' /etc/crontabs/root
echo ">>> [6/6] 更新系统定时任务..."
# 设定:每月 1 号凌晨 04:30 自动执行脚本
echo "30 4 1 * * $SCRIPT_PATH >/dev/null 2>&1" >> /etc/crontabs/root
# 重启 cron 服务
/etc/init.d/cron restart
echo "定时任务已设定:每月 1 号 04:30 自动强制续期。"
EOF
# 赋予脚本执行权限
chmod +x /usr/bin/upcert
echo "#########################################################"
echo " E路领航一键部署脚本已安装完毕!"
echo " 脚本路径: /usr/bin/upcert"
echo " 使用方法: 直接输入 upcert 即可运行"
echo "#########################################################"
四、 详细操作步骤说明
第一步:连接 OpenWrt
使用 SSH 工具(如 Putty, Xshell, 或者 macOS 的 Terminal)连接到你的 OpenWrt 路由器。
Bash
ssh root@192.168.1.1 # 请替换为你的实际 IP
第二步:执行部署
将上方你修改好的代码块,完整地复制,在 SSH 窗口中点击鼠标右键粘贴,然后按下回车。 你会看到系统提示:
脚本安装完成,正在立即运行第一次...
第三步:手动验证
脚本安装后,为了确保一切正常,你可以立即输入以下命令进行第一次运行,输入此命令也可以手动运行更新:
Bash
upcert
此时,脚本会开始跑代码:
检查环境:安装 curl 和 socat。
调用 acme.sh:使用你的 Token 向 Cloudflare 验证 DNS 记录。
申请证书:你会看到绿色的
Cert success字样。部署:脚本会将
dns.key和dns.cer复制到/etc/adguardhome/ssl/。重启:AdGuard Home 服务自动重启。
结果:最后会打印出证书的“新有效期截止时间”。
第四步:AdGuard Home 设置
打开 AdGuard Home 网页后台 -> 设置 -> 加密设置。
勾选 启用加密。
服务器名称:填写你的域名
dns.xxxx.com。证书路径:
/etc/adguardhome/ssl/dns.cer私钥路径:
/etc/adguardhome/ssl/dns.key点击保存。
如果一切顺利,AdGuard Home 会提示“证书有效”。
五、 深度解析:为什么我的脚本更可靠?
在编写这个脚本时,我解决了很多新手容易踩的“坑”,以下是技术细节的深度解析:
1. 解决 _ecc 路径迷局
Let's Encrypt 和 acme.sh 现在默认推荐使用 ECC (Elliptic Curve Cryptography) 证书,因为它比传统的 RSA 证书更小、更快、更安全。 但是,acme.sh 生成 ECC 证书时,会在域名文件夹后加上 _ecc 后缀(例如 /root/.acme.sh/dns.xxxx.com_ecc)。很多网上的旧教程是写死的路径,导致脚本找不到文件而报错。 本脚本解决方案:加入了智能判断逻辑(if [ -d ..._ecc ]),无论 acme.sh 生成哪种证书,脚本都能自动找到正确的文件。
2. Android 设备的“信任危机”
很多用户反馈,配置好证书后,电脑浏览器访问正常,但 Android 手机使用 Private DNS (私人DNS) 连接时却一直连不上。 原因:Android 系统极其严格,要求服务器必须提供完整的证书链(Fullchain),包含根证书和中间证书。如果只提供单一的域名证书,安卓会拒绝连接。 本脚本解决方案:在部署阶段,我使用 cat fullchain.cer > dns.cer 命令,强制将证书链合并写入最终文件。这确保了无论是 iOS, Android 还是 Windows,都能完美信任你的 DNS 服务器。
3. “每月强制” vs “每日检查”
acme.sh 自带的定时任务是每天检查,但只有在过期前 30 天才续期(Skipping 状态)。 为了防止意外情况(比如 OpenWrt 经常重置、日志被清空等),我在脚本中使用了 --force 参数,并设定为 每月 1 号 运行。 这意味着,每个月你的证书都会被强制刷新一次。虽然这看起来有点“浪费”,但在复杂的家庭网络环境中,确定性远比节省资源重要。我们可以确保每个月证书都是崭新的,永远不会遇到过期的尴尬。
六、 常见问题 (FAQ)
Q: 运行脚本时提示 Skipping, Next renewal time is... 怎么办? A: 这是正常的。说明你的证书还很新鲜(有效期 > 60天)。但如果你使用的是本教程提供的最新脚本,它包含 --force 参数,会忽略这个提示,强行续期,以确保你能看到“成功”的结果。
Q: 为什么生成的证书是 .cer 而不是 .pem 或 .crt? A: 扩展名只是代号,内容才是关键。.cer 是 AdGuard Home 推荐的后缀。本质上它们都是 X.509 编码的文本文件。我的脚本生成的 dns.cer 实际上包含了完整的 PEM 格式数据。
Q: 重启路由器后,定时任务还在吗? A: 在!脚本将任务写入了 /etc/crontabs/root,这是 OpenWrt 的持久化配置。除非你重置了路由器固件,否则任务会一直存在。
结语
通过这个脚本,我们将原本复杂的证书申请、部署、续期流程浓缩成了一个单词:upcert。这不仅体现了运维自动化的魅力,更让我们的家庭网络基础设施变得坚不可摧。
运维的最高境界,就是“忘记它的存在”。希望这个脚本能帮你省下宝贵的时间,去探索更多有趣的技术。
如果你在部署过程中遇到任何问题,欢迎在博客下方留言交流!
本文首发于 E路领航 ( blog.oool.cc),转载请注明出处。