ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

公网K8S集群搭建实战:安全架构设计与避坑指南

公网K8S集群搭建实战:安全架构设计与避坑指南

1. 公网K8S搭建的整体设计与思路拆解

把一套 Kubernetes 集群暴露到公网,和在内网里搭个玩具集群完全是两码事。内网集群你随便折腾,节点之间裸奔通信也没人管;一旦挂到公网,每天扫描 6443、2379、10250 这些端口的机器人流量能把你日志刷爆。我前后搭过几套面向公网提供服务的集群,踩过的坑足够写一本小册子,这里把整套思路和落地细节摊开讲。

先说清楚这套东西是什么、解决什么问题。公网K8S搭建,指的是在云主机或独立服务器上,从零构建一套 Kubernetes 集群,并让它能够安全、稳定地对外提供 HTTP/HTTPS 服务。它解决的核心问题是:如何让集群的控制面和节点在不可信网络环境下依然保持可用、可控、可观测。适合谁来参考?有一定 Linux 基础、想自己掌控一套生产级集群的开发者、运维人员,以及需要把内部服务对外发布的小团队。如果你只是想学 K8S 的基本概念,Minikube 或 Kind 更合适;但如果你要让真实流量打进来,那这套流程就是绕不开的。

整体设计上,我倾向于把集群拆成三个层次来考虑:控制面层、工作负载层、入口层。控制面层负责调度和状态管理,工作负载层跑业务 Pod,入口层负责把公网流量安全地引进来。这三层在公网环境下的安全要求完全不同,必须分开设计。控制面要尽量少暴露端口,工作负载层要隔离网络,入口层才是真正对外的那道门。

方案选型上,我最终采用的是kubeadm 引导 + 容器运行时 containerd + CNI 选 Calico + 入口用 Ingress-Nginx + 证书用 cert-manager这套组合。为什么这么选?kubeadm 是官方维护的引导工具,升级路径清晰,比二进制手工搭建省心太多,也比各种封装发行版更透明——出了问题你能直接查到 K8S 原生文档。containerd 相比 Docker 更轻,K8S 从 1.24 起就移除了 dockershim,直接用 containerd 少一层抽象,资源占用和故障面都更小。Calico 的 BGP 模式和 NetworkPolicy 支持成熟,公网环境下网络策略是刚需,能防止某个被攻破的 Pod 横向扫描整个集群。Ingress-Nginx 生态最全,注解丰富,和 cert-manager 配合自动签发续期证书几乎是标配。

这里有个关键取舍要讲清楚:控制面到底要不要暴露公网。我的做法是控制面 API Server 只对固定的运维出口 IP 开放,通过安全组白名单限制,绝不 0.0.0.0/0 全开。节点加入集群时走的是内网地址,公网只留入口层的 80/443。这样即使 API Server 有未公开漏洞,攻击面也被压到最小。很多人图省事把 6443 全开,结果被扫到弱口令或者未授权访问,集群直接被人拿去挖矿,这种案例我见过不止一次。

还有一个容易被忽略的设计点:etcd 的部署形态。kubeadm 默认把 etcd 以静态 Pod 形式跑在控制面节点上,三节点控制面就是三个 etcd 成员。公网环境下 etcd 的 2379/2380 端口绝对不能对外,节点间通信必须走内网或者加密通道。如果你的云主机跨可用区,内网延迟要提前测,etcd 对延迟敏感,跨区延迟超过 10ms 就可能出现选主抖动。

2. 核心细节解析与实操要点

2.1 主机规划与系统预配置

动手之前先把机器规划定死,后面所有步骤都依赖这个基础。我一般用三台控制面加若干工作节点的结构,控制面奇数台保证 etcd 仲裁。假设用某云厂商的三台 4C8G 机器做控制面,两台 8C16G 做工作节点,操作系统统一用较新的 LTS 版本,内核版本不低于 5.4,因为 Calico 的 eBPF 特性和一些网络优化需要较新内核。

系统预配置这块,每一步都有它的道理,不是照抄命令就完事。关闭 swap 是因为 kubelet 的调度假设内存是可预测的,swap 会让 Pod 的内存限制失效,导致 OOM 行为不可控。关闭防火墙或者放行特定端口,是因为 K8S 组件之间通信端口众多,用 firewalld 逐条放行容易漏,公网环境下更推荐用云安全组在外部做第一道过滤,主机内部用 iptables 规则精细控制。设置内核参数net.bridge.bridge-nf-call-iptables=1是为了让桥接流量经过 iptables,否则 Service 的 DNAT 规则对跨桥流量不生效,这是新手最常踩的坑之一。

时间同步必须做,而且要用可靠的时间源。etcd 和证书校验都依赖时间,节点间时间偏差超过几秒就可能出现证书验证失败或者选主异常。我习惯装 chrony 并指向内网时间服务器,公网 NTP 源在云环境里经常被限速。

主机名规划也有讲究。kubeadm 初始化时会把主机名注册进集群,如果主机名重复或者含特殊字符,后续节点加入会出问题。我统一用k8s-master-01、k8s-worker-01这种规范命名,并在/etc/hosts里把内网 IP 和主机名对应写好,避免依赖 DNS 解析带来的不确定性。

注意:所有节点的主机名必须唯一,且初始化后不要再改。改主机名会导致 kubelet 注册的 Node 对象和证书里的 CN 不匹配,节点直接 NotReady。

2.2 容器运行时与 kubelet 安装

containerd 的安装我倾向用官方二进制包而不是发行版仓库里的版本,仓库版本往往偏旧,和较新 K8S 的 CRI 接口可能有兼容问题。安装后要生成默认配置并调整两个关键点:一是SystemdCgroup = true,让 containerd 用 systemd 管理 cgroup,和 kubelet 的 cgroup 驱动保持一致,否则会出现 Pod 启动后资源限制不生效的诡异现象;二是配置镜像加速或者私有仓库地址,公网拉取镜像慢会拖垮整个部署节奏。

kubelet、kubeadm、kubectl 三个组件的版本必须严格一致,小版本都不能差。我见过有人 kubeadm 是 1.28.2 而 kubelet 是 1.28.0,初始化时直接报版本偏差错误。安装时用包管理器锁定版本,避免后续apt upgrade把版本升乱。kubelet 安装后先别急着启动,等 kubeadm init 跑完再让它起来,否则它会反复重启刷日志。

这里补充一个实操细节:kubelet 的--node-ip参数在双网卡机器上必须显式指定。云主机经常有内网网卡和公网网卡两块,kubelet 默认可能选错 IP 注册到集群,导致其他节点连不上它。我在 kubeadm 的配置里通过nodeRegistration.kubeletExtraArgs把node-ip固定为内网地址,这个坑踩过一次就忘不了。

2.3 集群初始化与网络插件部署

kubeadm 的初始化配置文件是整个搭建过程的核心,我习惯写一个kubeadm-config.yaml而不是用命令行参数,因为配置可版本化、可复用、可审计。配置文件里几个关键字段:controlPlaneEndpoint指向一个内网负载均衡地址或者第一台控制面的内网 IP 加端口,podSubnet和serviceSubnet要和后续 CNI 的网段规划一致,imageRepository指向可访问的镜像仓库。

初始化命令跑完后,按提示配置 kubectl 的 kubeconfig,然后立刻部署 CNI。Calico 我用的是 operator 方式或者 manifest 方式,取决于集群规模。小集群直接 apply manifest 最快,注意把CALICO_IPV4POOL_CIDR改成和podSubnet一致,否则 Pod 拿不到 IP。Calico 的IP_AUTODETECTION_METHOD要设成can-reach或者指定网卡名,确保它选对内网网卡做 BGP 宣告,公网网卡上跑 BGP 是灾难。

网络插件起来后,用kubectl get nodes确认所有节点 Ready,用kubectl get pods -n kube-system确认 CoreDNS 和 Calico 的 Pod 全部 Running。如果 CoreDNS 一直 Pending,八成是 CNI 没装好或者网段冲突;如果 Calico 的 Pod CrashLoop,去看日志里是不是网卡选错了或者内核模块没加载。

提示:初始化前先用kubeadm config images pull把镜像拉下来,能提前暴露镜像仓库访问问题,避免 init 跑到一半卡住。

3. 实操过程与核心环节实现

3.1 控制面高可用与安全加固落地

单控制面集群在公网环境里没有容错能力,机器一挂整个集群失联。我推荐至少三控制面。kubeadm 支持 stacked etcd 模式,三台控制面各自跑一个 etcd 成员,前面挂一个内网负载均衡把 6443 端口转发到三台机器。负载均衡用云厂商的内网 CLB 或者自己用 keepalived 加 haproxy 都行,关键是健康检查要探 API Server 的/healthz端点。

初始化第一台控制面时用--upload-certs把证书加密上传,后续两台用kubeadm join带--control-plane和--certificate-key加入。加入顺序有讲究,一台一台来,每台加入后确认 etcd 成员健康再操作下一台。三台都加入后,用kubectl get pods -n kube-system -l component=etcd确认三个 etcd Pod 都在各自节点上 Running,再用etcdctl endpoint health逐个检查。

安全加固这块,我做了几件事。第一,API Server 的--authorization-mode确保包含Node,RBAC,默认就是,但有人改配置时容易漏。第二,给 kubelet 开启--anonymous-auth=false和--authorization-mode=Webhook,防止未认证请求访问 kubelet 的 10250 端口,这个端口在公网暴露且未加固的话,攻击者可以直接在节点上执行命令。第三,用 NetworkPolicy 默认拒绝所有入站流量,再按需放行,Calico 支持这个能力,一条默认 deny 策略能挡掉大量横向渗透。

审计日志也要开。API Server 的--audit-log-path和--audit-policy-file配置好,记录谁在什么时候对什么资源做了什么操作。公网集群出问题时,审计日志是排查的第一手材料。日志轮转用--audit-log-maxbackup和--audit-log-maxsize控制,别让日志把磁盘写满。

3.2 入口层与证书自动化配置

入口层是公网流量的唯一入口,这块配好了,后面的服务发布就是流水线作业。Ingress-Nginx 我用 Helm 安装,values 里重点调几个参数:controller.service.type设为 LoadBalancer 或者 NodePort 配合外部负载均衡,controller.config.proxy-body-size按业务调整默认 1m 往往不够,controller.metrics.enabled打开监控,controller.podAntiAffinity让多个副本分散到不同节点。

cert-manager 负责证书全生命周期。装完后创建 ClusterIssuer,用 ACME 的 HTTP-01 或者 DNS-01 验证。HTTP-01 要求域名解析到入口 IP 且 80 端口可达,适合简单场景;DNS-01 适合泛域名或者入口不直接暴露 80 的情况,需要配置 DNS 服务商的 API 凭证。我一般用 DNS-01 加泛域名证书,一张证书覆盖所有子域名,省去每个服务单独签发的麻烦。

证书自动续期要验证。cert-manager 默认在证书到期前 30 天续期,但续期失败不会主动告警。我加了一条 Prometheus 告警规则,监控certmanager_certificate_expiration_timestamp_seconds减去当前时间小于 14 天就触发,给自己留足排查时间。实测下来,DNS-01 续期偶尔会因为 DNS 服务商 API 限流失败,有告警才能及时发现。

注意:Ingress 的 TLS secret 和 cert-manager 的 Certificate 资源要对应好,域名变更时记得同步改 Ingress 的tls.hosts,否则证书签了但没被引用,浏览器照样报不安全。

3.3 工作节点加入与污点调度策略

工作节点加入集群用kubeadm join带 token 和 CA 证书 hash。token 默认 24 小时过期,大批量加节点时记得用kubeadm token create --ttl 0生成长期 token,加完再清理。加入后给节点打标签,比如node-role.kubernetes.io/worker=,方便后续用 nodeSelector 或者 affinity 调度。

控制面节点默认带node-role.kubernetes.io/control-plane:NoSchedule污点,业务 Pod 不会调度上去。如果集群资源紧张想让控制面也跑业务,可以去掉污点,但我不建议——控制面节点跑业务会争抢资源,etcd 对 IO 和 CPU 敏感,业务高峰时可能拖垮控制面。真要利用资源,单独加一台工作节点更稳妥。

节点维护时用kubectl drain驱逐 Pod,加--ignore-daemonsets和--delete-emptydir-data,维护完kubectl uncordon恢复。公网集群里节点重启后,kubelet 会自动重连,但如果有 Pod 用了 hostNetwork 或者本地存储,重启后状态可能不一致,维护窗口内要重点检查这类 Pod。

4. 常见问题与排查技巧实录

公网集群的问题排查和内网差别很大,很多问题是被外部流量触发的。我把踩过的典型问题整理成速查表,附上排查思路。

现象可能原因排查与解决
节点 NotReady,kubelet 日志报证书错误节点时间偏差大或证书过期检查 chrony 同步状态,kubeadm certs check-expiration查看证书有效期
Pod 无法解析域名CoreDNS Pod 异常或 CNI 网段冲突kubectl logs看 CoreDNS,检查 podSubnet 与节点网段是否重叠
Ingress 返回 502后端 Service 无可用 Endpoint 或端口不匹配kubectl get endpoints确认后端 Pod 就绪,核对 Service targetPort
证书签发一直 PendingACME 验证失败,DNS 未生效或 80 端口不通kubectl describe certificate看事件,用dig确认解析,检查安全组
API Server 偶发超时etcd 跨区延迟高或磁盘 IO 瓶颈测节点间延迟,etcdctl check perf看 etcd 性能,换 SSD 云盘
节点被扫描端口告警安全组规则过宽或 kubelet 未加固收紧安全组,确认 kubeletanonymous-auth=false

除了表格里的,还有几个独家避坑技巧值得单独说。第一,公网集群一定要配 Pod 的出口 IP 伪装。Calico 默认对出集群流量做 SNAT,但如果配置不当,Pod 访问外网时源 IP 是 Pod IP,外部服务可能拒绝或者日志里全是内网地址。检查calicoctl get ippool的natOutgoing是否为 true。

第二,Ingress 的 real_ip 配置。流量经过云负载均衡后,后端看到的源 IP 是负载均衡的地址,业务日志里全是同一个 IP。要在 Ingress-Nginx 的 ConfigMap 里配use-forwarded-headers: "true"和compute-full-forwarded-for: "true",并设置proxy-real-ip-cidr为负载均衡的网段,这样X-Forwarded-For才能正确传递真实客户端 IP。

第三,etcd 备份必须做且要验证恢复。公网集群被攻击或者误操作的概率比内网高,etcd 快照是最后的救命稻草。我写了个定时任务每小时etcdctl snapshot save到对象存储,每周做一次恢复演练到临时集群验证快照可用。只备份不验证等于没备份,这个道理在真出事的时候体会最深。

第四,监控告警要覆盖控制面组件。kube-scheduler、kube-controller-manager、etcd 的指标默认只监听 127.0.0.1,Prometheus 抓不到。要么改监听地址加认证,要么用 kube-state-metrics 加 node-exporter 的组合间接监控。我倾向后者,改动小、风险低。

我个人在实际操作中的体会是,公网 K8S 搭建的难点从来不在“搭起来”,而在“搭起来之后怎么守住”。初始化那半小时的顺利程度,和后面几个月的稳定运行没有必然关系。真正决定成败的是安全组规则有没有收紧、证书续期有没有告警、etcd 备份有没有验证、审计日志有没有人看。这几件事做到位,集群才能安心对外提供服务。最后再分享一个小技巧:每次变更集群配置前,先用kubeadm config view导出当前配置存档,出问题时能快速对比差异,比凭记忆回滚靠谱得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进