服务网格与零信任安全:Istio、OPA、SPIFFE 与合规治理

2022-02-11T10:00:00+08:00 | 25分钟阅读 | 更新于 2022-02-11T10:00:00+08:00

@

前言:这一篇在考什么

这一篇把「服务网格(Service Mesh)」和「零信任安全(Zero Trust)」放在一起,再延伸到第 11 章的「合规治理」。面试里它常以两种姿态出现:

  • 实操题:给你一个故障/风险场景,让你配 Istio、OPA、Vault、Cilium。
  • 架构题:问你"为什么这样设计"“出事怎么回滚"“规模上来怎么不崩”。

所以本文的写法不是堆命令,而是先用类比建立直觉,再给可落地的 YAML/代码,最后点出权衡与面试话术。你可以把它当成一份"带答案的复习讲义”。

阅读约定:所有命令假设你已有 kubectlistioctlvaultopagator 等 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/403

6. 面试速答

  • “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 -->|允许/拒绝| C

4. 灾难回滚:OPA 策略更新把所有新建 Pod 拒了,怎么办

这是高频"翻车"场景。三板斧:

  1. 立刻切 dryrun(最快止血):把约束的 enforcementAction 改成 dryrun,新 Pod 不再被拒,只记审计。
  2. 回滚到上一版策略:Gatekeeper 的 Constraint/Template 都是 K8s 资源,用 kubectl apply 重放旧 YAML 即可。
  3. 豁免已有工作负载:新策略只作用于"新建请求",已运行的 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_sizeexporter_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 还原内存现场。

自测题与动手练习

概念题

  1. 根 CA 轮换时,为什么不需要重启 5k 个 Sidecar?请说出信任锚重叠的核心机制。
  2. Istio 默认不依赖 CRL 做 mTLS 吊销,靠的是什么机制?什么场景下才需要 CRL?
  3. DestinationRule 的 OutlierDetection 只能作用到 subset 粒度,如何实现对"特定 URL Path"独立熔断?
  4. OPA Gatekeeper 的策略更新把所有新建 Pod 拒了,请给出不超过 3 步的止血方案。
  5. 头采样(head sampling)与尾采样(tail sampling)的本质区别是什么?各自适合什么场景?
  6. HTTPS 不解密内容的前提下,出口流量审计能拿到什么信息?靠什么协议字段?
  7. 等保 2.0 落地到 K8s,kube-bench 跑出的 FAIL 项应该如何既修复又可审计?
  8. 格式保持加密(FPE)相比 AES 普通加密,解决了什么工程痛点?
  9. Cosign 的"签名"和"In-Toto attestation"分别证明了什么?两者为何要结合?
  10. Kubernetes Audit Policy 设 level: Metadata 而非 RequestResponse,主要为了避免什么风险?

动手练习

  1. opa test 为你常用的一个准入约束写两个测试用例(一条应被拒绝、一条应通过),并贴上运行结果。
  2. 在本地起一个 Vault dev server,配置 Transform FPE 把 4111-1111-1111-1111 加密再解密,验证格式保持。
  3. trivy image 扫一个你自己的镜像,把其中一条可接受的 CVE 写进 .trivyignore 并注明豁免理由。
  4. 画一张你们集群当前的出口流量拓扑图,标出 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 弹性伸缩——它是可观测性与资源成本之间的桥梁,把指标转成扩缩决策。
About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!