跳到主要内容

配置与安全

前面几篇解决了「应用怎么跑起来、怎么暴露服务、数据怎么存」,这篇讲剩下的两件大事:配置怎么管、安全怎么搞。前半部分的 ConfigMap 和 Secret 负责把配置从镜像里剥离出来,后半部分从 ServiceAccount、RBAC 一路讲到准入控制和多租户——内容有点多,但都是生产集群绕不开的基本功。

ConfigMap​

ConfigMap 用来把非敏感的配置数据以键值对的形式存进集群,Pod 启动时再注入进去。这样同一个镜像可以在开发、测试、生产环境挂不同的配置,不用为每个环境单独打镜像。

ConfigMap 的边界

ConfigMap 只适合放不敏感的配置:数据库地址、日志级别、特性开关之类。密码、Token 这类敏感信息要交给下一节的 Secret。另外单个 ConfigMap 大小上限是 1 MiB,别拿它当文件仓库用。

三种创建方式​

方式一:命令行字面量,适合临时塞几个键值对:

kubectl create configmap app-config \
--from-literal=LOG_LEVEL=debug \
--from-literal=DB_HOST=mysql.default.svc.cluster.local

方式二:从文件创建,适合现成的配置文件,键默认取文件名:

# 键为文件名 nginx.conf,值为整个文件内容
kubectl create configmap nginx-conf --from-file=nginx.conf

# 也可以自定义键名
kubectl create configmap nginx-conf --from-file=my-key=nginx.conf

方式三:YAML 声明式创建,能纳入 Git 管理,是生产环境推荐的方式:

apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: debug
DB_HOST: mysql.default.svc.cluster.local
app.properties: |
server.port=8080
spring.profiles.active=prod

data 下的值都是字符串,多行配置用 | 块标量保留换行。kubectl apply -f 提交后,可以用 kubectl get cm app-config -o yaml 回查内容。

两种消费方式​

方式一:环境变量注入。用 envFrom 把 ConfigMap 里所有键一次性灌进容器环境变量:

apiVersion: v1
kind: Pod
metadata:
name: app-env
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "env | grep -E 'LOG_LEVEL|DB_HOST'"]
envFrom:
- configMapRef:
name: app-config

只想取单个键的话,用 env 搭配 configMapKeyRef:

      env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL

方式二:Volume 挂载。把每个键变成挂载目录下的一个文件:

apiVersion: v1
kind: Pod
metadata:
name: app-volume
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "cat /etc/config/app.properties"]
volumeMounts:
- name: config
mountPath: /etc/config
volumes:
- name: config
configMap:
name: app-config

两种方式的对比如下:

对比项环境变量Volume 挂载
注入形式键值对变成环境变量每个键变成挂载目录下的一个文件
更新后是否同步进容器不同步,启动那一刻就固定了kubelet 会定期同步(有几十秒级延迟)
适合场景少量、简单的开关型配置整份配置文件、需要热更新的配置

更新 ConfigMap 不会重建 Pod​

这是新手最常踩的坑:改了 ConfigMap,Pod 不会自动重建。

  • 用环境变量注入的配置:容器启动那一刻就定死了,之后改 ConfigMap 毫无效果,必须重建 Pod。
  • 用 Volume 挂载的配置:kubelet 会异步把新内容同步进容器(默认同步周期加上缓存,可能延迟一两分钟),但应用得自己重新读文件才生效,而多数应用启动时读一次就不管了。

所以最常见的做法是改完配置手动滚一次:

kubectl rollout restart deployment/my-app

更优雅的办法是把 ConfigMap 内容的哈希写进 Pod 模板注解(Helm 里常见的 checksum/config 注解就是这个思路),内容一变 Pod 模板就跟着变,滚动更新自动触发。

immutable:不可变 ConfigMap​

Kubernetes 1.21 起可以给 ConfigMap 加 immutable: true,把它彻底钉死:

apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v1
immutable: true
data:
LOG_LEVEL: debug

钉死之后任何修改都会被拒绝,想改配置只能新建一个 ConfigMap(名字带上版本后缀)再让工作负载换引用。好处有两个:一是防止误改配置酿成线上事故;二是 kubelet 不用再 watch 它,集群里 ConfigMap 很多时能明显减轻 API Server 的压力。

Secret​

Secret 和 ConfigMap 用法几乎一样,区别是它专门存敏感信息:密码、Token、TLS 证书、镜像仓库凭据等。结构上同样是键值对,只是 data 里的值要求是 base64 编码后的字符串。

三种常用类型​

Secret 的 type 字段决定用途和必填的键:

type用途关键字段
Opaque通用类型,任意键值对(默认值)自定义
kubernetes.io/tls存 TLS 证书和私钥tls.crt、tls.key
kubernetes.io/dockerconfigjson存镜像仓库登录凭据.dockerconfigjson

对应的创建命令:

# 通用 Secret
kubectl create secret generic db-secret \
--from-literal=username=admin \
--from-literal=password='S3cret!'

# TLS Secret
kubectl create secret tls web-tls --cert=tls.crt --key=tls.key

# 镜像仓库凭据
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=bot \
--docker-password='S3cret!'

stringData:免去手工 base64​

写 YAML 时用 stringData 字段可以直接填明文,API Server 写入时会自动转成 base64 存进 data,之后 stringData 字段本身消失:

apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
stringData:
username: admin
password: S3cret!

回查时 data 里是编码后的值,一条 echo 'xxxx' | base64 -d 就能解出明文。Secret 的消费方式和 ConfigMap 完全一致——envFrom、secretKeyRef、Volume 挂载,这里不再重复。

base64 只是编码,不是加密

这一点必须划重点:base64 不是加密,是编码,任何人拿到 data 里的值,一条命令就能还原明文。Secret 的「安全」体现在别的地方:API 层可以用 RBAC 做更细的权限管控、etcd 可以配静态加密、Secret 只被分发到需要它的 Pod 所在节点、容器内以 tmpfs 内存文件系统挂载不落盘。

所以别把「用了 Secret」当成安全终点:集群里能 list secrets 的人等于拿到了所有明文,etcd 磁盘被人拷走也等于全部泄露。真正的防护要靠 RBAC 收敛权限、下面讲的 etcd 静态加密,必要时再接外部密钥管理系统(Vault、云厂商 KMS)。

etcd 静态加密(Encryption at Rest)​

默认情况下 Secret 只是 base64 编码后明文躺在 etcd 里。Kubernetes 提供了 Encryption at Rest 机制:给 API Server 配一份加密配置,数据写进 etcd 之前先在内存里加密,读出时再解密。

# /etc/kubernetes/encryption.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64 编码的 32 字节密钥>
- identity: {}

然后在 API Server 启动参数里挂上它并重启:

--encryption-provider-config=/etc/kubernetes/encryption.yaml

几个要点:

  • providers 按顺序尝试解密,但加密永远用第一个。轮换密钥时把新密钥放最前面,旧密钥留在后面解密存量数据,全部重写后再删掉旧的。
  • identity 提供者是「不加密」,放在最后兜底,保证迁移期间旧的明文数据还能读。
  • 生产环境更推荐 kms 提供者,对接云厂商 KMS 或 Vault,密钥不出密钥管理系统,比静态写死的 aescbc 密钥更稳。
  • 配置生效后,已存在的 Secret 还是明文,需要执行 kubectl get secrets --all-namespaces -o json | kubectl replace -f - 全部重写一遍,触发加密落盘。

ServiceAccount​

前面讲的是「人怎么管集群」——kubectl 用 kubeconfig 里的用户身份。ServiceAccount(简称 SA)解决的是另一个问题:Pod 里的程序访问 API Server 时用什么身份。比如应用要列出同 namespace 的 Pod、CI 容器要 apply 资源,都得有一个集群内部身份。

每个 namespace 创建时自带一个 default SA,Pod 不指定 serviceAccountName 就用它:

apiVersion: v1
kind: Pod
metadata:
name: sa-demo
spec:
serviceAccountName: my-sa # 不指定则为 default
containers:
- name: app
image: nginx:1.27

Token 自动挂载​

只要 Pod 关联了 SA,kubelet 会自动把一份访问 API Server 的凭据投射进容器:

/var/run/secrets/kubernetes.io/serviceaccount/
├── ca.crt # 集群 CA 证书,用于验证 API Server
├── namespace # 当前所在 namespace
└── token # 调用 API 的 Bearer Token

1.24 之前这份 Token 是以 Secret 形式永久存在的(不过期、一直有效),泄露风险很大;1.24 起改为通过 TokenRequest API 签发有时效、绑定 Pod 的临时 Token,由 kubelet 定期轮换,安全性好了很多。测试时也可以用 kubectl create token my-sa 手动申请一个短时 Token。

加固:关掉自动挂载​

绝大多数业务 Pod 根本不需要访问 API Server,挂着 Token 等于白送攻击者一把钥匙。按最小权限原则,建议默认关掉:

apiVersion: v1
kind: Pod
metadata:
name: no-token
spec:
automountServiceAccountToken: false
containers:
- name: app
image: nginx:1.27

也可以在 SA 层面设置,影响所有使用它的 Pod:

apiVersion: v1
kind: ServiceAccount
metadata:
name: default
automountServiceAccountToken: false

真正需要访问 API 的 Pod(比如各类控制器),再单独建 SA、按需授权——授权方式就是下一节的 RBAC。

RBAC​

RBAC(Role-Based Access Control,基于角色的访问控制)是 Kubernetes 主流的鉴权方式,回答的问题是:谁(Subject)能对什么资源(Resource)做什么操作(Verb)。kubeadm、kind 等主流安装方式默认都启用了它(API Server 参数 --authorization-mode=RBAC)。

四件套的关系​

RBAC 就四个对象:两个负责「定义权限」,两个负责「绑定权限」。

RBAC 权限模型

对象作用范围说明
Role单个 namespace定义该 namespace 内的权限清单
ClusterRole整个集群定义集群级资源(nodes、pv 等)或可复用的权限模板
RoleBinding单个 namespace把 Role(或 ClusterRole)授予主体,只在该 namespace 生效
ClusterRoleBinding整个集群把 ClusterRole 授予主体,集群范围生效

常见的三种组合:

  • Role + RoleBinding:授予某 namespace 内权限,最常见的用法。
  • ClusterRole + RoleBinding:权限只在某个 namespace 生效,但清单复用集群级模板,比如内置的 view、edit。
  • ClusterRole + ClusterRoleBinding:授予整个集群的权限,威力最大,要格外谨慎。

权限怎么写:apiGroups、resources、verbs​

Role 的核心是 rules 列表,每条规则三个要素:

  • apiGroups:资源所属的 API 组。核心组(pods、services、configmaps、secrets 等)写空字符串 "";deployments、statefulsets 属于 "apps";ingresses 属于 "networking.k8s.io"。
  • resources:资源名,全小写复数,如 pods、deployments。可以精确到子资源,如 pods/log。
  • verbs:允许的动作,常见有 get、list、watch、create、update、patch、delete。写 ["*"] 表示全部,慎用。

拿不准某个资源属于哪个组、支持哪些动词时,直接查:

kubectl api-resources -o wide

输出里的 APIGROUP 和 VERBS 两列就是答案。

完整示例:给 SA 授 namespace 内只读​

目标:在 demo namespace 里建一个 SA,只允许它查看该 namespace 下的 Pod,别的什么都不让做。

# 1. 建 namespace 和 ServiceAccount
apiVersion: v1
kind: Namespace
metadata:
name: demo
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-reader
namespace: demo
---
# 2. 定义权限:只能读 Pod
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: demo
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
# 3. 绑定:把 Role 授给 SA
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: demo
subjects:
- kind: ServiceAccount
name: pod-reader
namespace: demo
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io

roleRef 指向权限清单,subjects 列出被授予的对象(可以一次写多个)。注意 roleRef 创建后不可修改,想换权限只能删掉 Binding 重建。

用 kubectl auth can-i 验证​

授权写得对不对,别靠猜,直接问 API Server:

kubectl auth can-i list pods --as=system:serviceaccount:demo:pod-reader -n demo
kubectl auth can-i delete pods --as=system:serviceaccount:demo:pod-reader -n demo
yes
no

--as 用来模拟某个身份——SA 的完整用户名是 system:serviceaccount: 加上 namespace 和名字,管理员排查权限问题全靠它。换 namespace 或加 -A 参数,还能对比不同范围下的结果。

Admission Controller​

一个请求打到 API Server,完整链路是这样的:

请求 → 认证(你是谁)→ 鉴权(你能不能做,如 RBAC)→ 准入控制(请求合不合规矩、要不要改写)→ 校验 → 写入 etcd

准入控制器(Admission Controller)是写库前的最后一道关卡:RBAC 说「你有权创建 Pod」,准入控制还可以进一步说「但你这个 Pod 没写资源限额,拒绝」,或者「我帮你补上默认的 storageClassName」。

Mutating vs Validating​

准入控制器分两型,执行顺序是先改后验:

类型能否修改请求对象典型用途
Mutating(变更型)能注入默认值、自动加 Sidecar、改写字段
Validating(校验型)不能,只能放行或拒绝强制规范、校验字段合法性

先跑完所有 Mutating(此时对象可能已被改写),再跑 Validating 对最终结果做校验。任何一个环节拒绝,整个请求就失败。

常用内置准入控制器​

API Server 自带三十多个准入控制器,常见安装默认启用了一批,最常用的几个:

准入控制器作用
NamespaceLifecycle拒绝在不存在的 namespace 里建资源,防止误删正在使用的 namespace
ServiceAccount自动给 Pod 关联默认 SA 并投射 Token
LimitRanger配合 LimitRange 对象,给没写 requests/limits 的容器补默认值,并检查是否超限
ResourceQuota配合 ResourceQuota 对象,检查 namespace 配额,超了拒绝
PodSecurity即下一节要讲的 Pod Security Admission,按安全级别校验 Pod

查看当前启用了哪些(kubeadm 集群可以直接看静态 Pod 清单的启动参数):

grep admission /etc/kubernetes/manifests/kube-apiserver.yaml

Webhook 扩展​

内置控制器不够用,也不用改 API Server 代码:通过 MutatingAdmissionWebhook 或 ValidatingAdmissionWebhook,把请求转发给自己写的 HTTP 服务做改写或校验即可——Istio 的 Sidecar 自动注入、各公司的「强制打标签」策略都是这么做的。知道有这条路就行,真落地时一般直接用成熟的策略引擎(如 Kyverno、OPA Gatekeeper),不必手写 Webhook 服务。

Pod Security​

PSP 已废弃​

老资料里经常出现 PodSecurityPolicy(PSP):一套功能强大但极其难用的集群级 Pod 安全策略。官方在 1.21 将它标记废弃,1.25 已彻底移除。再看到 PSP 相关的内容,直接跳过,别学。

Pod Security Admission​

PSP 的接班人是内置的 Pod Security Admission(PSA),思路从「灵活的复杂策略」换成「三档固定级别」,开箱即用:

级别严格程度大致要求
privileged最宽松不限制,任何东西都能跑
baseline适中禁止提权、hostNetwork、hostPID、hostPath 等明显危险的配置,日常业务够用
restricted最严格在 baseline 之上再要求 runAsNonRoot、禁 capabilities、seccomp 等,对应完整的安全加固标准

启用方式是给 namespace 打 label,三种模式可以叠加:

kubectl label namespace demo \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted

三种模式的分工:

  • enforce:违反就拒绝创建,真正干活的模式。
  • warn:放行但给客户端返回警告(kubectl 输出里能看到),适合灰度期观察。
  • audit:放行并记进审计日志,事后追溯用。
平滑上线姿势

别一上来就 enforce=restricted,存量业务大概率起不来。正确节奏:先挂 warn 和 audit 观察一段时间,把违规的 workload 改干净,最后再切 enforce。label 还可以带版本后缀(如 enforce-version: v1.31)钉住检查标准,避免集群升级后规则悄悄变严。

NetworkPolicy​

默认情况下 Kubernetes 里所有 Pod 之间互通,任何一个 Pod 被攻破,攻击者就能在集群内横向移动。NetworkPolicy 是 K8s 提供的 L3/L4 网络防火墙:按 Pod 维度声明「谁能访问我、我能访问谁」。

NetworkPolicy 需要 CNI 支持

NetworkPolicy 只是一份声明,真正执行它的是 CNI 网络插件。常见插件的支持情况:

CNI 插件支持 NetworkPolicy
Calico✅ 完整支持
Cilium✅ 完整支持,还提供 L7 扩展
Flannel❌ 不支持(只负责打通网络)
Weave✅ 支持

用 Flannel 的集群写了 NetworkPolicy 不会报任何错,但策略完全不生效——这是最常见的「假安全感」。要么换 Calico 或 Cilium,要么用 Flannel 加 Calico 组合的 Canal 方案。

一个具体例子​

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: web-policy
namespace: demo
spec:
podSelector:
matchLabels:
app: web # 策略作用的 Pod
policyTypes:
- Ingress # 声明本策略包含入站规则
ingress:
- from:
- podSelector: # 只允许带 app=frontend 标签的 Pod 访问
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 80

关键字段:

  • podSelector:选中被管控的 Pod。写空 {} 表示选中该 namespace 的全部 Pod。
  • policyTypes:声明策略管 Ingress(入站)、Egress(出站)还是两者。只要有 NetworkPolicy 选中某 Pod 并声明了某个方向,该方向就默认拒绝白名单之外的所有流量。
  • ingress[].from:允许的来源,可以用 podSelector(同 namespace 的 Pod)、namespaceSelector(整个 namespace 的 Pod)、两者组合,或 ipBlock 指定网段。
  • egress[].to:允许的出站目标,写法与 from 对称。

注意 from 列表中多个元素之间是或的关系,而同一个元素里的多个 selector 是与的关系——新手常在这里把白名单写宽。

实践:先默认全拒,再逐步放行​

安全基线的标准姿势:进 namespace 先一刀切全拒,再按需开白名单:

# 默认拒绝所有入站和出站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: demo
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress

切完会发现 DNS 也断了,所以通常紧接着放行 kube-dns 的 UDP/53 出站,再按「frontend 调 web、web 调 db」的链路一条条补放行规则。规则宁缺毋滥,每加一条都问自己一句:这条流量真的有业务需要吗?

SecurityContext​

SecurityContext 定义 Pod 和容器的权限与访问控制:以什么用户跑、能不能提权、文件系统可不可写、带哪些 Linux capabilities。它分两层:

层级字段位置作用范围
Pod 级spec.securityContext对 Pod 内所有容器生效
容器级containers[].securityContext只对当前容器生效,会覆盖 Pod 级的同名字段

加固模板​

一份符合 restricted 级别的加固模板长这样:

apiVersion: v1
kind: Pod
metadata:
name: hardened
spec:
securityContext: # Pod 级
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: nginx:1.27
securityContext: # 容器级
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}

逐条解释关键字段:

  • runAsNonRoot: true:禁止以 root(UID 0)启动,镜像默认用户是 root 时直接拒绝运行。它要求镜像或配置里能确定一个非 root 用户,所以搭配 runAsUser: 1000 更稳。
  • readOnlyRootFilesystem: true:容器根文件系统只读,攻击者拿到 shell 也改不了程序文件、写不了后门。代价是应用写临时文件会失败,所以挂一个 emptyDir 到 /tmp 兜底。
  • allowPrivilegeEscalation: false:禁止进程获得比父进程更高的权限,直接废掉 setuid 类提权路径。只要开了 runAsNonRoot,这条就该配上。
  • capabilities: drop: ["ALL"]:Linux capabilities 把 root 的权限拆成了细粒度的项(比如 NET_RAW 能伪造数据包、SYS_ADMIN 几乎等于 root)。业务容器一项都用不到,先全部 drop,真有需要再按需 add 个别项。
  • seccompProfile: RuntimeDefault:启用容器运行时自带的默认 seccomp 配置,拦掉一批危险系统调用,属于白捡的加固。
镜像也要配合

SecurityContext 不是万能的:镜像里如果内置了 setuid 程序,或者应用逻辑必须用 root 跑,runAsNonRoot 会直接让 Pod 起不来。选镜像时优先用官方的非 root 变体(如 bitnami 的 non-root 系列、distroless 镜像),从源头减少和加固策略的冲突。

ImagePullSecrets​

私有镜像仓库要求认证,kubelet 拉镜像用的凭据就由 ImagePullSecrets 提供。

创建仓库凭据​

kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=bot \
--docker-password='S3cret!' \
--docker-email=bot@example.com

这条命令生成的正是前面提到的 kubernetes.io/dockerconfigjson 类型 Secret,内容等价于 Docker 客户端的 config.json。

两种挂载方式​

方式一:挂在 Pod(或工作负载的 Pod 模板)上,影响范围明确:

apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
spec:
imagePullSecrets:
- name: regcred
containers:
- name: app
image: registry.example.com/team/app:v1

方式二:挂在 ServiceAccount 上,让该 SA 下所有 Pod 自动带上凭据,适合团队统一用私有仓库的场景:

kubectl patch serviceaccount default \
-p '{"imagePullSecrets": [{"name": "regcred"}]}' \
-n demo

方式一精确,但每个工作负载都要写一遍;方式二省事,但作用面大——该 SA 的所有 Pod 都能拿到这份凭据。多团队共用集群时,按 namespace 隔离 SA、各挂各的凭据。

凭据即权限

谁能在某 namespace 里读到这个 Secret,谁就拿到了仓库的登录凭据。仓库侧务必使用只读、权限最小化的机器人账号,别拿管理员账号来拉镜像。

TLS​

Kubernetes 集群内部通信全程走 HTTPS,背后是一整套证书体系。kubeadm 装集群时会自动签好,但心里要有一张地图。

集群内证书体系​

kubeadm 安装时会在 /etc/kubernetes/pki/ 下生成一套 CA 和证书:

/etc/kubernetes/pki/
├── ca.crt / ca.key # 集群根 CA
├── apiserver.crt # API Server 的服务端证书
├── apiserver-kubelet-client.* # API Server 访问 kubelet 的客户端证书
├── front-proxy-ca.* # 聚合层(扩展 apiserver)专用 CA
├── sa.key / sa.pub # ServiceAccount Token 的签名密钥对
└── etcd/ # etcd 自己的一套 CA 和证书

关键角色:

  • 集群 CA:一切的信任根。API Server、kubelet、各组件的证书都由它签发;kubeconfig 里的 certificate-authority-data 就是它,用来验证「我连的是真的 API Server」。
  • API Server 证书:6443 端口的服务端证书。签发时 SAN 里必须包含所有可能的访问地址(内外网 IP、kubernetes.default.svc 等),否则客户端会报证书域名不匹配。
  • kubelet 证书:每个节点 10250 端口的服务端证书,API Server 反向访问 kubelet(kubectl logs、exec 都走这条路)时要验证它;kubelet 还持有客户端证书,用于向 API Server 注册和汇报状态。
  • 这套证书默认有效期一年,kubeadm certs check-expiration 可以查看到期时间,kubeadm certs renew all 手动续期,用 kubeadm 升级集群时也会自动续。

cert-manager:应用证书自动化​

集群组件的证书 kubeadm 管,应用的证书(比如 Ingress 的 HTTPS 证书)就靠 cert-manager——它是事实标准的证书自动化控制器:

  • 用 Issuer(单 namespace 可用)或 ClusterIssuer(集群可用)描述证书从哪来:Let's Encrypt 等 ACME 服务、私有 CA、自签名。
  • 创建 Certificate 资源(或直接给 Ingress 加注解)声明「我要某域名的证书」,cert-manager 自动完成申请、域名验证、签发,结果存成 kubernetes.io/tls 类型的 Secret 供 Ingress 引用。
  • 到期前自动续期,彻底告别「证书忘了续、全站挂掉」的事故。
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-key
solvers:
- http01:
ingress:
class: nginx

之后给 Ingress 加上注解 cert-manager.io/cluster-issuer: letsencrypt,新站点的 HTTPS 证书就全自动了。

多租户实践​

把前面的内容串起来:一个集群要安全地给多个团队(租户)用,标准做法是以 namespace 为隔离边界,组合拳层层设防。假设给 team-a 团队开一个租户空间,整套清单长这样。

1. Namespace:隔离边界​

kubectl create namespace team-a

一切隔离的基础,也是后面所有策略的作用域。

2. RBAC:只给本 namespace 的管理权​

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: team-a-admin
namespace: team-a
subjects:
- kind: Group
name: team-a
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: admin
apiGroup: rbac.authorization.k8s.io

直接用内置的 admin、edit、view ClusterRole 搭配 RoleBinding,不用从头写规则。注意内置 admin 允许操作 Role 和 RoleBinding,意味着租户能在自己地盘里继续授权别人,介意的话自己裁剪一份。

3. ResourceQuota:防止一个团队吃光集群​

apiVersion: v1
kind: ResourceQuota
metadata:
name: team-a-quota
namespace: team-a
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
pods: "50"

配额用完后创建新 Pod 会被准入控制直接拒绝,报错信息里会写明哪一项超额。

4. LimitRange:给不设限额的容器补默认值​

光配 Quota 有个副作用:只要 Quota 管了 CPU 和内存,之后每个容器都必须显式写 requests/limits,否则创建被拒。用 LimitRange 给没写的容器兜底:

apiVersion: v1
kind: LimitRange
metadata:
name: team-a-limits
namespace: team-a
spec:
limits:
- type: Container
default: # 没写 limits 时的默认值
cpu: 500m
memory: 512Mi
defaultRequest: # 没写 requests 时的默认值
cpu: 100m
memory: 128Mi
max: # 单容器上限
cpu: "2"
memory: 4Gi

5. NetworkPolicy:租户间网络隔离​

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: team-a-isolation
namespace: team-a
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- podSelector: {} # 只允许同 namespace 内的 Pod 互访

配上以后其他 namespace 默认一概进不来,有跨团队协作需求再单独开口子。

6. Pod Security:统一安全基线​

kubectl label namespace team-a \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/warn=restricted

组合拳总览​

层对象解决的问题
隔离边界Namespace资源命名隔离与策略作用域
权限RBAC(RoleBinding + 内置 ClusterRole)租户只能管自己的地盘
资源ResourceQuota防止一个租户吃光集群总量
资源LimitRange防止单容器狮子大开口,补默认值
网络NetworkPolicy防止租户间横向访问
安全基线Pod Security Admission危险配置的 Pod 直接拒建
身份ServiceAccount + 最小权限Pod 访问 API 的身份收敛

这套配齐,一个「带护栏」的租户空间就成型了:团队在自己的 namespace 里有充分的自由,又碰不到别人、拖不垮集群。