AI Infra 训练营
总览
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
HiHuo 主站
GitHub
总览
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
HiHuo 主站
GitHub
  • Day 0 · 环境与硬件

    • Day 0:5 节点裸 Ubuntu → K8s 装机基线
  • Week 1:K8s 内核 + 周边基础设施

    • Day 1:3 CP HA 集群 + CNI 选型 + DNS 调优
    • Day 2: 控制面 deep dive + etcd 内核 + chaos drill
    • Day 3: CRD + Operator (kubebuilder 从 0 写)
    • Day 4: Storage 主线 + Cilium 二探
    • Day 5: Volume Expansion + 安全主线
    • Day 6: 调度 + 观测主线 + Day 2 遗留修复
    • Day 7: Harbor + ArgoCD + Cilium Service Mesh
  • Week 2:制品 + GitOps + AI Infra + 综合

    • Day 8 主线 — AI Infra: GPU + k3s + vLLM + Qwen2.5
    • Day 8 主线 — AI Infra 尝试 1 (跨 WAN GPU 加入主集群)
    • Day 8 (alt) — AlertManager 真接入 + PrometheusRule 实战
    • Day 8: CI Infrastructure — Gitea + Jenkins + Kaniko
    • Day 9: Triton + GPU Metrics + 推理性能对比
    • Day 10: MIG + 量化 + HPA Custom Metrics
    • Day 11: AI Agent 业务端到端 — 把 Day 1-10 全部串起来
    • Day 12: 灾难恢复 + 生产事故注入
    • Day 13: LLM Operator + 联邦 + Mesh + RAG
    • Day 14: CKA/CKS 真题演练 + 14 天 Bootcamp 终极总结

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 节)

  1. A — Volume Expansion: pg-0 PVC 5G→10G,Longhorn 加 vdb-extra 二盘,看 disk 容量翻倍
  2. B — 安全速通: RBAC / PSA / Secret 加密 / Policy as Code 理论 mini-book
  3. C — RBAC 实战: SA + Role + RoleBinding,生成只读 kubeconfig
  4. D — PSA: ns label enforce baseline/restricted,跑 privileged Pod 看拦
  5. E — Secret at-rest: EncryptionConfiguration aescbc,etcdctl 直查 Secret 已加密
  6. 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 个必要条件,缺一不可:

  1. SC 设了 allowVolumeExpansion: true(Longhorn 默认开 ✅)
  2. CSI driver 实现了 ControllerExpandVolume + NodeExpandVolume(Longhorn ✅)
  3. 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: 改资源,不能改 Roles
  • view: 只读,不能看 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 流程:

  1. 写 /etc/kubernetes/encryption-config.yaml(包含密钥,别提交 Git!)
  2. apiserver 启动加 --encryption-provider-config=...
  3. 重启 apiserver
  4. 重写所有 Secret 让密钥生效: kubectl get secrets -A -o json | kubectl replace -f -
  5. etcdctl 直查,验证密文

B5. Policy as Code — Kyverno vs OPA Gatekeeper

K8s 内置 admission control 能做基本 (Role / PSA)。复杂业务规则要用 policy engine:

KyvernoOPA Gatekeeper
语法YAML(K8s 原生风格)Rego(自有 DSL,陡峭)
生成 Resource支持 (generate policy)不支持
Mutate支持 (mutate policy)不支持 (只能 validate)
Image Verification支持 (verifyImages)通过 Constraint 自己写
学习曲线1 天1 周
生态CNCF IncubatingCNCF Graduated
适合中小团队, 想快速落地大型企业, 已有 OPA 团队

今天选 Kyverno — YAML 友好,demo 快

3 条入门策略:

  1. require-labels: 所有 Deployment 必须有 app.kubernetes.io/name label
  2. disallow-latest-tag: 拦截 image: foo:latest(线上事故温床)
  3. 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):

  1. ServiceAccount demo-viewer
  2. Secret type=kubernetes.io/service-account-token (1.24+ SA 不再自动给 Secret token,要手动建)
  3. Role pod-cm-reader (get/list/watch on pods/pods/log/configmaps/deployments/statefulsets)
  4. 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 场景测试

#命令预期实际
1get pod (storage-demo)允许✅ pg-0, pg-verify-restored
2get configmap允许✅ kube-root-ca.crt
3get secret拒✅ Forbidden: cannot list "secrets"
4get pod -n default拒(跨 ns)✅ Forbidden
5get nodes拒(cluster scope)✅ Forbidden ... "at the cluster scope"
6run nginx-pod拒(写操作)✅ Forbidden: cannot create "pods"
7auth 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 SubjectAccessReview API,不真发请求,问"我能不能"
  • 不需要 -v=8 看 audit log,清晰干脆
  • CI/CD pipeline 可以用 auth can-i 做前置检查,失败 fast-fail

生产 RBAC 调试套路:

  1. kubectl auth can-i list pods -n X 看能不能
  2. kubectl auth can-i list pods -n X --as=system:serviceaccount:X:Y 模拟特定 SA
  3. kubectl get rolebinding,clusterrolebinding -A -o yaml | grep -A 5 SA-name 看 SA 绑了哪些 Role
  4. 还不通 → 看 audit log 找 authorization.k8s.io/decision: forbid

C5. RBAC 最佳实践

  1. 最小权限: 默认拒一切, 一个一个 verb 加
  2. 聚合 ClusterRole (aggregationRule): 复用现有 view/edit/admin 的 rules
  3. ServiceAccount 一 Pod 一个: 别让多 Pod 共享 SA
  4. Token 短期化: 1.24+ 强制走 BoundServiceAccountTokens (Pod 内挂的 token 是 1h TTL 自动 refresh,SA Secret token 是长期)
  5. 审查 system:masters: 任何 RoleBinding 引用它 = 给 cluster-admin,必须审计
  6. 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

违规项privilegedbaselinerestricted
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 项:

  1. command arg: --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
  2. volumeMount: 把 encryption-config.yaml 挂进 apiserver 容器
  3. 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"""

滚动策略:

  1. 先 patch cp-1 → 等 apiserver Running 1/1(80s)
  2. 验证集群 API 仍可访问(per-node HAProxy 把流量路由到任意可用 apiserver)
  3. 并行 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 secretMyP@ssw0rd_BeforeEncryptionMyP@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

  1. 密钥管理: encryption-config.yaml 在 host 上,别放 Git, 别误 backup 进对象存储
  2. 密钥轮转: 第二个 key 写第一(用于 write),老 key 暂留(用于 read),rewrite Secret,删老 key
  3. KMS provider: 公有云推荐 (aws-encryption-provider / aliyun-kms-plugin) — key 在云 KMS,K8s 只持密钥 reference
  4. 不只加 Secret: 也可以加 configmaps, persistentvolumeclaims (按需)
  5. 审计配套: 加密后即使 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预期实际
1Deployment 无 app.kubernetes.io/name labelrequire-app-label拒✅ Resource must set label 'app.kubernetes.io/name'
2Pod image: nginx:latestdisallow-latest-tag拒✅ Image tag must not be 'latest' or empty
3Pod 没 resources.requestsrequire-resources拒✅ Container must set resources.requests.cpu / memory
4Pod 全合规(无)通✅ 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 都能 privilegedpsa-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
在 GitHub 上编辑此页
Prev
Day 4: Storage 主线 + Cilium 二探
Next
Day 6: 调度 + 观测主线 + Day 2 遗留修复