site logo

Marico's space

在公开暴露发生前加以预防:基于事件驱动的 AWS 安全护栏,结合 Terraform、Lambda 与 EventBridge

AI技术与应用 2026-11-06 14:49:08 9

最近折腾 AWS 安全护栏时,踩了几个异步处理和自动修复的坑,这篇把整套方案、代码和验证过程说清楚。重点不是再造一个定时扫描器,而是在危险配置真正生效前,直接盯着 API(应用程序接口)调用流。

实施状态:这套护栏已经部署到 AWS(Amazon Web Services,亚马逊云服务),并使用 EventBridge(事件总线服务)、Lambda(无服务器函数服务)、CloudTrail(云审计服务)、EC2(弹性云计算)、S3(对象存储)、DynamoDB(文档数据库)和 SNS(简单通知服务)完成验证。

引言

大多数云安全工具告诉你风险时,问题往往已经发生了。扫描器按计划跑一遍,结果出现在仪表盘里,然后有人建工单处理。可在这段时间里,一条允许从 0.0.0.0/0 访问 SSH 的安全组规则已经暴露给整个互联网;一个被移除公共访问阻止配置的存储桶,也可能因为一次 ACL(访问控制列表)或存储桶策略变更直接向公网敞开。

这里做出来的护栏直接响应 API 调用本身,不再等下一次扫描。一旦有人把敏感端口开放给全网,或者削弱 S3 存储桶的公共访问阻止配置,Lambda 函数会在几秒内被触发。它可以直接撤销变更,也可以只在演练模式下发送告警。每次处理结果都会写入审计表,团队同时收到邮件。整套基础设施由 Terraform(基础设施即代码工具)创建,并按 AWS 免费套餐的使用规模来设计。

完整项目已经放在我的 GitHub 仓库中,包含 Terraform 代码、修复程序、测试脚本和脱敏后的验证证据,文末有说明。

1. 问题

有两类配置错误,贡献了大量本可以避免的云端暴露:

  • 管理端口向公网开放。 SSH(安全外壳协议,22)和 RDP(远程桌面协议,3389)开放给 0.0.0.0/0 后,通常几分钟内就会被自动化扫描器盯上。
  • S3 存储桶公开访问。 公共访问阻止配置被移除后,存储桶就会变成公开状态。很多时候只是为了让临时测试跑起来,改完就忘了恢复。

这两类问题都很容易引入。一条 CLI(命令行界面)命令或一次控制台点击就够了,而且也很难及时发现:单看某次变更似乎很合理,更关键的是,通常没人盯着 API 调用流。

单纯检测存在天然的短板。定时扫描的发现窗口以小时计算,但危险配置生效只需要几秒。自动修复可以压缩这个窗口,但也会带来新的风险。一个悄无声息破坏正常业务流程的护栏,比没有护栏更糟。因此,这套设计必须回答三个问题:如何快速响应、如何避免误操作,以及事后如何证明到底发生了什么。

2. 架构

整个处理流程分为六步:

  1. 工程师或自动化流水线调用 AuthorizeSecurityGroupIngress、DeleteBucketPublicAccessBlock 或 PutBucketPublicAccessBlock。
  2. CloudTrail 记录这次调用。它的管理事件会以 AWS API Call via CloudTrail 的形式进入默认 EventBridge 总线,因此检测环节不需要额外创建事件轨迹。
  3. EventBridge 规则只匹配这三个与违规直接相关的事件名,并调用修复程序 Lambda,最多重试三次。
  4. Lambda 在 DynamoDB 中认领事件 ID,评估变更内容,再根据演练模式配置执行实际修复或模拟修复。
  5. 处理结果写入发现项表,告警通过 SNS 发布并以邮件形式送达。
  6. 所有重试均失败的事件会进入 SQS(简单队列服务)的死信队列,等待排查。

选择这些服务时,成本和能力同等重要。我没有使用 AWS Config(配置管理服务)和 Security Hub(安全中心),因为它们虽然强大,但会按规则评估或发现项计费;也没有使用客户托管的 KMS(密钥管理服务)密钥,因为这会产生月度费用。Lambda、默认总线上的 EventBridge 规则、按请求计费的 DynamoDB、SNS 邮件通知、SQS,以及保留 14 天的 CloudWatch Logs(日志服务),对于这个规模的负载都可以控制在 AWS 免费套餐内。

3. 设计决策

采用事件驱动,而不是轮询。 护栏直接响应 API 调用,因此能在几秒内采取行动,也不需要为周期性全账号扫描付费。

精确撤销违规规则,只处理开放给全网的部分。 修复程序不会直接清空整个安全组,只删除刚授权的那条规则,而且只移除其中的 0.0.0.0/0 或 ::/0 条目。如果一条规则同时允许访问私有 CIDR(无类别域间路由)网段,私有网段会继续保留。这样可以把修复的影响范围控制在违规项本身。

幂等处理。 EventBridge 至少投递一次,同一个事件可能到达两次。修复程序通过带条件的 PutItem 把事件 ID 写入 DynamoDB。第二次投递会因为不满足条件而失败,并被记录为重复事件。如果处理过程中中途崩溃,程序会在重新抛出异常前删除认领记录,让后续重试可以继续完成,而不是被错误地跳过。

防止循环触发。 修复操作本身也会产生 CloudTrail 事件。撤销入站规则会触发 RevokeSecurityGroupIngress,该事件不在规则的匹配范围内。恢复公共访问阻止配置会触发 PutBucketPublicAccessBlock,这个事件确实会命中规则,但处理程序会先检查存储桶是否已经受到完整阻止;如果已经合规,就不会执行任何操作。最终系统会收敛,不会陷入无限循环。

默认启用演练模式。 首次部署会设置 DRY_RUN=true。它会记录原本准备执行的修改并发送告警,但不会真正改动资源。这样团队可以先观察几天真实流量,再开启强制执行。切换状态只需要修改一个 Terraform 变量。

通过标签设置豁免。 有些资源确实需要公开访问,例如静态网站存储桶或堡垒机。带有 guardrail-exempt=true 标签的资源会被跳过,并记录为豁免。代价是,任何能够添加标签的人都能申请豁免,因此标签写入权限也变成了敏感权限。后面会在限制部分讨论如何治理这一点。

最小权限。 每条 IAM(身份与访问管理)权限声明都对应处理程序里的一个具体 API 调用。Describe 类调用无法按资源范围收窄,因此被单独放在一条声明中。目前已知的缺口是 S3 权限声明覆盖了所有存储桶。后续计划通过标签条件进一步缩小范围,我把这个限制写进了代码,没有藏着。

4. 实现

4.1 检测规则

EventBridge 模式根据源服务和事件名匹配 CloudTrail 事件封装:

event_pattern = jsonencode({ "detail-type" = ["AWS API Call via CloudTrail"] detail = { eventSource = ["ec2.amazonaws.com", "s3.amazonaws.com"] eventName = concat(["AuthorizeSecurityGroupIngress"], local.bucket_events) }
}
)

目标同时配置了重试策略和死信队列。正是这两项配置,把“Lambda 失败了一次”变成了“事件重试了一小时,最终被保存下来等待排查”。

retry_policy { maximum_event_age_in_seconds = 3600 maximum_retry_attempts = 3
} dead_letter_config { arn = aws_sqs_queue.dlq.arn
}

4.2 只撤销违规规则

安全组处理程序会检查 CloudTrail 记录中的每一条授权权限,并构造撤销违规行为所需的最小请求:

def _revoke_rule(perm, v4, v6): rule = {"IpProtocol": perm["ipProtocol"]} if "fromPort" in perm: rule["FromPort"] = perm["fromPort"] rule["ToPort"] = perm["toPort"] if v4: rule["IpRanges"] = [{"CidrIp": cidr} for cidr in v4] if v6: rule["Ipv6Ranges"] = [{"CidrIpv6": cidr} for cidr in v6] return rule

撤销操作必须与原始协议和端口范围完全匹配,所以处理程序会直接从事件中复制这些字段,而不是根据配置的敏感端口重新构造。

4.3 每个事件只认领一次

def _claim(event_id): try: findings.put_item( Item={"event_id": event_id, "status": "PROCESSING"}, ConditionExpression="attribute_not_exists(event_id)", ) return True except ClientError as err: if err.response["Error"]["Code"] == "ConditionalCheckFailedException": return False raise

调用这个函数的包装逻辑会在失败时释放认领记录:

try: outcome = evaluate(detail)
except Exception: findings.delete_item(Key={"event_id": event_id}) raise

如果缺少这段释放逻辑,一次临时 API 错误就会让事件永久停留在处理中状态,后续每次重试都会被直接跳过。

4.4 基础设施

所有资源都定义在五个 Terraform 文件里:versions.tf 固定 AWS 和归档文件提供程序的版本,provider.tf 应用默认标签,variables.tf 暴露演练开关、敏感端口列表和告警地址,main.tf 声明资源,outputs.tf 输出测试与证据脚本需要的名称。处理程序通过归档数据源从 lambda/handler.py 打包,因此代码变化后会产生新的 source_code_hash,下次执行 apply 时 Terraform 就会更新函数。

5. 测试护栏

scripts/simulate-violations.sh 覆盖三个场景:

  1. 在普通安全组上开放 SSH。这应该被判定为违规。
  2. 在带有 guardrail-exempt=true 标签的安全组上开放 SSH。这应该被允许。
  3. 删除新建存储桶的公共访问阻止配置。这应该被判定为违规。

我先在演练模式下运行测试,确认告警和审计记录都正确,且资源没有被修改;随后设置 dry_run = false 并重新执行。

场景 演练模式下的预期结果 强制执行模式下的预期结果 实际结果
1. 开放 SSH,未设置标签的安全组 记录 would_revoke,发送告警,规则仍然存在 撤销规则,发送告警

2. 开放 SSH,设置豁免标签的安全组 none(豁免),不发送告警 相同
3. 删除公共访问阻止配置 记录 would_restore,发送告警 恢复阻止配置,发送告警

从 API 调用到撤销规则之间的时间为:[insert measured seconds from CloudTrail eventTime to the revoke call in the logs]。这就是暴露实际存在的时间窗口,也是拿定时扫描方案作对比时最值得关注的指标。

6. 验证证据

evidence/ 目录保存了本次运行的脱敏输出,账号 ID 已替换为 111122223333。

文件 内容
terraform-plan-summary.json 技术栈创建的资源
remediator-logs-sanitized.json Lambda 的处理决定,包括重复事件和豁免路径
cloudtrail-authorize-ingress-sanitized.json 场景 1 的原始 API 调用
cloudtrail-delete-public-access-block-sanitized.json 场景 3 的原始 API 调用
sg-after-remediation-sanitized.json 强制执行后的安全组状态
screenshots/ 告警邮件、DynamoDB 记录和存储桶配置的控制台截图

7. 成本

如果这个账号只运行当前项目,负载应该能保持在 AWS 免费套餐内:少量 Lambda 调用、几十次 DynamoDB 写入、几封邮件通知,以及少量日志数据。我运行模拟脚本后在 Billing(账单)控制台核对过,实际费用为 [insert observed charges, or "zero"]。价格可能变化,长期运行前仍应检查自己账号的免费套餐使用量。

8. 限制与后续计划

护栏是响应式的,不是预防式的。 从 API 调用到修复完成之间仍有几秒窗口。SCP(服务控制策略)可以直接阻止部分调用,在生产环境中,我会在上层再叠加一层。

它不能发现已经存在的暴露。 护栏只能看到新的 API 调用。还需要对现有安全组和存储桶做一次全面排查,作为补充。

豁免机制需要治理。 豁免依赖标签,因此控制强度取决于标签写入权限。生产版本应当限制受保护资源上的标签修改,或者要求提供审批记录。

存储桶策略和 ACL 不在当前范围内。 这条规则只处理公共访问阻止配置。如果存储桶通过存储桶策略或 ACL 变成公开状态,还需要增加其他规则。

S3 的 IAM 权限声明范围偏大。 它覆盖所有存储桶。后续可以通过基于标签的条件,把权限限制到团队拥有的存储桶。

结语

公开暴露是云环境中常见度最高、也最容易被预防的配置错误之一。扫描最终会发现问题,但暴露从 API 调用成功的那一刻就开始了。这套项目用事件驱动护栏补上了这段缺口:几秒内响应,只撤销违反策略的规则,记录每一次处理决定,并且采用安全失败策略。

真正重要的不是具体用了哪些服务,而是围绕这些服务设计出的行为:先演练再强制执行、幂等处理、死信队列、精确撤销以及明确的豁免路径。正是这些设计,让自动化安全控制可以安全地运行在真实账号中。

GitHub 仓库:AWS-Community-Builder Repository