第07篇:K8s 安全加固指南:RBAC、NetworkPolicy、OPA——Java SaaS 多租户安全隔离深度实践

前言: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 关键字,然后为对应目录添加 emptyDir Volume 挂载。常见需要可写的目录:/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 客户端集成 --- 系列最受期待的一篇!

📝 系列文章持续更新中,欢迎关注、收藏、点赞三连支持!

相关推荐
NE_STOP1 小时前
Docker--搭建私有镜像中心Harbor
java
摇滚侠2 小时前
IDEA 新建 JavaWeb 项目 Tomcat 和 Servlet
java·ide·intellij-idea
码客日记2 小时前
Spring Boot 全局跨域配置与前后端联调避坑
java·spring boot·后端
兰令水2 小时前
leecodecode【回溯子集】【2026.6.4打卡-java版本】
java·开发语言·深度优先
闪电悠米2 小时前
黑马点评-Redisson-02_reentrant_lock
java·spring boot·redis·分布式·缓存
nbsaas-boot2 小时前
ToC 系统中的请求幂等、安全签名与防重复提交架构设计
安全
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【67】ReactAgent SSE 流式输出
java·人工智能·spring
我登哥MVP3 小时前
Spring Boo从“会用”到“精通”:Spring Boot 入门
java·spring boot·后端·spring·maven·intellij-idea·mybatis
染翰3 小时前
Java 实现 Git 自动克隆工具,打包成 Windows 独立 EXE(免安装JDK)
java·git·后端