前言:这一篇在考什么
这一篇把「服务网格(Service Mesh)」和「零信任安全(Zero Trust)」放在一起,再延伸到第 11 章的「合规治理」。面试里它常以两种姿态出现:
- 实操题:给你一个故障/风险场景,让你配 Istio、OPA、Vault、Cilium。
- 架构题:问你"为什么这样设计"“出事怎么回滚"“规模上来怎么不崩”。
所以本文的写法不是堆命令,而是先用类比建立直觉,再给可落地的 YAML/代码,最后点出权衡与面试话术。你可以把它当成一份"带答案的复习讲义”。
阅读约定:所有命令假设你已有
kubectl、istioctl、vault、opa、gator等 CLI 在 PATH 中,且 kubeconfig 指向目标集群。
一、4.1 根 CA 轮换不重启 Sidecar:Istio CA 与 Vault PKI 协同
1. 白话直觉:换"总钥匙"为什么要让租户重配钥匙
想象一栋楼有一个总钥匙(根 CA),物业用它给每个房间配分钥匙(中间 CA / 工作负载证书)。如果你要换总钥匙,最蠢的做法是:立刻把所有房门锁芯也换了——也就是重启每一个 Sidecar、让每个微服务重新拿证书。服务超过 5k 个时,这意味着一次全集群抖动。
正确做法:新旧总钥匙同时生效一段时间(信任重叠期)。先让物业用新总钥匙配一批新分钥匙,等所有房间都换好了,再撤掉旧总钥匙。Sidecar 不需要重启,它只是"信任列表里多/少了一个根"。
2. 为什么 Sidecar 可以不重启
Istio 的证书由 istiod 签发,Sidecar(Envoy)通过 SDS(Secret Discovery Service)热加载证书。Envoy 校验对端证书时,看的是**信任锚(trust bundle)**里是否包含签发该证书的根/中间 CA。只要信任锚里同时保留旧根和新根,旧证书和新证书都有效,天然无需重启。
# 关键认知:Envoy 的 root cert 是一个"证书集合",不是单个。
# istiod 下发的 ROOTCA 里可以同时包含:
# - 旧根 CA (仍信任)
# - 新根 CA (新签发的证书由它签发)
# 轮换分三步:双根共存 → 新证书铺满 → 撤旧根
3. 用 Vault PKI 做外部 CA,Istio 只签中间 CA
生产上常把根 CA 放在 Vault(有 HSM 后端、审计日志、自动吊销),让 Istio 向 Vault 申请一张中间 CA 证书,再由 istiod 用这张中间 CA 给工作负载签叶子证书。这样根 CA 的私钥几乎从不离开 Vault。
# ① 在 Vault 启用 pkii 引擎,并配置允许 istiod 申请中间 CA
vault secrets enable -path=pki-istio pki
vault write pki-istio/root/generate/internal \
common_name="istio-root-ca" \
ttl=87600h # 根 CA 有效期 10 年
# ② 生成一张"中间 CA 签名请求(CSR)"交给 istiod 一侧
vault write pki-istio/intermediate/generate/exported \
common_name="istio-intermediate-ca" \
| tee istio-int.json # 里面含 csr 和私钥占位
# ③ Istio 的 MeshConfig 指向外部 CA(Vault 签好的中间 CA + 根)
# 通过 istioctl 安装时提供 cert 链与私钥
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
components:
pilot:
k8s:
env:
- name: EXTERNAL_CA
value: "ISTIOD_RA_KUBERNETES_API" # 或自定义 RA 对接 Vault
values:
global:
caAddress: "istiod.istio-system.svc:15012"
4. CRL 分发策略:证书吊销怎么让 Envoy 知道
CRL(Certificate Revocation List,吊销列表)回答一个问题:“某张证书虽然没过期,但被盗了,怎么让它立刻失效?”
Vault PKI 会自动维护 CRL 并暴露一个 URL。让 istiod 签发的叶子证书带上 CRL Distribution Point,客户端(或网关)可定期拉取。
# 在 Vault 中开启 CRL 自动发布
vault write pki-istio/config/urls \
issuing_certificates="http://vault:8200/v1/pki-istio/ca" \
crl_distribution_points="http://vault:8200/v1/pki-istio/crl" \
ocsp_servers="http://vault:8200/v1/pki-istio/ocsp"
# 分发策略要点(面试可答):
# 1) 控制面下发:把 CRL 定期同步到 ConfigMap,再由 istiod 注入 Envoy 的信任上下文。
# 2) 频率与体积:CRL 可能很大,5k+ 工作负载时改为 OCSP 或"短生命周期证书(几分钟)"
# 让吊销天然失效——这是 Istio 的默认哲学:证书有效期短(默认1h),不依赖 CRL。
# 3) 真正吊销入口:在 Vault 执行 `vault write pki-istio/revoke serial_number=...`
权衡:Istio 默认不靠 CRL 做 mTLS 吊销,而是靠极短有效期 + 快速轮转。CRL 更适合"长生命周期证书"场景(如入口网关的外部客户端证书)。
5. SPIFFE ID + JWT + mTLS 双重认证(入口网关)
入口网关面对的是不可信的外部客户端。我们要同时验证两件事:
- mTLS(你是谁):客户端出示 SPIFFE ID 证书,证明它是某个合法工作负载。
- JWT(你有什么令牌):客户端带一个 OIDC/JWT,证明它已被业务系统授权。
# ① 要求对端做 mTLS,并校验其 SPIFFE ID 前缀
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: ingress-mtls
namespace: istio-system
spec:
mtls:
mode: STRICT
---
# ② 校验 JWT:iss 必须是我们的 OIDC,aud 必须匹配网关
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: jwt-check
namespace: istio-system
spec:
selector:
matchLabels:
istio: ingressgateway
jwtRules:
- issuer: "https://auth.example.com"
jwksUri: "https://auth.example.com/.well-known/jwks.json"
---
# ③ 授权:既要有合法 JWT,又要来自可信 SPIFFE ID(双重)
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: double-authz
namespace: istio-system
spec:
selector:
matchLabels:
istio: ingressgateway
action: ALLOW
rules:
- from:
- source:
principals: ["spiffe://example.com/ns/partner/sa/client"] # SPIFFE ID
when:
- key: request.auth.claims[scope]
values: ["api:read"] # JWT claim
sequenceDiagram
participant C as 外部客户端
participant GW as 入口网关(Envoy)
participant IDP as OIDC Provider
participant IDS as SPIFFE CA
C->>GW: mTLS 握手(出示 SPIFFE 证书)
GW->>IDS: 校验证书链/身份
C->>GW: HTTP 请求 + Bearer JWT
GW->>IDP: 拉取 JWKS 校验 JWT
GW-->>C: 双重通过才放行,否则 401/4036. 面试速答
- “5k 服务怎么轮换根 CA 不重启?” → 双信任锚重叠:先把新根加进 trust bundle,让 istiod 用新中间 CA 重签所有叶子证书(Sidecar 热加载),确认铺满后再撤旧根。
- “为什么 Istio 不依赖 CRL?” → 证书默认 1 小时有效,靠短有效期 + 快速轮转实现"事实吊销",CRL 仅用于长生命周期证书。
- “入口网关双重认证的两种身份分别是什么?” → mTLS 验证 SPIFFE 身份(你是谁),JWT 验证业务授权(你被允许做什么)。
二、4.2 东西向流量细粒度熔断:DestinationRule 与共享令牌桶
1. 白话直觉:不是"整条路塌了",而是"某家店排队太长"
普通熔断像"这条路封了",粒度是整个上游服务。但真实场景是:同一个服务,/v1/search 很重、/v1/health 很轻。你希望只熔断重的 search 接口,轻接口照常。这就是"URL Path 级细粒度熔断"。
2. 标准 DestinationRule 只能到"主机/子集",怎么落到 Path
Istio 的 DestinationRule.OutlierDetection 作用在**子集(subset)**粒度。技巧是:把"不同 path"映射成"不同 subset",再用 VirtualService 按 path 路由到对应 subset,每个 subset 配独立熔断阈值。
# ① 定义两个子集,分别对应轻/重接口,熔断阈值不同
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: orders-dr
spec:
host: orders.svc.cluster.local
subsets:
- name: heavy # 重接口:阈值更严
labels: { tier: orders }
trafficPolicy:
outlierDetection:
consecutive5xxErrors: 3
interval: 30s
baseEjectionTime: 60s
maxEjectionPercent: 50
- name: light # 轻接口:容忍度高
labels: { tier: orders }
trafficPolicy:
outlierDetection:
consecutive5xxErrors: 20
interval: 30s
baseEjectionTime: 30s
---
# ② VirtualService 按 path 把流量分到不同子集,实现"按 path 熔断"
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: orders-vs
spec:
hosts: ["orders.svc.cluster.local"]
http:
- match: [{ uri: { prefix: "/v1/search" } }]
route: [{ destination: { host: orders, subset: heavy } }]
- match: [{ uri: { prefix: "/v1/health" } }]
route: [{ destination: { host: orders, subset: light } }]
3. 熔断触发后,自定义响应头通知客户端"稍后重试"
Envoy 触发熔断/过载时默认回 503,并带 x-envoy-overloaded: true。我们可以自定义 local reply,加上 Retry-After 让客户端退避。
# 通过 EnvoyFilter 改写过载响应,注入友好重试头
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: custom-overload-reply
namespace: istio-system
spec:
workloadSelector:
labels: { app: orders }
configPatches:
- applyTo: NETWORK_FILTER
match: { listener: { filterChain: { filter: { name: "envoy.filters.network.http_connection_manager" } } } }
patch:
operation: MERGE
value:
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
local_reply_config:
mappers:
- filter: { status_code_filter: { comparison: { op: EQ, value: { default_value: 503 } } } }
headers_to_add:
- header: { key: "Retry-After", value: "2" }
- header: { key: "X-Circuit-State", value: "open" }
body: { inline_string: "{\"error\":\"circuit_open\",\"retry_after\":2}" }
4. 集群级共享令牌桶:Envoy Local Rate Limit + Redis(Lua)
局部限流(per-Pod)会有"各自 100 QPS,10 个 Pod 就是 1000 QPS"的漏洞。要集群总量守恒,需要把令牌存在 Redis 里,所有 Sidecar 共享一把"全局令牌桶"。下面用 Envoy 的 rate_limit + 一个 Redis Lua 脚本保证原子扣减。
-- redis_global_token_bucket.lua
-- KEYS[1] = 桶的 key(如 rl:orders:search)
-- ARGV[1] = 容量 ARGV[2] = 每秒 refill 速率 ARGV[3] = 本次请求令牌数 ARGV[4] = 当前时间戳(ms)
local key = KEYS[1]
local cap = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local cost = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
local data = redis.call("HMGET", key, "tokens", "ts")
local tokens = tonumber(data[1]) or cap
local ts = tonumber(data[2]) or now
-- 按时间比例补充令牌(毫秒)
local delta = (now - ts) / 1000 * rate
tokens = math.min(cap, tokens + delta)
local allowed = 0
if tokens >= cost then
tokens = tokens - cost
allowed = 1
end
redis.call("HMSET", key, "tokens", tokens, "ts", now)
-- 设置过期,避免空桶无限堆积
redis.call("PEXPIRE", key, math.ceil(cap / rate * 1000) + 1000)
return { allowed, math.floor(tokens) }
# 在 Envoy 侧通过 ratelimit 服务(或直接在 sidecar 里用 lua filter)调用上面的脚本:
# 这里给出"本地验证脚本逻辑"的等价测试(用 redis-cli 模拟一次扣减)
redis-cli --eval redis_global_token_bucket.lua rl:orders:search \
, 100 10 1 "$(date +%s)000"
# 返回值 [1, 99] 表示:放行(1),剩余 99 令牌
flowchart LR
A[Sidecar Envoy] -->|请求令牌| B[Redis 共享令牌桶]
B -->|Lua 原子扣减| C{余额足够?}
C -->|是| D[放行并设置 X-RateLimit 头]
C -->|否| E[返回 429 + Retry-After]
D --> F[后端服务]5. 面试速答
- “DestinationRule 不能直接按 path 熔断怎么办?” → 把 path 路由成不同 subset(VirtualService match + DestinationRule subset),在每个 subset 上配独立 OutlierDetection。
- “集群级限流为什么要用 Redis + Lua?” → 单 Pod 局部限流总和会放大;Redis 做全局令牌桶,Lua 保证"查询+扣减"原子,避免竞态超发。
三、4.3 零信任身份与 OPA Gatekeeper:从"信任网络"到"校验身份"
1. 白话直觉:城堡护城河 vs 每扇门查工牌
传统安全是"城堡模式"——墙外不可信、墙内全可信。零信任是"每扇门都要查工牌":不再因"它在内网"就放行。OPA(Open Policy Agent)就是那个"工牌校验器",它用一种叫 Rego 的策略语言判断"这个 Pod 能不能创建"。
2. 用 OPA Gatekeeper 约束 Pod 标签(正则匹配 + 拒绝详情)
Gatekeeper 用 ConstraintTemplate 定义可复用规则,Constraint 是具体实例。下面约束:所有 Pod 必须带 team 标签,且值必须匹配正则 ^(pay|search|risk)-[a-z]+$,否则拒绝并给出明确原因。
# ① 定义模板:deny 时返回详细 message
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8spodlabelregex
spec:
crd:
spec:
names: { kind: K8sPodLabelRegex }
validation:
openAPIV3Schema:
type: object
properties:
label: { type: string }
pattern: { type: string }
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8spodlabelregex
violation[{"msg": msg}] {
label := input.parameters.label
pat := input.parameters.pattern
not input.review.object.metadata.labels[label]
msg := sprintf("缺少必需标签 %v", [label])
}
violation[{"msg": msg}] {
label := input.parameters.label
pat := input.parameters.pattern
v := input.review.object.metadata.labels[label]
not regex.match(pat, v)
msg := sprintf("标签 %v 的值 %v 不匹配正则 %v", [label, v, pat])
}
---
# ② 实例化约束
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPodLabelRegex
metadata:
name: require-team-label
spec:
match:
kinds: [{ apiGroups: [""], kinds: ["Pod"] }]
parameters:
label: "team"
pattern: "^(pay|search|risk)-[a-z]+$"
3. SPIRE + OPA 实现工作负载动态身份注入(架构图)
SPIRE 给每个工作负载发 SVID(SPIFFE 可验证身份文档)——本质是张带 SPIFFE ID 的证书。OPA 在做授权时,不只看"请求来自哪个 IP",而是看"这张 SVID 上的身份"。身份随 Pod 启停动态签发/轮换,无需人工维护。
flowchart TD
A[Workload Pod] -->|1. 证明身份(节点证明/PSAT)| B[SPIRE Agent]
B -->|2. 签发 SVID| A
A -->|3. 携带 SVID 调用服务| C[目标服务 Envoy]
C -->|4. 把对端身份喂给 OPA| D[OPA]
D -->|5. 查策略: 该 SPIFFE ID 是否允许?| E[(Policy)]
E -->|允许/拒绝| C4. 灾难回滚:OPA 策略更新把所有新建 Pod 拒了,怎么办
这是高频"翻车"场景。三板斧:
- 立刻切 dryrun(最快止血):把约束的
enforcementAction改成dryrun,新 Pod 不再被拒,只记审计。 - 回滚到上一版策略:Gatekeeper 的 Constraint/Template 都是 K8s 资源,用
kubectl apply重放旧 YAML 即可。 - 豁免已有工作负载:新策略只作用于"新建请求",已运行的 Pod 不受影响;若要对某命名空间豁免,用
spec.match.excludedNamespaces。
# ① 一键把"生产命名空间"从约束中排除,先止血
kubectl patch k8spodlabelregex require-team-label --type=merge \
-p '{"spec":{"match":{"excludedNamespaces":["prod"]}}}'
# ② 或直接把动作降级为 dryrun
kubectl patch k8spodlabelregex require-team-label --type=merge \
-p '{"spec":{"enforcementAction":"dryrun"}}'
# ③ 回滚:用 git 里的历史版本重放
kubectl apply -f policies/archive/require-team-label-v2.yaml
flowchart LR
P[策略误更新] --> Q{影响范围?}
Q -->|新建 Pod 全部被拒| R[enforcementAction=dryrun 止血]
R --> S[回滚历史 YAML]
S --> T[用 excludedNamespaces 豁免 prod]
T --> U[确认 Audit 日志无新违规]
U --> V[改回 deny 灰度生效]5. 面试速答
- “OPA 怎么给拒绝原因?” → Rego 里
violation[{"msg": ...}]返回结构化 message,Gatekeeper 会把它写进 admission 拒绝响应的message字段。 - “动态身份相比静态 IP 白名单好在哪?” → Pod 漂移/重建 IP 会变,但 SPIFFE ID 是工作负载语义身份,天然适配弹性伸缩。
- “策略把集群搞挂了怎么救?” → dryrun 止血 → 回滚旧版 → excludedNamespaces 豁免,三步走。
四、4.4 分布式追踪采样:别让 Trace 把后端冲垮
1. 白话直觉:监控摄像头不用 24 小时全录
全量采样的 Trace 等于"每笔请求都存一段录像",成本高到离谱。采样就是"按比例抽查"。难点在于:既要省钱,又不能漏掉出问题的那笔请求。按租户(业务线)动态调采样率,就是"出问题的租户多录,平稳的租户少录"。
2. Istio 1.19 按租户动态调整采样率
Istio 1.19 用 Telemetry CR 配置采样,可针对特定 workload 设置不同比例。再结合请求头里的租户标识,用 custom_tags 把租户带入 Trace 上下文。
# ① 全局默认低采样(10%),省钱
apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
metadata:
name: mesh-tracing
namespace: istio-system
spec:
tracing:
- providers: [{ name: otel }]
randomSamplingPercentage: 10
customTags:
tenant:
requestHeader:
name: x-tenant-id # 把租户 ID 带进 span
---
# ② 对"风控租户"单独高采样(100%),因为它的请求最该被看
apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
metadata:
name: risk-tenant-tracing
namespace: risk
spec:
workloadSelector:
labels: { app: risk-engine }
tracing:
- providers: [{ name: otel }]
randomSamplingPercentage: 100
3. OpenTelemetry Collector + Tail Sampling 层级配置
头采样(head sampling)在请求一开始就决定采不采,可能漏掉慢请求。尾采样(Tail Sampling) 等请求结束、看到真实耗时/错误后再决定,能"只保留慢的和错的"。
# otel-collector-config.yaml(节选)
processors:
tail_sampling:
decision_wait: 10s # 等 10s 收集完 span 再决策
num_traces: 50000
policies:
- name: errors-policy # 策略1:有错误的全采
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow-policy # 策略2:超过 500ms 的采
type: latency
latency: { threshold_ms: 500 }
- name: tenant-risk # 策略3:风控租户全采
type: string_attribute
string_attribute: { key: tenant, values: ["risk"], enabled_regex: false }
- name: default-policy # 兜底:其余 5%
type: probabilistic
probabilistic: { sampling_percentage: 5 }
4. 追踪数据写 Kafka 延迟:定位是 Agent 批处理还是后端消费
OTel Agent 把 span 批量发往 Kafka,后端消费入存储。延迟来源只有两端:
- Agent 端批处理慢:批没满 / flush 间隔长 → 看
otelcol_exporter_queue_size、exporter_send_duration。 - 后端消费慢:Kafka 消费 lag 涨 → 看 consumer group lag。
# ① 看 Collector 导出耗时(Agent 侧)
kubectl exec -n observability deploy/otel-collector -- \
curl -s localhost:8889/metrics | grep exporter_send_duration
# ② 看 Kafka 消费积压(后端侧)
kafka-consumer-groups.sh --bootstrap-server kafka:9092 \
--describe --group trace-ingest
# 若 LAG 持续上涨 → 后端消费跟不上,扩 consumer;
# 若 LAG 为 0 但端到端延迟高 → 是 Agent 批处理/网络问题,调小 batch_timeout。
flowchart LR
S[服务 Sidecar] -->|span| A[OTel Agent]
A -->|批量写| K[(Kafka)]
K -->|消费| B[后端 Ingest]
B -->|入库| DB[(存储)]
A -.延迟高?.-> Q1{Agent batch/flush?}
K -.延迟高?.-> Q2{Consumer lag 涨?}
Q1 -->|是| F1[调小 batch_timeout]
Q2 -->|是| F2[扩 consumer 副本]5. 面试速答
- “头采样和尾采样区别?” → 头采样请求一开始决定,可能漏慢请求;尾采样等结果出来再决定,能精准保留错误/慢请求,但需缓存 span 等决策。
- “Trace 写 Kafka 慢怎么分责?” → 看 consumer lag:涨就是后端消费慢,不涨就是 Agent 批处理/网络慢。
五、4.5 安全 Egress 审计:看清"出去的流量"但不偷看内容
1. 白话直觉:海关只查"你去哪国",不拆你的行李
出口流量(Egress)是安全的盲区:Pod 偷偷调用外部 API、外传数据,你却看不见。审计不等于解密——就像海关看护照上的"目的地"(SNI),但不拆开信封读内容。这既是合规要求,也保护了隐私。
2. Egress Gateway + Squid 对 HTTPS 做 SNI 审计(不解密)
HTTPS 加密了 URL,但 TLS 握手时的 SNI(Server Name Indication) 是明文,告诉你"对方域名"。用 Squid 的 peek-and-splice:只看 SNI、不解密、放行或拦截。
# squid.conf 关键配置:peek(看 SNI)然后 splice(放行不解密)
acl step1 at_step SslBump1
acl step2 at_step SslBump2
ssl_bump peek step1 # 看 ClientHello 里的 SNI
ssl_bump peek step2 # 看 ServerHello
ssl_bump splice all # 不解密,直接透传
# 把看到的 SNI 写进访问日志,实现审计
access_log /var/log/squid/access.log squid
# Istio 让所有出口流量走 Egress Gateway,再转给 Squid
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: external-api
spec:
hosts: ["api.vendor.com"]
ports: [{ number: 443, name: https, protocol: HTTPS }]
location: MESH_EXTERNAL
resolution: DNS
---
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: egress-gw
spec:
servers:
- port: { number: 443, name: https, protocol: HTTPS }
hosts: ["*"]
selector: { istio: egressgateway }
3. Cilium Tetragon 监控外部 API 调用并生成 Seccomp 配置
Tetragon 用 eBPF 在内核层观察系统调用,能看见"这个进程调了哪个外部 IP/域名",无需改应用。更进一步:观察进程实际用到哪些 syscall,反推出最小 Seccomp 配置(只放行用到的 syscall)。
flowchart TD
A[Pod 进程发起 connect()] --> B[Tetragon eBPF 捕获 syscalls]
B --> C[生成"实际 syscall 清单"]
C --> D[比对默认 Seccomp Profile]
D --> E[产出最小权限 Seccomp JSON]
E --> F[注入 Pod securityContext.seccompProfile]# 用 Tetragon 观察某 Pod 的对外连接(节选自带 tracer)
kubectl exec -n kube-system ds/tetragon -- tetra getevents \
-o json | jq 'select(.process_exec.process) | .process_exec.process.exec_id'
# 观察 connect 事件,记录 dst 端口/地址,用于生成网络策略与 Seccomp。
4. 出口 IP 被第三方封禁:自动切换 EgressIP 池并通知 SRE
很多 SaaS 按源 IP 白名单放行。当我们的出口 IP 被封,需要秒级换一批 IP。Cilium / K8s EgressIP 支持把出口流量绑定到特定节点 IP 池;监控到 403 激增即切换并告警。
# 用 Cilium EgressGatewayPolicy 把出口流量绑定到一组 IP
apiVersion: cilium.io/v2
kind: CiliumEgressGatewayPolicy
metadata:
name: vendor-egress
spec:
destinationCIDRs:
- "203.0.113.0/24" # 第三方网段
selectors:
- podSelector:
matchLabels: { app: orders }
egressGateway:
nodeSelector:
matchLabels: { egress-pool: primary } # 当前出口池
# 切换到备用池(primary -> backup),并通知 SRE
kubectl patch ciliumegressgatewaypolicy vendor-egress --type=merge \
-p '{"spec":{"egressGateway":{"nodeSelector":{"matchLabels":{"egress-pool":"backup"}}}}}'
# 通知:用 kubectl 触发一个告警事件(实际可接 Alertmanager/飞书)
kubectl annotate --overwrite pod -n sre alert=1 \
"message=Egress IP 被封禁,已切换至 backup 池 @sre-oncall"
5. 面试速答
- “HTTPS 不解密怎么审计?” → 利用 TLS 握手的 SNI(明文域名)做审计,配合 Egress Gateway 集中转发,Squid peek-and-splice 只记录不解密。
- “Seccomp 配置怎么来?” → 用 Tetragon 观测进程真实用到的 syscall,反推最小权限 Profile,避免手猜导致进程起不来。
六、11.1 OPA 策略模板单元测试与 CI 集成
1. 白话直觉:策略也是代码,也要有测试
很多人把安全策略当"配置文件"随手改,结果一次误改把集群搞挂(见 4.3)。把策略当代码对待:写单测、进 CI、出报告。这样改策略前先在 CI 跑一遍"假请求",确认不会误杀。
2. Rego 单元测试 + CI
Rego 自带 opa test,每个策略配一个 <name>_test.rego。
# require_team_label.rego
package k8spodlabelregex
violation[{"msg": msg}] {
not input.review.object.metadata.labels.team
msg := "missing team label"
}
# require_team_label_test.rego
package k8spodlabelregex
test_missing_label {
input := {"review": {"object": {"metadata": {}}}}
results := violation with input as input
count(results) == 1
}
test_has_label {
input := {"review": {"object": {"metadata": {"labels": {"team": "pay-core"}}}}}
results := violation with input as input
count(results) == 0
}
# 跑测试
opa test . -v
3. gator CLI 离线验证约束并输出违规详情
Gatekeeper 的 gator 能在不上集群的情况下,用本地模板+约束+样例资源验证策略输出。
# 把模板、约束、待测资源放一起跑
gator test \
--templates policies/templates/ \
--constraints policies/constraints/ \
--objects samples/bad-pod.yaml
# 输出:哪个约束、哪条 violation、msg 详情——CI 里据此 fail。
4. 策略超 1k:用 OPA Bundle Server 动态加载
千级策略直接塞进 Gatekeeper CRD 会拖慢准入、难以版本管理。解法是 Bundle:把策略打成 bundle(含 Rego + 数据),由 OPA Bundle Server 托管,Gatekeeper 周期性拉取,支持灰度与回滚。
# ① 把策略目录打成 bundle
opa build -b policies/ -o bundle.tar.gz
# ② 起一个 bundle server(或用 OCI 仓库)
opa run -s -b bundle.tar.gz # 暴露 /v1/bundles/<name>
# ③ Gatekeeper 侧引用远程 bundle(而非内联 CRD)
kubectl apply -f - <<'EOF'
apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata: { name: config }
spec:
sync: { syncOnly: [{ group: "", version: "v1", kind: "Pod" }] }
validation:
providers:
- name: <your-bundle-server>
url: http://bundle-server:8181/v1/bundles/policies
EOF
5. 面试速答
- “策略怎么保证不误杀?” → Rego 单测 + gator 离线跑样例,全部进 CI 门禁。
- “千级策略怎么不拖慢集群?” → 抽成 OPA Bundle,由 Bundle Server 统一托管与动态加载,Gatekeeper 只拉不内联。
七、11.2 等保 2.0 与容器安全基线:CIS Benchmark 落地
1. 白话直觉:安全基线是"建筑消防规范"
等保 2.0 是国家对系统安全的强制要求,落到 K8s 上就是 CIS Kubernetes Benchmark——一份"哪些配置不安全"的清单(如 kubelet 不能匿名访问)。kube-bench 是"消防检查员",逐项打分。
2. CIS 1.8 自动生成修复脚本
kube-bench 跑完会给出每项 FAIL 及建议修复命令。可以把它模板化成脚本,或接 Kyverno 自动修。
# 跑 CIS 1.8 检查(以 master 节点为例)
kube-bench run --targets master --version 1.8 \
--benchmark cis-1.8
# 输出 [FAIL] 项 + 修复建议;可加 --json 让 CI 解析并生成修复 PR。
# 示例:把"kubelet 匿名认证"自动修复脚本化
# 修复前先备份,再改 kubelet 配置并重启(生产用滚动而非硬重启)
cp /etc/kubernetes/kubelet.conf /backup/kubelet.conf.$(date +%s)
sed -i 's/--anonymous-auth=true/--anonymous-auth=false/' \
/etc/kubernetes/kubelet.conf
systemctl restart kubelet
3. KubeBench + SLS 定时巡检打分架构图
把每次扫描结果推到阿里云 SLS(日志服务),做趋势看板与阈值告警。
flowchart LR
A[CronJob 每周触发] --> B[kube-bench Pod]
B -->|JSON 结果| C[(SLS 日志库)]
C --> D[打分仪表盘]
D -->|低于阈值| E[告警通知 SRE]# 用 CronJob 定时跑 kube-bench 并上报(节选)
apiVersion: batch/v1
kind: CronJob
metadata: { name: kube-bench-weekly }
spec:
schedule: "0 3 * * 0"
jobTemplate:
spec:
template:
spec:
containers:
- name: kube-bench
image: aquasec/kube-bench:latest
args: ["run","--targets","master,node,etcd,policies","--json"]
restartPolicy: Never
4. kubelet 匿名认证开启:一键修复并审计责任人
匿名认证(anonymous-auth=true)意味着"不报身份也能调 kubelet",高危。修复要可审计是谁开的、谁修的。
# 一键修复:关闭匿名认证 + 记录审计注解
kubectl get node -o name | while read n; do
ssh "$n" "sed -i 's/anonymous-auth=true/anonymous-auth=false/' \
/etc/kubernetes/kubelet.conf && systemctl restart kubelet"
# 审计:在 CMDB/工单里记录 节点+操作人+时间
echo "$(date -Iseconds) fixed anonymous-auth on $n by sre-remediation" \
>> /var/log/security-audit.log
done
5. 面试速答
- “等保怎么落地到 K8s?” → 用 kube-bench 跑 CIS Benchmark,FAIL 项脚本化修复,结果进 SLS 做持续打分。
- “匿名认证为什么危险?” → 不认证即可访问 kubelet 只读/甚至执行接口,等于给内网留后门;必须关且留审计。
八、11.3 敏感数据分级脱敏:Vault 格式保持加密
1. 白话直觉:脱敏是"把身份证号打码",但不是乱码
普通加密会把 4111 1111 1111 1111 变成一串毫无格式的密文,数据库字段长度、校验位全变,应用存不下。 格式保持加密(FPE) 像"把数字整体平移",结果仍是合法卡号格式,应用无感,但原始值不可还原。
2. Vault Transform(FPE)加密信用卡号
Vault 的 Transform 引擎支持 FPE,配置一个 transformation + 一个会随密钥轮换的"令牌化"算法。
# ① 启用 transform 引擎并定义 FPE 转换
vault secrets enable transform
vault write transform/transformation/ccn-fpe \
type=fpe \
template=ccn \
tweak_source=internal \
allowed_roles=ccn-role
# ② 定义模板:只保留最后 4 位可见,其余格式保持加密
vault write transform/template/ccn \
type=regex \
pattern='(\d{4}-\d{4}-\d{4}-)(\d{4})' \
alphabet=builtin/numeric
# ③ 加密 / 解密示例
vault write transform/encode/ccn-role \
transformation=ccn-fpe value="4111-1111-1111-1111"
# 输出仍是 16 位数字,格式合法但值已变
3. Benthos + Kubernetes 日志实时脱敏(ConfigMap 示例)
Benthos 是轻量流处理,能在日志落盘前用 awk/processor 把敏感字段替换成 ***。下面是一个 ConfigMap,定义脱敏管道。
apiVersion: v1
kind: ConfigMap
metadata:
name: benthos-redact
data:
config.yaml: |
input:
kafka: # 从 Kafka 读原始日志
addresses: [ "kafka:9092" ]
topics: [ "app-logs" ]
pipeline:
processors:
- awk: | # 正则把卡号打码
{
gsub(/[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}/, "****-****-****-****")
gsub(/1[3-9][0-9]{9}/, "***") # 手机号
print
}
output:
kafka: # 脱敏后写回另一主题
addresses: [ "kafka:9092" ]
topic: "app-logs-redacted"
# 以 Sidecar 形式注入到业务 Pod,实时拦截日志流
kubectl apply -f benthos-redact-cm.yaml
4. 脱敏规则更新:对历史 S3 数据批量重放脱敏
新规则上线,历史日志(已存 S3)也要按新规则重脱敏,否则"旧日志仍含明文"。用一次性的 Spark/Arrow 作业重放。
# 伪流程:拉取 S3 历史日志 → 用新 Benthos 配置重处理 → 覆盖写回
aws s3 sync s3://logs-raw/2024-01/ /tmp/raw/
benthos -c new-redact.yaml <<< "$(cat /tmp/raw/*.log)" > /tmp/redacted/
aws s3 sync /tmp/redacted/ s3://logs-raw/2024-01/ --delete
# 注意:写回前确认新规则不会破坏不可篡改(WORM)存储的合规要求。
5. 面试速答
- “为什么用 FPE 而不是普通 AES?” → 普通加密破坏格式与长度,应用字段不兼容;FPE 保持格式,业务无感。
- “历史数据怎么补脱敏?” → 用新规则对 S3 存量做一次性重放作业,覆盖写回(注意 WORM 合规需另存版本)。
九、11.4 镜像漏洞扫描阻断:把供应链关进笼子
1. 白话直觉:上架前先过安检
镜像就像上架的商品,CI 阶段用 Trivy 扫描,高危漏洞直接拦下不让发布;对某些"明知有但暂时修不了"的组件,放进白名单放行。再往上,用 Cosign 给镜像签名+出证明,确保"线上跑的确实是我审过的那个"。
2. Harbor + Trivy:CI 阻断高危漏洞 + 白名单
Harbor 内置 Trivy,可在项目级设"阻止严重/高危"。CI 里也可先本地扫,按退出码决定是否继续。
# CI 步骤:扫描并阻断(CVE 严重/高危且不在白名单则非零退出)
trivy image --exit-code 1 --severity CRITICAL,HIGH \
--ignorefile .trivyignore \ # 白名单:已知可接受 CVE
registry.example.com/orders:1.2.3
# .trivyignore 示例(明确记录"为何豁免")
# CVE-2023-1234 仅影响我们不使用的 CLI 子命令,本期接受
CVE-2023-1234
# Harbor 项目策略(图形化等价的 API 表达)
# 在 Harbor UI: 项目 -> 配置 -> 漏洞扫描 -> 阻止高风险漏洞 = 开启
# 严重/高危 = 阻断;中/低 = 仅告警
3. Cosign + In-Toto 镜像供应链证明(Attestation)
签名证明"镜像来源可信";In-Toto 的 attestation 证明"构建过程合规"(谁、用什么源码、在什么环境构建)。
# ① 用 Cosign 对镜像签名(密钥来自 KMS/硬件)
cosign sign --key kms://hashicorp-vault/transit/cosign \
registry.example.com/orders:1.2.3
# ② 生成构建来源证明(SLSA/In-Toto 格式)
cosign attest --key kms://hashicorp-vault/transit/cosign \
--type slsaprovenance \
--predicate ./provenance.json \
registry.example.com/orders:1.2.3
# ③ 验证:签名 + 证明都过才允许准入(接 Kyverno/Gatekeeper)
cosign verify --key kms://... registry.example.com/orders:1.2.3
cosign verify-attestation --key kms://... registry.example.com/orders:1.2.3
4. 扫描积压:用 Kueue 对扫描 Pod 优先级调度
大仓里一次提几百个镜像,扫描 Pod 排成长队。用 Kueue 做队列与优先级,让"生产发布"的扫描先跑,“临时分支"的靠后。
# Kueue 队列 + 优先级(节选)
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata: { name: scan-queue, namespace: ci }
spec:
clusterQueue: scan-cq
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: Workload
metadata: { name: scan-prod-123, namespace: ci }
spec:
priority: 100 # 生产高优先级
queueName: scan-queue
podSets:
- name: scanner
template:
spec:
containers:
- name: trivy
image: aquasec/trivy:latest
args: ["image","registry.example.com/orders:1.2.3"]
5. 面试速答
- “扫描阻断会不会误伤?” → 用
.trivyignore白名单 + 注明豁免理由,平衡安全与交付效率。 - “签名和证明区别?” → 签名证"来源”,attestation 证"构建过程合规",两者结合才是完整供应链可信。
十、11.5 审计日志与电子取证:让操作留痕、可还原
1. 白话直觉:银行的监控录像 + 法医复现
审计日志是"谁在什么时候对集群做了什么"的不可篡改记录(等保强要求)。电子取证更进一层:服务崩了、Pod 被删了,能不能还原它死前的现场(内存状态)?
2. Kubernetes Audit Policy:只记 metadata,不记 requestObject
审计可能记录请求体(requestObject),但那里面常有 Secret、token。合规上常要求只记元数据(谁、什么资源、什么动作),不记敏感正文。
# apiserver 审计策略(节选)
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata # 只记 who/what/when,不记 requestObject/responseObject
verbs: ["create","update","delete","patch"]
omitStages: ["RequestReceived"]
- level: None # 只读 get/list/watch 不审计,降量
verbs: ["get","list","watch"]
# 给 kube-apiserver 挂上该策略
# --audit-policy-file=/etc/kubernetes/audit-policy.yaml
# --audit-log-path=/var/log/k8s-audit.log
3. Elastic + WORM 不可篡改审计日志(ILM 策略)
WORM(Write Once Read Many)保证日志写后不可改/删。Elastic 的 ILM 可以把热数据滚动到只读 + 冻结阶段,配合底层对象存储的 WORM 属性满足等保"留存不可篡改"。
{
"policy": {
"phases": {
"hot": { "actions": { "rollover": { "max_age": "7d", "max_size": "50gb" } } },
"warm": { "actions": { "readonly": {} } },
"cold": { "actions": { "freeze": {} } },
"delete": { "min_age": "365d", "actions": { "delete": {} } }
}
}
}
# 在对象存储侧开启 WORM(以阿里云 OSS 合规保留策略为例)
ossutil legal-hold --mode COMPLIANCE oss://audit-logs/
# 开启后该桶对象在保留期内不可删除/覆盖,满足等保留存要求。
4. 取证已删除 Pod:CRIU + checkpoint 还原内存现场
Pod 被删,常规手段只能看日志。若提前用 CRIU(Checkpoint/Restore In Userspace) 对运行中 Pod 做了 checkpoint(内存+状态快照),删除后仍能还原其"死亡瞬间"的内存现场做法医分析。
# ① 对目标 Pod 做 checkpoint(需启用 K8s 1.25+ 的 checkpoint API 或 CRIU 直连)
kubectl alpha checkpoint pod orders-xyz -n prod
# 生成 checkpoint 归档(含内存页、寄存器、打开的文件描述符)
# ② 在隔离环境还原,离线取证(不连生产网络)
criu restore --images-dir /checkpoints/orders-xyz/ \
--restore-detached
# 还原后可 gdb 附加、dump 堆、查未落盘的敏感状态。
flowchart TD
A[运行中 Pod] -->|定期 CRIU checkpoint| B[内存快照归档]
B --> C[Pod 被删/异常]
C --> D[取证需求触发]
D --> E[隔离环境 criu restore]
E --> F[内存现场分析: 堆/句柄/未落盘状态]5. 面试速答
- “审计为什么要 level=Metadata?” → 避免 requestObject 里的 Secret/token 落盘泄露,同时满足"谁动了集群"的合规留痕。
- “Pod 没了怎么取内存证?” → 靠事前 CRIU checkpoint 留快照,事后在隔离环境 restore 还原内存现场。
自测题与动手练习
概念题
- 根 CA 轮换时,为什么不需要重启 5k 个 Sidecar?请说出信任锚重叠的核心机制。
- Istio 默认不依赖 CRL 做 mTLS 吊销,靠的是什么机制?什么场景下才需要 CRL?
- DestinationRule 的 OutlierDetection 只能作用到 subset 粒度,如何实现对"特定 URL Path"独立熔断?
- OPA Gatekeeper 的策略更新把所有新建 Pod 拒了,请给出不超过 3 步的止血方案。
- 头采样(head sampling)与尾采样(tail sampling)的本质区别是什么?各自适合什么场景?
- HTTPS 不解密内容的前提下,出口流量审计能拿到什么信息?靠什么协议字段?
- 等保 2.0 落地到 K8s,kube-bench 跑出的 FAIL 项应该如何既修复又可审计?
- 格式保持加密(FPE)相比 AES 普通加密,解决了什么工程痛点?
- Cosign 的"签名"和"In-Toto attestation"分别证明了什么?两者为何要结合?
- Kubernetes Audit Policy 设
level: Metadata而非RequestResponse,主要为了避免什么风险?
动手练习
- 用
opa test为你常用的一个准入约束写两个测试用例(一条应被拒绝、一条应通过),并贴上运行结果。 - 在本地起一个 Vault dev server,配置 Transform FPE 把
4111-1111-1111-1111加密再解密,验证格式保持。 - 用
trivy image扫一个你自己的镜像,把其中一条可接受的 CVE 写进.trivyignore并注明豁免理由。 - 画一张你们集群当前的出口流量拓扑图,标出 Egress Gateway 与 SNI 审计点;找出一个"当前无审计盲区"。
本章小结
本篇把"服务网格与零信任"和"合规治理"串成了一条主线:身份可验证、流量可管控、操作可审计。
- 证书与身份(4.1 / 4.3):根 CA 靠双信任锚重叠实现无重启轮换;SPIFFE 给工作负载发动态身份,OPA 据身份做零信任授权,且策略必须可测试、可回滚。
- 流量韧性(4.2 / 4.4):熔断要细化到 path/subset,集群级限流用 Redis+Lua 保证令牌守恒;追踪采样用尾采样精准保留错误与慢请求,写 Kafka 慢时按 consumer lag 分责。
- 出口与合规(4.5 / 11.x):Egress 审计靠 SNI 不解密;CIS 基线用 kube-bench 持续打分;敏感数据用 FPE 脱敏;镜像供应链用 Trivy 阻断 + Cosign 签名;审计日志用 WORM 保不可篡改,必要时 CRIU 还原内存现场取证。
面试时记住一句话总则:“零信任不是某个产品,而是’永不默认信任、每次都校验、全程留痕迹’的设计哲学。” 任何具体技术(Istio/OPA/Vault/Cilium)都是这条哲学在某个层面的落地。
- 根 CA 轮换不重启 Sidecar:靠双信任锚重叠——新旧根同时信任一段时间,Sidecar 无需重新拉取证书。
- SPIFFE + OPA:SPIFFE 给工作负载发动态身份,OPA 凭身份做授权,策略可测试可回滚。
- 熔断细化到 Path/Subset:DestinationRule 粒度控制,集群级限流用 Redis+Lua 保令牌守恒。
- 合规三件套:kube-bench 打分 + FPE 脱敏 + Cosign 签名 + CRIU 取证,审计日志 WORM 保不可篡改。
- 下一篇我们讲 KEDA 弹性伸缩——它是可观测性与资源成本之间的桥梁,把指标转成扩缩决策。