<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mario</title>
    <description>The latest articles on DEV Community by Mario (@mariogongora).</description>
    <link>https://dev.to/mariogongora</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3948275%2F1686ad70-9bfd-4283-96f5-f8b3e1c23e3f.jpg</url>
      <title>DEV Community: Mario</title>
      <link>https://dev.to/mariogongora</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mariogongora"/>
    <language>en</language>
    <item>
      <title>Responding to a Compromised AWS Access Key</title>
      <dc:creator>Mario</dc:creator>
      <pubDate>Sun, 14 Jun 2026 22:21:35 +0000</pubDate>
      <link>https://dev.to/mariogongora/responding-to-a-compromised-aws-access-key-1ji3</link>
      <guid>https://dev.to/mariogongora/responding-to-a-compromised-aws-access-key-1ji3</guid>
      <description>&lt;p&gt;You wake up to this email from AWS:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Irregular Activity Detected for Your AWS Access Key&lt;/p&gt;

&lt;p&gt;As part of our standard monitoring of AWS systems, we observed anomalous activity in your AWS account that indicated your AWS access key(s), along with the corresponding secret key, may have been inappropriately accessed by a third party.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your stomach drops. The email links to a compromised access key: &lt;code&gt;AKIA1234567890ABCDEF&lt;/code&gt;. User: &lt;code&gt;app-integration-user&lt;/code&gt;. Event: &lt;code&gt;GetCallerIdentity&lt;/code&gt;. Time: yesterday at 12:11:58 UTC. IP: &lt;code&gt;198.51.100.50&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;AWS gives you four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rotate the key.&lt;/li&gt;
&lt;li&gt;Check CloudTrail for unwanted activity.&lt;/li&gt;
&lt;li&gt;Review account for unexpected usage.&lt;/li&gt;
&lt;li&gt;Respond to the support case.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Four steps. Clean. Linear. Assumes everything goes right.&lt;/p&gt;

&lt;p&gt;It won't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AWS Documentation Assumes
&lt;/h2&gt;

&lt;p&gt;AWS's steps assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudTrail is already enabled and logs are queryable.&lt;/li&gt;
&lt;li&gt;Someone on your team knows how to read CloudTrail.&lt;/li&gt;
&lt;li&gt;You have time to investigate without pressure.&lt;/li&gt;
&lt;li&gt;The only damage is the exposed key.&lt;/li&gt;
&lt;li&gt;Rotating the key is enough to fix it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In reality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudTrail might not be enabled. Or enabled but logs are in an S3 bucket nobody checks.&lt;/li&gt;
&lt;li&gt;The person who set up the account left months ago.&lt;/li&gt;
&lt;li&gt;You have 4 hours before customers start calling about errors.&lt;/li&gt;
&lt;li&gt;The attacker might have created backdoor credentials, roles, or policies while they were in.&lt;/li&gt;
&lt;li&gt;Rotating the key stops them from using &lt;em&gt;that&lt;/em&gt; key. But if they left a trail of IAM users, keys, or assumed roles behind, you're still exposed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Actually Happened
&lt;/h2&gt;

&lt;p&gt;You look at the details. The compromised key belongs to &lt;code&gt;app-integration-user&lt;/code&gt;. A user who was supposed to only send emails via SES. Instead, someone called &lt;code&gt;GetCallerIdentity&lt;/code&gt; from IP &lt;code&gt;198.51.100.50&lt;/code&gt; at 12:11 UTC.&lt;/p&gt;

&lt;p&gt;(If the compromised key is your &lt;strong&gt;root account's access key&lt;/strong&gt;: this is a P1 incident. Root cannot be restricted by IAM policies. Rotate immediately, audit all root activity in the last 30+ days, and contact AWS Security right now.)&lt;/p&gt;

&lt;p&gt;That one call tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The key was exfiltrated (not guessed in a bruteforce).&lt;/li&gt;
&lt;li&gt;The attacker tested it immediately to confirm it works.&lt;/li&gt;
&lt;li&gt;They got basic information about your account and role.&lt;/li&gt;
&lt;li&gt;The next calls happened after that test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now you need to answer: &lt;em&gt;What did they do next?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is where the 4-step plan breaks down. AWS doesn't tell you how to find that out if your logs aren't ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Things That Actually Save You
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Access to CloudTrail, Even If It's Basic
&lt;/h3&gt;

&lt;p&gt;If CloudTrail is off or inaccessible, you're blind. You can't answer the question: &lt;em&gt;What happened after that GetCallerIdentity call?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If CloudTrail is on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudtrail lookup-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lookup-attributes&lt;/span&gt; &lt;span class="nv"&gt;AttributeKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AccessKeyId,AttributeValue&lt;span class="o"&gt;=&lt;/span&gt;AKIA1234567890ABCDEF &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start-time&lt;/span&gt; 2026-05-21T12:00:00Z &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--end-time&lt;/span&gt; 2026-05-21T14:00:00Z &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see every API call made with that key. Not glamorous. Not a dashboard. But it works. And it shows you the sequence: &lt;code&gt;GetCallerIdentity&lt;/code&gt; → what came next.&lt;/p&gt;

&lt;p&gt;From a typical reconnaissance scenario, that query might show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GetCallerIdentity (12:11:58)
ListUsers (12:12:05)
ListAccessKeys (12:12:12)
ListRoles (12:12:19)
ListPolicies (12:12:25)
GetUser (12:12:33, targeting 'admin-user')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attacker was doing reconnaissance. They're mapping your account structure. That tells you what they &lt;em&gt;might&lt;/em&gt; do next: assume the admin role, create a backdoor key, or escalate.&lt;/p&gt;

&lt;p&gt;Without CloudTrail, you're guessing. With CloudTrail, even basic, you have facts.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A Playbook
&lt;/h3&gt;

&lt;p&gt;The four AWS steps are necessary but insufficient. A playbook is what you execute &lt;em&gt;while&lt;/em&gt; following those steps, what you execute &lt;em&gt;before&lt;/em&gt; the key is fully rotated, and what you execute &lt;em&gt;after&lt;/em&gt; you think it's over.&lt;/p&gt;

&lt;p&gt;A minimal playbook for a compromised key looks like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Immediate (first 30 minutes):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Do NOT delete the exposed key yet. Mark it as inactive. You need it in CloudTrail for the investigation.&lt;/li&gt;
&lt;li&gt;Query CloudTrail for all events from that key in the last 30 days (not just the past hour).&lt;/li&gt;
&lt;li&gt;Check if that key was used to assume any roles or create temporary credentials. If yes, those STS tokens are in the wild and valid until expiration. Monitor those roles' activity separately.&lt;/li&gt;
&lt;li&gt;In parallel: create a new key for the application using that user, update your code/deployment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Investigation (first 2 hours):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look for any new IAM users, roles, or policies created in the same timeframe.&lt;/li&gt;
&lt;li&gt;Check for any API calls to sensitive services: RDS, Secrets Manager, KMS, S3 policy changes.&lt;/li&gt;
&lt;li&gt;Check CloudTrail for any actions &lt;em&gt;after&lt;/em&gt; the GetCallerIdentity test that were anomalous (deletes, policy changes, cross-account AssumeRole).&lt;/li&gt;
&lt;li&gt;Verify the alternate contact (Billing, Operations, Security) was not modified. An attacker could reroute support tickets or recovery emails.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Containment (2-4 hours):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Once the new key is confirmed working in production, mark the exposed key as inactive in the console.&lt;/li&gt;
&lt;li&gt;If the attacker created backdoor credentials or roles, delete them.&lt;/li&gt;
&lt;li&gt;If they touched resources, take snapshots or note the state for forensics.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Post-incident (next business day):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Review all other IAM users in the account. Are there other keys that should be rotated? Other users with overprivileged access?&lt;/li&gt;
&lt;li&gt;Check S3 bucket policies, security group rules, and VPC peering for unexpected changes.&lt;/li&gt;
&lt;li&gt;Enable MFA on all human IAM users and the root account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A playbook turns panic into a sequence. It answers the question "what do I do first?" before you need the answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Rotate the Key Without Breaking Your Application
&lt;/h3&gt;

&lt;p&gt;Here's the trap: the application using &lt;code&gt;app-integration-user&lt;/code&gt; is running in production right now. It's sending emails, and it's using that exposed key.&lt;/p&gt;

&lt;p&gt;If you delete the key immediately, the application fails. Customers' emails don't send. You get paged. You panic. You revert.&lt;/p&gt;

&lt;p&gt;If you rotate the key slowly, the application keeps working while the attacker still has access.&lt;/p&gt;

&lt;p&gt;The solution is simple: &lt;strong&gt;rotate before you block&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new access key for &lt;code&gt;app-integration-user&lt;/code&gt; right now.&lt;/li&gt;
&lt;li&gt;Update your application to use the new key (redeploy or restart).&lt;/li&gt;
&lt;li&gt;Test that the application works with the new key.&lt;/li&gt;
&lt;li&gt;Only then mark the exposed key as inactive in the console.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This takes 10 to 15 minutes if you have automation. If you don't, it takes longer. But it works.&lt;/p&gt;

&lt;p&gt;The attacker can't use the old key once it's inactive. Your application never stops. You avoid the panic of choosing between security and uptime.&lt;/p&gt;

&lt;p&gt;If the key is embedded in a third-party tool or service, contact the vendor right away. Tell them the key is compromised. Ask them to help you rotate it. In parallel, mark the key as inactive in AWS. Once the vendor confirms the new key is working, you're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Build This Capacity &lt;em&gt;Before&lt;/em&gt; You Need It
&lt;/h2&gt;

&lt;p&gt;You don't build incident response by responding to incidents. You build it by preparing for them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start with CloudTrail
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudtrail create-trail &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-organization-trail &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--s3-bucket-name&lt;/span&gt; my-cloudtrail-logs-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1

aws cloudtrail start-logging &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-organization-trail &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CloudTrail has free tier: 1 trail, 90 days of event history in the console. That's not enough for forensics. Older events are archived to S3, which is where you'll do real investigation.&lt;/p&gt;

&lt;p&gt;For long-term retention, query CloudTrail logs in S3 using Athena:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;useridentity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;eventname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sourceipaddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;eventtime&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cloudtrail_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;eventtime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-05-21'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;eventtime&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this query in the Athena console after you've configured Athena tables on your CloudTrail S3 bucket (AWS CloudTrail documentation has the setup steps).&lt;/p&gt;

&lt;p&gt;If you're in an AWS Organization, one trail in the management account logs all accounts. Do that instead of one trail per account.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write Your Playbook Now
&lt;/h3&gt;

&lt;p&gt;Don't write it during an incident. Write it next Tuesday.&lt;/p&gt;

&lt;p&gt;Use the structure above or a template. Share it with your team. Update it when you learn something new. Version control it (GitHub, not a Google Doc).&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Key Rotation Without Pressure
&lt;/h3&gt;

&lt;p&gt;Pick a test application (or a test user with a limited policy) and rotate its key. How long does it take? Where do you get stuck? Fix those problems now.&lt;/p&gt;

&lt;p&gt;If you have 15 applications using access keys, and rotation takes 30 minutes per app, an incident will take you 7.5 hours under pressure. That's a problem.&lt;/p&gt;

&lt;p&gt;If you can rotate a key in 5 minutes because you automated it or have a runbook, an incident is 1.25 hours. Still not fun. But survivable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem Isn't Technical
&lt;/h2&gt;

&lt;p&gt;The email from AWS assumes you're ready. The four steps assume you have the foundation.&lt;/p&gt;

&lt;p&gt;Most teams don't.&lt;/p&gt;

&lt;p&gt;Not because they're careless. Because incident response looks like overhead until the incident happens.&lt;/p&gt;

&lt;p&gt;Then it becomes the only thing that matters.&lt;/p&gt;

&lt;p&gt;The three things that save you — CloudTrail access, a playbook, and the ability to rotate a key in 15 minutes — aren't expensive. They don't require a SIEM or a SOC or a fancy tool.&lt;/p&gt;

&lt;p&gt;They require 4 hours of work before something breaks.&lt;/p&gt;

&lt;p&gt;That's the gap AWS doesn't mention.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudsecurity</category>
      <category>incidentresponse</category>
      <category>securityoperations</category>
    </item>
    <item>
      <title>Detect, Alert, Search: The SIEM You Already Have on AWS</title>
      <dc:creator>Mario</dc:creator>
      <pubDate>Sun, 07 Jun 2026 03:27:07 +0000</pubDate>
      <link>https://dev.to/mariogongora/detect-alert-search-the-siem-you-already-have-on-aws-5f49</link>
      <guid>https://dev.to/mariogongora/detect-alert-search-the-siem-you-already-have-on-aws-5f49</guid>
      <description>&lt;p&gt;The question came up in a security assessment. "Do you have a SIEM?"&lt;/p&gt;

&lt;p&gt;The honest answer was no. There was no budget for one either. A commercial SIEM meant a license, an ingestion bill that grew with every log source, and someone to run it. None of that was happening this quarter.&lt;/p&gt;

&lt;p&gt;But the question behind the question was fair. Can you see what is happening in your AWS accounts? Would you know if a credential was being used from somewhere it should not be? Could you go back and check?&lt;/p&gt;

&lt;p&gt;That is what a SIEM is for. And the account already had most of the parts to answer those questions. They just were not connected.&lt;/p&gt;

&lt;p&gt;You do not buy a SIEM to get detection and searchable history on AWS. You connect four services you may already be paying for.&lt;/p&gt;

&lt;p&gt;If you read &lt;a href="https://blog.leastprivilege.cloud/a-starting-point-for-aws-service-control-policies" rel="noopener noreferrer"&gt;the last article&lt;/a&gt;, this is the other half of it. The guardrails there kept an attacker from deleting CloudTrail or switching off GuardDuty. Here we use the logs and findings those guardrails protect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a SIEM Actually Does
&lt;/h2&gt;

&lt;p&gt;Strip away the marketing and a SIEM does three things that matter here.&lt;/p&gt;

&lt;p&gt;It collects signals from many sources and puts them in one format. It alerts you in real time when something important happens. And it keeps history you can search later, when you need to investigate.&lt;/p&gt;

&lt;p&gt;Collect, alert, search. That is the job.&lt;/p&gt;

&lt;p&gt;What we are not rebuilding is the rest of the brochure: a correlation engine that stitches twenty events into one story, user behavior analytics, prebuilt compliance dashboards, a case management workflow. Those are real features. Most teams asking "do we have a SIEM?" do not need them yet. They need to know when something fires, and be able to look back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Four services, each doing one job.&lt;/p&gt;

&lt;p&gt;GuardDuty is the sensor. It watches CloudTrail, VPC flow logs, and DNS queries, and raises findings when it sees credential abuse, cryptomining, or traffic to known-bad infrastructure. You do not write rules for it. You turn it on.&lt;/p&gt;

&lt;p&gt;Security Hub is the part that makes this a SIEM instead of a pile of consoles. It ingests findings from GuardDuty, Inspector, IAM Access Analyzer, Macie, and its own security checks, and normalizes all of them into one schema, the AWS Security Finding Format (ASFF). That normalization is work a SIEM would otherwise charge you for.&lt;/p&gt;

&lt;p&gt;EventBridge is the router. Security Hub emits an event every time it imports a finding. A rule decides which of those events are worth waking someone up for.&lt;/p&gt;

&lt;p&gt;Lambda is the processor. It reads the finding, pulls out what a human needs, sends it to Slack, and writes the raw record to S3.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GuardDuty ─┐
Inspector ─┤
Config    ─┼─► Security Hub ──► EventBridge ──► Lambda ─┬─► Slack  (real-time alert)
Access    ─┤   normalize        filter by      format   │
Analyzer  ─┘   (ASFF)           severity                └─► S3 ──► Athena  (searchable history)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;S3 plus Athena is the half people skip, and it is the half that earns the word SIEM. Security Hub keeps findings for about 90 days. The S3 archive is your real history, and Athena lets you query it with SQL when you need to answer "has this happened before?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting It Up
&lt;/h2&gt;

&lt;p&gt;The order matters. Sensor first, then aggregator, then the pipe.&lt;/p&gt;

&lt;p&gt;If GuardDuty or Security Hub is already on in your account, skip ahead. Most of these steps are idempotent. The point is not turning things on, it is connecting what is already running.&lt;/p&gt;

&lt;p&gt;Step 1. Turn on GuardDuty. This is the detection sensor. Without it, Security Hub has thin threat signal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws guardduty create-detector &lt;span class="nt"&gt;--enable&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command returns a detector ID. Save it. You will use it to send test findings later.&lt;/p&gt;

&lt;p&gt;Step 2. Enable Security Hub with its default standards. This turns on the aggregator and a baseline set of checks, and it wires GuardDuty findings in automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws securityhub enable-security-hub &lt;span class="nt"&gt;--enable-default-standards&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give it a few hours. Findings do not appear instantly. Security Hub runs its checks and pulls in GuardDuty on its own schedule.&lt;/p&gt;

&lt;p&gt;Step 3. Create the archive bucket, with public access blocked and versioning on. This is where the searchable history lives, so it should outlive any single finding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BUCKET_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-account-id-siem-findings"&lt;/span&gt;

aws s3api create-bucket &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1

aws s3api put-public-access-block &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--public-access-block-configuration&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;BlockPublicAcls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;,IgnorePublicAcls&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;,BlockPublicPolicy&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;,RestrictPublicBuckets&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true

&lt;/span&gt;aws s3api put-bucket-versioning &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--versioning-configuration&lt;/span&gt; &lt;span class="nv"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 4. Wire the EventBridge rule. The piece that decides what is worth an alert is the event pattern. This is the one to get right.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"aws.securityhub"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail-type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Security Hub Findings - Imported"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"findings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"HIGH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CRITICAL"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Workflow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"NEW"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"RecordState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ACTIVE"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it from the inside out. &lt;code&gt;RecordState&lt;/code&gt; ACTIVE ignores findings Security Hub has already archived. &lt;code&gt;Workflow.Status&lt;/code&gt; NEW skips anything someone has already triaged or suppressed. &lt;code&gt;Severity.Label&lt;/code&gt; HIGH and CRITICAL is the filter that keeps this from becoming noise on day one. Inside an attribute the values are OR, across attributes they are AND. So this matches a new, active, high-or-critical finding, and nothing else.&lt;/p&gt;

&lt;p&gt;That last filter is a decision, not a default. Start narrow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lambda
&lt;/h2&gt;

&lt;p&gt;The function does the unglamorous work. Turn a finding into a message a person can act on, and keep a copy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;s3&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;secrets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secretsmanager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;BUCKET&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ARCHIVE_BUCKET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;WEBHOOK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_secret_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;SecretId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WEBHOOK_SECRET_ARN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SecretString&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;findings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Processed &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;findings&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; findings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;fid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;findings/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CreatedAt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AwsAccountId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:rotating_light: *&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Severity&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;*  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AwsAccountId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;`  region `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Region&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;`&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;WEBHOOK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few decisions worth calling out. One EventBridge event can carry several findings, so the handler loops, it does not assume one. The function returns a clean response so failures are explicit. If the Lambda throws, EventBridge retries twice by default (configurable up to 185 retries with exponential backoff on the rule's retry policy). That means a transient Slack outage will not lose findings permanently. The S3 key is built from the finding date, account, and ID, so a redelivery of the same finding overwrites instead of duplicating. The webhook URL is not in the code. It comes from Secrets Manager, because a webhook in source control is a credential leak waiting to happen. And the function uses &lt;code&gt;urllib&lt;/code&gt; from the standard library, not &lt;code&gt;requests&lt;/code&gt;, so there is nothing to package. It deploys as a single file.&lt;/p&gt;

&lt;p&gt;The full version, with the IAM role scoped to one bucket prefix and the EventBridge wiring, is in the repo. Link is at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Querying the Archive with Athena
&lt;/h2&gt;

&lt;p&gt;Once findings land in S3, Athena turns the bucket into a searchable database. Create the table once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;EXTERNAL&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;siem_findings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;Id&lt;/span&gt;             &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;AwsAccountId&lt;/span&gt;   &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Region&lt;/span&gt;         &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Title&lt;/span&gt;          &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Description&lt;/span&gt;    &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;CreatedAt&lt;/span&gt;      &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Severity&lt;/span&gt;       &lt;span class="n"&gt;STRUCT&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Normalized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Workflow&lt;/span&gt;       &lt;span class="n"&gt;STRUCT&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ProductName&lt;/span&gt;    &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Resources&lt;/span&gt;      &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;STRUCT&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;SERDE&lt;/span&gt; &lt;span class="s1"&gt;'org.openx.data.jsonserde.JsonSerDe'&lt;/span&gt;
&lt;span class="k"&gt;LOCATION&lt;/span&gt; &lt;span class="s1"&gt;'s3://your-account-id-siem-findings/findings/'&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then query as needed. Example — all critical findings in the last 30 days:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AwsAccountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Region&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;siem_findings&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'CRITICAL'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;date_format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;current_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'30'&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%Y-%m-%d'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the piece that earns the word SIEM. Not a live dashboard, but on-demand investigation when an incident requires context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Findings I Route First
&lt;/h2&gt;

&lt;p&gt;Turning on every finding at once is how a SIEM becomes a channel everyone mutes. Start with the ones that usually mean someone is already inside.&lt;/p&gt;

&lt;p&gt;From GuardDuty: &lt;code&gt;UnauthorizedAccess&lt;/code&gt; on IAM credentials, &lt;code&gt;CryptoCurrency&lt;/code&gt; activity, &lt;code&gt;Backdoor&lt;/code&gt; and &lt;code&gt;Trojan&lt;/code&gt; types, and any use of the root account. From Security Hub's own checks: root account in use, S3 buckets open to the public, console users without MFA, security groups open to &lt;code&gt;0.0.0.0/0&lt;/code&gt; on admin ports.&lt;/p&gt;

&lt;p&gt;That is a short list on purpose. You widen the EventBridge pattern later, by adding &lt;code&gt;MEDIUM&lt;/code&gt; to the severity filter, once you trust the signal and have somewhere to put it. Widening is a one-line change. Earning back a team's attention after you have flooded them is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Is Not
&lt;/h2&gt;

&lt;p&gt;This is honest detection and searchable history. It is not an enterprise SIEM, and pretending otherwise will get someone hurt.&lt;/p&gt;

&lt;p&gt;It does not correlate. It routes one finding at a time. It will not connect a failed login, a new access key, and a data transfer into a single story. AWS has a reference pattern for that with EventBridge, Lambda, and DynamoDB, but that is a build, not a checkbox.&lt;/p&gt;

&lt;p&gt;Athena is query on demand, not a live dashboard. You write SQL when you investigate. If you want charts, that is QuickSight, and QuickSight costs.&lt;/p&gt;

&lt;p&gt;It is single account as written. Multi-account means setting up Security Hub and GuardDuty with a delegated administrator and aggregating findings into one account. Same pattern, more setup.&lt;/p&gt;

&lt;p&gt;And "without buying anything" means no third-party license and no new product. It does not mean free. Security Hub bills per finding ingested and per check, GuardDuty bills on the events and volume it analyzes, Athena bills per terabyte scanned. For a small to mid AWS footprint this lands in the tens of dollars a month. A commercial SIEM for the same footprint starts in the thousands. That gap is the whole point.&lt;/p&gt;

&lt;p&gt;To put numbers on it: a small account generating 100 findings per day and running 200 security checks hits roughly $4/month for Security Hub ($0.00003/finding + $0.0010/check), $10–15/month for GuardDuty (CloudTrail and VPC flow analysis on modest traffic), and pennies for Athena queries against a few gigabytes in S3. Call it $15–25/month total. A commercial SIEM ingesting the same data starts at $1,000/month before you count the engineer running it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Is Enough, and When It Is Not
&lt;/h2&gt;

&lt;p&gt;It is enough when you have a small to mid AWS footprint, you need to know when something fires, you need to be able to look back, and no auditor is requiring a specific named product.&lt;/p&gt;

&lt;p&gt;It is not enough when a compliance framework names a SIEM you have to use, when you need to correlate AWS with on-prem or another cloud, when you need monitored response around the clock, or when your finding volume is high enough to need real tuning and a team to do it.&lt;/p&gt;

&lt;p&gt;"We don't have a SIEM" and "we have no visibility" are two different problems. Only one of them needs a budget.&lt;/p&gt;

&lt;p&gt;Detection is the easy half. The next article is the hard half: what happens after the alert fires, when the incident response playbook assumes a team, centralized logs, and time you do not have.&lt;/p&gt;

&lt;p&gt;Open Security Hub in your account right now. If there are findings sitting there with no one watching, you already have the signal. The only missing piece is the wire.&lt;/p&gt;

&lt;p&gt;The deployable template, the Lambda, and a test script that fires a sample finding through the whole pipeline are in a companion repo: &lt;a href="https://github.com/Kiruma/aws-lightweight-siem" rel="noopener noreferrer"&gt;https://github.com/Kiruma/aws-lightweight-siem&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: AWS, Cloud Security, Security Hub, GuardDuty, SIEM&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudsecurity</category>
      <category>securityhub</category>
      <category>guardduty</category>
    </item>
    <item>
      <title>A Starting Point for AWS Service Control Policies</title>
      <dc:creator>Mario</dc:creator>
      <pubDate>Sat, 30 May 2026 21:12:34 +0000</pubDate>
      <link>https://dev.to/mariogongora/a-starting-point-for-aws-service-control-policies-2ohf</link>
      <guid>https://dev.to/mariogongora/a-starting-point-for-aws-service-control-policies-2ohf</guid>
      <description>&lt;p&gt;A team opens an AWS account. They deploy everything in us-east-1. Reasonable choice.&lt;/p&gt;

&lt;p&gt;Six months later, their AWS bill has EC2 instances running in ap-southeast-1.&lt;/p&gt;

&lt;p&gt;Nobody on the team worked in that region. Nobody authorized those instances. They weren't in any Terraform state. They didn't appear in any monitoring dashboard because GuardDuty wasn't enabled there.&lt;/p&gt;

&lt;p&gt;A credential had been compromised three weeks earlier. The attacker scanned the account, saw that monitoring was concentrated in us-east-1, and picked a region where nobody was watching. They ran there quietly for weeks.&lt;/p&gt;

&lt;p&gt;That's the case that made me take SCPs seriously.&lt;/p&gt;

&lt;p&gt;Not a compliance requirement. Not a Well-Architected checklist item. A bill with instances nobody recognized, in a region nobody was watching.&lt;/p&gt;




&lt;h2&gt;
  
  
  What SCPs Actually Do
&lt;/h2&gt;

&lt;p&gt;AWS Organizations lets you attach Service Control Policies to an organizational unit. An SCP sets a permission ceiling. It defines the maximum permissions any account in that OU can use, regardless of what IAM says.&lt;/p&gt;

&lt;p&gt;If your IAM policy allows an action and your SCP denies it, the action is denied. SCP wins.&lt;/p&gt;

&lt;p&gt;That's the leverage. One policy, attached at the OU level, governs every account underneath it, including accounts that don't exist yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Practical OU Structure
&lt;/h2&gt;

&lt;p&gt;Before you write a single SCP, you need a structure to attach it to.&lt;/p&gt;

&lt;p&gt;The simplest model that works in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Root
├── Security       (audit account, log archive)
├── Infrastructure (shared services, networking)
├── Workloads
│   ├── Production
│   ├── Staging
│   └── Development
└── Sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each OU gets its own SCPs. Production gets the strictest. Sandbox gets the most relaxed. Development sits in the middle, with enough freedom to build but without the blast radius of production.&lt;/p&gt;

&lt;p&gt;In practice, many organizations end up with a different shape. One OU per project, or one per business unit. Both work. The structure matters less than the rule: accounts that share the same security requirements belong in the same OU.&lt;/p&gt;

&lt;p&gt;One mistake I see repeatedly is applying everything at Root. Root-level SCPs apply to every member account, including your security account, your audit account, your billing tools. One important exception: SCPs never apply to the management account, regardless of what you attach at Root. Apply at Root only what must be true everywhere without exception. Everything else belongs at the OU level.&lt;/p&gt;




&lt;h2&gt;
  
  
  SCP Examples Worth Considering
&lt;/h2&gt;

&lt;p&gt;Quick note before the code: these are examples, not a definitive list. Your environment will be different. You may need fewer of these, more of them, or completely different ones. Take what's useful and leave the rest.&lt;/p&gt;

&lt;p&gt;One more thing about the format. Each block below is a single SCP statement, not a complete policy. To use one, drop it into the &lt;code&gt;Statement&lt;/code&gt; array of a policy document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;one&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;more&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;statements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;below&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can combine several statements into one SCP, as long as the whole policy stays under the 5,120-character limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Deny Non-Approved Regions
&lt;/h3&gt;

&lt;p&gt;AWS accounts have multiple regions enabled by default. Most teams use two or three. The rest are attack surface with no monitoring.&lt;/p&gt;

&lt;p&gt;When a credential is compromised, attackers look for regions where GuardDuty isn't enabled and where no one is watching CloudTrail. This SCP closes that gap by denying API calls to regions outside your approved list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DenyNonApprovedRegions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"NotAction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"iam:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"sts:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"route53:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"cloudfront:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"budgets:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"ce:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"support:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"organizations:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"account:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"waf:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"globalaccelerator:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"health:*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"StringNotEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"aws:RequestedRegion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"us-west-2"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;NotAction&lt;/code&gt;, not &lt;code&gt;Action: *&lt;/code&gt;. Services like IAM, STS, Route53, and CloudFront are global, not regional. If you use &lt;code&gt;Action: *&lt;/code&gt;, you break them. This is the most common mistake when teams write this SCP for the first time.&lt;/p&gt;

&lt;p&gt;The NotAction list above is simplified for readability. The official AWS reference list is much longer and includes services like &lt;code&gt;config:*&lt;/code&gt;, &lt;code&gt;kms:*&lt;/code&gt;, &lt;code&gt;sso:*&lt;/code&gt;, and &lt;code&gt;wafv2:*&lt;/code&gt;. Before applying this in production, compare against the &lt;a href="https://docs.aws.amazon.com/controltower/latest/controlreference/primary-region-deny-policy.html" rel="noopener noreferrer"&gt;AWS Control Tower region deny policy&lt;/a&gt; to avoid blocking legitimate cross-region operations.&lt;/p&gt;

&lt;p&gt;Adjust the approved region list to match where your workloads actually run.&lt;/p&gt;

&lt;p&gt;Apply this to a sandbox OU first. As written, it can also block your IaC deployment role and AWS service-linked operations across regions, so watch CloudTrail for AccessDenied before promoting it to production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Deny Large Instance Types
&lt;/h3&gt;

&lt;p&gt;This looks like a cost control. It's also a security control.&lt;/p&gt;

&lt;p&gt;When an attacker compromises credentials, they launch GPU instances for cryptomining. The financial damage from a credential leak drops significantly when the largest instance they can run is an m5.xlarge. This SCP limits the impact of a compromised credential, not just the bill.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DenyLargeInstances"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ec2:RunInstances"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ec2:*:*:instance/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"StringNotLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ec2:InstanceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"t3.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"t2.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"m5.large"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"m5.xlarge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"m5.2xlarge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"c5.large"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"c5.xlarge"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adjust the allowed list to match your actual workload needs. If your production workloads require larger instances, add them explicitly. The goal is to make the approved list specific enough that anything outside it stands out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Deny Disabling Security Services
&lt;/h3&gt;

&lt;p&gt;This is the one that stops attackers from going dark after they get in.&lt;/p&gt;

&lt;p&gt;Standard post-compromise playbook: gain access, disable GuardDuty, delete CloudTrail, operate without detection. This SCP breaks that sequence. Even with AdministratorAccess in IAM, the attacker cannot disable these services. SCP overrides IAM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DenyDisableSecurityServices"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"guardduty:DeleteDetector"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"guardduty:DisassociateFromAdministratorAccount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"guardduty:StopMonitoringMembers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"guardduty:UpdateDetector"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"securityhub:DeleteHub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"securityhub:DisableSecurityHub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"securityhub:DisassociateFromAdministratorAccount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"cloudtrail:DeleteTrail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"cloudtrail:StopLogging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"cloudtrail:UpdateTrail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"config:DeleteConfigurationRecorder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"config:StopConfigurationRecorder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"config:DeleteConfigRule"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"access-analyzer:DeleteAnalyzer"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use SNS for security alerting, add &lt;code&gt;cloudwatch:DisableAlarmActions&lt;/code&gt; and &lt;code&gt;sns:DeleteTopic&lt;/code&gt;. An attacker who can't disable the detector can still silence the alarm.&lt;/p&gt;

&lt;p&gt;One caveat on &lt;code&gt;guardduty:UpdateDetector&lt;/code&gt;. It blocks every update to the detector, not just the ones that weaken it, so changing finding-publishing frequency or enabling a new feature goes through the same deny. If that friction bothers your team, scope it with a condition instead of denying the action outright.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Deny Disabling Logging
&lt;/h3&gt;

&lt;p&gt;Logs are forensic evidence. An attacker who can't turn off detection services will try to erase the trail instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DenyDisableLogging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"cloudtrail:DeleteTrail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"cloudtrail:StopLogging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"logs:DeleteLogGroup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"logs:DeleteLogStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"logs:DeleteRetentionPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DeleteFlowLogs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"wafv2:DeleteLoggingConfiguration"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll notice &lt;code&gt;cloudtrail:DeleteTrail&lt;/code&gt; and &lt;code&gt;cloudtrail:StopLogging&lt;/code&gt; appear here and in the previous example. That overlap is fine. If you apply both statements, the duplication costs nothing. If you'd rather keep one source of truth, drop them from whichever statement you apply second.&lt;/p&gt;

&lt;p&gt;One operational note: &lt;code&gt;logs:DeleteLogGroup&lt;/code&gt; will block developers from cleaning up log groups in test environments. Apply this SCP to production and staging OUs only. For development, use a CloudWatch Logs retention policy instead of blocking deletion entirely.&lt;/p&gt;

&lt;p&gt;For RDS and ELB logging, SCPs are a poor fit. The actions that control their logging configuration are too broad and block legitimate operations. A more reliable approach is AWS Config rules with automatic remediation: if logging gets disabled, Config detects it and re-enables it. Same result, less friction.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Control Tower Actually Covers
&lt;/h2&gt;

&lt;p&gt;If you're on AWS Control Tower, it's worth reading the actual SCP content before assuming you're protected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CloudTrail:&lt;/strong&gt; Control Tower's mandatory SCP for CloudTrail targets this resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:cloudtrail:*:*:trail/aws-controltower-*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It only protects trails named &lt;code&gt;aws-controltower-*&lt;/code&gt;. Any trail you create yourself is not covered. And if you're on Landing Zone 4.0 or above, this SCP was removed entirely. AWS deprecated it when Control Tower moved away from account-level trails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Config:&lt;/strong&gt; This one is different. The mandatory Config SCP uses &lt;code&gt;"Resource": ["*"]&lt;/code&gt;, so it does protect your Config setup broadly, not just Control Tower's.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GuardDuty and Security Hub:&lt;/strong&gt; No mandatory preventive guardrail by default. There are elective options, but you have to enable them explicitly per OU.&lt;/p&gt;

&lt;p&gt;The practical check: open your Organizations console, read the SCP attached to your OU, and look at the Resource field. If it's scoped to &lt;code&gt;aws-controltower-*&lt;/code&gt; resources, it's not protecting yours.&lt;/p&gt;




&lt;h2&gt;
  
  
  When SCPs Are Not Enough
&lt;/h2&gt;

&lt;p&gt;SCPs are preventive controls. They stop things from happening.&lt;/p&gt;

&lt;p&gt;They don't tell you what happened before you put them in place. They don't detect misconfigurations that already exist. They don't replace monitoring.&lt;/p&gt;

&lt;p&gt;The full picture: SCPs for prevention, AWS Config for drift detection, GuardDuty and Security Hub for threat detection, CloudTrail with a centralized destination for forensics.&lt;/p&gt;

&lt;p&gt;SCPs are the foundation. Not the ceiling.&lt;/p&gt;

&lt;p&gt;If you're implementing these for the first time, start with the region restriction and the security services protection. Those two give you the most immediate security return. Instance types and logging have more operational nuance and will need adjustment for your workloads.&lt;/p&gt;

&lt;p&gt;Start there. Tune as you go.&lt;/p&gt;

&lt;p&gt;Prevention is the first layer. The next one is seeing what's happening inside the accounts you just put a ceiling on. In the next article I'll build a lightweight SIEM on AWS without buying anything, using the logs these guardrails keep an attacker from deleting.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: AWS, Cloud Security, IAM, Cloud Governance, DevSecOps&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudsecurity</category>
      <category>iam</category>
      <category>cloudgovernance</category>
    </item>
    <item>
      <title>IAM Misconfiguration: Why It Keeps Happening and How to Start Fixing It</title>
      <dc:creator>Mario</dc:creator>
      <pubDate>Sat, 23 May 2026 22:06:56 +0000</pubDate>
      <link>https://dev.to/mariogongora/the-iam-mistake-i-keep-finding-in-aws-environments-and-why-its-not-your-fault-16oo</link>
      <guid>https://dev.to/mariogongora/the-iam-mistake-i-keep-finding-in-aws-environments-and-why-its-not-your-fault-16oo</guid>
      <description>&lt;h2&gt;
  
  
  The Most Common Pattern I See
&lt;/h2&gt;

&lt;p&gt;Across the AWS environments I've reviewed — financial services, retail, healthcare, telecom — there is one pattern that shows up almost everywhere.&lt;/p&gt;

&lt;p&gt;It is not a sophisticated attack vector.&lt;br&gt;
It is not a zero-day vulnerability.&lt;br&gt;
It is IAM.&lt;/p&gt;

&lt;p&gt;Specifically, it is IAM configured the way it was configured three years ago, by someone who no longer works there, using a tutorial that was written for a single-account startup.&lt;/p&gt;

&lt;p&gt;The most frequent findings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IAM Users with long-lived access keys attached to production workloads&lt;/li&gt;
&lt;li&gt;Admin roles without permission boundaries or conditions&lt;/li&gt;
&lt;li&gt;No SCPs enforced at the organization level — because there is no organization level&lt;/li&gt;
&lt;li&gt;Root account used regularly, sometimes for daily operations&lt;/li&gt;
&lt;li&gt;Access keys that have not been rotated in over 400 days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is surprising.&lt;br&gt;
All of it is fixable.&lt;br&gt;
But first, you need to understand why it happens.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why This Is Not a Skill Problem
&lt;/h2&gt;

&lt;p&gt;When I point out these findings to the teams responsible, the reaction is almost always the same.&lt;/p&gt;

&lt;p&gt;They know.&lt;/p&gt;

&lt;p&gt;They are not unaware that long-lived access keys are a risk. They have read the AWS documentation. They have seen the warnings in Security Hub. In some cases, they have even flagged it internally, written a ticket, added it to a backlog.&lt;/p&gt;

&lt;p&gt;The problem is not knowledge. The problem is context.&lt;/p&gt;

&lt;p&gt;AWS documentation, AWS Well-Architected Framework, AWS Security Best Practices — all of it was written assuming a certain baseline:&lt;/p&gt;

&lt;p&gt;A dedicated security team.&lt;br&gt;
A mature cloud governance process.&lt;br&gt;
An organization that started with multi-account in mind.&lt;br&gt;
Budget for tooling beyond what comes free with the account.&lt;/p&gt;

&lt;p&gt;For most organizations, the reality looks different.&lt;/p&gt;

&lt;p&gt;They entered AWS through a single team that needed to ship something fast. IAM was configured to make that happen. There was no time to design a permission model, no security architect on the team, and no organizational structure to enforce standards later.&lt;/p&gt;

&lt;p&gt;That is not negligence. That is how cloud adoption actually happens.&lt;/p&gt;


&lt;h2&gt;
  
  
  Where to Start Without Rewriting Everything
&lt;/h2&gt;

&lt;p&gt;The worst advice I can give someone in this situation is "start over."&lt;/p&gt;

&lt;p&gt;Tearing down IAM and rebuilding it properly sounds good in a blog post. In reality, it means production risk, migration work, and political capital you probably do not have.&lt;/p&gt;

&lt;p&gt;Here is what I actually recommend as a starting point — a sequence that reduces risk without requiring a full redesign:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Stop the bleeding.&lt;/strong&gt;&lt;br&gt;
Identify every IAM User with an access key that has not been rotated in 90+ days. Rotate or deactivate them. This takes a few hours and removes a real risk immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam generate-credential-report
aws iam get-credential-report &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Content'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt; &lt;span class="s1"&gt;'NR&amp;gt;1 &amp;amp;&amp;amp; $9 != "N/A" { print $1, $9 }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Tag everything you touch.&lt;/strong&gt;&lt;br&gt;
Before changing any role or policy, add tags. This is how you build the visibility you need to understand what you have before you change it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Enable the basics in Security Hub.&lt;/strong&gt;&lt;br&gt;
Turn on AWS Foundational Security Best Practices standard. Let it run for a week. Do not try to fix everything — use the findings to build your backlog in priority order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: No new IAM Users.&lt;/strong&gt;&lt;br&gt;
Draw a line. From today, every new workload that needs AWS access gets a role, not a user. Not because roles are perfect, but because the habit of using roles changes how your team thinks about identity.&lt;/p&gt;

&lt;p&gt;None of this requires a project. None of it requires approval from three stakeholders.&lt;br&gt;
It requires a decision to start.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Problem Is Actually Telling You
&lt;/h2&gt;

&lt;p&gt;IAM misconfiguration is not a technical problem with a technical solution.&lt;/p&gt;

&lt;p&gt;It is a signal.&lt;/p&gt;

&lt;p&gt;It tells you that cloud adoption happened faster than governance frameworks could follow. It tells you that security was positioned as a blocker rather than an enabler. It tells you that most teams building on AWS have been doing so without the baseline the documentation assumes they already have.&lt;/p&gt;

&lt;p&gt;That is the real gap — not skill, not intention. Baseline.&lt;/p&gt;

&lt;p&gt;The good news: you do not need a transformation program to close it.&lt;br&gt;
You need a decision, a starting point, and consistency over time.&lt;/p&gt;

&lt;p&gt;This article is part of that starting point.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;In the next article, I will go deeper into the governance layer that prevents these problems from appearing in the first place: AWS Service Control Policies, and how I use them to enforce security guardrails across environments in nine countries.&lt;/p&gt;

&lt;p&gt;If you are managing AWS accounts across multiple clients or countries, that one is for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;[Tags: AWS, IAM, Cloud Security, AWS Security, Cloud Governance, DevSecOps]&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>iam</category>
      <category>cloudsecurity</category>
      <category>cloudgovernance</category>
    </item>
  </channel>
</rss>
