内部服务的TLS证书正确实践

Hacker News Top 工具

摘要

介绍了如何使用分视域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 证书验证

Lobsters Hottest

深入解释 Linux 上的 TLS 证书验证,涵盖信任存储、链构建以及常见陷阱(如缺少中间证书)。文章阐明了为什么不同工具可能对证书有效性存在分歧。

如果你喜欢MITM攻击,就别管DNSSEC

Lobsters Hottest

文章认为,忽略DNSSEC会让用户暴露在中间人攻击之下,并以电子邮件、Matrix和XMPP为例,将其与历史上对HTTPS的抵制进行类比。

使用双向TLS保护的私有pkg仓库

Lobsters Hottest

本文介绍如何搭建一个由双向TLS保护的私有FreeBSD软件包仓库,包括创建自定义证书颁发机构以及配置nginx要求客户端证书。

合法TLS监听的并行重建

Lobsters Hottest

基于俄罗斯XMPP服务Jabber.ru的真实事件,使用ACME自动化对合法TLS监听进行分析和复现,展示了如何实现和检测基于证书的拦截。