F1、kubeadm 集群 etcd Sweet32 加固指南
适用环境:kubeadm 安装的 Kubernetes 集群,CNI 为 Calico,etcd 以
hostNetwork静态 Pod 运行在控制平面节点。文档目的:说明 iptables 限制 2379 失效的原因,并给出可照着执行、不易出错的修复步骤。
1. 问题描述
安全扫描发现控制平面节点 etcd 客户端端口 2379 存在 Sweet32(CVE-2016-2183) 风险:TLS 仍协商 3DES 等 64-bit 块密码,长连接可能被生日攻击破解。
尝试在宿主机 INPUT 链用 iptables 限制 2379 仅白名单 IP 可访问:
iptables -I INPUT -p tcp -m multiport --dports 2379 -m set --match-set whitelist src -j ACCEPT
iptables -I INPUT 2 -p tcp -m multiport --dports 2379 -j DROP
现象:规则刚加上时有效,几分钟后失效,任意 IP 又能访问 2379。
2. 原因分析
失效有两层原因,缺一不可。
2.1 Calico 会周期性把规则插回第一位
这是刻意设计的:防止有人把规则插在 Calico 前面,从而绕过网络策略。Calico 官方文档和 GitHub issue #2672 都明确说明了这一点。
Calico 数据面进程 Felix(calico-node Pod)默认配置:
| 参数 | 默认值 | 含义 |
|---|---|---|
chainInsertMode |
Insert |
始终把 -j cali-INPUT 插在 INPUT 链最前面 |
iptablesRefreshInterval |
约 60–90 秒 | 定期全量刷新 iptables,修正被手动改乱的规则 |
Felix 刷新后,INPUT 链顺序变为:
1. -j cali-INPUT ← Calico 自动插回
2. 你的 whitelist ACCEPT ← 被挤到后面
3. 你的 DROP ← 更靠后
参考:Calico FelixConfiguration - chainInsertMode
2.2 2379 在 cali-INPUT 内部已被 ACCEPT(关键)
常见误解是「cali-INPUT 处理完会 RETURN 回来,继续匹配我的 DROP」。
实际 iptables 行为:
| 子链动作 | 效果 |
|---|---|
RETURN |
回到父链继续匹配 |
ACCEPT |
立即终止,父链后续规则不再执行 |
Calico 为防止误配策略导致宿主机失联,内置 failsafe 规则。官方文档写明,2379/2380 默认对全网开放:
| 端口 | CIDR | 说明 |
|---|---|---|
| 2379 | 0.0.0.0/0 |
etcd 客户端 |
| 2380 | 0.0.0.0/0 |
etcd peer |
参考:Calico Protect hosts and VMs - Failsafe rules
因此实际流量路径是:
INPUT 第1条 → cali-INPUT
└─ failsafe 匹配 dport 2379 → ACCEPT(在此结束)
你的 whitelist / DROP 规则 → 永远执行不到
结论:在 Calico 管理的节点上,手写 INPUT 规则无法长期可靠地限制 2379;应通过 etcd 自身配置(方案 A)或 Calico 官方机制(方案 B)处理。
3. 操作前准备(两个方案共用)
在每一台控制平面节点上执行。必须先完成 3.2 版本确认,再决定方案 A 能否执行。
3.1 确认 etcd 运行方式
# 应为 Running,且 NETWORK 模式为 host
crictl ps | grep etcd
# 确认监听地址
ss -lntp | grep -E '2379|2380'
kubeadm 默认 etcd 是静态 Pod,manifest 路径:
/etc/kubernetes/manifests/etcd.yaml
3.2 确认实际 etcd 版本(必做)
容器镜像里可能同时存在多个 etcd-3.x.y 二进制(如 etcd-3.2.24、etcd-3.3.17、etcd-3.4.13),这不代表实际在跑哪个版本。必须以 manifest 和运行进程为准,否则方案 A 可能直接导致 etcd 启动失败。
# 1) 看 manifest 里指定的镜像和启动命令(以这里为准)
grep -E 'image:|command:' -A 30 /etc/kubernetes/manifests/etcd.yaml | head -40
# 2) 看运行中 etcd 自身报告的版本(最可靠)
ETCDCTL_API=3 etcdctl version
# 或进入容器执行
docker exec -it $(docker ps --filter name=etcd -q | head -1) etcd --version
# 3) 交叉确认 Kubernetes 版本(决定 etcd 可升级上限)
kubectl version --short
记录结果,后续对照 3.3 节表格。三台 master 版本应一致,不一致先排查再操作。
3.3 版本兼容性对照表
--cipher-suites 参数并非所有 etcd 版本都支持。官方说明:该参数在 v3.2.22+、v3.3.7+、v3.4+ 引入(etcd v3.3 security 文档)。
| 实际 etcd 版本 | --cipher-suites |
方案 A | 方案 B | 说明 |
|---|---|---|---|---|
| 3.0.x | 不支持 | 不可做 | 可做 | 参数不存在,加了会导致启动失败 |
| 3.1.x | 不支持 | 不可做 | 可做 | 同上 |
| 3.2.0 – 3.2.21 | 不支持 | 不可做 | 可做 | 需先升级到 3.2.22+ |
| 3.2.22 – 3.2.x | 支持 | 可做 | 可做 | 使用保守 cipher 列表(见 4.1) |
| 3.3.0 – 3.3.6 | 不支持 | 不可做 | 可做 | 需先升级到 3.3.7+ |
| 3.3.7 – 3.3.x | 支持 | 可做 | 可做 | 使用保守 cipher 列表(见 4.1) |
| 3.4.x | 支持 | 可做 | 可做 | 使用完整 cipher 列表(见 4.1) |
| 3.5.x+ | 支持 | 可做 | 可做 | 注意 cipher 顺序和 HTTP/2 要求(见 4.1 注意项) |
kubeadm 集群常见对应关系(etcd 由 kubeadm 管理时,官方捆绑版本):
| Kubernetes 版本 | kubeadm 默认 etcd 版本 | 方案 A |
|---|---|---|
| 1.16 | 3.3.17-0 | 可做(≥ 3.3.7) |
| 1.17 – 1.18 | 3.4.3-0 | 可做 |
| 1.19 – 1.21 | 3.4.13-0 | 可做 |
| 1.22+ | 3.5.x | 可做 |
若你的镜像里同时有
etcd-3.4.13且etcdctl version显示3.4.13,方案 A 可用。若实际版本是 3.0.x / 3.1.x,不要执行方案 A,只能做方案 B,或通过
kubeadm upgrade升级集群(不要单独改镜像 tag 超出版本兼容范围)。
3.4 收集必须放行的 IP
封 2379/2380 前,先列出所有合法访问方,漏掉会导致 apiserver 或 etcd 集群异常。
# 1) 本机回环(apiserver 健康检查、本机 etcd 客户端)
echo "127.0.0.1"
# 2) 所有控制平面节点 IP(HA 时 etcd peer 走 2380,跨节点也可能访问 2379)
kubectl get nodes -l node-role.kubernetes.io/control-plane -o wide
# 3) 确认 apiserver 连 etcd 的地址
grep -E 'etcd-servers|etcd-servers-overrides' /etc/kubernetes/manifests/kube-apiserver.yaml
kubeadm 典型放行清单(按实际 IP 替换):
| 来源 | 用途 | 是否必须 |
|---|---|---|
127.0.0.1/32 |
本机 apiserver → 本机 etcd | 必须 |
| 各 control-plane 节点 IP/32 | etcd peer(2380)、多 master 互通 | HA 必须 |
管控网段(如 10.0.1.0/24) |
运维、监控、跳板机 | 按需 |
工作节点 Pod 不需要直接访问 2379,不要把整个 Pod CIDR 放进白名单。
3.5 备份
cp /etc/kubernetes/manifests/etcd.yaml /root/etcd.yaml.bak.$(date +%F)
kubectl get felixconfiguration default -o yaml > /root/felix-default.bak.$(date +%F) 2>/dev/null || true
4. 方案 A:禁用弱 TLS 密码套件(根治 Sweet32)
作用:从协议层移除 3DES,消除 Sweet32 根因。
特点:不限制谁能连 2379,只限制能用什么密码协商。
前提:3.2 节确认的版本在 3.3 节表格中标记为「方案 A:可做」。不满足则跳过本章,直接做第 5 章。
4.1 修改 etcd 静态 Pod
在每台控制平面节点编辑:
vi /etc/kubernetes/manifests/etcd.yaml
在 containers[0].command 参数列表末尾追加(保持 YAML 列表格式)。
按实际版本选择 cipher 列表,不要混用:
etcd 3.4.x / 3.5.x(含常见的 3.4.13-0)
- --cipher-suites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
etcd 3.2.22+ / 3.3.7+(较老版本,用保守列表)
- --cipher-suites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
参考:etcd Transport security model(按你的 etcd 大版本选对应文档)
注意项(按版本):
| 版本 | 注意 |
|---|---|
| 所有支持版本 | TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 必须保留 |
| 3.5.x+ | cipher 顺序有要求;缺少上述 GCM 套件可能导致 etcd 异常退出(etcd #10652) |
| 3.4.13 及以下 | 通常不受 #10652 影响,但保守列表更稳妥 |
| 3.2.x / 3.3.x | 不要加 CHACHA20 等该版本 Go 运行时不确定的套件,先用两个 GCM |
不支持 --cipher-suites 的版本(3.0 / 3.1 / 过低 patch):加了该参数 etcd 会报 unknown flag 无法启动。此时只能:
- 执行方案 B 限制访问来源(缓解,不根治 Sweet32);
- 规划
kubeadm upgrade升级 Kubernetes 及捆绑的 etcd 版本(不要单独修改镜像 tag 到 kubeadm 未支持的版本)。
4.2 等待 kubelet 自动重启 etcd
# 无需手动重启,保存 manifest 后 kubelet 会自动重建 Pod
watch -n 2 'crictl ps | grep etcd'
4.3 验证
# 1) 集群健康
kubectl get nodes
kubectl get pods -A | grep -v Running | grep -v Completed
# 2) etcd 成员健康(在任一 master 上)
ETCDCTL_API=3 etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
endpoint health
# 3) 密码套件(Sweet32 应消失)
nmap --script ssl-enum-ciphers -p 2379 127.0.0.1
期望结果:不再出现 TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA 及 Sweet32 警告。
4.4 回滚
cp /root/etcd.yaml.bak.<日期> /etc/kubernetes/manifests/etcd.yaml
5. 方案 B:限制 2379/2380 访问来源(配合方案 A)
cursor 科谱,实践方案 A 成功,方案 B 没有实践,请以实践为准
作用:缩小暴露面,只允许指定 IP 访问 etcd 端口。
5.1 kubeadm 环境下的正确做法
etcd 使用 hostNetwork,流量走宿主机网络栈,不经过 Pod 网络策略。
Calico 默认不会自动保护宿主机端口;但 Felix 的 failsafe 规则始终在 cali-INPUT 里对 2379/2380 全网放行——这正是 iptables 失效的原因。
因此 kubeadm 场景下,方案 B 的正确做法是:修改 Felix 的 failsafe 配置,把 2379/2380 从「全网放行」改为「仅控制平面网段/节点 IP」。
不建议在 kubeadm 集群直接套用「创建 HostEndpoint + GlobalNetworkPolicy」完整主机防护方案——需要额外规划接口标签、策略顺序,操作复杂、容易锁死节点。下文为适合 kubeadm 的简化路径。
5.2 规划 failsafe 放行网段
根据第 3.2 节收集的 IP,确定一个覆盖所有合法来源的 CIDR。
单 master 示例(节点 IP 192.168.1.10):
127.0.0.1/32
192.168.1.10/32
三 master HA 示例(节点 IP 分别为 .10、.11、.12):
127.0.0.1/32
192.168.1.10/32
192.168.1.11/32
192.168.1.12/32
若控制平面 IP 连续,可合并为一个网段(如 192.168.1.0/28),但不要用过大网段(如 10.0.0.0/8),否则失去限制意义。
5.3 修改 FelixConfiguration
以下配置通过 Kubernetes API 下发,只需执行一次(Felix 会同步到所有 calico-node),建议在运维窗口操作。
kubectl get felixconfiguration default -o yaml > /root/felix-etcd-restrict.yaml
vi /root/felix-etcd-restrict.yaml
修改为(将 net 换成 5.2 节规划的网段;以下为三 master 示例):
apiVersion: projectcalico.org/v3
kind: FelixConfiguration
metadata:
name: default
spec:
failsafeInboundHostPorts:
# 保留集群必需端口(去掉默认全网开放的 2379/2380)
- protocol: tcp
port: 22
- protocol: udp
port: 68
- protocol: tcp
port: 179
- protocol: tcp
port: 6443
- protocol: tcp
port: 5473
- protocol: tcp
port: 6666
- protocol: tcp
port: 6667
# etcd:仅允许控制平面来源
- protocol: tcp
port: 2379
net: "127.0.0.1/32"
- protocol: tcp
port: 2379
net: "192.168.1.10/32"
- protocol: tcp
port: 2379
net: "192.168.1.11/32"
- protocol: tcp
port: 2379
net: "192.168.1.12/32"
- protocol: tcp
port: 2380
net: "127.0.0.1/32"
- protocol: tcp
port: 2380
net: "192.168.1.10/32"
- protocol: tcp
port: 2380
net: "192.168.1.11/32"
- protocol: tcp
port: 2380
net: "192.168.1.12/32"
failsafeOutboundHostPorts:
- protocol: udp
port: 53
- protocol: udp
port: 67
- protocol: tcp
port: 179
- protocol: tcp
port: 6443
- protocol: tcp
port: 2379
net: "192.168.1.10/32"
- protocol: tcp
port: 2379
net: "192.168.1.11/32"
- protocol: tcp
port: 2379
net: "192.168.1.12/32"
- protocol: tcp
port: 2380
net: "192.168.1.10/32"
- protocol: tcp
port: 2380
net: "192.168.1.11/32"
- protocol: tcp
port: 2380
net: "192.168.1.12/32"
应用:
kubectl apply -f /root/felix-etcd-restrict.yaml
5.4 等待生效并验证
Felix 会在 iptablesRefreshInterval 周期内自动刷新规则(通常 1–2 分钟)。
# 1) 确认 Felix 已加载新配置
kubectl get felixconfiguration default -o yaml | grep -A2 2379
# 2) 从白名单 IP 访问应成功(在 master 上)
ETCDCTL_API=3 etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
endpoint health
# 3) 从非白名单机器访问应失败(在工作节点或外部机器上)
nmap -p 2379 <control-plane-ip>
# 期望:filtered 或无法建立 TLS
# 4) 集群功能正常
kubectl get nodes
5.5 回滚
kubectl apply -f /root/felix-default.bak.<日期>
6. 推荐实施顺序
第零步:3.2 确认实际 etcd 版本 → 对照 3.3 表格决定能否做方案 A
↓
第一步:方案 A(仅版本支持时;每台 master 改 etcd.yaml)
↓ 验证 etcd 健康 + Sweet32 消失
(版本不支持则跳过,直接第二步)
第二步:方案 B(改 FelixConfiguration,收紧 2379/2380 源 IP)
↓ 验证白名单内外访问符合预期 + 集群正常
第三步:删除之前手写的 iptables 规则(已无用,且会被 Calico 覆盖)
iptables -D INPUT <规则编号>
| 方案 | 解决什么 | 版本要求 | kubeadm 操作范围 | 优先级 |
|---|---|---|---|---|
A --cipher-suites |
Sweet32 根因 | ≥ 3.2.22 / 3.3.7 / 3.4+ | 每台 master 的 etcd.yaml |
版本支持时必做 |
| B failsafe 收紧 | 限制访问来源 | 无 etcd 版本要求 | 一次 kubectl apply(全局) |
建议做 |
| kubeadm upgrade | 升级 etcd 获得方案 A 能力 | 按 K8s 版本规划 | 集群升级流程 | 版本过低时考虑 |
| 手写 iptables | — | — | 不推荐 | 放弃 |
7. 常见错误
| 错误操作 | 后果 |
|---|---|
未确认版本,对 3.0/3.1 加 --cipher-suites |
etcd 报 unknown flag,静态 Pod 反复 CrashLoop,控制平面宕机 |
看容器里有 etcd-3.4.13 就认为在跑 3.4.13 |
实际可能跑 3.0/3.1 二进制,方案 A 配置错误 |
| 对 3.2/3.3 使用 3.5 的完整 cipher 列表 | 含不支持的套件时 TLS 握手失败或 etcd 启动异常 |
| 单独把 etcd 镜像 tag 改到 kubeadm 不支持的版本 | 集群组件不兼容,etcd 数据损坏风险 |
| 只封 2379 不封 2380 | HA 集群 etcd peer 同步失败 |
白名单漏掉 127.0.0.1 |
本机 apiserver 无法连 etcd,控制平面宕机 |
| 白名单漏掉某个 master IP | 多 master etcd 选主/同步异常 |
设 chainInsertMode: Append 绕过 Calico |
可能同时绕过全部 Calico 网络策略,安全风险大 |
| 未备份直接改 failsafe | 配置错误可能导致 SSH/etcd 不可达,需 console 恢复 |
