site logo

Marico's space

30天从创意到Docker:我是如何用AWS构建AI云安全守护者的

服务器技术 2026-05-15 11:33:02 8

最近折腾了一个云安全扫描工具,从想法到跑通Docker镜像花了30天,中间踩了几个坑,这篇把整个过程说清楚。

让我睡不着觉的问题

每周都会看到新的数据泄露新闻:S3存储桶配置错误、IAM用户拿着管理员权限却没人管、SSH 22端口直接暴露给0.0.0.0/0、安全组半年没人审查过。

最让人无语的是什么?这些根本不是高级漏洞,就是清单没检查到位。这类问题本来应该被自动捕获的。

所以我决定自己做一个工具——能按需扫描任何阿里云账号、用机器学习给每个资源打风险分、从操作日志里检测异常、实时给出修复建议。给它起了个名字叫"AI Cloud Security Guardian"。

下面详细说说怎么实现的、每个云服务在架构里干什么、以及我踩过的坑。

这个平台能做什么

先说Guardian实际运行的流程,技术细节后面再讲:

  1. 登录SOC风格的操作面板
  2. 输入你的阿里云RAM凭证——绝不存储,只在当次会话使用
  3. Guardian通过SDK连接你的云账号,遍历所有ECS实例、OSSbucket、RAM用户、RAM角色和安全组
  4. 规则引擎对每个资源执行9项配置检查
  5. Isolation Forest模型对操作日志做异常检测
  6. 生成告警、按严重程度排序、在面板上展示AI生成的修复步骤

整个后端是FastAPI + Python。前端是React + TypeScript + Tailwind。全套跑在Docker里。连接的是真实的云账号——不是模拟数据。

用到的云服务和选型理由

阿里云STS — GetCallerIdentity

Guardian发出的第一个API调用是sts:GetCallerIdentity。在扫描之前,先验证凭证是否有效,并告诉你当前使用的是哪个账号和哪个RAM身份。

sts = session.client("sts")
identity = sts.get_caller_identity()

返回:Account ID、ARN、UserId

这是成本最低的API调用——任何有效凭证都允许、完全免费,而且能在用坏密钥跑完整60秒扫描之前快速失败。如果这个调用失败,Guardian会直接告诉你具体原因——密钥无效、Token过期、地域错误——而不是扫到一半静默失败。

学到的经验:先用STS验证凭证,再做任何其他云操作。能节省大量调试时间,还能给用户清晰的错误提示。

阿里云ECS — DescribeInstances + DescribeSecurityGroups

对ECS实例,Guardian使用两个SDK调用:

DescribeInstances发现每个运行中的实例并收集:

  • 实例ID、类型和状态
  • 公网IP(是否暴露在互联网?)
  • 关联的安全组ID
  • RAM实例角色(权限是否正确?)
  • 密钥对名称(访问有记录吗?)

DescribeSecurityGroups检查每条入方向规则,找出阿里云里最危险的配置错误——端口暴露给0.0.0.0/0:

def _check_open_to_world(rules):
for rule in rules:
for ipv4 in rule.get("IpRanges", []):
if ipv4.get("CidrIp") == "0.0.0.0/0":
return True # CRITICAL finding
return False

当Guardian发现SSH(22端口)或RDP(3389端口)对整个互联网开放时,立即触发Critical告警。这个检查在实际账号扫描中捕获了最严重的问题。

触发的检测规则:

  • SG_001 — 对0.0.0.0/0开放(Critical)
  • ECS_001 — 有公网IP但没有RAM实例角色(Medium)
  • ECS_002 — 运行中的实例没有密钥对(Low)

阿里云OSS — 多API Bucket分析

OSS是大多数数据泄露的起点。Guardian对每个bucket运行五个独立的API调用:

API调用 检查内容 缺失时的严重程度
get_public_access_block 全部4个阻止公网访问的开关 Critical
get_bucket_encryption 是否启用服务端加密 High
get_bucket_logging 访问日志是否配置 Medium
get_bucket_versioning 对象版本控制是否启用 Info
get_bucket_location 地域(用于上下文)

公网访问阻止检查是最重要的。阿里云有四个独立的开关来阻止公网访问(BlockPublicAcls、IgnorePublicAcls、BlockPublicPolicy、RestrictPublicBuckets)。Guardian检查全部四个是否启用——只要有一个是False,bucket就被标记为潜在公开:

fully_blocked = all([
cfg.get("BlockPublicAcls", False),
cfg.get("IgnorePublicAcls", False),
cfg.get("BlockPublicPolicy", False),
cfg.get("RestrictPublicBuckets", False),
])
data["is_public"] = not fully_blocked

学到的经验:没有公网访问阻止配置和把它设为False是两回事。如果get_public_access_block抛出NoSuchPublicAccessBlockConfiguration异常,说明bucket完全没有保护——Guardian把这种情况视为is_public = True

阿里云RAM — 权限和MFA(多因素认证)分析

RAM扫描是Guardian发现最高严重级别问题的地方。三个API调用覆盖关键检查:

ListAttachedUserPolicies — 检查每个用户是否有AdministratorAccess。一个拥有完全管理员权限且没开MFA的RAM用户,一旦凭证泄露,游戏结束。

ListMFADevices — 对任何有控制台访问的用户,Guardian检查是否启用了MFA。控制台用户没开MFA是自动High级别问题。

GetLoginProfile — 判断用户是否有控制台访问。服务账号绝对不应该有控制台密码。

阿里云里最危险的组合:

控制台访问 + AdministratorAccess + 无MFA

if user.has_console_access and user.is_admin and not user.has_mfa:
# 这是三响警报
generate_critical_alert(user)

RAM扫描也覆盖角色——任何附加了AdministratorAccess的角色都被标记为High,因为被攻陷的ECS实例或函数如果绑定了这个角色,影响范围是无限的。

触发的检测规则:

  • RAM_001 — 用户有AdministratorAccess(Critical)
  • RAM_002 — 控制台用户没有MFA(High)
  • RAM_003 — 角色有AdministratorAccess(High)

阿里云操作审计 — 日志分析与异常检测

这里是机器学习真正上场的地方。操作审计记录账号里每次API调用。Guardian摄入这些事件,用Isolation Forest——一种无监督机器学习算法——识别统计异常点,不需要标注好的训练数据。

特征提取在时间窗口内聚合每个用户的行为:

features = {
"api_call_count": 1, # API调用量
"failed_logins": 1, # 拒绝访问错误
"hour_of_day": 3, # 异常时段 = 可疑
"is_new_region": True, # 从未见过这个地域
"bytes_transferred": 0,
"unique_resources": 1,
}

Isolation Forest通过随机划分特征空间来工作。异常数据点——比如某个用户突然在凌晨3点从新地域发出5000次API调用、带着50个Access Denied错误——会被快速隔离,因为它们远离正常分布。算法给出异常分数,越低越异常。

Guardian标记的内容:

  • 异常API调用量(可能是凭证被盗 / 挖矿)
  • 来自之前未见过地域的访问(可能是账号被接管)
  • 异常时段的访问模式(横向移动)
  • 重复的AccessDenied错误(暴力破解 / 权限提升尝试)

为什么选Isolation Forest而不是监督学习?因为你几乎不可能有标注好的"恶意"操作日志来训练。Isolation Forest不需要标签——它只学习"正常"是什么样,然后标记偏离。这正是真实SOC工具的工作方式。

机器学习风险评分引擎

除了基于规则的检测,每个资源还会通过Random Forest分类器获得从0.0到1.0的连续风险评分。

模型使用从每个资源派生的6个特征:

FEATURES = [
"public_access", # 0/1 — 是否互联网暴露?
"open_ports", # 对全网开放的入方向规则数量
"encryption_enabled", # 0/1 — 数据是否加密存储?
"iam_privilege_level", # 0=无, 1=只读, 2=读写, 3=管理员
"mfa_enabled", # 0/1 — 是否强制MFA?
"logging_enabled", # 0/1 — 审计日志是否开启?
]

模型在启动时用合成数据训练,用joblib保存到磁盘。在实际生产部署中,如果有真实的历史安全结果,你会把合成训练数据替换成过去扫描中的实际标注安全发现——让模型随着每次扫描越来越准确。

风险等级:

  • Critical — 评分 ≥ 0.75
  • High — 评分 ≥ 0.55
  • Medium — 评分 ≥ 0.35
  • Low — 评分 ≥ 0.15
  • Minimal — 评分 < 0.15

安全架构决策

做一个处理云凭证的工具,迫使我在每一层都认真考虑安全问题。

凭证绝不触碰存储

最重要的设计决策:云凭证绝不存储在任何地方。不存数据库、不存日志、不存浏览器localStorage。凭证只存在于:

  1. 用户浏览器的状态里(React useState),仅在弹窗期间
  2. HTTP请求体中,传输过程中
  3. Python函数参数中,扫描期间
  4. 扫描完成后立即清除

扫描完成后——凭证超出作用域,被垃圾回收
def run_full_scan_with_credentials(access_key_id, secret_access_key, ...):
... 扫描进行中 ...
return result
access_key_id和secret_access_key绝不会被写入任何地方

后端只记录密钥前缀(AKIA...8chars...)用于调试——绝不记录完整密钥或Secret。

JWT存在内存里,不在localStorage

面板的JWT token存在模块级JavaScript变量里——不在localStoragesessionStorage。这防止XSS攻击窃取token,代价是页面刷新会丢失会话(对安全工具来说可以接受)。

// 仅存内存——XSS无法通过document.cookie或localStorage读取
let _accessToken: string | null = null

自动登出计时器根据JWT的exp声明设置,加上30秒缓冲。当token快要过期时,用户被自动登出。

每层都做输入验证

  • 前端:Access Key ID格式用正则验证,所有字段长度检查
  • 后端:每个凭证字段用Pydantic field_validator验证,在任何云调用之前
  • JSON解析:safeJsonParse()阻止__proto__constructor键,防止用户提交的日志数据出现原型污染

技术栈

后端:

  • FastAPI — 异步Python web框架,自动生成OpenAPI文档
  • SQLAlchemy + SQLite(开发)/ PostgreSQL(生产) — findings存储的ORM
  • boto3 — 云SDK,所有凭证操作
  • scikit-learn — Random Forest(风险评分)+ Isolation Forest(异常检测)
  • Pydantic v2 — 请求验证和设置管理
  • JWT via python-jose — 无状态认证

前端:

  • React 18 + TypeScript — 组件框架
  • Tailwind CSS — 工具优先样式,自定义SOC终端设计令牌
  • React Query (TanStack) — 服务端状态管理,带缓存
  • Recharts — 风险评分可视化(柱状图、饼图、雷达图)
  • Axios — HTTP客户端,带请求/响应拦截器
  • DOMPurify — XSS清理,用于任何服务器返回的字符串

DevOps:

  • Docker — 多阶段构建(builder → slim运行时),两个服务都这样
  • Docker Compose — 编排PostgreSQL + FastAPI + Nginx为一个栈
  • Kubernetes — 7个清单,覆盖namespace、secrets、deployments、ingress和HPA自动扩缩容
  • Nginx — 前端容器的反向代理,生产环境完全消除CORS问题

踩过的坑和真正学到的东西

坑1:变量名冲突导致每次扫描都500

好几天,每个扫描请求都返回500 Internal Server Error。后端日志显示TypeError: 'bool' is not callable。调了好几个小时才发现:

有问题的代码——参数名scan_security_groups遮蔽了同名函数
def run_full_scan(scan_security_groups: bool = True):
...
"security_groups": scan_security_groups(session) # 调用了一个bool!

函数scan_security_groups()和布尔参数scan_security_groups同名了。Python用了参数而不是函数。修复方法:所有内部扫描函数加_do_前缀:

"security_groups": _do_scan_security_groups(session) if scan_security_groups else []

教训:Python里,函数参数在它们的作用域内会遮蔽模块级名称。给参数起名时要明确,避免与它们可能调用的函数冲突。

坑2:那个不是CORS的CORS问题

前端被CORS策略阻止——但后端明明设置了allow_origins=["*"]。折腾了一个下午,才发现症结所在:FastAPI的CORSMiddlewareallow_origins=["*"]allow_credentials=True同时设置时是不兼容的。同时设置两者违反CORS规范,FastAPI会静默破坏中间件。

最终修复甚至跟CORS中间件没关系——而是开发时切换到Vite代理。浏览器调用localhost:5173/api/scan/aws,Vite在服务端转发到localhost:8000/scan/aws。浏览器根本没发出跨域请求,CORS不适用。

教训:开发环境下正确的CORS修复方案是代理,不是CORS头。CORS配置留到生产环境真正需要的时候再用。

坑3:TypeScript严格模式 vs Docker构建

代码在本地用VS Code的TypeScript服务器编译没问题。但Docker构建时跑的是严格模式的tsc,发现了15个错误——未使用参数、import.meta.env类型问题、缺少模块声明、类型断言错误。

修复方法是综合的:

  1. tsconfig.json里设置"strict": false"noUnusedLocals": false用于构建
  2. 通过(import.meta as any).env访问import.meta.env,绕过严格类型检查
  3. tsconfig.json移除"references",这样构建时不会在Docker容器里找tsconfig.node.json

教训:在你觉得完成之前,一定要在CI上测试Docker构建。本地TypeScript编译和Docker builder阶段的编译行为可能很不一样。

最小权限RAM策略

给想扫描自己账号的人,这里是需要的最少权限集:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sts:GetCallerIdentity",
"ecs:DescribeInstances",
"ecs:DescribeSecurityGroups",
"oss:ListBuckets",
"oss:GetBucketPublicAccessBlock",
"oss:GetBucketEncryption",
"oss:GetBucketLogging",
"ram:ListUsers",
"ram:ListRoles",
"ram:ListMFADevices",
"ram:ListAttachedUserPolicies",
"ram:GetLoginProfile",
"actiontrail:LookupEvents"
],
"Resource": "*"
}
]
}

创建一个专用RAM用户,只给这个策略。绝不要用根凭证或个人管理员账号。

完整项目——后端、前端、Docker和Kubernetes清单——在GitHub上。

技术栈汇总:FastAPI · boto3 · scikit-learn · React · TypeScript · Tailwind · Docker · Kubernetes

如果你做云安全、GRC(公司治理风险与合规)或DevSecOps,想聊聊架构或合作,欢迎联系我。

原文链接:https://dev.to/...