首页
/ Kubernetes metadata-proxy 组件解析:拦截 GCE 元数据服务、向 Pod 隐藏 kubelet 的 kube-env 凭据

Kubernetes metadata-proxy 组件解析:拦截 GCE 元数据服务、向 Pod 隐藏 kubelet 的 kube-env 凭据

2026-09-06 18:44:17作者:翟萌耘Ralph

导读:metadata-proxy 是 Kubernetes 仓库中为 GCE/GKE 类云环境准备的集群级 addon,它以 DaemonSet 形式在每个节点上拦截 Pod 对云平台元数据服务(169.254.169.254)的访问:对 kubelet 初始化所需的 kube-env 敏感数据一律返回 403,其余请求则照常转发给元数据服务器。读完本文你将掌握该组件的核心设计(返回 403 的隔离策略)、完整部署清单逐项拆解、集群启动脚本中的开关与节点标签联动,以及底层 iptables 流量接管原理。

一、组件定位:解决什么问题

在 GCE 等云平台上,每个虚拟机都能访问链路本地地址 169.254.169.254 上的实例元数据服务。元数据中除了实例信息外,还包含 kubelet 引导节点所需的 kube-env 数据,其中携带节点加入集群用的敏感凭据。若集群中运行的 Pod 也能直接读取这些元数据,就存在凭据泄露面——任何被攻破或恶意的容器都可能借此拿到节点级凭据。

metadata-proxy 的说明文档 对该组件的职责给出了精炼定义:

This metadata proxy returns a 403 for kubelet's kube-env data, but otherwise allows pods access to the metadata server.

即:对 kubelet 的 kube-env 数据返回 HTTP 403,除此之外,允许 Pod 正常访问元数据服务器。这是典型的“最小特权”做法——不去切断 Pod 对元数据服务的合法访问(某些 Pod 依赖元数据做云厂商能力发现、实例标签读取等),而是只对含敏感凭据的 kube-env 路径做定向封禁。该组件正是集群启动配置中 ENABLE_METADATA_CONCEALMENT(元数据隐藏)特性的实现载体。

二、部署形态:一份清单 = ServiceAccount + DaemonSet

组件清单位于 cluster/addons/metadata-proxy/gce/metadata-proxy.yaml,由两个资源构成:一个用于运行身份的 ServiceAccount,以及一个确保“每个节点一个代理实例”的 DaemonSet

2.1 ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  name: metadata-proxy
  namespace: kube-system
  labels:
    k8s-app: metadata-proxy
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile

需要注意标签 addonmanager.kubernetes.io/mode: Reconcile:这是 addon-manager 的声明式管理模式标志,表明该资源由 kube-addon-manager 持续调和(Reconcile),清单内容与集群实际状态不一致时会被自动修正。

2.2 DaemonSet 关键配置

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: metadata-proxy-v0.1
  namespace: kube-system
  labels:
    k8s-app: metadata-proxy
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
    version: v0.1
spec:
  selector:
    matchLabels:
      k8s-app: metadata-proxy
      version: v0.1
  updateStrategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        k8s-app: metadata-proxy
        kubernetes.io/cluster-service: "true"
        version: v0.1
    spec:
      priorityClassName: system-node-critical
      serviceAccountName: metadata-proxy
      hostNetwork: true
      dnsPolicy: Default
      tolerations:
      - operator: "Exists"
        effect: "NoExecute"
      - operator: "Exists"
        effect: "NoSchedule"
      containers:
      - name: metadata-proxy
        image: registry.k8s.io/metadata-proxy:v0.1.12
        args: ["--addr=0.0.0.0:988"]
        securityContext:
          privileged: true
        resources:
          requests:
            memory: "25Mi"
            cpu: "30m"
          limits:
            memory: "25Mi"
            cpu: "30m"
      nodeSelector:
        cloud.google.com/metadata-proxy-ready: "true"
        kubernetes.io/os: linux
      terminationGracePeriodSeconds: 30

逐项理解这份清单的设计意图:

配置项 取值 作用与影响
hostNetwork: true 布尔 代理直接使用宿主机网络栈,从而能绑定链路本地地址、接收被 iptables DNAT 转来的流量;监听地址由 --addr=0.0.0.0:988 指定在 988 端口
dnsPolicy: Default 策略 hostNetwork 下使用节点默认 DNS 配置
securityContext.privileged: true 布尔 代理需要对网络/路由做底层操作(监听链路本地地址、处理转发流量)而授予特权
priorityClassName: system-node-critical 优先级 属于节点关键系统组件,资源紧张时优先保证调度与存活
tolerations(NoExecute/NoSchedule,operator: Exists) 容忍 允许调度/运行到存在污点的节点,保证全节点覆盖
nodeSelector.cloud.google.com/metadata-proxy-ready: "true" 标签 只在节点被显式标记为“已就绪”后调度,实现与防火墙规则部署顺序解耦(见下文第五节)
resources(requests=limits,25Mi / 30m) 配额 请求与限制相等,Pod 获得 Guaranteed QoS,避免被驱逐
terminationGracePeriodSeconds: 30 滚动更新时给代理 30 秒优雅退出时间
updateStrategy: RollingUpdate 策略 版本升级(如 v0.1 → v0.2)时采用滚动替换而非全量重建

2.3 可选的 prometheus-to-sd 监控边车

清单中以注释块 # BEGIN_PROMETHEUS_TO_SD# END_PROMETHEUS_TO_SD 包围着第二个容器,它不是代理本体,而是把指标推送到 Google Cloud Monitoring(Stackdriver)的边车:

- name: prometheus-to-sd-exporter
  image: gke.gcr.io/prometheus-to-sd:v0.11.1-gke.1
  resources:
    requests: { memory: "20Mi", cpu: "2m" }
    limits:  { memory: "20Mi", cpu: "2m" }
  command:
    - /monitor
    - --stackdriver-prefix={{ prometheus_to_sd_prefix }}/addons
    - --api-override={{ prometheus_to_sd_endpoint }}
    - --source=metadata_proxy:http://127.0.0.1:989?whitelisted=request_count
    - --pod-id=$(POD_NAME)
    - --namespace-id=$(POD_NAMESPACE)

关键信息:代理本体在 989 端口暴露了 Prometheus 格式的监控端点,边车只采集 request_count(请求计数)这一指标,通过 POD_NAME/POD_NAMESPACE(来自 fieldRef 的 Downward API)标注来源。两个模板变量 {{ prometheus_to_sd_prefix }}{{ prometheus_to_sd_endpoint }} 由部署脚本在执行时注入——即集群启动时调用的 update-daemon-set-prometheus-to-sd-parameters 函数。

三、集群启动脚本中的联动开关

3.1 特性开关与节点标签(config-default.sh)

该组件并非默认启用。在 cluster/gce/config-default.sh 中:

# TODO(#8867) Enable by default.
ENABLE_METADATA_CONCEALMENT="${ENABLE_METADATA_CONCEALMENT:-false}" # true, false
METADATA_CONCEALMENT_NO_FIREWALL="${METADATA_CONCEALMENT_NO_FIREWALL:-false}" # true, false
if [[ ${ENABLE_METADATA_CONCEALMENT:-} == "true" ]]; then
  # Put the necessary label on the node so the daemonset gets scheduled.
  NODE_LABELS="${NODE_LABELS},cloud.google.com/metadata-proxy-ready=true"
  # Add to the provider custom variables.
  PROVIDER_VARS="${PROVIDER_VARS:-} ENABLE_METADATA_CONCEALMENT METADATA_CONCEALMENT_NO_FIREWALL"
fi

可以总结出三条联动逻辑:

  1. ENABLE_METADATA_CONCEALMENT(默认 false 是总开关,源码注释中的 TODO(#8867) 表明作者期望未来默认开启;而 cluster/gce/config-test.sh 中该值为 true,即测试集群默认启用以覆盖验证。
  2. 开启后,节点会被打上 cloud.google.com/metadata-proxy-ready=true 标签——这正是 DaemonSet 的 nodeSelector 所依赖的标签,二者必须配合:只打标签而没开启则 DaemonSet 清单不会下发;只开启而没打标签则代理不会调度到节点。
  3. METADATA_CONCEALMENT_NO_FIREWALL(默认 false)用于部分节点不创建防火墙/接管规则的逃生开关,与代理的部署解耦。

3.2 清单下发与参数注入(configure-helper.sh)

GCE kube-up 集群在节点初始化脚本 cluster/gce/gci/configure-helper.sh 中完成部署:

if [[ "${ENABLE_METADATA_CONCEALMENT:-}" == "true" ]]; then
  setup-addon-manifests "addons" "metadata-proxy/gce"
  local -r metadata_proxy_yaml="${dst_dir}/metadata-proxy/gce/metadata-proxy.yaml"
  update-daemon-set-prometheus-to-sd-parameters ${metadata_proxy_yaml}
done

即:先通过 setup-addon-manifests 把本组件的清单(相对路径 addons/metadata-proxy/gce)渲染到 addon 目录,再由 kube-addon-manager 依据 Reconcile 模式应用到集群;随后调用 update-daemon-set-prometheus-to-sd-parameters 把上一节提到的 Stackdriver 前缀/端点等模板参数替换进 YAML。

3.3 旧标签的平滑迁移

由于历史原因,早期版本使用 beta.kubernetes.io/metadata-proxy-ready 标签。节点初始化脚本通过 update-legacy-addon-node-labels 完成迁移(configure-helper.sh):

update-node-label "beta.kubernetes.io/metadata-proxy-ready=true,cloud.google.com/metadata-proxy-ready!=true" "cloud.google.com/metadata-proxy-ready=true"

该命令借助 kubectl 选择器(selector)找出仍带旧标签、尚未打新标签的节点,为其补上 cloud.google.com/metadata-proxy-ready=trueupdate-node-label 内部使用 kubectl label --overwrite nodes -l ... 并最多重试 5 次、每次间隔 3 秒,等待 kube-apiserver 就绪(外层先 until kubectl get nodes 轮询),保证 DaemonSet 能最终调度覆盖全部旧节点。

四、数据平面:iptables 如何把流量交给代理

代理能“隐身”拦截 Pod 流量的关键在于宿主机网络规则。同一份节点初始化脚本在配置网络时写入如下规则(configure-helper.sh):

if [[ "${ENABLE_METADATA_CONCEALMENT:-}" == "true" ]] && [[ ! "${METADATA_CONCEALMENT_NO_FIREWALL:-}" == "true" ]]; then
  echo "Add rule for metadata concealment"
  ip addr add dev lo 169.254.169.252/32 scope host
  iptables -w -t nat -I PREROUTING -p tcp ! -i eth0 -d "${METADATA_SERVER_IP}" --dport 80 \
    -m comment --comment "metadata-concealment: bridge traffic to metadata server goes to metadata proxy" \
    -j DNAT --to-destination 169.254.169.252:988
  iptables -w -t nat -I PREROUTING -p tcp ! -i eth0 -d "${METADATA_SERVER_IP}" --dport 8080 \
    -m comment --comment "metadata-concealment: bridge traffic to metadata server goes to metadata proxy" \
    -j DNAT --to-destination 169.254.169.252:987
fi
iptables -w -t mangle -I OUTPUT -s 169.254.169.254 -j DROP

这些规则的语义与 DaemonSet 配置一一对应,可以还原出完整的数据流:

  1. 分配链路本地虚拟地址ip addr add dev lo 169.254.169.252/32 scope host 在宿主机的 loopback 接口上挂了一个内部地址 169.254.169.252。由于代理以 hostNetwork: true 运行并监听 0.0.0.0:988,它就能在宿主机命名空间内接收发往该地址的流量。
  2. DNAT 定向劫持:对非 eth0 入向! -i eth0 过滤掉宿主机自身管理流量,只拦桥接/容器网络进入的流量)、目的为云元数据服务地址 $METADATA_SERVER_IP(即 169.254.169.254)的 TCP 连接:
    • 目标端口 80 被改写为 169.254.169.252:988——对应代理的 --addr=0.0.0.0:988
    • 目标端口 8080 被改写为 169.254.169.252:987
  3. 封死源地址欺骗回程-t mangle OUTPUT -s 169.254.169.254 -j DROP 丢弃任何声称来自元数据服务 IP 的出站包,杜绝绕过代理直接“钓鱼”返回路径。
  4. 代理决策:接管后的请求到达代理进程。代理对 kube-env 相关请求直接回 403,其余请求则由代理以宿主身份代为访问真正的元数据服务器并把结果回传——这正是 README 所述“403 隔离 + 其余放行”策略在转发层面(代理会代答/代转)的体现。

结合代码可推断整体架构:Pod → 元数据请求 → 节点 PREROUTING DNAT → hostNetwork 代理(169.254.169.252:988)→ 判定 kube-env 则 403,否则转发至 169.254.169.254:80 的元数据服务。代理与 kubelet 等系统组件运行在同一网络命名空间,因此 kubelet 读取 kube-env 走的是 eth0 直连路径,不受 ! -i eth0 规则影响。

五、运维要点与适用前提

适用前提:本文所述组件路径与配置均面向仓库自带的 GCE kube-up / 集群初始化体系cluster/gce),核心事实依据为 cluster/addons/metadata-proxy/README.mdmetadata-proxy.yaml,以及节点初始化脚本 configure-helper.sh 中的联动逻辑。托管型 GKE 的元数据隐藏由其自身控制面管理,不属于该仓库此清单的职责范围。

关键运维结论汇总

  • 默认不启用ENABLE_METADATA_CONCEALMENT 默认 false(config-test.sh 例外为 true),开启需在 kube-up 配置中显式置为 true
  • 两层保护缺一不可:DaemonSet 依赖 cloud.google.com/metadata-proxy-ready=true 节点标签调度,而该标签由脚本在开关打开时统一注入;若标签缺失,代理不会运行,同时 iptables 仍可能把流量 DNAT 到无人监听的 988 端口,因此生产环境务必保证开关、标签、防火墙规则三者状态一致。
  • 资源占用极低:代理本体 25Mi/30m,监控边车 20Mi/2m,均为 Guaranteed QoS;若不需要将 request_count 指标推送至 Stackdriver,可依据注释块裁剪边车容器(需同步处理清单中的 {{ prometheus_to_sd_* }} 模板变量)。
  • 版本演进方式:DaemonSet 名称携带版本号(metadata-proxy-v0.1),镜像当前为 registry.k8s.io/metadata-proxy:v0.1.12;升级通过改名称版本与滚动更新策略完成,旧版本实例由 addon-manager 按 Reconcile 模式清理。
  • 故障排查切入点:可优先检查节点是否具备 cloud.google.com/metadata-proxy-ready=true 标签、kubectl -n kube-system get ds metadata-proxy-v0.1 是否满调度、iptables -t nat -S PREROUTING 中两条 metadata-concealment 注释规则是否存在,以及代理容器日志中 403 命中情况(结合 989 端口的 request_count 指标)。

参考文件索引

说明:文中涉及组件镜像版本、端口、标签与默认开关等,均以当前仓库实际内容为准;组件在代理内部的 403 判定细节(kube-env 路径匹配方式)位于独立维护的 metadata-proxy 镜像源码中,不在本仓库范围内,因此本文不做展开。

登录后查看全文
热门项目推荐
相关项目推荐