前言:K8s 安全不是"装完就忘"的开关
K8s 默认配置在便利性和安全性之间偏向了便利------默认 ServiceAccount 有较大权限、Pod 可以以 root 运行、任意 Pod 可以访问任意其他 Pod。这对开发环境无所谓,但对托管多个企业客户数据的 SaaS 生产环境,每一项默认值都可能是安全漏洞。
云原生安全遵循**零信任(Zero Trust)**原则:不因为"在集群内部"就默认信任,任何访问都需要显式授权。
本文从三个层面构建 Java SaaS 的安全防线:
bash
第一层:身份与权限 → RBAC(谁能做什么)
第二层:网络隔离 → NetworkPolicy(谁能访问谁)
第三层:准入控制 → OPA Gatekeeper(配置是否符合安全策略)
一、RBAC:最小权限原则
1.1 RBAC 的四个核心对象
K8s RBAC 的设计思路是"角色 + 绑定":先定义角色(能做什么),再把角色绑定给用户/ServiceAccount。
bash
┌──────────────────────────────────────────────────────┐
│ RBAC 对象关系图 │
│ │
│ Role / ClusterRole │
│ (定义权限:可以对哪些资源做什么操作) │
│ │ │
│ │ RoleBinding / ClusterRoleBinding │
│ │ (把角色授予谁) │
│ ▼ │
│ Subject(主体) │
│ ├── User(人) │
│ ├── Group(用户组) │
│ └── ServiceAccount(Pod 用的"身份证") │
└──────────────────────────────────────────────────────┘
| 对象 | 作用范围 | 典型用途 |
|---|---|---|
Role |
单个 Namespace | 给某个服务的 SA 读取 ConfigMap 的权限 |
ClusterRole |
整个集群 | 给运维人员只读查看所有 Pod 的权限 |
RoleBinding |
单个 Namespace | 把 Role 绑定到 SA |
ClusterRoleBinding |
整个集群 | 把 ClusterRole 绑定到用户 |
1.2 为 Java SaaS 配置最小权限 ServiceAccount
默认情况下,Pod 使用 default ServiceAccount,而 default SA 在很多集群中拥有过大的权限。正确做法是为每个服务创建独立的 SA,只授予必要权限:
yaml
# rbac-java-saas.yaml
# 1. 为 java-saas-api 创建专属 ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: java-saas-api-sa
namespace: production
annotations:
# 如果使用阿里云 RAM Role,在此绑定(避免在 Pod 中配置 AK/SK)
# eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT:role/java-saas-role
automountServiceAccountToken: false # 不自动挂载 SA Token(减少攻击面)
---
# 2. 定义角色:只允许读取本 namespace 的 ConfigMap 和 Secret
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: java-saas-api-role
namespace: production
rules:
# 允许读取 ConfigMap(加载应用配置)
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
resourceNames: ["java-saas-config", "java-saas-feature-flags"] # 只能访问指定名称的 ConfigMap
# 允许读取 Secret(加载数据库密码、API Key)
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
resourceNames: ["java-saas-secret"] # 只能访问指定 Secret
# 允许读取同 namespace 下的 Pod 信息(服务发现用)
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
---
# 3. 绑定角色到 ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: java-saas-api-rolebinding
namespace: production
subjects:
- kind: ServiceAccount
name: java-saas-api-sa
namespace: production
roleRef:
kind: Role
apiGroup: rbac.authorization.k8s.io
name: java-saas-api-role
---
# 4. 在 Deployment 中使用专属 SA
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-saas-api
namespace: production
spec:
template:
spec:
serviceAccountName: java-saas-api-sa # 使用专属 SA
automountServiceAccountToken: false # 双重保险
1.3 团队权限分层设计
多团队共用集群时,按职能分配不同级别的 RBAC 权限:
yaml
# 开发人员:只能在 staging namespace 读日志、查 Pod 状态
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: developer-role
namespace: staging
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "services", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
# 允许 port-forward(本地调试用)
- apiGroups: [""]
resources: ["pods/portforward"]
verbs: ["create"]
---
# 运维人员:可以操作 production,但不能删除核心资源
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ops-role
namespace: production
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch", "delete"] # 可以删 Pod(触发重启)
- apiGroups: [""]
resources: ["secrets", "configmaps"]
verbs: ["get", "list"] # 只读 Secret(不能修改)
⚠️ 踩坑记录 #1:给 CI/CD 机器人账号过大权限导致供应链攻击
曾见过一个团队给 GitHub Actions 的 kubeconfig 赋予了
cluster-admin权限,方便 CI 流水线部署。后来 GitHub Secrets 泄漏,攻击者用这个 kubeconfig 在集群里部署了挖矿程序。正确做法 :CI/CD 账号只授予特定 Namespace 的 Deployment 更新权限(
update/patch),使用resourceNames限制只能操作特定名称的 Deployment,无法创建新的恶意资源。
二、NetworkPolicy:Pod 级防火墙
NetworkPolicy 在第 03 篇网络章节已有基础介绍,本节重点讲多租户隔离场景。
2.1 多租户网络隔离
SaaS 多租户架构中,不同租户的 Pod 必须网络隔离------租户 A 的服务不能访问租户 B 的数据库:
yaml
# 方案:每个租户独立 namespace,namespace 间默认拒绝
# 每个租户 namespace 内部互通,但不同 namespace 之间不通
# 步骤 1:拒绝所有跨 namespace 的入站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-cross-namespace
namespace: tenant-a # 在租户 A 的 namespace 中
spec:
podSelector: {} # 匹配所有 Pod
policyTypes:
- Ingress
ingress:
# 只允许来自同一 namespace 的流量
- from:
- podSelector: {} # 同 namespace 内所有 Pod
---
# 步骤 2:允许 monitoring namespace 的 Prometheus 抓取指标
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-prometheus-scrape
namespace: tenant-a
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- port: 8080 # 只允许 Prometheus 访问指标端口
---
# 步骤 3:允许 ingress-nginx namespace 的入站(外部请求进来)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-controller
namespace: tenant-a
spec:
podSelector:
matchLabels:
tier: frontend # 只有前端 Pod 接受外部流量
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- port: 8080
2.2 数据库访问白名单
明确规定只有特定服务可以访问数据库,其他所有服务的连接都会被拒绝:
yaml
# 只允许 java-saas-api 访问 MySQL(第 03 篇已介绍,此处补充 egress 方向)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: java-saas-api-egress
namespace: production
spec:
podSelector:
matchLabels:
app: java-saas-api
policyTypes:
- Egress
egress:
# 允许访问 MySQL
- to:
- podSelector:
matchLabels:
app: mysql
ports:
- port: 3306
# 允许访问 Redis
- to:
- podSelector:
matchLabels:
app: redis
ports:
- port: 6379
# 允许访问 ai-platform namespace 的 LLM 服务
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ai-platform
ports:
- port: 8080
# 允许 DNS 查询(必须!否则域名解析失败)
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
# 允许访问外部 AI API(如 OpenAI、通义千问)
- to:
- ipBlock:
cidr: 0.0.0.0/0 # 允许访问任意外部 IP
except:
- 10.0.0.0/8 # 但不允许访问内网其他段(防止横向移动)
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- port: 443 # 只允许 HTTPS
三、Pod Security Standards:容器运行时安全
K8s 1.25 起内置了 Pod Security Standards(PSS),替代已废弃的 PodSecurityPolicy,分三个级别:
| 级别 | 限制强度 | 适用场景 |
|---|---|---|
privileged |
无限制 | 系统组件(不建议业务服务用) |
baseline |
防止已知权限提升 | 大多数业务服务 |
restricted |
最严格,接近最佳实践 | 安全敏感服务、多租户 |
yaml
# 为 production namespace 开启 restricted 级别(不符合则拒绝)
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
# enforce: 不符合则拒绝创建 Pod
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
# warn: 不符合则警告(不拒绝),用于迁移过渡期
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
restricted 级别要求 Pod 必须满足:
yaml
spec:
securityContext:
runAsNonRoot: true # 不以 root 运行
runAsUser: 1000 # 指定 UID(非 0)
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault # 使用默认 seccomp 配置(限制系统调用)
containers:
- name: spring-boot-app
securityContext:
allowPrivilegeEscalation: false # 禁止权限提升
readOnlyRootFilesystem: true # 根文件系统只读(防止容器内写文件)
capabilities:
drop: ["ALL"] # 丢弃所有 Linux capabilities
volumeMounts:
# 如果根文件系统只读,需要挂载可写目录
- name: tmp-dir
mountPath: /tmp
- name: app-logs
mountPath: /app/logs
volumes:
- name: tmp-dir
emptyDir: {}
- name: app-logs
emptyDir: {}
⚠️ 踩坑记录 #2:readOnlyRootFilesystem 导致 Java 应用报错
开启只读根文件系统后,Spring Boot 应用启动时可能因为无法写入临时文件而失败(比如 Tomcat 的 work 目录、JVM 的 hsperfdata 目录)。
排查方法:在报错信息中找
Read-only file system关键字,然后为对应目录添加emptyDirVolume 挂载。常见需要可写的目录:/tmp、/app/logs、/var/log、/home/<user>/.java。也可以通过 JVM 参数调整临时目录:
-Djava.io.tmpdir=/tmp(通常已指向 emptyDir 挂载点)。
四、OPA Gatekeeper:策略即代码
4.1 为什么需要 OPA?
RBAC 控制的是"谁能对 K8s API 做什么操作",但它无法约束"创建的资源内容是否符合规范"。例如:
- 有人创建了没有
resources.limits的 Pod(可能拖垮节点) - 有人用了不允许的镜像仓库(安全合规要求只用内部 Harbor)
- 有人创建了
privileged: true的特权容器 - 有人忘了给 Deployment 加
readinessProbe
OPA Gatekeeper 是 K8s 的准入控制器(Admission Controller),在资源创建/更新时触发策略检查,不满足策略则拒绝操作并返回清晰的错误信息。
4.2 安装 Gatekeeper
bash
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.14/deploy/gatekeeper.yaml
# 验证
kubectl get pods -n gatekeeper-system
4.3 编写安全策略
Gatekeeper 用 ConstraintTemplate(定义策略逻辑,用 Rego 语言)+ Constraint(应用策略到目标资源)两个 CRD 配合使用:
策略 1:强制所有容器设置 resources.limits
yaml
# constraint-template-require-limits.yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8srequireresourcelimits
spec:
crd:
spec:
names:
kind: K8sRequireResourceLimits
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequireresourcelimits
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.cpu
msg := sprintf("容器 '%v' 必须设置 CPU limits", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("容器 '%v' 必须设置 Memory limits", [container.name])
}
---
# 应用策略到 production namespace
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireResourceLimits
metadata:
name: require-resource-limits
spec:
enforcementAction: deny # deny: 拒绝 | warn: 只警告(迁移期用)
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet", "DaemonSet"]
namespaces: ["production", "staging"]
策略 2:强制只能使用 Harbor 内部镜像
yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
validation:
openAPIV3Schema:
properties:
repos:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
satisfied := [good | repo := input.parameters.repos[_]
good := startswith(container.image, repo)]
not any(satisfied)
msg := sprintf("容器镜像 '%v' 不在允许的镜像仓库列表中", [container.image])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: allowed-repos-production
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces: ["production"]
parameters:
repos:
- "harbor.yoursaas.com/" # 只允许内部 Harbor 镜像
- "registry.k8s.io/" # 允许官方 K8s 组件镜像
策略 3:强制 Deployment 必须有 readinessProbe
yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8srequireadinesssprobe
spec:
crd:
spec:
names:
kind: K8sRequireReadinessProbe
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequirereadinesssprobe
violation[{"msg": msg}] {
container := input.review.object.spec.template.spec.containers[_]
not container.readinessProbe
msg := sprintf("容器 '%v' 必须配置 readinessProbe,否则滚动更新期间可能流量打到未就绪的 Pod", [container.name])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireReadinessProbe
metadata:
name: require-readiness-probe
spec:
enforcementAction: warn # 先用 warn 模式,团队适应后改为 deny
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
namespaces: ["production", "staging"]
五、Secrets 加密存储(etcd 加密)
K8s 默认 Secret 在 etcd 中以 Base64 明文存储,任何能访问 etcd 的人都可以读取所有 Secret。开启 etcd 加密:
yaml
# /etc/kubernetes/encryption-config.yaml(API Server 配置)
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
- configmaps # 也可以加密 ConfigMap
providers:
- aescbc: # AES-CBC 加密
keys:
- name: key1
secret: <base64 编码的 32 字节随机密钥>
- identity: {} # identity 作为 fallback(读取未加密的旧数据)
启用后,新创建的 Secret 自动加密存储。对已有 Secret 重新加密:
bash
# 对所有现有 Secret 重新写入(触发加密)
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
六、常见问题 FAQ
Q1:RBAC 如何验证配置是否生效?
使用 kubectl auth can-i 命令验证:
bash
# 验证 java-saas-api-sa 是否能列出 ConfigMap
kubectl auth can-i list configmaps \
--namespace production \
--as system:serviceaccount:production:java-saas-api-sa
# 输出 yes/no
# 验证是否能删除 Secret(应该是 no)
kubectl auth can-i delete secrets \
--namespace production \
--as system:serviceaccount:production:java-saas-api-sa
Q2:OPA Gatekeeper 会影响资源创建性能吗?
Gatekeeper 是同步准入控制器,每次资源创建/更新都会调用。策略复杂时延迟约 10-50ms。建议:① 策略 Rego 逻辑保持简单;② 不要在 Gatekeeper 中做外部网络调用;③ 合理设置 Gatekeeper 的副本数(生产建议 3 副本)。
Q3:NetworkPolicy 的调试方法有哪些?
NetworkPolicy 不生效时很难直接看出原因,推荐工具:① kubectl exec 进 Pod 内用 curl 测试连通性;② 安装 netshoot debug 容器(集成了所有网络诊断工具);③ 使用 Network Policy Editor 可视化查看策略规则;④ 查看 CNI 插件的日志(如 Calico 的 calico-node 日志)。
Q4:多租户 SaaS 用 Namespace 隔离够吗?
Namespace 是软隔离,共享同一 K8s 控制面。对于安全要求极高的场景(如金融、医疗),建议使用:① 独立集群(每租户一个集群,成本最高但隔离最彻底);② vCluster(虚拟集群,在一个物理集群上创建多个虚拟 K8s 集群,隔离性强于 Namespace,成本低于独立集群)。
Q5:如何防止 K8s API Server 被暴力破解?
① 不把 API Server 直接暴露到公网;② 使用 VPN 或堡垒机访问;③ 开启审计日志(--audit-log-path);④ 配置 kube-apiserver 的请求限流(--max-requests-inflight);⑤ 使用 OIDC 接入公司 SSO,而非 static token 认证。
总结
| 安全层 | 工具 | 防御什么 |
|---|---|---|
| 身份权限 | RBAC | 越权操作、账号泄漏后的爆炸半径 |
| 网络隔离 | NetworkPolicy | 横向移动、租户间数据泄露 |
| 运行时安全 | Pod Security Standards | 容器逃逸、特权提升 |
| 准入控制 | OPA Gatekeeper | 不合规配置进入集群 |
| 数据加密 | etcd 加密 | etcd 被直接访问时的数据泄露 |
💬 一句话总结:K8s 安全是"深度防御"------没有任何单一措施是万能的,RBAC 限制权限、NetworkPolicy 限制网络、OPA 限制配置、PSS 限制运行时,四层叠加才能真正构建多租户 SaaS 的安全护城河。
上一篇 :K8s 可观测性三剑客:Prometheus + Grafana + Loki------Java SaaS 生产监控告警实战
下一篇 :K8s 部署 AI 大模型推理服务:GPU 调度 × vLLM × Java 客户端集成 --- 系列最受期待的一篇!
📝 系列文章持续更新中,欢迎关注、收藏、点赞三连支持!