一、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 是否需要时,答案一定是"不需要"。给权限容易收权限难,越晚收紧越痛苦。