内部服务的TLS证书正确实践
摘要
介绍了如何使用分视域DNS、带DNS解析器的VPN以及ACME客户端(如acme.sh配合Let's Encrypt)来为内部服务设置TLS证书,为自签名证书提供了实用的替代方案。
暂无内容
查看缓存全文
缓存时间: 2026/07/09 16:36
# 内部服务的TLS证书:正确做法
来源:https://tuxnet.dev/posts/tls-for-internal-services/
标题有点标题党——效果因人而异,但让我解释为什么我认为“这才是正确方式”。先从一个简单例子开始:我们有一台服务器,上面托管着多个HTTP服务。部分服务对外,部分对内。要访问内部服务,你需要连接到VPN。
为简单起见,我们考虑两种选择:
1. 使用ICANN为私有用途保留的顶级域名(https://itp.cdn.icann.org/en/files/root-system/identification-tld-private-use-24-01-2024-en.pdf)——例如 `.internal`。
2. 使用我们拥有的公共根域名——例如 `tuxnet.dev`。
以Grafana作为内部应用示例。假设它可通过内部IP地址 `10.0.1.10` 访问,并且我们的VPN具备DNS解析功能。
## 那么 `.internal` 有什么问题?
我们可以简单地创建一个类型为“A”的DNS记录,解析到内部IP `10.0.1.10` —— 例如 `grafana.tuxnet.internal`。但是,如果我们不想让它成为明文HTTP服务,就需要创建一个自签名证书。
好处是有大量教程教你如何操作(例如这个(https://www.digitalocean.com/community/tutorials/how-to-create-a-self-signed-ssl-certificate-for-nginx-in-ubuntu-16-04))。坏处是突然之间每个HTTP客户端都需要配置信任这个自签名证书。或者,我们也可以直接告诉用户忽略TLS证书错误 ` ̄\_(ツ)_/ ̄`。
## 如何“正确”地做
认识“分裂域DNS”配置。对于公共DNS解析器,我们的 `grafana.tuxnet.dev` 域名解析到一个公共IP;而对于连接到VPN的客户端,该域名解析到一个内部IP。
好处是,由于它解析到公共IP,我们可以使用像Let's Encrypt或ZeroSSL这样的公共CA。坏处是,我们仍然需要某种WAF来拒绝非来自VPN的流量。
综合考虑两种方案的利弊,我认为在服务器上统一设置一个WAF,比在加入内部网络的每台机器上安装自签名证书(或者建议用户忽略TLS错误)要容易得多。
## “空谈无用,show me the code”
理论我们已经有了,现在动手实践。我们需要:
1. 具备DNS解析功能的VPN——我选择 NetBird(https://netbird.io/)。
2. 用于签发证书的ACME客户端——我选择 acme.sh(https://github.com/acmesh-official/acme.sh)。
3. 在grafana前端的反向代理(带WAF功能)——我选择 nginx(https://nginx.org/)。
如果你读过我其他博客,可能注意到我是NetBird的粉丝(抱歉啦Tailscale)。得益于自定义区域(https://docs.netbird.io/manage/dns/custom-zones)功能,NetBird帮我们完成了分裂域DNS的所有繁重工作。通过使用用户组(https://docs.netbird.io/manage/access-control#user-groups)或对等组(https://docs.netbird.io/manage/access-control#peer-groups),我们可以有选择地应用自定义区域,使服务器使用公共DNS解析器来解析 `grafana.tuxnet.dev`。
为什么要把服务器排除在该自定义区域之外?除非我们要使用http-01挑战(https://letsencrypt.org/docs/challenge-types/),否则不是必须的。也可以使用其他方法,但为了这篇博文,我选择 `http-01`。好了,现在用以下命令获取证书:
``
acme.sh --issue -d grafana.tuxnet.dev --server letsencrypt --standalone
``
`acme.sh` 非常灵活,支持多种模式(https://github.com/acmesh-official/acme.sh#%EF%B8%8F-supported-modes)。独立模式(通过 `--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;
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;
access_log /var/log/nginx/grafana.tuxnet.dev.access.log main;
error_log /var/log/nginx/grafana.tuxnet.dev.error.log warn;
location / {
proxy_pass http://grafana;
proxy_set_header Host $host;
}
# 代理Grafana Live WebSocket连接。
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;`。我们绑定到了服务器的VPN网络接口。这里可以用VPN IP地址替代 `our-server.netbird.cloud`。实际操作中,这会拒绝任何源自公共互联网、发往 `grafana.tuxnet.dev` 的流量——这就是我们的Web访问防火墙(WAF)。
安全是分层级的(就像洋葱和食人魔)。第一层是分裂域DNS,但如果由于某种原因失败或被巧妙绕过,我们还有第二层——WAF——来守住防线。
最后但同样重要的是——证书自动续期。`acme.sh` 自带 `--cron` 标志。我们需要一个每日cron任务来调用:
``
acme.sh --cron
``
`acme.sh` 会自动选择需要续期的证书。我们只需要确保cron任务将新证书复制到nginx配置中 `ssl_certificate` 和 `ssl_certificate_key` 指定的位置。还需要重载nginx以使用新证书。我们的cron任务可以这样写:
``
main() {
refresh_certs
sync_api_tuxnet_dev_certs
sync_internal_tuxnet_dev_certs
reload_nginx
}
refresh_certs() {
setcap CAP_NET_BIND_SERVICE=+ep /usr/bin/socat1
sudo -u acmesh /home/acmesh/acme.sh/acme.sh --cron
setcap -r /usr/bin/socat1
}
sync_api_tuxnet_dev_certs() {
local green=$(get_checksum "$API_SRC_KEY")
local blue=$(get_checksum "$API_DST_KEY")
local key_allowed_group=www-data
if [[ "$green" != "$blue" ]]; then
sync_certs "$API_SRC_KEY" "$API_DST_KEY" "$API_SRC_CERT" "$API_DST_CERT" "$key_allowed_group"
fi
}
sync_internal_tuxnet_dev_certs() {
local green=$(get_checksum "$INTERNAL_SRC_KEY")
local blue=$(get_checksum "$INTERNAL_DST_KEY")
local key_allowed_group=www-data
if [[ "$green" != "$blue" ]]; then
sync_certs "$INTERNAL_SRC_KEY" "$INTERNAL_DST_KEY" "$INTERNAL_SRC_CERT" "$INTERNAL_DST_CERT" "$key_allowed_group"
fi
}
reload_nginx() {
nginx -t
systemctl reload nginx
}
get_checksum() {
sha256sum "$1" | cut -d' ' -f1
}
sync_certs() {
local src_key="$1"
local dst_key="$2"
local src_cert="$3"
local dst_cert="$4"
local key_allowed_group="$5"
cp -v "$src_key" "$dst_key"
logger "synced $dst_key"
cp -v "$src_cert" "$dst_cert"
logger "synced $dst_cert"
chown root:${key_allowed_group} "$dst_key"
chown root:ssl-cert "$dst_cert"
chmod 640 "$dst_key" "$dst_cert"
}
main "$@"
``
关于 `setcap CAP_NET_BIND_SERVICE=+ep /usr/bin/socat1` 的一点说明:`acme.sh` 在独立模式下使用 `socat` 来监听80端口。一方面,我们不想在非必要情况下以root身份运行 `acme.sh`。另一方面,80端口是“特权端口”之一,默认情况下非root进程无法绑定。这就是 `CAP_NET_BIND_SERVICE=+ep` 发挥作用的地方。如果你对此感兴趣,可以查看这篇文章(https://www.baeldung.com/linux/bind-process-privileged-port)。
当TLS完美工作时那种感觉
生活很美好,我们拥有了一个正常工作的TLS——无论服务是内部还是外部,无论是一天后还是一年后。
## 额外福利:SAN和CNAME
如果还有其他内部服务怎么办?如果希望它们位于不同的子域名下呢?我们需要为每个服务生成单独的证书吗?答案是否定的,我们有两种解决方案:
1. 通配符证书——我并不喜欢它,因为其安全隐患(https://knowledge.digicert.com/quovadis/ssl-certificates/ssl-general-topics/what-are-the-pros-and-cons-of-a-wildcard-certificate)。
2. TLS SAN(主题备用名称)——除了CN(通用名称)之外,定义提到的SAN。更多信息请查看 https://www.ssl.com/faqs/common-name/
所以实际上,我们可以为 `internal.tuxnet.dev` 创建一个“A”记录,然后为 `grafana.tuxnet.dev` 和例如 `analytics.tuxnet.dev` 创建“CNAME”记录,都解析到 `internal.tuxnet.dev`。然后像这样生成一个证书:
``
acme.sh --issue -d internal.tuxnet.dev -d grafana.tuxnet.dev -d analytics.tuxnet.dev --server letsencrypt --standalone
``
其详细信息如下:
``
echo | openssl s_client -connect internal.tuxnet.dev:443 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
``
``
subject=CN=internal.tuxnet.dev
X509v3 Subject Alternative Name:
DNS:analytics.tuxnet.dev, DNS:grafana.tuxnet.dev, DNS:internal.tuxnet.dev
``
现在我们需要做的就是将同一个证书重用于 nginx 配置中的不同 `server` 定义。
## 总结
我们学会了如何安全地为内部服务设置TLS证书,而不会给下游HTTP客户端带来TLS问题。这都归功于分裂域DNS、WAF和ACME协议。而且完全免费!
相似文章
Linux 上的 TLS 证书验证
深入解释 Linux 上的 TLS 证书验证,涵盖信任存储、链构建以及常见陷阱(如缺少中间证书)。文章阐明了为什么不同工具可能对证书有效性存在分歧。
搭建自己的DoH(基于HTTPS的DNS)服务
一篇关于搭建自己的基于HTTPS的DNS(DoH)服务的指南,通过加密DNS查询来提升隐私和安全性。
如果你喜欢MITM攻击,就别管DNSSEC
文章认为,忽略DNSSEC会让用户暴露在中间人攻击之下,并以电子邮件、Matrix和XMPP为例,将其与历史上对HTTPS的抵制进行类比。
使用双向TLS保护的私有pkg仓库
本文介绍如何搭建一个由双向TLS保护的私有FreeBSD软件包仓库,包括创建自定义证书颁发机构以及配置nginx要求客户端证书。
合法TLS监听的并行重建
基于俄罗斯XMPP服务Jabber.ru的真实事件,使用ACME自动化对合法TLS监听进行分析和复现,展示了如何实现和检测基于证书的拦截。