ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

自建网关自签名证书信任库注入方案:Azure APIM 容器化实践

自建网关自签名证书信任库注入方案:Azure APIM 容器化实践 自建网关Self-hosted Gateway跑在客户自己的基础设施里调用后端 API 时经常被自签名证书卡住日志里抛“self signed certificate in certificate chain”这类错误。这个坑我踩过很多次网上讨论大多停留在“关掉证书校验”或者“在 APIM 实例里上传证书”这两层但对于自建网关来说前者不安全后者又未必生效。我自己最后用的是“把自签名证书直接注入网关容器信任库”的做法也就是这里说的方案三。这篇文章把原理、步骤和踩坑细节完整记录下来适合正在用 Azure API Management 自建网关对接内部服务的架构师或运维同学参考。1. 问题根源自建网关为何拒绝自签名证书1.1 TLS 握手时的信任判定流程任何一次 HTTPS 调用客户端拿到服务端的证书后要做的核心动作就两件事第一沿着证书链一层层往上找直到找到根证书第二在本地信任库中查找这个根证书是否存在。如果本地信任库里找不到能对上号的根证书SSL 握手直接失败客户端会拒绝发出任何 HTTP 请求。很多人以为“证书是自签名的所以客户端不认”这个理解不准确。真正导致失败的原因不是“自签名”这个动作本身而是“这个证书没有出现在客户端的信任存储里”。换句话说如果我手动把一个自签名证书导入到客户端的受信任根存储中那在 TLS 校验层面它和正规 CA 签发的证书没有任何区别——握手照样能通过。这就意味着解决问题的大方向不是“绕过校验”而是“想办法让网关信任这张证书”。1.2 自建网关容器里的信任库长什么样Azure APIM 的自建网关本质上是一个打包好的容器镜像底层运行时在早期版本基于 Nginx现在的新版本基于 Envoy。无论底层是哪个它们的共同点是TLS 校验依赖操作系统提供的 CA 信任库也就是类似/etc/ssl/certs/这个目录。容器基础镜像里自带的信任库内容通常是 Ubuntu/Debian 官方镜像预置的那批标准 CA 证书比如 DigiCert、GlobalSign、Lets Encrypt 这些。企业内部的根 CA 或者后端服务自己生成的证书天然不在里面。这里容易忽略一个细节云上托管的 APIM 网关Cloud Gateway和自建网关虽然都是 APIM 的运行时但证书管理路径完全不同。云网关可以通过 Azure 门户的“证书”管理页面上传根证书再由 APIM 服务自动分发给它而自建网关运行在你的环境里和 Azure 控制面的交互仅限于配置拉取证书同步机制并不像云网关那样“自动且透明”。所以很多在云网关好用的方法搬到自建网关上会出现“配置了却还是报错”的诡异现象。1.3 常见报错信息解读自建网关调用后端时证书相关报错通常有以下几种self signed certificate in certificate chain这说明客户端在证书链中遇到了自签名证书并且不在信任库中。unable to get local issuer certificate说明客户端找不到签发该证书的 CA 证书典型情况是你只提供了叶子证书没有把根 CA 或中间 CA 放进去。certificate signed by unknown authority链是完整的但上级证书不被信任。这三种错误都指向同一个核心——信任库缺东西。实际操作时我习惯在修改网关配置之前先用openssl s_client -connect 后端地址:443 -showcerts拉一遍证书链看看后端到底返回了哪些证书是否需要中间证书。这一步能帮你判断后面该导入哪个证书文件。1.4 方案一和方案二为什么不推荐方案一在 APIM 策略中关闭 TLS 校验。严格来说APIM 策略层面你可以在forward-request或后端调用时把 SSL 校验关掉甚至配置http协议访问后端。这样确实能立刻消除证书报错但这等于把 HTTPS 降级成了裸 HTTP中间人可以随意篡改请求。内部网络里短时间调试可以长期跑在生产环境上是要担责任的。方案二在 APIM 实例的“证书”页面导入根证书然后在策略中通过authentication-certificate或 backend settings 指定证书。这个方法在云网关场景下是标准做法但放到自建网关上你会发现证书虽然上传到了 APIM 实例自建网关却不一定认它。因为自建网关自身的 TLS 校验发生在 Envoy/Nginx 进程层面这个进程只认操作系统的信任库而不是 APIM 控制面的证书列表。所以方案三的思路就非常明确了干脆绕过 APIM 控制面直接在自建网关容器的操作系统信任库中把我们的自签名证书“登记”进去。2. 方案三设计思路把自签名证书注入网关容器信任库2.1 方案三到底做了什么方案三的核心动作可以概括成一句话在网关容器启动前把目标证书文件挂载到容器内的系统信任目录并让系统 CA 证书更新机制重新生成信任索引。这么做的效果是网关容器内运行的所有进程——不管是 Envoy 还是 Nginx——在做 SSL 校验时都能在系统信任库里匹配到你的证书握手自然就通了。这个方案不依赖 Azure 控制面也不改动网关的请求转发策略安全性和合规性上都说得过去。证书的信任范围被限制在自建网关容器内部不会影响其它服务。2.2 为什么用挂载而不是手动进入容器改文件有一种偷懒做法是docker exec -it 网关容器 /bin/bash进容器后手动把证书放到/usr/local/share/ca-certificates/再执行update-ca-certificates。这方法当场有效但下次容器重建或滚动升级后所有改动都会丢失。自建网关经常需要跟着 APIM 控制面版本升级每次升级都要重新进容器操作一次麻烦不说还容易遗漏。用挂载方案的好处是宿主机上只维护一份证书文件容器每次启动时自动加载而且对 Docker Compose 和 Kubernetes 都能用同样的一套思想表达。2.3 方案选型必须满足的条件不是所有环境都能直接套用方案三动手前先确认几件事自建网关必须跑在 Linux 容器环境绝大多数情况都满足。你有宿主机文件系统的管理权限或者在 Kubernetes 里能创建 ConfigMap/Secret。你拿到的证书文件可以被转换成 PEM 格式也就是带有-----BEGIN CERTIFICATE-----明文头尾的文本文件。后端服务的证书链是完整的或者你至少能把根 CA 证书单独导出来。这些条件只要满足方案三基本是通用的。如果你们用的自建网关是 Windows 容器部署思路类似但路径不一样本文不展开。2.4 三种方案的对比方案操作位置安全性生效范围维护成本方案一关闭 TLS 校验APIM 策略低等于明文传输按 API/策略生效最低方案二APIM 实例上传证书Azure 门户中证书分发到云网关对云网关生效好对自建网关不稳定中方案三容器信任库注入自建网关容器高仅在网关内部扩展信任所有经过该网关的后端调用低但需要管理证书文件从表格能看出来方案三是唯一兼顾“安全”和“对自建网关有效”的方案。3. 实操全流程把自签名证书塞进自建网关信任库3.1 准备后端证书文件这一步是根基。先把后端服务的自签名证书或自建 CA 根证书导出来。如果你的后端是 Nginx 或 Apache 配置的自签名证书通常你已经有一个.crt或.pem文件直接跳过导出步骤。如果没有可以用 openssl 从正在运行的服务上直接拉取openssl s_client -connect backend.example.com:443 -showcerts /dev/null 2/dev/null | openssl x509 -outform PEM backend-cert.pem这个命令会把后端返回的证书链中第一张证书保存为 PEM 文件。如果想导出完整链可以去掉最后的openssl x509管道把原始输出重定向到文件然后手工截取需要的那一段。导出后用文本编辑器打开backend-cert.pem确认第一行是-----BEGIN CERTIFICATE-----最后一行是-----END CERTIFICATE-----。如果文件是乱码或者有PKCS7字样说明不是 PEM 格式需要转换。注意不要把.pfx或.jks这类二进制格式直接挂载进容器Envoy/Nginx 都不认识。3.2 判断需要导入哪张证书这里分两种情况后端证书本身就是自签名证书没有上级 CA那就把这张证书导入信任库。后端证书由企业内部自建 CA 签发那优先导入根 CA 证书这样以后同一根 CA 签发的其它服务证书也都会被信任。导入根 CA 证书时务必确认文件里包含的是完整 PEM而不是只有一段公钥。可以使用openssl x509 -in backend-cert.pem -text -noout查看证书详细信息重点关注Issuer和Subject字段确认证书层级关系。3.3 Docker Compose 部署的自建网关配置自建网关最常见的部署方式就是 Docker Compose。修改docker-compose.yml给gateway服务增加挂载配置version: 3.8 services: self-hosted-gateway: image: mcr.microsoft.com/azure-api-management/gateway:latest container_name: apim-gateway ports: - 8080:8080 - 8443:8443 environment: - APIM_ENDPOINTyour-gateway-endpoint - APIM_AUTHENTICATION_TOKENyour-token # 视版本而定部分镜像支持用环境变量覆盖 CA 路径 - SSL_CERT_FILE/trusted-ca/backend-cert.pem - CURL_CA_BUNDLE/trusted-ca/backend-cert.pem volumes: - ./certs/backend-cert.pem:/trusted-ca/backend-cert.pem:ro这里有两个关键点解释一下。第一./certs/backend-cert.pem是宿主机上的文件路径冒号后面是容器内路径。我把证书统一放在容器的/trusted-ca/目录下方便管理。第二SSL_CERT_FILE和CURL_CA_BUNDLE这两个环境变量是很多 TLS 客户端库默认读取的 CA 路径。但坦率说Envoy 对这两个环境变量的支持程度在不同版本上有差异光靠环境变量并不保险。更稳的做法是挂载到系统信任目录并触发证书索引更新。由于 Docker 原生不支持在容器启动后自动执行命令最优雅的方式是自建一个基于官方镜像的衍生镜像FROM mcr.microsoft.com/azure-api-management/gateway:latest COPY ./certs/backend-cert.pem /usr/local/share/ca-certificates/backend-cert.crt RUN update-ca-certificates如果你的环境不允许自己构建镜像也可以把挂载路径直接指向/usr/local/share/ca-certificates/然后通过command覆盖容器默认启动命令command: sh -c chmod 644 /usr/local/share/ca-certificates/backend-cert.crt update-ca-certificates /bin/gateway注意最后那一段/bin/gateway要视官方镜像实际入口命令而定不同版本可能有差异。所以条件允许时我更推荐Dockerfile方案逻辑最可控。3.4 Kubernetes 部署时的配置方式Kubernetes 下的自建网关部署通常是一个 Deployment 或 Helm Chart。先创建包含证书的 ConfigMapkubectl create configmap apim-ca-cert --from-filebackend-cert.pemcerts/backend-cert.pem然后在 Deployment 里挂载 ConfigMap并把证书放到/usr/local/share/ca-certificates/目录。由于 ConfigMap 挂载的文件默认为只读而且不能为某个文件单独设置权限所以update-ca-certificates可能需要放在容器的启动命令里执行。也可以通过 initContainer 做一次“证书复制并刷新信任库”的动作spec: initContainers: - name: ca-cert-updater image: mcr.microsoft.com/azure-api-management/gateway:latest command: - sh - -c - | cp /configmap/backend-cert.pem /usr/local/share/ca-certificates/backend-cert.crt update-ca-certificates volumeMounts: - name: ca-configmap mountPath: /configmap - name: ca-store mountPath: /usr/local/share/ca-certificates containers: - name: gateway image: mcr.microsoft.com/azure-api-management/gateway:latest volumeMounts: - name: ca-store mountPath: /usr/local/share/ca-certificates这种做法的思路是用 initContainer 把证书复制到共享卷里再执行系统证书更新命令正式网关容器启动时直接复用这个已经更新好的卷。其实在生产环境里我建议直接用公司内部的镜像仓库托管一个“网关基础镜像 自签名 CA”的二层镜像。每次升级官方网关版本时重新构建一次镜像团队只要维护 Dockerfile 里的 COPY 命令就够了。3.5 验证证书是否被信任配置完后不能只看日志要主动验证。验证分三个层次先进入容器内测试docker exec -it 自建网关容器名 /bin/bash curl -v https://backend.example.com/health如果curl能正常返回结果没有证书报错说明信任库已经生效。再验证系统信任库是否包含你的证书docker exec -it 自建网关容器名 bash -c grep -l YOUR_CA_SUBJECT /etc/ssl/certs/*.pem如果能在/etc/ssl/certs/下看到类似backend-cert.pem的符号链接说明update-ca-certificates确实把证书登记进去了。最后在 APIM 上做一个转发测试配置一个指向后端的 API发送真实请求观察响应是否正常。这一步能验证网关转发链路中 TLS 校验确实生效了。提示如果是在 Kubernetes 里直接用kubectl exec -it pod名 -c gateway -- bash进入容器执行同样的验证命令。4. 避坑实录常见问题与排查方向4.1 配置了证书仍然报错这是遇到最多的反馈。明明挂载了证书update-ca-certificates也执行了日志里还是报证书不受信任。排查顺序很重要。先确认容器内挂载路径是否正确docker exec -it 自建网关容器名 ls -l /usr/local/share/ca-certificates/再看文件内容是否完整docker exec -it 自建网关容器名 cat /usr/local/share/ca-certificates/backend-cert.crt如果文件内容只有一堆十六进制数字没有-----BEGIN CERTIFICATE-----头说明文件格式不对。很多运维直接把.cer或.der后缀的文件扔进容器里这是最常见的翻车点。另外要注意update-ca-certificates默认只处理带.crt后缀的文件。如果你把文件命名为backend-cert.pem扔进/usr/local/share/ca-certificates/它根本不会被索引。Dockerfile 方案里我特意把 COPY 的容器内文件名改成了.crt后缀就是这个原因。4.2 证书链不完整自建 CA 签发的证书通常会有“根 CA → 中间 CA → 服务端证书”三层结构。如果你只把服务端本身的证书导入信任库那校验时客户端会去找上一级签发者找不到就报unable to get local issuer certificate。排查方法是把后端服务的证书链完整拉出来看看openssl s_client -connect backend.example.com:443 -showcerts输出里会有一段段证书从服务端证书开始逐级往上。你要导入的是最顶层的“根 CA”证书而不是第一张服务端证书。有些场景下后端只返回了服务端证书那就要向证书管理员索要根 CA 的 PEM 文件。4.3 时间不同步导致验证失败这个坑隐蔽且容易被忽略。自签名证书在验证时有一个前置条件当前时间必须在证书的notBefore和notAfter之间。如果自建网关容器所在的主机时间偏差过大比如差了几分钟证书有效期判定就会出问题表现同样是握手失败。解决办法很简单确保宿主机和容器内时间同步date -u看到的时间如果和当前 UTC 时间差了几分钟就需要配置 NTP 或 Chrony。尤其是在虚拟机环境里宿主机休眠后时间漂移很常见。4.4 证书的 SAN 字段不匹配还有一类报错不是信任问题而是主机名校验问题。即使证书被信任如果证书中的Subject Alternative NameSAN不包含你访问时使用的域名TLS 客户端照样拒绝连接。报名错误通常是Hostname mismatch或SSL: certificate subject name ... does not match target host name。这时候有两个方向一是修改后端证书的 SAN 添加内网域名二是保证自建网关访问后端时使用的域名和签发证书里的域名一致。前者涉及证书重新签发后者只需要调整 APIM 策略中的后端地址写法。如果你用 IP 地址访问后端确保证书 SAN 里有对应的 IP 类型条目很多自签名证书只写了域名没写 IP直接访问 IP 就会挂。4.5 证书更新后不生效证书是有有效期的企业内部自签名证书可能一年甚至两年就要轮换。更新证书后如果你只替换了宿主机上的文件没有重建 Docker 镜像或者重启 Pod网关可能仍然用着旧证书。Docker Compose 场景下换了证书文件后需要docker compose restart gatewayKubernetes 场景下更新了 ConfigMap 后一般要滚动重建 Pod 才能让新配置生效。注意如果改的是 ConfigMap 内容但没有更新 ConfigMap 的资源版本某些 K8s 版本下 Pod 挂载的文件不会自动刷新。稳妥做法是修改 ConfigMap 名称引用一个带版本号的名字比如apim-ca-cert-v2触发 Deployment 滚动更新。4.6 如何在容器里快速调试证书问题调试证书问题最顺手的还是 openssl 和 curl。进入容器后的标准化排查三步# 1. 直接看服务端证书链 openssl s_client -connect backend.example.com:443 -showcerts # 2. 强制使用系统信任库做一次完整校验 curl --cacert /etc/ssl/certs/ca-certificates.crt https://backend.example.com/health # 3. 加上详细输出定位握手发生在哪一步 curl -v https://backend.example.com/health如果第二步通过了说明信任库配置是好的如果第一步和第三步都失败问题大概率在主机名或证书链完整性上。5. 最后再聊聊我的实际体会做自建网关证书信任这件事最忌讳的就是图省事直接关掉 TLS 校验。我见过有团队把方案一用在了生产环境结果内部网络流量被劫持提了工单查了一个星期才定位到是网关把 HTTPS 降级了。那一次教训之后所有到我这咨询的人我都建议直接上方案三。实际操作中给我最大帮助的一个经验是把证书信任操作前置到镜像构建阶段而不是运行时挂载。虽然挂载方式更灵活但镜像构建带来的可复现性、版本可追溯性在排障时实在太重要了。我甚至会在镜像标签里加上 CA 证书版本号比如gateway:latest-ca-v2这样一旦发现证书轮换后有问题可以快速回退到旧镜像。另外还要说一句证书信任只是自建网关和内部服务打通的第一步。接下来还有 mTLS、流量限流、IP 白名单这些事但至少“证书不受信任”这个最恶心的拦路虎解决了后面的工作就顺畅多了。这个方案三的思路不仅适用于 Azure APIM 自建网关其实任何跑在容器里的反向代理、API 网关要信任自签名证书时都能通用——把证书放进系统信任库让所有进程都认它。理解了这一层以后再遇到自建网关、Nginx Ingress、Envoy 的类似报错你都能举一反三了。
返回列表