
最近折腾了一个云安全扫描工具,从想法到跑通Docker镜像花了30天,中间踩了几个坑,这篇把整个过程说清楚。
让我睡不着觉的问题
每周都会看到新的数据泄露新闻:S3存储桶配置错误、IAM用户拿着管理员权限却没人管、SSH 22端口直接暴露给0.0.0.0/0、安全组半年没人审查过。
最让人无语的是什么?这些根本不是高级漏洞,就是清单没检查到位。这类问题本来应该被自动捕获的。
所以我决定自己做一个工具——能按需扫描任何阿里云账号、用机器学习给每个资源打风险分、从操作日志里检测异常、实时给出修复建议。给它起了个名字叫"AI Cloud Security Guardian"。
下面详细说说怎么实现的、每个云服务在架构里干什么、以及我踩过的坑。
这个平台能做什么
先说Guardian实际运行的流程,技术细节后面再讲:
整个后端是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发现每个运行中的实例并收集:
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标记的内容:
为什么选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保存到磁盘。在实际生产部署中,如果有真实的历史安全结果,你会把合成训练数据替换成过去扫描中的实际标注安全发现——让模型随着每次扫描越来越准确。
风险等级:
安全架构决策
做一个处理云凭证的工具,迫使我在每一层都认真考虑安全问题。
凭证绝不触碰存储
最重要的设计决策:云凭证绝不存储在任何地方。不存数据库、不存日志、不存浏览器localStorage。凭证只存在于:
useState),仅在弹窗期间扫描完成后——凭证超出作用域,被垃圾回收
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变量里——不在localStorage或sessionStorage。这防止XSS攻击窃取token,代价是页面刷新会丢失会话(对安全工具来说可以接受)。
// 仅存内存——XSS无法通过document.cookie或localStorage读取
let _accessToken: string | null = null
自动登出计时器根据JWT的exp声明设置,加上30秒缓冲。当token快要过期时,用户被自动登出。
每层都做输入验证
field_validator验证,在任何云调用之前safeJsonParse()阻止__proto__和constructor键,防止用户提交的日志数据出现原型污染技术栈
后端:
python-jose — 无状态认证前端:
DevOps:
踩过的坑和真正学到的东西
坑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的CORSMiddleware在allow_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类型问题、缺少模块声明、类型断言错误。
修复方法是综合的:
tsconfig.json里设置"strict": false和"noUnusedLocals": false用于构建(import.meta as any).env访问import.meta.env,绕过严格类型检查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/...