← 返回信息流
AI 资讯Hacker News·1 小时前

内部服务TLS证书的正确配置方案

原标题:TLS certificates for internal services done right

速览

本文讨论了内部服务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 解析功能。

两种常见选择及其问题

  1. 使用 ICANN 保留的私有顶级域名(如 .internal
    我们可以创建一条 A 记录,例如 grafana.tuxnet.internal 指向 10.0.1.10。但若想启用 HTTPS,就必须生成自签名证书。问题在于:每个加入内部网络的客户端都需要手动配置信任该自签名证书,或者更糟——让用户忽略 TLS 证书错误。这显然不可取。

  2. 使用自己拥有的公共 apex 域名(如 tuxnet.dev
    通过“split-horizon DNS”配置:对公共 DNS 解析器,grafana.tuxnet.dev 解析到公共 IP;对 VPN 内的客户端,该域名解析到内部 IP 10.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.devanalytics.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 部署的长期痛点:既不需要自签名证书带来的信任问题,也不需要为每个服务单独

查看原文 →tuxnet.dev