☰
微服务架构中禁止服务直连:NetworkPolicy防绕行实战
2026/10/20 18:15:40 网站建设 项目流程

1. 项目概述:为什么微服务绕过网关不是“省事”,而是埋雷

“防止微服务跳过网关直接访问服务”——这短短十几个字,背后是我在某中型互联网公司做后端架构支撑三年间踩过最深、复盘最多、也最常被新同事误判的一类问题。它不是某个冷门配置项,而是微服务落地过程中几乎必然遭遇的治理断层点:当团队刚完成服务拆分、API网关上线、基础路由和鉴权跑通后,开发同学一句“我这个服务A调B服务,走网关太慢/太绕,我直接HTTP调用内部地址吧”,就可能在两周内让整个安全策略形同虚设、监控链路断裂、灰度发布失效,甚至引发一次线上P0级事故。

核心关键词“微服务”“网关”“直接访问”已经点明矛盾本质:网关是微服务架构的统一出入口与治理中枢,而服务间直连则是对这套治理机制的主动绕行。它不涉及任何敏感技术或政策边界,纯粹是工程实践中的认知偏差与协作惯性问题。适合三类人重点参考:刚从单体转型的开发同学(常把“能通”当成“该通”)、负责中间件或平台建设的SRE/架构师(需设计防绕行机制)、以及正在制定微服务规范的技术负责人(这是治理落地的第一道防线)。

我见过太多真实场景:测试环境里服务A通过http://service-b:8080/api/user直连B,上线后因未配置服务发现,导致B扩容后A始终只打到旧实例;又或者某支付服务为“提升性能”绕过网关调用风控服务,结果风控侧升级了JWT校验规则,A因无Token透传直接500;更隐蔽的是,所有链路追踪ID在直连时丢失,排查一个超时问题要翻三套日志。这不是理论风险,而是每天都在发生的工程现实。解决它,不靠喊口号,而靠可落地的架构约束、可验证的准入检查、可感知的违规告警——接下来我会从设计逻辑、实操细节、真实问题排查三个维度,把这套防绕行方案拆解到你能直接抄作业的程度。

2. 整体设计思路:为什么“禁止直连”不能只靠文档和开会

很多团队第一步就错了:发一版《微服务调用规范》,强调“所有跨服务调用必须经网关”,然后等大家自觉遵守。结果呢?三个月后审计发现,37%的服务存在直连行为,其中21%是生产环境。原因很简单——规范没有对抗开发者的自然倾向。开发者追求的是“快速验证”“最小改动”“避免引入新依赖”,而网关调用意味着要改URL、处理Token透传、适配网关限流响应码、增加一次网络跳转。如果架构层不提供比直连更简单、更可靠、更符合直觉的替代方案,规范就是废纸。

所以真正的设计起点,不是“如何禁止”,而是“如何让走网关成为默认且最省力的选择”。我们最终采用的是三层防御模型:

  • 第一层:网络层隔离(强约束):在K8s集群内,通过NetworkPolicy严格限制Pod间通信,只允许网关Pod访问所有服务,其他服务Pod之间默认拒绝互通。这是最硬的闸门,连TCP连接都建不起来,绕无可绕。
  • 第二层:服务注册与发现强制(流程卡点):所有服务启动时,必须向注册中心(如Nacos)上报“网关可调用路径”,而非原始IP+Port。客户端SDK(如Spring Cloud OpenFeign)默认只从注册中心拉取网关暴露的虚拟服务名(如gateway://user-service),根本看不到真实后端地址。
  • 第三层:可观测性兜底(主动发现):在服务网格(Istio)Sidecar中注入流量分析模块,持续扫描所有出站HTTP请求的目标Host。若发现非网关域名(如service-b.default.svc.cluster.local)且未命中白名单,则自动上报至告警平台,并记录完整调用栈。

这三层不是并列关系,而是递进:网络层是底线,确保物理上不可行;服务发现层是日常习惯,让正确方式成为唯一便捷路径;可观测层是保险丝,捕获漏网之鱼并推动根治。选择这个组合,是因为它覆盖了“事前预防—事中拦截—事后追溯”的全生命周期,且每层都有明确的技术实现载体,不依赖人的记忆或自觉。比如NetworkPolicy,一旦配置生效,哪怕开发同学在代码里硬编码了http://service-b:8080,K8s内核直接丢包,连超时错误都不会抛出——这种确定性,远胜于任何文档宣贯。

3. 核心细节解析:NetworkPolicy配置不是写个YAML就完事

很多人以为NetworkPolicy就是复制粘贴几行YAML,但实际落地时,90%的失败案例都出在细节没抠准。我带团队实施时,光是NetworkPolicy的调试就花了整整两周,反复验证了七种典型场景。下面把最关键的四个细节掰开讲透,全是血泪经验。

3.1 策略作用域必须精确到命名空间,且需显式启用

K8s的NetworkPolicy默认是“全放行”(Allow All),只有当你在某个命名空间下创建了至少一条Policy,该命名空间才进入“默认拒绝”(Default Deny)模式。这意味着:

  • 如果你的网关部署在istio-system命名空间,而业务服务在default命名空间,你必须在default命名空间下创建Policy,否则default里的服务依然可以互相直连;
  • 更关键的是,Policy本身不会自动应用到所有命名空间。你得手动为每个业务命名空间(prod、staging、dev)分别部署对应的Policy文件。我们曾因忘记给dev环境部署,导致测试同学在开发联调时疯狂报错,还以为是网关故障。

实操中,我们用Helm模板统一管理,核心片段如下:

# templates/network-policy.yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: {{ .Release.Name }}-deny-direct namespace: {{ .Release.Namespace }} spec: podSelector: {} # 匹配该命名空间下所有Pod policyTypes: - Ingress - Egress ingress: # 入站:只允许来自网关的流量 - from: - namespaceSelector: matchLabels: istio-injection: enabled # 网关Pod所在命名空间的标签 podSelector: matchLabels: app: istio-ingressgateway # 网关Pod的标签 egress: # 出站:只允许访问网关和必要外部服务(如数据库) - to: - namespaceSelector: matchLabels: istio-injection: enabled podSelector: matchLabels: app: istio-ingressgateway - to: - ipBlock: cidr: 10.96.0.0/12 # K8s Service CIDR,允许访问ClusterIP

提示:namespaceSelector和podSelector必须同时匹配,缺一不可。只写namespaceSelector会匹配该命名空间下所有Pod,包括非网关服务,导致策略失效。

3.2 “允许访问网关”必须包含两种流量路径

网关流量其实分两类,新手常只考虑第一类:

  • 类型1:外部请求经网关转发到后端服务(Ingress方向):这是网关的核心功能,对应NetworkPolicy的ingress规则;
  • 类型2:后端服务主动调用网关提供的聚合API(Egress方向):比如订单服务需要调用网关暴露的“用户中心聚合接口”,此时订单Pod是客户端,网关Pod是服务端,流量属于egress。

如果只配置ingress规则,egress方向仍默认放行,服务就能直接调用网关以外的任何地址。我们初期就漏了这点,导致订单服务虽不能直连用户服务,却能直连Redis和MySQL——这虽不违反“防绕网关”目标,但暴露了更大的安全漏洞。因此egress规则必须显式定义,且只放行网关和绝对必要的基础设施(如配置中心、日志服务)。

3.3 Sidecar注入状态直接影响策略生效范围

在Istio服务网格中,NetworkPolicy的podSelector匹配的是原生Pod,而非Sidecar代理后的逻辑单元。这意味着:

  • 如果某个服务Pod未注入Sidecar(即istio-injection=disabled),它的网络流量不受Istio控制,但NetworkPolicy依然对其生效;
  • 反之,如果Pod已注入Sidecar,其所有出站流量会被Envoy劫持,此时egress规则需额外考虑Envoy的出口配置。

我们遇到的真实问题是:某批遗留Java服务因兼容性问题禁用了Sidecar,但NetworkPolicy的egress规则只放行了网关,导致这些服务连K8s DNS(kube-dns)都解析不了,启动失败。解决方案是,在egress中增加DNS服务的显式放行:

- to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system podSelector: matchLabels: k8s-app: kube-dns

注意:kubernetes.io/metadata.name是K8s 1.21+版本的命名空间标签键,旧版本需用name。务必先kubectl get ns kube-system -o yaml确认标签。

3.4 策略调试必须用Pod内命令,而非节点层面

验证NetworkPolicy是否生效,最可靠的姿势是进入目标Pod内部执行curl。因为NetworkPolicy工作在K8s网络插件(如Calico、Cilium)层面,影响的是Pod的iptables规则,节点上的curl或telnet完全无法模拟真实场景。我们曾用kubectl exec -it service-a-pod -- curl http://service-b:8080验证,返回Connection refused,但用节点curl却成功——这说明策略已生效,但节点网络未受限制。

调试步骤必须闭环:

  1. 进入A服务Pod:kubectl exec -it <a-pod-name> -n <namespace> -- sh;
  2. 安装curl(若无):apk add curl(Alpine)或apt-get update && apt-get install -y curl(Debian);
  3. 测试直连B服务:curl -v http://service-b:8080/health;
  4. 测试调用网关:curl -v http://istio-ingressgateway.istio-system:80/health;
  5. 检查iptables规则(高级):iptables -L -t filter | grep NETWORK,确认有对应链。

实操心得:首次部署后,务必用kubectl get networkpolicy -A检查所有命名空间的Policy状态,并用kubectl describe networkpolicy <name> -n <ns>确认podSelector匹配到了预期Pod。我们曾因标签拼写错误(app: istio-ingressgateway写成app: istio-gateway),导致策略完全不生效,排查了八小时。

4. 实操过程:从零搭建可验证的防绕行环境(含完整配置)

现在,我们把前面所有设计和细节,整合成一套可立即运行的实操流程。以下基于K8s 1.24+、Istio 1.18+、Calico CNI的环境,所有配置均经过生产环境简化验证。你不需要理解所有参数含义,照着复制粘贴就能跑通,关键步骤我会标注原理和避坑点。

4.1 环境准备:三步建立最小可行验证集

第一步:创建隔离命名空间并启用Istio注入

# 创建命名空间,并打上Istio注入标签 kubectl create namespace demo-security kubectl label namespace demo-security istio-injection=enabled # 验证标签是否生效 kubectl get namespace demo-security -o yaml | grep istio-injection # 输出应为:istio-injection: enabled

原理:istio-injection=enabled是Istio识别自动注入Sidecar的开关。没有这一步,后续的Service Mesh能力(如流量劫持、mTLS)全部失效,NetworkPolicy虽仍生效,但无法捕获加密后的应用层协议。

第二步:部署模拟服务(A服务与B服务)
我们用极简的Python Flask服务模拟,代码仅10行,重点在于暴露清晰的调用路径:

# service-b/app.py from flask import Flask app = Flask(__name__) @app.route('/api/data') def get_data(): return {"service": "B", "status": "ok"} if __name__ == '__main__': app.run(host='0.0.0.0:5000')

构建镜像并部署(使用Dockerfile和kubectl apply):

# service-b/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: service-b namespace: demo-security spec: replicas: 1 selector: matchLabels: app: service-b template: metadata: labels: app: service-b spec: containers: - name: service-b image: your-registry/service-b:v1 ports: - containerPort: 5000 --- apiVersion: v1 kind: Service metadata: name: service-b namespace: demo-security spec: selector: app: service-b ports: - port: 5000 targetPort: 5000

同理部署service-a,其代码中包含两种调用方式:

# service-a/app.py import requests from flask import Flask app = Flask(__name__) # 方式1:直连(将被NetworkPolicy拦截) def call_direct(): try: return requests.get("http://service-b:5000/api/data", timeout=2) except Exception as e: return str(e) # 方式2:经网关(正确路径) def call_via_gateway(): # 网关URL格式:http://<gateway-svc>.<ns>:<port>/<route-prefix> return requests.get("http://istio-ingressgateway.istio-system:80/service-b/api/data") @app.route('/test') def test(): return f"Direct: {call_direct()}, Gateway: {call_via_gateway()}"

第三步:部署网关虚拟服务(VirtualService)
这是让网关知道如何路由的关键配置:

# gateway-vs.yaml apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: service-b-route namespace: demo-security spec: hosts: - "service-b.demo-security.svc.cluster.local" # 内部服务名 gateways: - istio-system/istio-ingressgateway # 绑定到全局网关 http: - match: - uri: prefix: /service-b/ route: - destination: host: service-b.demo-security.svc.cluster.local port: number: 5000

关键点:hosts字段必须填服务的FQDN(全限定域名),这是Istio路由匹配的基础。填错会导致网关404,但NetworkPolicy仍会拦截直连——你会误以为策略没生效。

4.2 NetworkPolicy部署与验证:五步闭环测试

现在进入核心环节。按顺序执行以下五步,每步都附带验证命令和预期结果:

步骤1:部署NetworkPolicy(默认拒绝所有直连)

# network-policy.yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: deny-direct-access namespace: demo-security spec: podSelector: {} policyTypes: - Ingress - Egress ingress: - from: - namespaceSelector: matchLabels: istio-injection: enabled podSelector: matchLabels: app: istio-ingressgateway egress: - to: - namespaceSelector: matchLabels: istio-injection: enabled podSelector: matchLabels: app: istio-ingressgateway - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system podSelector: matchLabels: k8s-app: kube-dns

部署:kubectl apply -f network-policy.yaml -n demo-security

步骤2:验证直连是否被拦截
进入service-aPod,执行直连测试:

kubectl exec -it deploy/service-a -n demo-security -- sh # 在Pod内执行 curl -v http://service-b:5000/api/data

预期结果:curl: (7) Failed to connect to service-b port 5000: Connection refused

这表示NetworkPolicy的egress规则已生效,service-a无法建立到service-b的TCP连接。

步骤3:验证网关调用是否畅通
在同一Pod内,执行网关调用:

curl -v http://istio-ingressgateway.istio-system:80/service-b/api/data

预期结果:返回{"service": "B", "status": "ok"},HTTP状态码200

这证明egress规则中对网关的放行正确,且VirtualService路由配置无误。

步骤4:验证服务间mTLS是否启用(增强安全性)
Istio默认开启mTLS,这意味着即使NetworkPolicy被绕过(理论上不可能),直连流量也会因证书校验失败而中断。验证命令:

# 在service-a Pod内,用openssl测试TLS握手 openssl s_client -connect service-b:5000 -servername service-b.demo-security.svc.cluster.local

预期结果:连接立即关闭,输出verify error:num=20:unable to get local issuer certificate

这表明服务B期望mTLS连接,而直连未提供证书,双重保险。

步骤5:模拟违规并触发告警(可观测性层)
我们用Istio的Telemetry资源定义一个检测规则,当发现service-a向service-b发送HTTP请求时告警:

# telemetry-alert.yaml apiVersion: telemetry.istio.io/v1alpha1 kind: Telemetry metadata: name: detect-direct-call namespace: demo-security spec: metrics: - providers: - name: prometheus match: - metric: REQUEST_COUNT match: - source: "service-a" destination: "service-b" response_code: "500" # 直连失败时常见

部署后,观察Prometheus中istio_requests_total{source_workload="service-a", destination_workload="service-b"}指标。若有人尝试直连,该指标会突增,配合Alertmanager即可触发企业微信/钉钉告警。

实操心得:首次部署NetworkPolicy后,务必等待2分钟再测试。Calico需要时间同步iptables规则,过早测试可能得到“假阳性”结果。我们曾因此误判策略无效,浪费半天时间。

5. 常见问题与排查技巧实录:那些文档里不会写的坑

在落地防绕行方案的半年里,我和团队整理了17个高频问题,其中8个是“看似网关问题,实为NetworkPolicy配置错误”。下面精选5个最具代表性的,附带真实排查过程和独家技巧。

5.1 问题:curl直连返回Connection refused,但telnet却能连通

现象描述:在service-aPod内执行curl http://service-b:5000失败,但telnet service-b 5000显示Connected。
排查过程:

  • 第一反应是NetworkPolicy没生效,但kubectl get networkpolicy -n demo-security确认策略存在;
  • 执行iptables -L -t filter | grep service-b,发现规则中REJECT目标端口是5000,但telnet连通说明TCP SYN包能到达;
  • 最终用tcpdump抓包发现:curl发的是HTTP/1.1请求,而telnet只是建立TCP连接。NetworkPolicy的REJECT规则在SYN阶段就丢包,telnet的“Connected”是客户端误判——实际服务端根本没收到SYN。

根本原因:telnet的“Connected”仅表示TCP三次握手的第一次SYN发出且未被本机拒绝,不代表服务端接收。NetworkPolicy在K8s节点的OUTPUT链拦截,SYN包根本发不出去,telnet的“Connected”是假象。
解决方案:永远用curl或应用代码测试,telnet仅用于初步端口探测。验证NetworkPolicy,必须用应用层协议。

5.2 问题:服务能调通网关,但返回404,且VirtualService配置确认无误

现象描述:curl http://istio-ingressgateway.istio-system:80/service-b/api/data返回404,但kubectl get vs -n demo-security显示VirtualService存在。
排查过程:

  • 检查网关Pod日志:kubectl logs -l app=istio-ingressgateway -n istio-system | grep "404",发现大量no healthy upstream;
  • kubectl get endpoints service-b -n demo-security,发现ENDPOINTS为空;
  • 进一步查kubectl get pods -n demo-security,发现service-bPod处于CrashLoopBackOff状态;
  • kubectl logs deploy/service-b -n demo-security,发现Python Flask启动报错:OSError: [Errno 98] Address already in use。

根本原因:service-b容器内多个进程争抢5000端口,导致Pod启动失败,Endpoint未注册。网关找不到可用后端,自然404。NetworkPolicy虽生效,但问题根源在服务健康态。
解决方案:NetworkPolicy只能防“恶意直连”,不能保“服务可用”。必须配套健康检查(Liveness Probe)和Endpoint监控。我们在service-bDeployment中增加了:

livenessProbe: httpGet: path: /health port: 5000 initialDelaySeconds: 30 periodSeconds: 10

5.3 问题:NetworkPolicy部署后,所有服务都无法解析DNS

现象描述:service-aPod内curl http://service-b:5000返回Could not resolve host: service-b。
排查过程:

  • cat /etc/resolv.conf,发现nameserver指向10.96.0.10(CoreDNS ClusterIP);
  • ping 10.96.0.10失败,确认DNS服务不可达;
  • kubectl get svc -n kube-system,发现kube-dns服务存在,但kubectl get endpoints kube-dns -n kube-system为空;
  • 最终发现:NetworkPolicy的egress规则未放行kube-system命名空间,导致DNS查询流量被拒。

根本原因:NetworkPolicy的egress默认拒绝,必须显式放行DNS。我们之前只放行了kube-dnsPod,但忘了kube-system命名空间的Service也需要访问。
解决方案:在egress中增加对kube-systemService CIDR的放行:

- to: - ipBlock: cidr: 10.96.0.0/12 # K8s Service CIDR

独家技巧:用nslookup service-b.demo-security.svc.cluster.local代替ping测试DNS,因为nslookup直接调用DNS协议,更精准。

5.4 问题:Istio Sidecar注入后,服务启动变慢,且部分HTTP调用超时

现象描述:service-a注入Sidecar后,启动时间从3秒增至45秒,且调用网关时偶发503 UST(Upstream Request Timeout)。
排查过程:

  • kubectl logs -l app=service-a -n demo-security -c istio-proxy,发现大量upstream connect error or disconnect/reset before headers. reset reason: connection failure;
  • kubectl get pods -n istio-system,发现istiodPod CPU使用率98%;
  • kubectl top pods -n istio-system,确认istiod资源不足;
  • 根本原因是:istiod负责为每个Pod生成Envoy配置,当服务数超过200个时,单Pod配置生成耗时剧增,导致Sidecar启动阻塞。

根本原因:Istio控制平面(istiod)性能瓶颈,与NetworkPolicy无关,但会放大其影响。
解决方案:

  • 扩容istiod:kubectl scale deploy istiod -n istio-system --replicas=3;
  • 启用配置缓存:在istiodDeployment中添加环境变量PILOT_ENABLE_CONFIG_CACHE=true;
  • 对非核心服务,改用istio-injection=disabled,仅对需网关治理的服务注入。

5.5 问题:NetworkPolicy生效,但Prometheus监控显示仍有直连流量

现象描述:istio_requests_total{destination_workload="service-b"}指标中,source_workload为service-a的条目持续增长,但NetworkPolicy应已拦截。
排查过程:

  • 登录Grafana,下钻该指标,发现response_code="200"且connection_security_policy="none";
  • kubectl get destinationrule -n demo-security,发现service-b的DestinationRule未启用mTLS;
  • kubectl get peers -n demo-security,确认service-a和service-b未建立mTLS信任关系。

根本原因:NetworkPolicy工作在网络层(L3/L4),而Istio的istio_requests_total指标统计的是应用层(L7)HTTP请求。如果service-a通过网关调用service-b,指标会显示source_workload="istio-ingressgateway";但如果service-a在代码中硬编码了http://service-b:5000,而NetworkPolicy配置有误(如egress规则放行了service-b),则直连成功,指标自然增长。
解决方案:指标本身没错,错的是NetworkPolicy配置。用kubectl describe networkpolicy deny-direct-access -n demo-security检查egress规则,确认service-b未被意外放行。我们曾因ipBlockCIDR写错,导致整个10.0.0.0/8网段被放行。

最后分享一个小技巧:在CI/CD流水线中加入自动化检查。我们用一个简单的Shell脚本,在每次服务部署前扫描代码库,禁止出现http://.*:.*或https://.*:.*硬编码(除网关地址外),并检查Dockerfile中是否包含curl/wget等工具——因为这些往往是直连调试的痕迹。这比事后审计高效十倍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询