跳转至

SOP — 修复宿主机重启后 haproxy 被 SELinux 拦截导致 16443 入口不可用

变更主题:为 Kubernetes 控制面入口 VIP 192.168.31.8:16443 的 haproxy 补齐 SELinux 策略并恢复服务 执行时间:2026-09-29 20:27–20:36(CST)|主机:k8s-master-01(192.168.31.10,唯一控制面)

目录

  1. 背景 2. 目标 3. 前置条件 4. 影响范围 5. 分步操作 6. 验证 7. 回滚 8. 风险说明

附录 A:时间线与证据|附录 B:离线环境替代方案|附录 C:交付物清单

1. 背景

2026-09-29 18:45 前后宿主机(dell-R620,192.168.31.249/.250)重启,连带 18 台虚拟机全部重启 (各机 uptime 1h37m–1h44m,与本机一致)。

重启后 K8s 控制面入口不可用,现场现象:

E0929 20:19:58 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list:
  Get \"https://192.168.31.8:16443/api?timeout=32s\": dial tcp 192.168.31.8:16443: connect: connection refused"

根因:haproxy.service 启动失败。haproxy 以 haproxy_t 域运行,SELinux(Enforcing)拒绝其 绑定 16443(unreserved_port_t)与连接后端 6443。

该配置自 2026-09-16 起就存在,但当时 SELinux 实际处于 permissive(audit 留有 permissive=1 记录), 故一直被放行;本次重启后 SELinux 以 Enforcing 生效,隐患才显现——这解释了"重启前一直正常、重启后立即故障"。

apiserver(6443)、etcd、kubelet、containerd、keepalived、VIP 本身均正常,故障只在入口代理这一层。

2. 目标

变更完成后应达到并可验证的状态:

  1. haproxy 服务 active,监听 0.0.0.0:16443;
  2. curl -sk https://192.168.31.8:16443/healthz 返回 ok;
  3. kubectl get nodes 返回 4 个节点全部 Ready;
  4. SELinux 仍为 Enforcing(不以降低安全等级换取可用性);
  5. 上述修复持久:以后重启主机仍然生效。

3. 前置条件

项 要求
权限 本机 root 免密 ssh 到 k8s-master-01(192.168.31.10)
系统 Rocky Linux 9.8,targeted 策略,SELinux=Enforcing
工具 semanage 需 policycoreutils-python-utils(初始缺失,需可访问 dnf 源);semodule / setsebool / ausearch 已具备
依赖 无。apiserver/etcd/kubelet 不动,无需停机
执行前检查 systemctl is-active haproxy 为 failed、ss -lntp \| grep 16443 无监听

4. 影响范围

  • 主机:仅 k8s-master-01(192.168.31.10)。本集群已是单控制面。
  • 组件:haproxy.service(VIP 入口代理)、SELinux 策略(一条端口标签类型 + 一个布尔)。
  • 是否停服:否。apiserver/etcd/kubelet/containerd 均不重启,业务容器不受影响。
  • 窗口:修复动作耗时以秒计;16443 在修复前已中断约 1.7 小时,修复即恢复。
  • 业务影响:K8s 数据面(Calico 转发)在期间仍可工作,已在运行的 Pod 未被中断; 控制面 API 不可用导致 kubelet 无法上报、控制器无法调谐(表现为节点上一批 Pod 被标记为 Unknown), API 恢复后由 kubelet 自行重建容器并收敛。

5. 分步操作

全部命令在 k8s-master-01 上执行;前缀 ssh root@192.168.31.10 表示从本机远程执行,可省略。 本变更为单机、动作少,未编写实现体,全部为可手工复现的 shell 步骤。

步骤 1:定位根因(只读取证)

先确认 haproxy 失败而非 apiserver/etcd 故障:6443 与 2379 在监听、16443 无监听, 即问题只在 haproxy 这一层;再用 audit 日志锁定 SELinux 拒绝。

坑:ss -lntp | grep 16443 无输出时返回码非 0,脚本中需容忍;journalctl 要限时间窗,不要全量拉。

# 1) 控制面组件是否正常
for s in kubelet containerd haproxy keepalived; do printf "%-12s %s\n" "$s" "$(systemctl is-active $s)"; done
ss -lntp | grep -E ":16443|:6443|:2379" || true
# 预期:kubelet/containerd/keepalived=active,haproxy=failed;有 6443、2379 监听,无 16443

# 2) haproxy 失败原因
journalctl -u haproxy --since "-30min" --no-pager -l | tail -n 20
# 预期:cannot bind socket (Permission denied) for [0.0.0.0:16443]

# 3) SELinux 拒绝证据(关键)
getenforce
grep 'type=AVC' /var/log/audit/audit.log | grep -E 'comm="haproxy"|comm="keepalived"' | tail -n 10

失败判断:若 haproxy 的报错不是 Permission denied(例如 Address already in use), 本流程不适用,改按端口占用方向排查;若第 3 步无 AVC 记录,改用 ausearch -m AVC -ts recent。

步骤 2:安装 semanage 工具

Rocky 9 的 policycoreutils 不含 semanage,需补 policycoreutils-python-utils。

坑:首次 dnf install 可能因元数据下载耗时而超时(实测 180s 未返回); 加大超时并限制重试次数后成功。若环境离线,可改用 semodule -i 加载等价 CIL 模块(见附录 B)。

dnf -q install -y --setopt=timeout=60 --setopt=retries=1 policycoreutils-python-utils
command -v semanage audit2allow        # 预期:/usr/sbin/semanage、/usr/bin/audit2allow

失败判断:命令返回非 0 或 command -v 仍无输出 → 走附录 B 的 CIL 方案。

步骤 3:为 16443 定义 SELinux 端口标签

把 16443 归入 http_port_t(haproxy 域被允许绑定的端口类型),这是官方推荐做法。 未用 setenforce 0——那只是临时止血且降低安全等级。

semanage port -l | grep 16443 || echo "(未定义)"
semanage port -a -t http_port_t -p tcp 16443
semanage port -l | grep 16443
# 预期:http_port_t  tcp  16443, 80, 81, 443, ...

失败判断:若报 already defined 说明已打过标签,直接进入下一步(幂等)。

步骤 4:开启 haproxy 出站布尔

haproxy 需连后端 192.168.31.10:6443,该连接同样被 haproxy 域策略拒绝, 用官方布尔放行(作用域仅限 haproxy_t 域)。-P 表示持久化。

getsebool haproxy_connect_any                 # 变更前:off
setsebool -P haproxy_connect_any on
getsebool haproxy_connect_any                 # 预期:on

步骤 5:启动 haproxy

systemctl start haproxy
sleep 3
systemctl is-active haproxy                   # 预期:active
ss -lntp | grep 16443                         # 预期:0.0.0.0:16443 LISTEN
journalctl -u haproxy --since "-1min" --no-pager -l | grep -E "UP|DOWN"
# 预期:Server k8s-api-backend/master-1 is UP(master-2/3 指向已关闭的 .11/.12,仍 DOWN,属预期)

失败判断:仍 bind ... Permission denied → 回步骤 3;进程起来但后端全 DOWN → 回步骤 4。

6. 验证

# 1) 入口健康
ssh root@192.168.31.10 "curl -sk --max-time 8 https://192.168.31.8:16443/healthz"    # 预期:ok
# 2) 集群可见
kubectl get nodes -o wide                                                            # 预期:4 节点全部 Ready
# 3) 策略持久性
ssh root@192.168.31.10 "semanage port -l | grep 16443; semanage boolean -l | grep haproxy_connect_any"
# 预期:http_port_t 含 16443;haproxy_connect_any (on, on)
# 4) 幂等复跑(第二次应全部跳过、零变更)
ssh root@192.168.31.10 'semanage port -l | grep -q 16443 && echo "port 已定义 → 跳过"
  getsebool haproxy_connect_any | grep -q "on$" && echo "boolean 已 on → 跳过"
  systemctl is-active haproxy | grep -q active && echo "haproxy 已运行 → 跳过"'

实测结果:第 1–3 项全部通过,第 4 项三条均为"跳过"(零变更)。 集群侧同步观测到收敛:Running Pod 由 20 → 59,Unknown 由 36 → 0,Terminating 由 6 → 0; server 命名空间业务(GitLab 各组件、MariaDB、MinIO、OpenLDAP、Traefik)全部 Running。

收尾时仍有 3 个 Pod 处于 ContainerCreating/PodInitializing(snapshot-controller、 kube-prometheus-stack-operator、gitlab-toolbox),事件显示为从外网拉取镜像中 (registry.k8s.io / quay.io / registry.gitlab.com),非故障,等待即完成。

7. 回滚

回滚目标状态 = 变更前:haproxy 因 SELinux 拒绝而 failed、16443 无监听。 两步均与步骤一一对应,可独立或同时回退。

# 1) 移除端口标签(回到"未定义")
semanage port -d -t http_port_t -p tcp 16443
# 2) 关闭布尔(回到 off)
setsebool -P haproxy_connect_any off
# 3) 停止 haproxy(回到 failed/未监听)
systemctl stop haproxy

回滚后验证:getenforce 仍为 Enforcing;semanage port -l | grep 16443 无输出; systemctl is-active haproxy 为 inactive。

备份存放位置:k8s-restart-16443-selinux-20260929/backup/, 变更前状态快照见 backup/pre-change-state-master01.txt,台账见 backup/README.md。 本次为"新增策略"类变更,无被覆盖的原文件,故无 .bak.<时间戳> 文件;回滚依据即上表两步删除动作。

8. 风险说明

风险 说明 缓解
布尔放宽出站范围 haproxy_connect_any=on 允许 haproxy 域连接任意端口,范围大于"仅 6443" 作用域限 haproxy_t 域;该机唯一职责就是代理 apiserver,实际暴露面未扩大。若需更严,可用 semanage port -a -t http_port_t -p tcp 6443 替代(未采用,因 6443 语义上属 apiserver 端口)
持久性未做实机重启验证 未在本次变更多做一次重启 semanage port、setsebool -P 均写入策略存储(已由 semanage boolean -l 显示 (on, on) 佐证),非临时 setenforce 手段
haproxy 后端残留已关闭主机 haproxy.cfg 的 backend 仍配置 192.168.31.11/.12:6443(master-02/03 已关闭、禁用 autostart),会持续产生健康检查失败告警 本次未改配置(避免混入非必要变更);建议后续单独清理 backend 列表
同类隐患仍在其他组件 本机 SELinux 曾在 permissive 下运行,可能还有别的服务依赖"实际未拦截" 建议后续做一次全节点 ausearch -m AVC 巡检,集中收口

附录 A:时间线与证据

时间(CST) 事件 证据
18:45:39 宿主机重启后 systemd 拉起 haproxy,bind 16443 被 SELinux 拒绝 audit(1790678740.337:37) denied { name_bind } src=16443 ... permissive=0
18:45:42 keepalived 对 check_haproxy.sh 的 setattr 被拒(脚本仍执行成功,非致命) audit(1790678742.963:39) denied { setattr } name="check_haproxy.sh"
20:19:58 现场 kubectl 报 connection refused(192.168.31.8:16443) 用户截图
20:27 开始处置,确认 apiserver/etcd/keepalived 正常,仅 haproxy failed 本 SOP 步骤 1
20:30:48 打端口标签后启动 haproxy,进程 active 但三个后端全 DOWN journalctl -u haproxy:Permission denied at initial connection step of tcp-check
20:33 开启 haproxy_connect_any,master-1 转 UP Server k8s-api-backend/master-1 is UP
20:33 VIP 入口恢复 curl -sk https://192.168.31.8:16443/healthz → ok
20:36–20:40 集群 Pod 收敛(Unknown 36→0),业务全部 Running kubectl get pods -A

原始 AVC 记录见 evidence/selinux-avc-master01.txt;变更前状态快照见 backup/pre-change-state-master01.txt。

附录 B:离线环境替代方案(未使用)

若无法安装 policycoreutils-python-utils,可用 semodule -i 加载 CIL 端口定义,效果等价:

cat > /root/haproxy_16443.cil <<'EOF'
(portcon tcp 16443 (system_u object_r http_port_t ((s0) (s0))))
EOF
semodule -i /root/haproxy_16443.cil
# 回滚:semodule -r haproxy_16443

出站 6443 若同样不便开布尔,也可用 AUDIT 记录生成最小策略(需已装 audit2allow):

ausearch -m AVC -ts recent | grep haproxy | audit2allow -M haproxy_16443_out
semodule -i haproxy_16443_out.pp

附录 C:交付物清单

文件 内容
SOP.md 本文件:变更实施、验证与回滚
evidence/selinux-avc-master01.txt master-01 的 SELinux AVC 原始记录(根因证据)
backup/pre-change-state-master01.txt 变更前状态快照(SELinux 配置、haproxy 状态、AVC 摘要)
backup/README.md 备份台账(含本次无 .bak 文件的说明与回滚依据)
../../k8s_136_cluster_deploy/docs/RUNBOOK.md 已同步更新「场景 1」:新增 SELinux 分支诊断

来源:/home/rain/Documents/k8s_ins/k8s-restart-16443-selinux-20260929/SOP.md | 最后更新:2026-09-29 20:48(由 build/sync.py 同步,请勿在站点侧修改)