很多团队希望把 GitLab、NAS、Wiki、跳板机或内部面板放在私网里,却仍然使用浏览器熟悉的域名和可信 HTTPS。这里其实是两个问题:DNS 决定客户端连接哪个 IP,TLS 证书只证明“这个域名的服务确实持有对应私钥”。只要把两件事拆开,就能在不暴露内网服务的前提下获得浏览器信任的免费证书,并让证书自动部署和续签。

本文以 app.example.com 为例,目标是让办公网或 VPN 用户访问 https://app.example.com 时命中 10.10.20.15,公网用户看不到这个私有地址;证书由 ACME 公共 CA 签发,续签后自动交给 Nginx 或 Ingress 热加载。

域名解析到内网:免费 SSL 自动部署与续签实现指南

一、先理解“解析到内网”到底意味着什么

RFC 1918 的 10.0.0.0/8172.16.0.0/12192.168.0.0/16 地址只在私网路由中有效。公网 DNS 即使返回 10.10.20.15,互联网路由器也不会把请求送到你的办公室。因此,可靠做法不是把私有 IP 写进公共解析,而是部署 Split-horizon DNS(水平分割 DNS):同一个域名在不同解析视图里得到不同答案。

内网解析器可以用 Windows DNS、BIND、CoreDNS、AdGuard Home 或路由器自带 DNS。DHCP、VPN 和移动办公配置必须把客户端指向该解析器;否则客户端可能继续使用公共 DNS,出现“域名能打开但解析到公网入口”或“浏览器开启 DoH 后绕过内网 DNS”的现象。

域名建议使用自己控制的真实公共域名,例如 git.example.com,不要使用 .local.lan 之类无法被公共 CA 识别的后缀。公共权威 DNS 可以不配置该主机名,也可以只配置一个受控的公网网关;内网 DNS 则把它覆盖为负载均衡器或反向代理的私网地址,而不是直接暴露应用节点。

域名解析到内网:免费 SSL 自动部署与续签实现指南

二、推荐架构:私有 DNS + DNS-01 + 部署钩子

这套架构的关键是:证书的控制权通过 DNS 证明,业务流量仍然只走内网。ACME 客户端向 DNS 服务商写入临时的 _acme-challenge TXT 记录,CA 从公网查询该记录来确认你控制这个域名;验证成功后签发证书,客户端再把证书文件交给 Web 服务器。

DNS-01 不要求 app.example.com 能从互联网访问,因此适合完全隔离的办公网、VPN 后端和没有公网入站端口的服务。它也支持通配符证书,例如 *.example.com,但通配符的私钥影响面更大,应只在确实需要时使用。

  1. 解析层:内网 DNS 将 app.example.com 指向 10.10.20.15;公网 DNS 不返回私有地址。

  2. 证书层:ACME 客户端使用 DNS 服务商 API 完成 DNS-01,不依赖业务主机的公网可达性。

  3. 入口层:Nginx、Caddy、Traefik 或 Kubernetes Ingress 终止 TLS,再代理到内网应用。

  4. 自动化层:续签任务只在证书确实变化时执行部署钩子,先做配置检查,再 reload 服务。

三、落地前的准备与边界

至少准备以下内容:一个可管理 DNS 记录的域名、一个固定的内网入口地址、DNS 服务商的最小权限 API Token、NTP 时间同步,以及可以查看定时任务日志的运维账号。API Token 只授予目标 Zone 的 DNS 编辑权,不要使用能管理整个账户的全局密钥;将它放在权限为 600 的文件中,避免写进 Git、镜像层或命令历史。

如果由多台 Nginx 节点提供服务,优先在一台证书节点签发,再通过 Ansible、Vault、Kubernetes Secret 或受控的 rsync 分发到其他节点。不要让每台机器都拥有一份高权限 DNS Token,也不要在共享目录中长期保留私钥。

四、用 acme.sh 实现签发、部署和自动续签

下面以支持 DNS API 的服务商为例,命令中的变量名会随服务商插件变化,请以对应插件文档为准。示例只展示流程,不放入真实 Token:

# 仅在当前 shell 临时注入,生产环境应放入 root 可读的凭据文件
export CF_Token='REDACTED_DNS_API_TOKEN'
export CF_Account_ID='REDACTED_ACCOUNT_ID'

# DNS-01 签发单域名与通配符证书
acme.sh --issue --dns dns_cf \
  -d app.example.com -d '*.app.example.com' \
  --keylength ec-256 --server letsencrypt

# 安装证书,并在更新后自动校验和热加载 Nginx
acme.sh --install-cert -d app.example.com \
  --key-file /etc/nginx/ssl/app.key \
  --fullchain-file /etc/nginx/ssl/app.fullchain.pem \
  --reloadcmd "nginx -t && systemctl reload nginx"

acme.sh 通常会安装自己的 cron 任务,后续由 --cron 定期检查证书是否接近到期。建议在安装后立即验证任务、日志和部署钩子,而不是等到第一次续签才发现路径或权限错误。若 DNS 传播较慢,可以在签发命令中增加等待时间,但不要把等待时间设得过长而掩盖解析配置错误。

Nginx 入口可以保持简单。内网 DNS 应指向这个入口的私网地址,应用节点不必直接暴露 443:

server {
    listen 443 ssl;
    server_name app.example.com;

    ssl_certificate     /etc/nginx/ssl/app.fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/app.key;

    location / {
        proxy_pass http://10.10.20.15:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

证书文件应由 root 拥有,私钥建议 0600;如果 Nginx 以专用用户运行,可用组权限或 ACL 精确授权。部署钩子中必须先执行 nginx -t,检查失败就停止 reload,保留上一份可用证书。

域名解析到内网:免费 SSL 自动部署与续签实现指南

五、Kubernetes 场景:交给 cert-manager 管理

如果服务运行在 Kubernetes,建议使用 cert-manager 的 ClusterIssuer 配置 DNS-01。DNS API 凭据放在专用 Namespace 的 Secret 中,通过 RBAC 限制读取范围;Ingress 引用 Certificate 生成的 Secret,cert-manager 会在到期前重新签发并更新 Secret,Ingress 控制器负责重新加载。

这套方案的优点是证书生命周期与声明式配置绑定,扩容新服务时无需手工复制证书。需要特别关注 DNS API 的权限、Secret 的备份与轮换,以及控制器日志和事件;不要仅依赖“Secret 已更新”判断业务完成,最好再用探针检查实际 TLS 握手。

六、三种常见方案的优劣对比

方案适用场景优势代价与限制
私有 DNS + DNS-01内网或 VPN 服务,需要浏览器信任无公网入站要求,支持通配符,域名与流量都能留在私网需要 DNS API;TXT 传播、Token 泄露和多节点分发需要治理
公网反代 + HTTP-01已经有公网网关,业务允许从外部访问部署直观,很多托管平台开箱即用必须让 CA 访问挑战路径;增加公网暴露面,不适合纯内网域名
内部 CA(step-ca / AD CS)设备完全受组织管理,需要内网 PKI 或 mTLS可签发内部后缀、支持更完整的身份策略,长期成本可控每台客户端都要安装根证书;外部设备和普通浏览器默认不信任

如果目标是“员工电脑无需额外安装根证书,直接看到浏览器可信锁标”,优先选择真实公共域名配合 DNS-01。如果是设备、服务到服务通信,或者组织能够统一下发根证书,内部 CA 反而更适合做细粒度的 mTLS。

七、把续签做成可观测的运维流程

自动化不等于无人值守。至少要检查四类信号:ACME 客户端最近一次运行时间、DNS TXT 创建与清理结果、部署钩子退出码、业务端口实际返回的证书。监控端点的到期天数时,要用带 SNI 的握手检查,例如:

openssl s_client -connect 10.10.20.15:443 \
  -servername app.example.com -showcerts < /dev/null 2> /dev/null \
  | openssl x509 -noout -subject -issuer -dates

# 绕过 DNS,直接验证“域名 + 私有 IP + SNI”
curl --resolve app.example.com:443:10.10.20.15 \
  --fail --silent --show-error https://app.example.com/healthz

排障时先分层定位:用 dig @内网DNS app.example.comdig @1.1.1.1 app.example.com 对比解析视图;再用 curl --resolve 验证路由和 SNI;最后检查 Nginx 的证书路径、权限与 reload 日志。若 DNS-01 失败,常见原因是 Token 权限不足、TXT 记录还未传播、域名实际使用了另一组权威 DNS,或客户端时钟偏差过大。

八、安全清单与最终建议

  • 公网 DNS 不返回 RFC 1918 地址;外部访问必须经过明确的反代、VPN 或零信任入口。

  • DNS API Token 按 Zone、按用途最小授权,定期轮换,日志中不打印 Token。

  • 私钥只在证书节点和必要的服务节点落盘,权限最小化,分发过程可审计。

  • 部署钩子采用“校验通过才 reload”,失败时保留旧证书并触发告警。

  • 同时监控 DNS、ACME、服务 reload 和真实 TLS 握手,避免只看某一个环节。

综合来看,最稳妥的默认选择是:使用自己控制的真实域名,内网 DNS 做分流,ACME 通过 DNS-01 完成验证,Nginx 或 Ingress 用部署钩子热加载证书。它把“网络不可达”和“证书不可签”这两个经常被混淆的问题分离开来,既守住内网边界,也保留了浏览器信任和自动续签的便利。

参考:Let's Encrypt Challenge Typesacme.sh 官方项目