一、Kubernetes 安全模型全景图

在深入具体组件之前,先建立对 K8s 安全体系的整体认知:

                    ┌──────────────────────────────────────┐
                    │         Kubernetes API Server        │
                    │  (etcd 唯一入口 - 所有操作都经过这里)  │
                    └─────────┬──────────┬────────────────┘
                              │          │
              认证 (Authentication)   授权 (Authorization)
              "你是谁?"                "你能做什么?"
              ┌───┐ ┌───┐ ┌───┐       ┌───┐ ┌────┐ ┌────┐
              │x509│ │SA  │ │OIDC│       │RBAC│ │ABAC│ │Webhook│
              └───┘ └───┘ └───┘       └───┘ └────┘ └────┘
                              │          │
                              └────┬─────┘
                                   │
                            准入控制 (Admission)
                            "允许这个请求吗?"
                            ┌─────────────────────┐
                            │ Kyverno / OPA / PSA │
                            └─────────────────────┘

关键认知:K8s 安全的唯一核心是 API Server。一切漏洞最终都是绕过了这堵墙。

二、ServiceAccount:被忽视的默认攻击者

2.1 什么是 ServiceAccount

ServiceAccount (SA) 是 K8s 中 Pod 访问 API Server 时使用的身份凭证。当你在 Pod 中跑 kubectl 时,用的就是 SA 的 token。

2.2 一个典型的危险场景

# app-pod.yaml - 看起来无害,但有大问题
apiVersion: v1
kind: Pod
metadata:
  name: my-app
  namespace: default  # ← 默认命名空间
spec:
  # 没指定 serviceAccountName,默认用 default SA
  containers:
  - name: app
    image: my-app:latest
    # 容器里挂载了 /var/run/secrets/kubernetes.io/serviceaccount/token
    # 应用可能被攻击者入侵,然后用这个 token 调 API

2.3 获取 SA token 并利用

# 假设攻击者拿到了 Pod shell
# 1. SA token 自动挂载到这里
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

# 2. CA 证书也自动挂载
CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

# 3. 直接调 API Server 探测权限
KUBE=https://kubernetes.default.svc

# 列出所有 Pod(如果 SA 有 list 权限)
curl --cacert $CA -H "Authorization: Bearer $TOKEN"   $KUBE/api/v1/namespaces/default/pods

# 检查 SA 到底有什么权限(K8s 内省机制)
# 注意:K8s 自带 authorization.k8s.io API 可以自我查询权限
curl --cacert $CA -H "Authorization: Bearer $TOKEN"   -H "Content-Type: application/json"   -d '{"apiVersion":"authorization.k8s.io/v1","kind":"SubjectAccessReview","spec":{"resourceAttributes":{"namespace":"default","resource":"pods","verb":"*"}}}'   $KUBE/apis/authorization.k8s.io/v1/subjectaccessreview | python3 -m json.tool

# 快速判断脚本
check_sa_perms() {
  local KUBE=https://kubernetes.default.svc
  local TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
  local CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
  local resources=("pods" "deployments" "secrets" "configmaps" "services" "nodes")
  local verbs=("get" "list" "watch" "create" "update" "patch" "delete")
  for res in "${resources[@]}"; do
    for verb in "${verbs[@]}"; do
      local resp=$(curl -s --cacert $CA -H "Authorization: Bearer $TOKEN"         -H "Content-Type: application/json"         -d "{"apiVersion":"authorization.k8s.io/v1","kind":"SubjectAccessReview","spec":{"resourceAttributes":{"resource":"$res","verb":"$verb"}}}"         $KUBE/apis/authorization.k8s.io/v1/subjectaccessreview)
      if echo "$resp" | grep -q '"allowed":true'; then
        echo "✅ SA 可以 $verb $res (cluster-wide)"
      fi
    done
  done
}
check_sa_perms

2.4 禁止自动挂载 SA token

# 正确配置: 如果 Pod 不需要访问 API
apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  automountServiceAccountToken: false  # ← 关键!
  containers:
  - name: app
    image: my-app:latest

# 或者在 ServiceAccount 级别禁用
apiVersion: v1
kind: ServiceAccount
metadata:
  name: no-token-sa
automountServiceAccountToken: false  # ← 这个 SA 启动的所有 Pod 都不挂 token

2.5 TTL Token:好消息

K8s 1.24+ 默认使用有过期时间的 token(默认 1 小时),旧版本是永不过期的 JWT。但请注意:

# 检查自己的 token 有效期
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# 看 exp 字段

三、RBAC:90% 的安全事件都源于过宽权限

3.1 RBAC 核心概念

Role / ClusterRole          RoleBinding / ClusterRoleBinding
"能做什么"                  "谁来做" + "作用范围"
  │                           │
  ├── apiGroups: [""]         ├── subjects:
  ├── resources: ["pods"]     │   - kind: ServiceAccount
  ├── verbs: ["get","list"]   │     name: my-sa
  └── resourceNames: []       │     namespace: default
                              └── roleRef:
                                    kind: Role
                                    name: my-role

3.2 常见的危险 RBAC 模式

反模式 1:Cluster-admin

# ❌ 绝对不要这么写(相当于 give root)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: give-cluster-admin-to-everyone
subjects:
- kind: ServiceAccount
  name: default
  namespace: default
roleRef:
  kind: ClusterRole
  name: cluster-admin  # ← 管理员权限
  apiGroup: rbac.authorization.k8s.io

反模式 2:通配符 verbs + 通配符 resources

# ❌ 覆盖所有资源的所有操作
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: too-broad
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]  # ← get, list, create, delete, ... 全放开
  nonResourceURLs: ["*"]  # ← 连 metrics 接口也放开

反模式 3:允许对 secrets 进行 list/watch

# ❌ 允许 list secrets = 能偷看所有密钥
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: secret-reader
  namespace: production
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "list", "watch"]  # ← list/watch 能批量枚举

3.3 最小权限:正确的 RBAC 写法

场景:一个 CI 系统需要在特定 namespace 创建 Job 来跑测试。

# Step 1: 专用 ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ci-runner
  namespace: ci
automountServiceAccountToken: false
---
# Step 2: 最小权限 Role
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: ci-runner-role
  namespace: ci
rules:
# 可以创建/删除 Job
- apiGroups: ["batch"]
  resources: ["jobs"]
  verbs: ["get", "list", "create", "delete"]
# 可以获取 Job 的 Pod 日志
- apiGroups: [""]
  resources: ["pods", "pods/log"]
  verbs: ["get", "list"]
# 仅此而已!不能动 secrets、不能动其他 namespace、不能动 nodes
---
# Step 3: 绑定
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ci-runner-binding
  namespace: ci
subjects:
- kind: ServiceAccount
  name: ci-runner
  namespace: ci
roleRef:
  kind: Role
  name: ci-runner-role
  apiGroup: rbac.authorization.k8s.io

3.4 kubectl 命令行快速检查过宽 RBAC

# 列出所有 ClusterRoleBinding
kubectl get clusterrolebinding

# 找出所有使用 cluster-admin 的绑定
kubectl get clusterrolebinding -o json |   jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name'

# 列出所有拥有 secret 读取权限的主体
kubectl get clusterrole,role -A -o json |   jq -r '.items[] as $role | 
    select(.rules[]?.resources[]? == "secrets") | 
    "($role.metadata.namespace // "cluster")/($role.metadata.name): ($role.rules[].verbs | join(","))"'

# kubectl-can 插件: 可视化谁能对什么做什么
kubectl krew install access-matrix
kubectl access-matrix -f -o md

3.5 kubescape 自动扫描 RBAC 风险

# 安装 kubescape
curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash

# 扫描集群,重点找 RBAC 风险
kubescape scan --framework rbac

# 输出示例
# ┌─────────────────────────────────────────────────────────┐
# │           RISKIEST RBAC RULES                           │
# ├──────────────┬─────────┬──────────────────────────────┤
# │ Resource     │ Severity│ Who Can Access               │
# ├──────────────┼─────────┼──────────────────────────────┤
# │ secrets      │ CRITICAL│ default/dan, default/tom     │
# │ nodes        │ HIGH    │ default/root                 │
# │ pods/exec    │ HIGH    │ kube-system/ingress-sa       │
# └──────────────┴─────────┴──────────────────────────────┘

# 详细查看某个风险
kubescape scan --control C-0016  # 检查过多权限

四、Secret:K8s 中最常被泄露的资源

4.1 K8s Secret 的真相

很多人以为 Secret 是"加密存储"的。默认情况下不是

# 创建一个 Secret
apiVersion: v1
kind: Secret
metadata:
  name: db-password
data:
  password: cGFzc3dvcmQxMjM=  # base64("password123")
# 查看 etcd 里的实际存储
# 除非你配置了 EncryptionConfiguration,否则就是明文
ETCDCTL_API=3 etcdctl get /registry/secrets/default/db-password
# value 字段就是 base64 编码后的原文
echo cGFzc3dvcmQxMjM= | base64 -d
# password123

4.2 开启 etcd 静态加密

# apiserver encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets
  - configmaps
  - events
  - extensions.apiregistration.k8s.io
providers:
- aescbc:
    keys:
    - name: key1
      secret: VmVyeVBsb3R5Q2hhbmdlTG9va2hhb3Bpbmch  # base64 32字节密钥
- identity: {}  # 兜底,保证可以解密旧数据
# apiserver 启动参数追加
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml

# 更新后需要重新加密现有数据
kubectl get secrets -A -o yaml | kubectl apply -f -
kubectl get configmaps -A -o yaml | kubectl apply -f -

4.3 Secret 的 N 种泄露方式

泄露方式 1:Pod 内直接读文件

# 攻击者控制了 Pod shell
# 读取挂载的 Secret
ls /etc/secrets  # 假设 Secret 以 volume 形式挂载
cat /etc/secrets/db-password  # 直接拿到明文

# 或者用 SA token 调 API
curl --cacert $CA -H "Authorization: Bearer $TOKEN"   $KUBE/api/v1/namespaces/default/secrets/db-password |   jq -r '.data.password' | base64 -d

泄露方式 2:kubectl 日志泄露

# 应用不小心把 Secret 打到日志里
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  config.yaml: |
    # 糟糕的配置模板
    db:
      password: ${DB_PASSWORD}  # 如果应用把这个渲染后的配置打到日志...

泄露方式 3:Git 提交(历史泄漏)

# 有人不小心把 Secret YAML 提交到 Git
git log --all -p | grep -i "password|secret|token"

# git-secrets 可以扫描
git clone https://github.com/awslabs/git-secrets
./git-secrets --install
./git-secrets --scan

泄露方式 4:Etcd 未授权访问

# 如果 etcd 暴露且没装 mTLS
ETCDCTL_API=3 etcdctl --endpoints=http://etcd:2379 get "" --prefix --keys-only
# 直接拿到所有 Secret

# 正确的 etcd 启动参数
etcd   --cert-file=/etc/kubernetes/pki/etcd/server.crt   --key-file=/etc/kubernetes/pki/etcd/server.key   --client-cert-auth=true   --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt   --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt   --peer-key-file=/etc/kubernetes/pki/etcd/peer.key   --peer-client-cert-auth=true   --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt

泄露方式 5:Image Pull Secret 泄露

# Pod 可以读自己的 imagePullSecrets
kubectl get pod <target> -o jsonpath='{.spec.imagePullSecrets}' | jq
# 拿到 Docker Registry 的认证 token

4.4 Secret 安全加固清单

措施 命令
启用 etcd 加密 EncryptionConfiguration + apiserver --encryption-provider-config
使用外部 Secret Manager AWS Secrets Manager / GCP Secret Manager / Vault / External Secrets Operator
轮转 Secret Sealed Secrets 支持自动轮转
禁止 SA list secrets RBAC 中不要给 secrets 的 list/watch
Falco 检测 监控 Pod 内读取 /var/run/secrets/
镜像中不要打包 Secret 使用 runtime 注入(envFrom/secretKeyRef)

4.5 Sealed Secrets:让 Git 可以安全存 Secret

# 安装 sealed-secrets controller
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.26.2/controller.yaml

# 安装 kubeseal CLI
wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.26.2/kubeseal-linux-amd64 -O /usr/local/bin/kubeseal
chmod +x /usr/local/bin/kubeseal

# 用集群的公钥加密 Secret(可以安全提交到 Git)
kubectl create secret generic db-password --from-literal=password=mysecret123 --dry-run=client -o yaml |   kubeseal --format yaml > db-password-sealed.yaml

# 提交到 Git,任何人都无法解密
git add db-password-sealed.yaml
git commit -m "chore: add sealed db password"

# 部署到集群后 controller 自动解密
kubectl apply -f db-password-sealed.yaml
kubectl get secret db-password -o jsonpath='{.data.password}' | base64 -d
# mysecret123

五、PSA (Pod Security Admission):内置准入控制

K8s 1.25+ 将 PodSecurityPolicy 替换为 Pod Security Admission,基于标签生效。

5.1 三个级别

级别 说明 核心限制
Privileged 无限制 允许一切
Baseline 限制已知逃逸路径 禁止特权容器、禁止 hostNetwork/hostPID/hostIPC、禁止挂载宿主路径
Restricted 严格遵循 Pod Security Standard 额外禁止 sysctls、强制 runAsNonRoot、强制 seccomp

5.2 按 Namespace 启用

apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted
    # warn 会在 kubectl apply 时给出警告但不阻止
    # enforce 会直接拒绝不符合的 Pod

5.3 快速检查集群哪些 Pod 会违反 Restricted

# kubectl 插件: pod-security
kubectl krew install pod-security
kubectl pod-security audit --level restricted -A

# 或者用 kubesec
curl -sL https://github.com/controlplaneio/kubesec/releases/download/v2.14.0/kubesec_linux_amd64.tar.gz | tar xz -C /usr/local/bin
kubectl get pods -A -o yaml | kubesec scan -

六、实战:完整加固一个 Namespace 下的 RBAC

6.1 需求

"在 production namespace 下,给部署工程师只读权限,给 CI 工程师仅限 deployment 的 create/update,禁止任何人访问 secrets"

6.2 实现

# 1. 部署工程师角色(只读)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployer-readonly
  namespace: production
rules:
- apiGroups: ["", "apps", "batch", "networking.k8s.io"]
  resources:
    - pods
    - pods/log
    - pods/exec
    - deployments
    - replicasets
    - services
    - ingresses
    - jobs
    - cronjobs
    - configmaps  # 只读 configmap OK
  verbs: ["get", "list", "watch"]
# 故意没有 secrets!
---
# 2. CI 角色(仅限 deployment)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: ci-deployer
  namespace: production
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "create", "update", "patch"]
- apiGroups: [""]
  resources: ["pods", "pods/log"]
  verbs: ["get", "list"]
---
# 3. 绑定
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: bind-deployer-readonly
  namespace: production
subjects:
- kind: User
  name: alice@corp.com
- kind: Group
  name: sre-team
roleRef:
  kind: Role
  name: deployer-readonly
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: bind-ci-deployer
  namespace: production
subjects:
- kind: ServiceAccount
  name: ci-bot
  namespace: ci
roleRef:
  kind: Role
  name: ci-deployer
  apiGroup: rbac.authorization.k8s.io

七、总结:K8s 安全自检 Checklist

按优先级排序:

优先级 检查项 命令
P0 没有 SA 使用 cluster-admin kubectl get clusterrolebinding -o yaml
P0 Secret 没有 list/watch 权限 kubectl get clusterrole,role -A -o yaml
P0 生产 Namespace 启用 PSA Restricted kubectl get ns --show-labels
P0 etcd 启用 mTLS + 加密 检查 apiserver 启动参数
P1 所有 SA 禁用自动挂载 token kubectl get sa -A -o yaml
P1 没有特权 Pod / hostPath kubectl get pods -A -o yaml
P1 使用 External Secrets Operator kubectl get externalsecrets -A
P2 Falco 运行时检测 kubectl get pods -n falco
P2 定期 kubescape 扫描 kubescape scan cluster

K8s 安全的第一原则永远是 最小权限。当你在纠结某个 RBAC 是否需要时,答案一定是"不需要"。给权限容易收权限难,越晚收紧越痛苦。