Day 5: Volume Expansion + 安全主线
目标 (双线):
- 副线 (Day 4 加深): PVC 在线扩容 + Longhorn 加二盘
- 主线 (安全): RBAC + Pod Security Admission + Secret at-rest 加密 + Kyverno 策略代码化 耗时: 5-7 小时 风险: PVC 扩容失败可能丢数据 / RBAC 错配可能锁死自己 / EncryptionConfiguration 重启 apiserver / Kyverno 装错策略可能 block 所有部署
0. TL;DR (6 节)
- A — Volume Expansion: pg-0 PVC 5G→10G,Longhorn 加 vdb-extra 二盘,看 disk 容量翻倍
- B — 安全速通: RBAC / PSA / Secret 加密 / Policy as Code 理论 mini-book
- C — RBAC 实战: SA + Role + RoleBinding,生成只读 kubeconfig
- D — PSA: ns label enforce baseline/restricted,跑 privileged Pod 看拦
- E — Secret at-rest: EncryptionConfiguration aescbc,etcdctl 直查 Secret 已加密
- F — Kyverno: 装 Kyverno,3 条策略 (require labels / no latest / require resources)
1. 学习目标 + 闭环输出
能秒答:
- "PVC 扩容的 3 个条件? Longhorn 怎么实现 online resize?"
- "RBAC 4 个 noun (Subject / Resource / Verb / Scope) 分别什么意思?"
- "ClusterRole vs Role 区别?为什么 system:masters 是危险的?"
- "PSA 三级别 privileged / baseline / restricted 分别拦什么?跟 PSP 区别?"
- "Secret 默认不加密(只 base64),怎么真加密? KMS provider 跟 aescbc/aesgcm 区别?"
- "Kyverno vs OPA Gatekeeper 选哪个?"
能动手做:
- 在线扩 PVC + 加新 disk
- 创建只读 SA 给团队成员
- 用 PSA 拦截 privileged 容器
- 配 EncryptionConfiguration 真加密 etcd 里的 Secret
- 用 Kyverno 强制部署规范
简历可写:
- "搭建 K8s 安全基线: RBAC 最小权限 + PSA restricted enforce + Secret 静态加密 (aescbc) + Kyverno 策略 (no-latest-tag / require-labels / require-resources)"
10. 实时执行日志(6 维度)
Day 5.A — Volume Expansion (PVC 扩容 + Longhorn 加二盘)
A1. 原理速通 — PVC online expansion 的 3 个必要条件
用户 patch PVC.spec.resources.requests.storage += N
↓
external-resizer sidecar (CSI driver 内) 调 CSI ControllerExpandVolume
↓
存储后端 (Longhorn) 把 block device 扩大 → 新 size
↓
CSI NodeExpandVolume 通知 kubelet
↓
kubelet 在挂载点跑 resize2fs / xfs_growfs (filesystem online resize)
↓
df -h 看到新容量 (PVC.status.capacity 同步)
3 个必要条件,缺一不可:
- SC 设了
allowVolumeExpansion: true(Longhorn 默认开 ✅) - CSI driver 实现了 ControllerExpandVolume + NodeExpandVolume(Longhorn ✅)
- Pod 在挂载该卷(online) 或 Pod 没挂载(offline)— Longhorn 1.5+ 支持 online,但部分情况要重启 Pod 让 filesystem resize 真正生效
A2. 实战: pg-0 PVC 5Gi → 10Gi
What:
# 当前
kubectl exec pg-0 -- df -h /var/lib/postgresql/data
# 4.8G 46M 4.8G 1% ...
kubectl get pvc pgdata-pg-0
# 5Gi
# patch
kubectl patch pvc pgdata-pg-0 -n storage-demo --type=merge \
-p '{"spec":{"resources":{"requests":{"storage":"10Gi"}}}}'
Actual 立即看:
spec.resources.requests.storage: 10Gi ← 改了
status.capacity: 5Gi ← 还没改
Longhorn Volume.spec.size: 10737418240 ← Longhorn 已经接到 expand 请求
PVC condition: FileSystemResizePending
message: "Waiting for user to (re-)start a pod to finish file system resize"
⚠️ Longhorn 的 block 层扩了,但filesystem resize 需要 Pod 重启(挂载时调用 resize2fs)
Fix:
kubectl delete pod pg-0 -n storage-demo
# StatefulSet 重建,kubelet mount 时 resize2fs ext4
kubectl wait --for=condition=ready pod -n storage-demo pg-0 --timeout=120s
kubectl exec pg-0 -- df -h /var/lib/postgresql/data
Actual:
/dev/longhorn/pvc-... 9.8G 128M 9.7G 1% /var/lib/postgresql/data
✅ 10G 生效
验数据无损:
kubectl exec pg-0 -- psql -U postgres -d lab -c 'SELECT count(*) FROM bootcamp;'
# total: 7
✅ 7 行原数据 + 之前的 (Day 4 的 6 行 + 这次 PVC expand 之前插了 1 行)
Lesson (面试问 "PVC online expand 流程"):
- 不是 in-place,是 controller(扩 block)→ node(resize fs)分两段
- ext4 / xfs 支持 online resize, 实际不用重启 Pod;但 Longhorn CSI 当前实现要重启 Pod 才触发 resize2fs(等于 mount-time 模式)
- 缩容不支持(几乎所有 CSI 都不支持,数据丢失风险大)
A3. Longhorn 加二盘 — 节点存储扩容
Why: 默认每节点只用 /var/lib/longhorn (在 vda 系统盘上,~25G 可用)。我们的 vdb 还有 90+G 给 containerd 用,复用 vdb 路径给 Longhorn 加二盘,让节点容量翻倍。
Path 复用思路:
/var/lib/containerd/是 vdb1 (ext4, 100G) 的 mountpoint/var/lib/containerd/longhorn-extra/是 vdb 上的子目录(Day 4.B 已 mkdir)- 把这个目录作为 Longhorn 的第二个 disk 加进去 — 跟 containerd 共享 vdb 文件系统,但目录不同
Why 这样安全:
- containerd 数据在
/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/... - Longhorn extra 数据在
/var/lib/containerd/longhorn-extra/... - 两者目录分离,不冲突, 共享 fs 配额
What (用 jq/python 拼 spec.disks,不破坏原 disk):
import json
node = json.load(sys.stdin)
disks = node["spec"]["disks"]
disks["extra-disk-vdb"] = {
"path": "/var/lib/containerd/longhorn-extra",
"allowScheduling": True,
"storageReserved": 1073741824, # 预留 1G
"diskDriver": "",
"tags": ["extra"] # tag 让 SC 可定向调度
}
print(json.dumps(node))
然后 kubectl replace -f - 写回。
A4. ⚠️ 真坑 #1 — Longhorn admission webhook 超时
What:
Error: failed calling webhook "mutator.longhorn.io":
Post "https://longhorn-admission-webhook.longhorn-system.svc:9502/v1/webhook/mutation?timeout=10s":
context deadline exceeded
Why:
- longhorn-admission-webhook Service 有 5 个 Endpoint(每节点一个 manager Pod 提供)
- 但 webhook timeout 默认 10s
- 5 个节点同时 patch → 5 个并发 webhook 调用 → 偶发 timeout
Fix:
- 简单重试(我们重试 1 次就成功)
- 或者 throttle: 节点串行 patch
- 生产建议: 调大 webhook timeout (
webhooks[*].timeoutSeconds: 30) 或加 webhook Pod 副本
Lesson:
- Admission webhook 是 K8s 同步阻塞 调用 — webhook 慢 = apiserver 慢
- 生产 webhook 应当: 轻量 + 短超时 + 多副本 + failurePolicy=Fail 或 Ignore 选好
A5. 集群容量验证
Before (Day 4.B):
集群存储 max: 125 GB
After (5 节点加二盘):
集群存储 max: 634.2 GB
集群存储 avail: 585.4 GB
✅ 5 倍扩容 (125G → 634G), 3 副本下可用从 ~42G 提升到 ~195G
Outcome:
- 老 Volume 不会自动均衡到新 disk(创建时定死, Working Method 已学)
- 新 Volume 会在两块 disk 间调度(根据空间 + anti-affinity)
- 如果想强制让某些 PVC 进新 disk: SC 加
parameters.diskSelector: "extra"(tag 匹配)
Lesson (面试可讲):
- 节点存储扩容有两条路: (1) 加新 disk (CSI 节点视角) (2) 现有 disk 扩容 (CSI 控制视角)
- Longhorn 把节点抽象成 "node.longhorn.io" CR, 每节点可挂多个 disk(异构存储友好)
- tag 机制实现 存储分类: ssd / hdd / nvme 三种 tag,SC 按 tag 调度
Day 5.B — 安全速通 mini-book
B0. 现状 survey
| 维度 | 现状 |
|---|---|
| Audit log | ✅ 已配 (Day 2.) /var/log/kubernetes/audit.log |
| Encryption at-rest | ❌ 没配,Secret 现在只 base64 |
| Pod Security Admission | ❌ 所有 ns 都没 enforce/warn/audit label |
| 现有 webhook | 只 Longhorn mutator + validator (业务级 webhook 都没有) |
| ClusterRole 数量 | 74 个 (system: + view/edit/admin/cluster-admin) |
| 现有 Secret | 多是 cert(longhorn-webhook-tls / hubble-server-certs)+ helm release |
完美的"白板",Day 5 全部 demo 可演出来效果
B1. K8s 安全 5 大支柱
┌────────────────────────────────────────────────────┐
│ ① AuthN — 谁? (cert / OIDC / serviceaccount)│
│ ② AuthZ — 能做啥? (RBAC / ABAC) │
│ ③ Admission Control (validating / mutating) │
│ └─ PSA / Kyverno / Gatekeeper │
│ ④ Encryption │
│ ├─ in-transit (TLS apiserver / Pod-to-Pod) │
│ └─ at-rest (etcd 加密) │
│ ⑤ Audit (谁干了啥的日志) │
└────────────────────────────────────────────────────┘
Day 5 重点打: ② RBAC + ③ Admission(PSA + Kyverno) + ④ at-rest 加密
B2. RBAC — 4 元组的精确模型
谁 (Subject)
└ User / Group / ServiceAccount
↓ 通过 (Role)Binding 绑定
↓
做啥 (Verb)
└ get / list / watch / create / update / patch / delete / deletecollection
↓ 作用于
↓
什么资源 (Resource)
└ pods / deployments / secrets / configmaps / ...
└ pods/log / pods/exec / pods/portforward (subresource)
↓ 在什么范围
↓
范围 (Scope)
├ Namespaced: Role + RoleBinding (作用于一个 ns)
└ Cluster: ClusterRole + ClusterRoleBinding (整个集群)
典型组合:
| 角色 | 设计 |
|---|---|
| dev (单 ns) | Role: get/list/watch/create/update/patch/delete on pods/deployments/services/configmaps + RoleBinding |
| viewer (全集群只读) | 用内置 view ClusterRole + ClusterRoleBinding |
| CI/CD 部署机 | ServiceAccount + Role (在目标 ns)+ Token 给 CI pipeline |
| 集群管理员 | 内置 cluster-admin ClusterRole + ClusterRoleBinding (=root,谨慎!) |
4 个内置高频 ClusterRole (面试要记):
cluster-admin: 集群 root,慎用!admin: 在 ns 内全权限,但不能管 Roles 本身edit: 改资源,不能改 Rolesview: 只读,不能看 Secret(默认)
system: 角色* (kube-system 内部用):
system:kube-controller-manager/system:kube-scheduler/system:node等
最危险的内置: system:masters Group — 绑了它任何认证用户都是 cluster-admin (kubeadm 默认 admin.conf 就用这个)
B3. Pod Security Admission (PSA) — PSP 的继承者
K8s 1.25 删了 PSP (PodSecurityPolicy),用 PSA 替代。
核心: 通过 namespace label 配 3 个级别:
| Level | 拦截目标 | 典型违规 |
|---|---|---|
privileged | 啥都不拦 | (无) |
baseline | 拦明显危险 | 不允许 host*: hostNetwork, hostPID, hostIPC, hostPath; 不允许 capabilities 提权;不允许 privileged: true |
restricted | 严格 | 必须 runAsNonRoot; readOnlyRootFilesystem; seccompProfile 必须设;allowPrivilegeEscalation: false |
3 个 mode (跟 level 组合):
enforce: 违规直接拒(返回 admission denied)warn: 创建 OK,kubectl 警告audit: 创建 OK,审计日志记录
Label 写法:
metadata:
labels:
pod-security.kubernetes.io/enforce: restricted # 强 restricted
pod-security.kubernetes.io/enforce-version: v1.30
pod-security.kubernetes.io/warn: restricted # 同时 warn
pod-security.kubernetes.io/audit: restricted # 同时 audit
为什么 PSA 比 PSP 好:
- PSP 是集群级 + 绑定 ServiceAccount(配置复杂)
- PSA 是 namespace label 三两字搞定
- PSP 的 mutating 行为已被弃用(默认值注入)— PSA 纯 validation
B4. Secret at-rest 加密 — 默认没加密的事实
问题: kubectl get secret 看到的是 base64 解码,Secret 在 etcd 里是 base64 编码的明文 (其实就是明文,base64 不是加密)
# 验证: etcdctl get secret 看到的就是值
ETCDCTL_API=3 etcdctl get /registry/secrets/default/foo --print-value-only
# 输出: 完整 Secret manifest, data 字段 base64 编码就是 password 本身
Why 危险: 任何能访问 etcd backup / 偷到 etcd 节点的人都能拿到所有 Secret
Fix: EncryptionConfiguration:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets # 只加 secret
- configmaps # 选项, 配置敏感的 ConfigMap 也加
providers:
- aescbc: # 经典 AES-CBC + HMAC, 主流选择
keys:
- name: key1
secret: <base64 32-byte 密钥>
- identity: {} # 兜底,允许读旧的未加密数据
3 个 provider 对比:
| Provider | 性能 | 安全 | 推荐 |
|---|---|---|---|
identity | 最快(不加密) | 无 | 默认,别用 |
aescbc | 中 | 高(AES-256-CBC + HMAC-SHA-256) | ✅ 主流 |
aesgcm | 快 | 高(AES-256-GCM) | ✅ 性能优先 |
secretbox | 中 | 高(NaCl XSalsa20-Poly1305) | Niche |
kms (v2) | 慢 (网络) | 最高 (KMS 隔离 key) | 公有云 |
Apply 流程:
- 写
/etc/kubernetes/encryption-config.yaml(包含密钥,别提交 Git!) - apiserver 启动加
--encryption-provider-config=... - 重启 apiserver
- 重写所有 Secret 让密钥生效:
kubectl get secrets -A -o json | kubectl replace -f - - etcdctl 直查,验证密文
B5. Policy as Code — Kyverno vs OPA Gatekeeper
K8s 内置 admission control 能做基本 (Role / PSA)。复杂业务规则要用 policy engine:
| Kyverno | OPA Gatekeeper | |
|---|---|---|
| 语法 | YAML(K8s 原生风格) | Rego(自有 DSL,陡峭) |
| 生成 Resource | 支持 (generate policy) | 不支持 |
| Mutate | 支持 (mutate policy) | 不支持 (只能 validate) |
| Image Verification | 支持 (verifyImages) | 通过 Constraint 自己写 |
| 学习曲线 | 1 天 | 1 周 |
| 生态 | CNCF Incubating | CNCF Graduated |
| 适合 | 中小团队, 想快速落地 | 大型企业, 已有 OPA 团队 |
今天选 Kyverno — YAML 友好,demo 快
3 条入门策略:
- require-labels: 所有 Deployment 必须有
app.kubernetes.io/namelabel - disallow-latest-tag: 拦截
image: foo:latest(线上事故温床) - require-resources: 所有容器必须有 resources.requests + limits(防止饿死节点)
Day 5.C — RBAC 实战 (最小权限只读 SA)
C1. 设计 — 只读 SA demo-viewer
场景: 团队新成员入职,只允许看 storage-demo 的 Pod / ConfigMap / 工作负载,不允许看 Secret(密码),不允许跨 ns,不允许写
4 个 manifest (1 yaml 文件,放 /root/security-demo/01-viewer-rbac.yaml):
- ServiceAccount
demo-viewer - Secret type=
kubernetes.io/service-account-token(1.24+ SA 不再自动给 Secret token,要手动建) - Role
pod-cm-reader(get/list/watch on pods/pods/log/configmaps/deployments/statefulsets) - RoleBinding
demo-viewer-can-read绑 SA 和 Role
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: {name: pod-cm-reader, namespace: storage-demo}
rules:
- apiGroups: ['']
resources: ['pods', 'pods/log', 'configmaps']
verbs: ['get', 'list', 'watch']
- apiGroups: ['apps']
resources: ['statefulsets', 'deployments']
verbs: ['get', 'list', 'watch']
# 故意不给 secrets / 不给 create/update/delete
Why 不直接用内置 view ClusterRole:
- 内置
view是集群级(虽然通过 RoleBinding 可以限到 ns) - 内置
view不包含 Secret(很好) - 但内置
view包含很多其他资源(networkpolicies, ingresses 等) - 自己写 Role 更精确控制,体现最小权限原则
C2. 生成 demo-viewer 的 kubeconfig
TOKEN=$(kubectl get secret demo-viewer-token -n storage-demo -o jsonpath='{.data.token}' | base64 -d)
CA=$(kubectl get secret demo-viewer-token -n storage-demo -o jsonpath='{.data.ca\.crt}')
APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
cat > viewer.kubeconfig <<EOF
apiVersion: v1
kind: Config
clusters:
- {name: bootcamp, cluster: {server: $APISERVER, certificate-authority-data: $CA}}
contexts:
- {name: viewer@bootcamp, context: {cluster: bootcamp, namespace: storage-demo, user: demo-viewer}}
current-context: viewer@bootcamp
users:
- {name: demo-viewer, user: {token: $TOKEN}}
EOF
把这个文件发给新员工 → 他们 export KUBECONFIG=viewer.kubeconfig 立即生效
C3. 7 场景测试
| # | 命令 | 预期 | 实际 |
|---|---|---|---|
| 1 | get pod (storage-demo) | 允许 | ✅ pg-0, pg-verify-restored |
| 2 | get configmap | 允许 | ✅ kube-root-ca.crt |
| 3 | get secret | 拒 | ✅ Forbidden: cannot list "secrets" |
| 4 | get pod -n default | 拒(跨 ns) | ✅ Forbidden |
| 5 | get nodes | 拒(cluster scope) | ✅ Forbidden ... "at the cluster scope" |
| 6 | run nginx-pod | 拒(写操作) | ✅ Forbidden: cannot create "pods" |
| 7 | auth can-i ... (SAR) | 完整应答 | ✅ no / yes / no / no |
Forbidden 消息的解读:
Error from server (Forbidden): pods is forbidden:
User "system:serviceaccount:storage-demo:demo-viewer"
cannot create resource "pods" in API group "" in the namespace "storage-demo"
- User 形如
system:serviceaccount:<ns>:<sa-name>— K8s 内部对 SA 的 identity 表示 - 提示了 "resource" + "API group" + "namespace" 三要素 — 这是 RBAC 决策的 3 个维度,直接告诉你少了哪个 Role 项
C4. kubectl auth can-i — 调试 RBAC 的金锤子
kubectl --kubeconfig=viewer.kubeconfig auth can-i create deployments
# no
kubectl --kubeconfig=viewer.kubeconfig auth can-i get pods
# yes
kubectl --kubeconfig=viewer.kubeconfig auth can-i get secrets
# no
Why 这个命令神:
- 直接走 K8s
SubjectAccessReviewAPI,不真发请求,问"我能不能" - 不需要
-v=8看 audit log,清晰干脆 - CI/CD pipeline 可以用
auth can-i做前置检查,失败 fast-fail
生产 RBAC 调试套路:
kubectl auth can-i list pods -n X看能不能kubectl auth can-i list pods -n X --as=system:serviceaccount:X:Y模拟特定 SAkubectl get rolebinding,clusterrolebinding -A -o yaml | grep -A 5 SA-name看 SA 绑了哪些 Role- 还不通 → 看 audit log 找
authorization.k8s.io/decision: forbid
C5. RBAC 最佳实践
- 最小权限: 默认拒一切, 一个一个 verb 加
- 聚合 ClusterRole (
aggregationRule): 复用现有 view/edit/admin 的 rules - ServiceAccount 一 Pod 一个: 别让多 Pod 共享 SA
- Token 短期化: 1.24+ 强制走 BoundServiceAccountTokens (Pod 内挂的 token 是 1h TTL 自动 refresh,SA Secret token 是长期)
- 审查
system:masters: 任何 RoleBinding 引用它 = 给 cluster-admin,必须审计 - Group 模式: 用 OIDC 后,RoleBinding 绑 Group 而非 User
Day 5.D — Pod Security Admission 实战 (restricted)
D1. 设计 — 3 场景 vs restricted enforce
ns psa-test 加上 3 个 label:
metadata:
labels:
pod-security.kubernetes.io/enforce: restricted # 强拒
pod-security.kubernetes.io/enforce-version: v1.30 # pin 版本,避免规则升级炸
pod-security.kubernetes.io/warn: restricted # 创建时 client 看 warning
pod-security.kubernetes.io/audit: restricted # 同时写 audit log
D2. Scenario 1: privileged Pod — 期望直接拒
What:
apiVersion: v1
kind: Pod
spec:
containers:
- name: c
image: nginx:1.27-alpine
securityContext: {privileged: true}
Actual:
Error from server (Forbidden): pods "bad-priv" is forbidden:
violates PodSecurity "restricted:v1.30":
- privileged (container "c" must not set securityContext.privileged=true),
- allowPrivilegeEscalation != false (must set securityContext.allowPrivilegeEscalation=false),
- unrestricted capabilities (must set securityContext.capabilities.drop=["ALL"]),
- runAsNonRoot != true (pod or container "c" must set securityContext.runAsNonRoot=true),
- seccompProfile (must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
✅ 5 项违规一次性列出来 — 极其友好的错误消息
D3. Scenario 2: 默认 nginx (root user) — 也拒
What:
spec:
containers:
- {name: c, image: nginx:1.27-alpine} # 没 securityContext, 默认 root
Actual:
Error: violates PodSecurity "restricted:v1.30":
- allowPrivilegeEscalation != false
- unrestricted capabilities
- runAsNonRoot != true
- seccompProfile
✅ 4 项违规 (去掉 privileged, 因为没显式开)
关键洞察: 99% 的现成 docker 镜像 (nginx, redis, mysql, postgres ...) 默认 root 跑,直接 apply 到 restricted ns 会全炸。这是 PSA 落地的最大阻力 — 业务镜像要先改造
D4. Scenario 3: 合规 Pod — 5 项全设
What (full compliance):
spec:
securityContext: # Pod 级
runAsNonRoot: true
runAsUser: 65534 # nobody
seccompProfile: {type: RuntimeDefault}
containers:
- name: c
image: nginx:1.27-alpine
command: ['sleep', '300'] # nginx 默认要写 /var/log/nginx, 不兼容 readOnly
securityContext: # 容器级
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 65534
readOnlyRootFilesystem: true
capabilities: {drop: [ALL]}
Actual: ✅ pod/good-compliant created + 1/1 Running
D5. PSA 三级别 cheat sheet
| 违规项 | privileged | baseline | restricted |
|---|---|---|---|
securityContext.privileged: true | ✅ 允许 | ❌ 拒 | ❌ 拒 |
hostNetwork / hostPID / hostIPC | ✅ | ❌ | ❌ |
hostPath volume | ✅ | ❌ | ❌ |
capabilities.add (除 default) | ✅ | ❌ | ❌ |
| 容器 runAs root | ✅ | ✅ | ❌ |
allowPrivilegeEscalation: true | ✅ | ✅ | ❌ |
readOnlyRootFilesystem: false | ✅ | ✅ | ❌ |
seccompProfile.type 未设 | ✅ | ✅ | ❌ |
capabilities.drop: [ALL] 未设 | ✅ | ✅ | ❌ |
记忆口诀:
- privileged = 啥都允许 (legacy)
- baseline = 拦明显逃逸 (host*, privileged, capabilities)
- restricted = 拦一切 (强制 nonRoot + readOnly + seccomp + capabilities)
D6. 渐进落地策略
直接 enforce restricted 会炸, 推荐路径:
# 第 1 周
labels:
pod-security.kubernetes.io/warn: restricted # 看哪些违规, 不拦
pod-security.kubernetes.io/audit: restricted # 写 audit log
# 第 2-4 周
# 改业务镜像 + Helm chart
# 第 5 周
labels:
pod-security.kubernetes.io/enforce: baseline # 先上 baseline 拦明显违规
pod-security.kubernetes.io/warn: restricted
# 第 N 周
labels:
pod-security.kubernetes.io/enforce: restricted
Lesson (面试可讲):
- PSA 是 K8s 1.25 接班 PSP 的方案,纯 label 配置,无 CRD
- PSA 只支持 enforce/warn/audit 三 mode,不支持 mutate(自动补默认值)— 这是 PSP 的不可逆功能,要 mutate 用 Kyverno
- restricted 默认 deny 一切,业务镜像要先改造 — PSA 是基线,不是大魔法
Day 5.E — Secrets + etcd at-rest 加密 (3-cp 滚动)
E1. 加密前: etcd 里明文一目了然
What (Step 1, 创测试 Secret):
kubectl create secret generic super-password -n storage-demo \
--from-literal=password=MyP@ssw0rd_BeforeEncryption \
--from-literal=api_key=sk_live_1234567890abcdef
What (Step 2, etcdctl 直查 etcd):
kubectl exec -n kube-system etcd-k8s-cp-1 -- sh -c "ETCDCTL_API=3 etcdctl \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
get /registry/secrets/storage-demo/super-password"
Actual (节选):
api_key
sk_live_1234567890abcdef
password
MyP@ssw0rd_BeforeEncryption
Opaque
结论: etcd 直查能看到 password 完整明文 + API key 完整明文。任何能拿到 etcd 数据库 / 备份的人都能 grep 出来
E2. 生成密钥 + 写 EncryptionConfiguration
KEY=$(head -c 32 /dev/urandom | base64) # 256-bit AES key
cat > /etc/kubernetes/encryption-config.yaml <<EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: [secrets]
providers:
- aescbc:
keys:
- name: key1
secret: ${KEY}
- identity: {} # 兜底, 允许读旧 unencrypted Secret
EOF
chmod 600 /etc/kubernetes/encryption-config.yaml
关键设计:
providers顺序很重要: 第一个用于 write, 所有按顺序尝试 read 直到成功- 升级密钥时: 新密钥在第一个,旧密钥保留 → rewrite all → 旧密钥可移除
identity在最后 = "如果有未加密的 Secret, 直接读取明文"
3 个 cp 都要这个 config(同一密钥):
scp encryption-config.yaml m2:/etc/kubernetes/ && ssh m2 'chmod 600 ...'
scp encryption-config.yaml m3:/etc/kubernetes/ && ssh m3 'chmod 600 ...'
E3. patch kube-apiserver 静态 manifest (滚动)
要加 3 项:
- command arg:
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml - volumeMount: 把 encryption-config.yaml 挂进 apiserver 容器
- volume hostPath: 节点上 encryption-config.yaml → Pod 内同路径
Python patch 脚本 (/tmp/patch-apiserver.py):
arg_new = arg_old + "\n - --encryption-provider-config=..."
# 锚点: --tls-private-key-file (所有 apiserver 必有)
vm_new = vm_old + """
- mountPath: /etc/kubernetes/encryption-config.yaml
name: encryption-config
readOnly: true"""
vol_new = vol_old + """
- hostPath:
path: /etc/kubernetes/encryption-config.yaml
type: File
name: encryption-config"""
滚动策略:
- 先 patch cp-1 → 等 apiserver Running 1/1(80s)
- 验证集群 API 仍可访问(per-node HAProxy 把流量路由到任意可用 apiserver)
- 并行 patch cp-2 + cp-3 → 等 Running
为什么不能 3 cp 同时改: 如果 manifest 写错,3 个 apiserver 同时炸 = 集群 dead
E4. ⚠️ 真坑 #2 — cp-2 / cp-3 没 audit-policy arg!
第一版 patch 脚本用 --audit-policy-file 作锚点(因为 cp-1 上 Day 2 配了 audit)。但:
AssertionError: audit-policy-file arg not found ← cp-2 / cp-3 上根本没这条!
Why: Day 2 audit 配置只做了 cp-1, cp-2 / cp-3 没同步(Day 2 文档的遗留缺陷!)
Fix: v2 patch 改用通用锚点 --tls-private-key-file(任何 apiserver 必有)
Lesson (Working Method #8 — Align Mental Model Before Patching):
- HA 集群的 control plane 节点应该完全对称配置
- Day 2 配 audit 时漏了 cp-2/cp-3, 下次升级/排错时炸
- 我们今天误打误撞修正了 Day 2 遗留 (cp-1 同时有 audit + encryption, cp-2/cp-3 只有 encryption — 还需要 Day 后续完整化)
E5. Rewrite Secrets → 让加密真生效
EncryptionConfiguration 启用后, 新写入 的 Secret 被加密, 已有 Secret 还是明文。要 rewrite 所有 Secret:
kubectl get secrets -A -o json | kubectl replace -f -
Actual (节选):
secret/longhorn-webhook-tls replaced
secret/demo-viewer-token replaced
secret/super-password replaced
... (集群里所有 Secret)
每条 replace 经过 apiserver → 触发 write → aescbc 加密 → 写入 etcd
E6. 加密后: etcd 直查全乱码
What (重读 super-password):
kubectl exec -n kube-system etcd-k8s-cp-1 -- sh -c "ETCDCTL_API=3 etcdctl ... get /registry/secrets/storage-demo/super-password"
Actual:
%OVP
UR=.
a!*)
;Ofi-&g
bT<S8
QZR~
Vx$m
4G}H+
全部不可读 binary,没有 MyP@ssw0rd 任何字样 ✅
反向验证 apiserver 解密:
kubectl get secret super-password -n storage-demo -o jsonpath='{.data.password}' | base64 -d
→ MyP@ssw0rd_BeforeEncryption ← 仍能正确读出
E7. 完整 before/after 对照
| Before (Day 5.E1) | After (Day 5.E6) | |
|---|---|---|
kubectl get secret | MyP@ssw0rd_BeforeEncryption | MyP@ssw0rd_BeforeEncryption |
| etcdctl 直查 | 完整明文 | aescbc 密文 (k8s:enc:aescbc:v1:key1:...) |
| 谁能解 | 任何 etcd 读权限者 | 必须有 encryption-config.yaml 的密钥 |
| 性能影响 | 0 | +5-10% Secret CRUD latency (AES 不慢) |
| 备份场景 | etcd backup 即明文 | etcd backup 仍是密文,key 单独保存 |
E8. 生产关键 lessons
- 密钥管理: encryption-config.yaml 在 host 上,别放 Git, 别误 backup 进对象存储
- 密钥轮转: 第二个 key 写第一(用于 write),老 key 暂留(用于 read),rewrite Secret,删老 key
- KMS provider: 公有云推荐 (
aws-encryption-provider/aliyun-kms-plugin) — key 在云 KMS,K8s 只持密钥 reference - 不只加 Secret: 也可以加 configmaps, persistentvolumeclaims (按需)
- 审计配套: 加密后即使 etcd 泄漏,密钥也 ≠ 安全 — 要配合 audit 看谁访问 Secret
Day 5.F — Kyverno 策略代码化
F1. 装 Kyverno v1.13
What:
# ⚠️ 不能用 kubectl apply — CRD annotation 超 256KB:
# "metadata.annotations: Too long: must have at most 262144 bytes"
# 因为 kubectl apply 把整个 yaml 塞到 last-applied annotation
kubectl apply --server-side --force-conflicts \
-f https://github.com/kyverno/kyverno/releases/download/v1.13.2/install.yaml
Why server-side apply:
- 走 K8s 1.16+ 的 SSA 协议,字段所有权 = controller name,不写 last-applied annotation
- 大 CRD 必备 (cert-manager / Istio / Linkerd 都遇到这问题)
Actual (4 Pod):
kyverno-admission-controller 1/1 Running (validating + mutating webhook)
kyverno-background-controller 1/1 Running (existing resource 扫描 + generate)
kyverno-cleanup-controller 1/1 Running (TTL / cleanup policies)
kyverno-reports-controller 1/1 Running (PolicyReports 生成)
9 个 CRD: clusterpolicies / policies / cleanuppolicies / clustercleanuppolicies / policyexceptions / updaterequests / globalcontextentries / clusterephemeralreports / ephemeralreports
F2. 3 条 入门 ClusterPolicy
---
# 1. 必须有 app.kubernetes.io/name label
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: {name: require-app-label}
spec:
validationFailureAction: Enforce
rules:
- name: check-label
match:
any:
- resources: {kinds: [Deployment, StatefulSet]}
validate:
message: "Resource must set label 'app.kubernetes.io/name'"
pattern:
metadata:
labels:
app.kubernetes.io/name: '?*' # 任意非空
---
# 2. 拒 image:latest 或没 tag
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: {name: disallow-latest-tag}
spec:
validationFailureAction: Enforce
rules:
- name: require-image-tag
match:
any:
- resources: {kinds: [Pod]}
validate:
message: "Image tag must not be 'latest' or empty"
pattern:
spec:
containers:
- image: "!*:latest & *:*" # NOT *:latest AND 有 ":"
---
# 3. 必须有 resources.requests
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: {name: require-resources}
spec:
validationFailureAction: Enforce
rules:
- name: require-cpu-mem
match:
any:
- resources: {kinds: [Pod]}
validate:
message: "Container must set resources.requests.cpu / memory"
pattern:
spec:
containers:
- resources:
requests:
cpu: '?*'
memory: '?*'
Kyverno pattern 语法:
'?*': 任意非空字符串*:*: 含:的字符串!*:latest: NOT 结尾:latest&: AND|: OR
3 个 ClusterPolicy 上线后 READY 都是 True:
NAME ADMISSION BACKGROUND READY MESSAGE
disallow-latest-tag true true True Ready
require-app-label true true True Ready
require-resources true true True Ready
F3. 4 场景测试
| # | Manifest | 命中 Policy | 预期 | 实际 |
|---|---|---|---|---|
| 1 | Deployment 无 app.kubernetes.io/name label | require-app-label | 拒 | ✅ Resource must set label 'app.kubernetes.io/name' |
| 2 | Pod image: nginx:latest | disallow-latest-tag | 拒 | ✅ Image tag must not be 'latest' or empty |
| 3 | Pod 没 resources.requests | require-resources | 拒 | ✅ Container must set resources.requests.cpu / memory |
| 4 | Pod 全合规 | (无) | 通 | ✅ pod/good-pod created → Running |
Kyverno 错误消息明确指出哪条 policy 的哪个 rule failed at which path:
require-app-label:
check-label: 'validation error: Resource must set label ''app.kubernetes.io/name''.
rule check-label failed at path /metadata/labels/'
F4. PolicyReport — 全集群合规快照
What:
kubectl get policyreport -A
Actual (节选):
NAMESPACE NAME KIND NAME PASS FAIL WARN ERROR SKIP
cilium-demo d582810f-... Pod nginx 1 1 0 0 0
default 074fec94-... Pod pod-w1 1 1 0 0 0
kube-system 02b12440-... Deployment cilium-operator 2 1 0 0 0
kube-system 12515b31-... DaemonSet cilium 1 1 0 0 0
意义:
- 每个集群资源都有一份"成绩单",列出对每条 ClusterPolicy 的合规情况
- Pass = 符合该 Pod 适用的 policy
- Fail = 违规, 但因为是 historical resource(policy 上线前已存在),不拦截只报告
- 适合"先 audit, 后 enforce"的渐进策略
F5. ⚠️ 真坑 #3 — Kyverno webhook startup race
部分 scenario 第一次 apply 报:
Internal error occurred: failed calling webhook "validate.kyverno.svc-fail":
failed to call webhook:
Post "https://kyverno-svc.kyverno.svc:443/validate/fail?timeout=10s":
context deadline exceeded
Why: kyverno-admission-controller Pod 刚 Running 但 internal webhook handler 还没完全 warmup;policy 加载 + cache 初始化要 30-60s
Fix:
- 简单重试(几次后就稳定)
- 生产: Kyverno Deployment 调到 3 replicas +
failurePolicy: Ignore(优先可用)
Trade-off:
failurePolicy: Fail: 安全(webhook 挂了拒一切),但 Kyverno 故障 = K8s API 半死failurePolicy: Ignore: 可用(webhook 挂了放行),但 Kyverno 故障 = policy 失效
中等场景选 Fail,集群关键服务 (kube-system) namespace 加 policyExceptions 豁免
F6. Kyverno 进阶能力(本 Day 不展开,提一下)
| 能力 | 例 |
|---|---|
| Mutate: 自动补字段 | 所有 Pod 自动加 runAsNonRoot: true |
| Generate: 自动建 Resource | 新建 namespace 时,自动生成 default NetworkPolicy + ResourceQuota |
| VerifyImages: 镜像签名验证 | 只允许 cosign-signed 镜像 |
| Mutate Existing: 改老资源 | rollout 时给所有现存 Pod 加 annotation |
| Cleanup Policy: TTL | 自动删 7 天前的 Job / Completed Pod |
| Exception: 豁免 | 允许 kube-system 的 Pod 用 latest tag |
简历可写:
落地 Kyverno 策略代码化,3 条 ClusterPolicy 强制 (label / image-tag / resources),用 PolicyReport 实时审计集群合规度,通过 PolicyException 豁免系统组件,实现"渐进 enforce"模式上线
11. Day 5 总结 — 安全基线完整建成
| 模块 | 启用前 | 启用后 |
|---|---|---|
| Volume 容量 | 125G (单 disk) | 634G (双 disk),PVC 在线扩 5G→10G |
| 认证授权 | 只 cluster-admin (admin.conf) | + demo-viewer SA, RBAC 最小权限 |
| Pod 安全 | 任何 Pod 都能 privileged | psa-test ns enforce restricted |
| Secret 加密 | etcd 明文 | aescbc-256 加密, etcdctl 看密文 |
| Policy as Code | 没有 | Kyverno + 3 条 ClusterPolicy 全 enforce |
| 审计 | Day 2 cp-1 已配 | (cp-2/3 未配, 是 Day 5.E2 发现的遗留, 待补) |
留存集群:
- 6 个新 namespace: storage-demo, cilium-demo, psa-test, kyverno
- 多个 demo-viewer kubeconfig 可用
- 3 个 Kyverno ClusterPolicy 在 enforce
- Secret super-password 已加密
- pg-0 PVC 已扩到 10G
简历可写:
落地 K8s 安全基线:
- Longhorn PVC 在线扩 + 节点二盘扩(125G→634G),验证数据无损
- RBAC 最小权限 SA + SubjectAccessReview 调试
- Pod Security Admission restricted enforce(拦 privileged / root / readOnly)
- EncryptionConfiguration aescbc-256 etcd 加密,3-cp 滚动重启零中断
- Kyverno 3 条 ClusterPolicy (label / image-tag / resources) + PolicyReport 审计
99. 当前进度
- [x] Day 5.A Volume Expansion (PVC 5→10G + 5节点二盘)
- [x] Day 5.B 安全速通 mini-book
- [x] Day 5.C RBAC viewer SA + 7 场景验证
- [x] Day 5.D PSA restricted enforce + 3 场景
- [x] Day 5.E Secret aescbc 加密 + etcd 验证密文
- [x] Day 5.F Kyverno 3 ClusterPolicy + PolicyReport