Skip to content

Project 02 / 03Open source

CloudGuard

Autonomous AWS security remediation: finds and fixes public S3 buckets, world-open SSH, and orphaned EC2 on a schedule.

CloudGuard architecture
CloudGuard architectureAn EventBridge schedule invokes a Python 3.12 scanner Lambda on arm64 that runs under a 12-action least-privilege IAM role. It scans S3 buckets, security groups, and opt-in tagged EC2 instances, remediates unambiguous findings, holds SSH rules wider than TCP/22 in a manual-review state, records findings and state in a DynamoDB single table with TTL, and publishes notifications to SNS. A React and TypeScript dashboard on S3 behind CloudFront with Origin Access Control reads findings through an API Gateway HTTP API.SCANNED & REMEDIATED · SCOPED BY IAM + AWS:RESOURCETAGinvokefindingsnotifyscan · remediatequeryHTTPSEventBridgescheduleScanner LambdaPython 3.12 · arm6412-action least-privilege roleDynamoDBsingle-table · TTLSNSnotificationsAPI GatewayHTTP APIDashboardReact · TypeScriptS3 + CloudFront (OAC)S3 bucketspublic → privateSecurity groupsworld-open SSH → revokedwider than TCP/22 → reviewEC2 instancesorphaned · opt-in tag only
Terraform (31 resources, S3 remote state). GitHub Actions with OIDC, Terraform plan posted as PR comments, apply gated on reviewer approval.

Problem

The same three misconfigurations keep showing up in AWS accounts: a bucket made public, a security group open to the world on SSH, and instances nobody owns any more. Finding them is easy; fixing them safely and automatically, without handing an automated tool the power to break things, is the hard part.

CloudGuard runs on a schedule, remediates the unambiguous cases itself, and hands everything else to a human.

Architecture

  • Python 3.12 Lambda functions on arm64, triggered by an EventBridge schedule.
  • Findings and remediation state in a DynamoDB single-table design with TTL, so records expire on their own.
  • SNS for notifications; an API Gateway HTTP API serves the dashboard.
  • React/TypeScript dashboard hosted on S3 behind CloudFront with Origin Access Control.
  • Detects and remediates public S3 buckets, world-open SSH ingress, and orphaned EC2 instances; ingress rules wider than TCP/22 move to a manual-review state instead of being revoked.

Key decisions & trade-offs

  • Remediate automatically only where the fix is unambiguous. Rules wider than TCP/22 wait in a manual-review state, so some findings need a person, but the service never revokes a rule it cannot reason about.
  • Least privilege by construction. The scanner role has exactly 12 actions and no delete, terminate, or authorize permissions: some remediations are impossible by design, which is the point.
  • Defence in depth on EC2. The opt-in tag is checked in the API filter, in the code, and in an aws:ResourceTag IAM condition, so a bug in one layer cannot widen the blast radius.
  • DynamoDB single-table with TTL. No cleanup job and no relational database to run; the trade-off is designing access patterns up front.
  • arm64 Lambda. Cheaper per millisecond than x86; dependencies need arm builds.
  • Reviewer-gated apply. Terraform plans are posted on pull requests and nothing is applied until a reviewer approves, at the cost of slower infrastructure changes.

Security, cost & testing

  • Security: 12-action least-privilege scanner IAM role with no delete/terminate/authorize; EC2 opt-in tag enforced in the API filter, the code, and an aws:ResourceTag IAM condition.
  • Infrastructure: Terraform with 31 resources and S3 remote state; GitHub Actions with OIDC, PR plan comments, reviewer-gated apply.
  • Cost: runs inside the AWS free tier.
  • Tests: 59 pytest cases, 98% line coverage, zero live AWS calls in the suite.

What I'd do differently

  • Close the loop on manual review. Rules wider than TCP/22 are parked for a human and SNS says so, but nothing lets that human act from the notification. An approval action with an expiring token would turn a message into a decision.
  • Scope by tag everywhere, not only for EC2. The opt-in tag enforced in the API, the code, and an aws:ResourceTag condition is the right pattern; extending it to buckets and security groups would bound the blast radius to resources someone chose to enrol.
  • Test the IAM policy, not just the code. 59 pytest cases cover the logic with zero live AWS calls, which is right for CI, but a permission regression would still pass. IAM policy simulation in the merge gate would make sure the 12-action role can only ever shrink.