内部服务TLS证书的正确配置方案
速览
本文讨论了内部服务TLS证书配置中的常见问题,包括自签名证书的风险、证书管理自动化及与现有基础设施的集成。提出了一套最佳实践,涵盖证书签发、续期和撤销流程,以降低运维复杂度和安全漏洞。对于企业IT团队优化内部服务安全有参考价值。
AI 深度解读
背景
在内部网络中部署 HTTPS 服务时,TLS 证书的管理常常令人头疼。许多团队选择使用 ICANN 保留的私有顶级域名(如 .internal)来命名内部服务,但这意味着必须为每个客户端配置自签名证书,或者干脆忽略 TLS 错误——这两种做法都不安全也不可扩展。另一种方案是使用自己拥有的公共域名,但需要解决 DNS 解析的“水平分割”问题:让内部客户端解析到私有 IP,外部客户端解析到公共 IP,同时还要防止外部流量误入内部服务。本文作者基于实际经验,提出了一套结合 split-horizon DNS、ACME 客户端、nginx 反向代理(兼作 WAF)的完整方案,并给出了可复现的代码示例。
核心内容
假设我们有一台服务器,上面运行着多个 HTTP 服务,其中一些对外公开,另一些仅限内部使用(需通过 VPN 连接)。以 Grafana 为例,它运行在内部 IP 10.0.1.10 上,VPN 内置 DNS 解析功能。
两种常见选择及其问题
-
使用 ICANN 保留的私有顶级域名(如
.internal)
我们可以创建一条 A 记录,例如grafana.tuxnet.internal指向10.0.1.10。但若想启用 HTTPS,就必须生成自签名证书。问题在于:每个加入内部网络的客户端都需要手动配置信任该自签名证书,或者更糟——让用户忽略 TLS 证书错误。这显然不可取。 -
使用自己拥有的公共 apex 域名(如
tuxnet.dev)
通过“split-horizon DNS”配置:对公共 DNS 解析器,grafana.tuxnet.dev解析到公共 IP;对 VPN 内的客户端,该域名解析到内部 IP10.0.1.10。这样可以使用公共 CA(如 Let's Encrypt 或 ZeroSSL)签发证书。但缺点是:必须部署一个 WAF(Web Application Firewall)来拒绝所有非来自 VPN 的流量。
作者认为的“正确做法”
权衡两种方案后,作者认为在服务器上统一设置一个 WAF 比在每台加入内网的机器上安装自签名证书(或让用户忽略 TLS 错误)要容易得多。因此推荐第二种方案,即 split-horizon DNS + 公共证书 + WAF。
具体实现步骤
所需工具
- VPN 带 DNS 解析功能:作者选择 NetBird(因其 Custom Zones 功能可自动实现 split-horizon DNS)。
- ACME 客户端:选择
acme.sh,支持多种模式。 - 反向代理兼 WAF:选择 nginx。
签发证书
使用 acme.sh 的 standalone 模式签发证书(注意:服务器需能从公共网络访问,且端口 80 需临时可用):
acme.sh --issue -d grafana.tuxnet.dev --server letsencrypt --standalone
standalone 模式的好处是 nginx 不需要一直监听 80 端口,仅在 acme.sh 运行时临时占用。
nginx 配置
关键配置示例:
upstream grafana {
server localhost:3000;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen our-server.netbird.cloud:443 ssl; # 绑定到 VPN 网络接口
server_name grafana.tuxnet.dev;
http2 on;
ssl_certificate /etc/ssl/certs/grafana.tuxnet.dev.crt;
ssl_certificate_key /etc/ssl/private/grafana.tuxnet.dev.key;
# ... 其他日志和代理配置
location / {
proxy_pass http://grafana;
proxy_set_header Host $host;
}
location /api/live/ {
proxy_pass http://grafana;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
}
关键点:listen our-server.netbird.cloud:443 ssl; 将 nginx 绑定到 VPN 网络接口(而非公共接口)。这意味着任何直接来自公网的请求(即便 DNS 解析到了服务器 IP)都会被拒绝,因为该 IP 端口并未监听。这就是 WAF 的第一道防线——实际上,它等同于一个网络层面的访问控制。
安全分层
- 第一层:split-horizon DNS,确保只有 VPN 客户端能解析到内部 IP。
- 第二层:WAF(即绑定到 VPN 接口的 nginx),即使 DNS 被绕过或出错,外部流量也无法到达内部服务。
证书自动续期
使用 acme.sh 的 --cron 功能,并通过 cron 任务每天执行。示例脚本中包含了:
- 使用
setcap授予socat绑定特权端口(80)的能力(避免以 root 运行 acme.sh)。 - 比较新旧证书的校验和,仅在变更时复制并重启 nginx。
main() {
refresh_certs
sync_api_tuxnet_dev_certs
sync_internal_tuxnet_dev_certs
reload_nginx
}
额外技巧:SAN 和 CNAME
如果内部有多个服务(如 grafana.tuxnet.dev、analytics.tuxnet.dev),无需为每个子域名单独签发证书。可以:
- 创建一个 A 记录
internal.tuxnet.dev指向内部 IP。 - 为每个服务创建 CNAME 记录指向
internal.tuxnet.dev。 - 使用 SAN(Subject Alternative Name)签发一张包含所有域名的证书:
acme.sh --issue -d internal.tuxnet.dev -d grafana.tuxnet.dev -d analytics.tuxnet.dev --server letsencrypt --standalone
验证证书 SAN 内容:
echo | openssl s_client -connect internal.tuxnet.dev:443 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
输出显示所有子域名均在 SAN 列表中,同一张证书可被多个服务共用。
关键要点
- 避免使用私有顶级域名(如
.internal),因为需要为每个客户端手动配置自签名证书,扩展性差且易出错。 - 使用自己拥有的公共域名 + split-horizon DNS 是更优方案:内部用户通过 VPN 解析到私有 IP,外部用户解析到公共 IP,同时利用公共 CA 签发证书。
- 必须部署 WAF 作为第二层防护:将 nginx 绑定到 VPN 网络接口,拒绝所有非 VPN 来源的流量,即使 split-horizon DNS 失效也能保证安全。
- 使用 acme.sh 的 standalone 模式简化证书签发:无需 nginx 一直监听 80 端口,仅临时占用。
- 证书自动续期是必备环节:通过 cron 每日检查并更新,使用
setcap避免 root 运行 ACME 客户端。 - 对于多个内部服务,使用 SAN 证书而非通配符证书:通配符证书存在安全风险,SAN 证书更可控且同样支持多个子域名。
- CNAME 记录可让多个子域名共享同一张证书:只需为一个内部域名(如
internal.tuxnet.dev)签发证书,其余服务通过 CNAME 指向它,并包含在 SAN 列表中。
意义与影响
该方案解决了内部服务 HTTPS 部署的长期痛点:既不需要自签名证书带来的信任问题,也不需要为每个服务单独
