快轉到主要內容

ExternalIPs 棄用?從設計、漏洞到 KEP-5707

·717 字·4 分鐘·
ChengHao Yang
作者
ChengHao Yang
SRE / CNCF Ambassador
目錄

Kubernetes 從 v1.0 出現後,ExternalIPs 一直都在 Service 當中,沒想到經歷了 11 年,SIG Network 決定在 v1.36 版以後棄用,預計在 v1.43 後鎖定該功能不再使用。這之中發生什麼事?

今天我們一起來閱讀「KEP-5707: Deprecate Service.spec.externalIPs」吧!

註記

本篇內容當下撰寫是 2026/4/18,會以此 commit 當前內容來介紹。

ExternalIPs 是什麼?
#

開始閱讀 KEP-5707 之前,先來理解一下 ExternalIPs 欄位在做什麼。

Service 建立出來後,預設會分配到一組 Internal IP,只要符合 Selector 標籤的 Pod,都會被隨機分配連線。

但是,Internal IP 只能用在叢集內部,如果外部想要連線進來,要怎麼處理?公有雲的 Kubernetes 直接寫 type: LoadBalancer,剩下全部交給雲端廠商處理。但是,私有雲沒有 Cloud Provider 的支持,除了 type: NodePort 以外,其他就靠路由器或防火牆自行轉發了。

更何況,2015 年實作當下並沒有所謂的 MetalLBCilium 等專案存在,如果想要指定外部 IP 要如何處理?於是就有了"自行"指定 External IP。

External IP 使用情境示意圖

ExternalIPs 的設計很單純,只要在 Service 的 .spec 中填入一組 IP,kube-proxy 會在每個節點上建立 iptables / IPVS 規則,把目標為該 IP 的流量轉發到對應的 Service endpoints,省下還需要連去外面的時間。

apiVersion: v1
kind: Pod
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  containers:
    - name: http-echo
      image: hashicorp/http-echo:latest
      args:
        - "-listen=:8080"
        - "-text=hello from my-app"
      ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080
  externalIPs:
    - 203.0.113.10

換句話說,只要節點收到目標 IP 為 203.0.113.10 的封包,kube-proxy 就會透過 iptables 規則將流量轉發到 Service 後端的 Pod。以下是 kube-proxy 中建立 External IP 轉送規則的實作:

		// Capture externalIPs.
		for _, externalIP := range svcInfo.ExternalIPs() {
			if hasEndpoints {
				// Send traffic bound for external IPs to the "external
				// destinations" chain.
				natRules.Write(
					"-A", string(kubeServicesChain),
					"-m", "comment", "--comment", fmt.Sprintf(`"%s external IP"`, svcPortNameString),
					"-m", protocol, "-p", protocol,
					"-d", externalIP.String(),
					"--dport", strconv.Itoa(svcInfo.Port()),
					"-j", string(externalTrafficChain))
			}
			if !hasExternalEndpoints {
				// Either no endpoints at all (REJECT) or no endpoints for
				// external traffic (DROP anything that didn't get
				// short-circuited by the EXT chain.)
				filterRules.Write(
					"-A", string(kubeExternalServicesChain),
					"-m", "comment", "--comment", externalTrafficFilterComment,
					"-m", protocol, "-p", protocol,
					"-d", externalIP.String(),
					"--dport", strconv.Itoa(svcInfo.Port()),
					"-j", externalTrafficFilterTarget,
				)
			}
		}

程式碼來源:Kubernetes v1.35.4 iptables 實作

部署上面的 Service 後,可以在節點上看到 kube-proxy 產生的 iptables 規則:

$ docker exec -it externalips-lab-worker iptables -t nat -S | grep "external IP"

-A KUBE-SERVICES -d 203.0.113.10/32 -p tcp -m comment --comment "default/my-service external IP" -m tcp --dport 80 -j KUBE-EXT-FXIYY6OHUSNBITIX

目標為 203.0.113.10:80 的 TCP 封包會被導入 KUBE-EXT-* chain,最終 DNAT 到 Pod IP。

不過 Kubernetes 本身不負責讓這個 IP 能被路由到節點上,剩下外部 IP 怎麼到節點,完全是使用者自己要處理的事。

當下設計看起來還可以,但實際暗藏危機悄然現身 —— CVE-2020-8554

CVE-2020-8554
#

  • 2019/12/27 champtar 回報問題
  • 2020/01/09 確定為有效漏洞
  • 2020/03/03 編號 CVE-2020-8554 確定
  • 2020/12/05 Issue#97076 更新
  • 2020/12/07 公開 PoC 細節

雖然 Kubernetes 官方在 2020/12/05 於 Issue#97076 公開了 CVE-2020-8554 資訊,不過這則到筆者目前撰文為止,還是開啟狀態,也就是說,這漏洞依然還存在於現在版本中,並且尚未被修復。

上一段提及的設計情形,是建立在自己持有 IP 狀況下,但如果設定不是 Infra 本身或自己持有的 IP(e.g. CNCF 官網 IP),整個情形就變得極其詭異。

以接下來的例子說明,我設定 nginx Pod 和 my-evil-service Service,把 externalIPs 指定為 CNCF 的網站 IP。

本實驗改編自 GitHub Issue#97076 留言

apiVersion: v1
kind: Pod
metadata:
  labels:
    run: nginx
  name: nginx
spec:
  containers:
  - image: nginx:1.29.8-alpine
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: my-evil-service
spec:
  selector:
    run: nginx
  type: LoadBalancer
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80
  externalIPs:
    - 23.185.0.3 # cncf.io

上段提及,Kubernetes 不會對 External IP 做 ARP 回應,也沒有 BGP 宣告,完全沒有任何驗證機制kube-proxy 就會設定 iptables 規則上去,把內部導向 cncf.io23.185.0.3 流向指向我的 nginx Pod。

原本要瀏覽 CNCF 官網,結果被有權限建立 Service 的人直接劫持 IP。

$ kubectl run --rm -i --tty curl --image=curlimages/curl --restart=Never -- curl -I http://cncf.io
HTTP/1.1 200 OK
Server: nginx/1.29.8
Date: Sat, 18 Apr 2026 10:27:22 GMT
CVE-2020-8554 示意圖

要如何緩解?這個 issue 沒有 patch,也沒有辦法升級解決,只能透過 admission webhook 限制欄位使用,於是有了 k-sigs/externalip-webhook 子專案。

後來在 v1.21 以後 kube-apiserver 有內建 DenyServiceExternalIPs admission webhook,不過預設是關閉,需要搭配 --enable-admission-plugins 參數打開。

如果有使用 Kyverno 專案,可以搭配 Restrict External IPs 政策使用。

替代方案
#

ARP (Layer 2) or BGP (Layer 3)
#

現在已經有很多實作 ARP 和 BGP 的專案,舉凡像 MetalLBCilium 都是不錯的 CNCF 專案。

Cilium LB IPAM 可以使用 lbipam.cilium.io/ips 的 annotation 來表示偏好使用的 IP。

apiVersion: v1
kind: Service
metadata:
  name: service-blue
  annotations:
    "lbipam.cilium.io/ips": "20.0.10.100,20.0.10.200"
spec:
  type: LoadBalancer
  ports:
  - port: 1234

MetalLB Usage 頁面說明可以用 metallb.io/loadBalancerIPs 的 annotation。

apiVersion: v1
kind: Service
metadata:
  name: nginx
  annotations:
    metallb.io/loadBalancerIPs: 192.168.1.100
spec:
  ports:
  - port: 80
    targetPort: 80
  selector:
    app: nginx
  type: LoadBalancer

上面 YAML 只是讓 operator 可以參考 annotations 協助指派 IP,詳細設定不會在這裡展開,可以參考官網文件。

Gateway API
#

每個雲端供應商或 CNCF 專案實作方式皆不同,需要詳細看各家說明文件。

如果是用 Cilium Gateway API 可以直接用 .spec.addresses 來指定。

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: my-gateway
spec:
  gatewayClassName: cilium
  addresses:
    - type: IPAddress
      value: 192.168.1.100
  listeners:
    - name: http
      protocol: HTTP
      port: 80

但如果碰到像是 Istio 不是直接跟 CNI 掛鉤,可以用 .spec.infrastructure.annotations 讓 service 指定 annotation,讓 MetalLBCilium 指派偏好的 IP。

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
spec:
  gatewayClassName: istio
  infrastructure:
    annotations:
      lbipam.cilium.io/ips: "192.168.1.100"
  listeners:
    - name: http
      protocol: HTTP
      port: 80

棄用階段
#

  • v1.36 宣布棄用,kube-proxy 新增 AllowServiceExternalIPs feature gate,預設為 true,改為 false 即可關閉 ExternalIPs。
  • 預計 v1.40 以後預設關閉 AllowServiceExternalIPs
  • 預計 v1.43 以後鎖定 AllowServiceExternalIPs feature gate。
  • 預計 v1.46 以後移除所有相關實作(AllowServiceExternalIPs feature gate 和 DenyServiceExternalIPs admission controller)

後記
#

用現在的眼光看,ExternalIPs 的設計顯然有漏洞。但回到 2015 年,Cloud Native 生態系才剛起步,沒有 MetalLB、沒有 Gateway API,私有雲上想讓外部流量進來,除了 NodePort 就只能自己想辦法。讓使用者直接填 IP 是當時最直覺的做法。

最早提案Tim Hockin 也承認 .spec.externalIPs很糟糕的設計。只是 Kubernetes 一向重視穩定性,避免破壞性變更,即便知道有問題也不會輕易移除既有欄位。

直到 2026 年,CNCF 生態系逐漸成熟,替代方案已經到位,ExternalIPs 從「唯一選項」變成「有安全風險的遺留設計」,才終於有條件正式走向棄用。

Reference
#

相關文章