sycnnj
发布于 2026-01-20 / 12 阅读

OpenWrt下AdGuard Home证书自动续期的一键脚本

关键词: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 天

这意味着,每隔两个多月,你就需要:

  1. 登录服务器手动申请证书。

  2. 下载证书文件。

  3. 上传到 OpenWrt 的指定目录。

  4. 重启 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

  1. 登录 Cloudflare Dashboard

  2. 点击你的域名(例如 oool.cc)。

  3. 在页面右下角的 API 区域,你可以直接看到:

    • Zone ID (区域 ID)

    • Account ID (账户 ID)

    • 请复制这两个字符串备用。

2. 生成 API Token

  1. 点击“获取您的 API 令牌”或访问 API Tokens 设置页

  2. 点击 Create Token (创建令牌)

  3. 选择 Edit zone DNS (编辑区域 DNS) 模板。

  4. Zone Resources (区域资源) 中,选择 Include -> Specific zone -> 你的域名

  5. 点击生成,你将获得一串字符,这就是 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

此时,脚本会开始跑代码:

  1. 检查环境:安装 curl 和 socat。

  2. 调用 acme.sh:使用你的 Token 向 Cloudflare 验证 DNS 记录。

  3. 申请证书:你会看到绿色的 Cert success 字样。

  4. 部署:脚本会将 dns.keydns.cer 复制到 /etc/adguardhome/ssl/

  5. 重启:AdGuard Home 服务自动重启。

  6. 结果:最后会打印出证书的“新有效期截止时间”。

第四步:AdGuard Home 设置

  1. 打开 AdGuard Home 网页后台 -> 设置 -> 加密设置

  2. 勾选 启用加密

  3. 服务器名称:填写你的域名 dns.xxxx.com

  4. 证书路径/etc/adguardhome/ssl/dns.cer

  5. 私钥路径/etc/adguardhome/ssl/dns.key

  6. 点击保存。

如果一切顺利,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),转载请注明出处。