How hard it is to disable an IAM user when it does something suspicious?

I wanted to automatically disable an IAM user when it does something suspicious. Since this IAM user is used by a script I know that when it deviates that is a good indicator that it was compromised and I need to investigate.

How hard could it be?

Well, it turned out to be a frustrating experience. CloudTrail records events done by users, so this should be easy to setup. But then I started to encounter problems:

  • Only the first CloudTrail event is free, so I did not want to create more than one trail
  • CloudTrail sends events to EventBridge but only for the current region, which is not enough

I have an organizational trail in the management account. Let's see how easy it is to send these events into a member account!

  • CloudTrail can send events to a CloudWatch log then I could set up a subscription filter. This worked for a PoC but ultimately there is a limit of 2 for subscription filters for a log group. So this was a no-go
  • Otherwise it writes to S3, so I had to have a Lambda reading the objects as CloudTrail writes them

At this point I had a Lambda that got all CloudTrail events and filters out the interesting ones: ones with AccessDenied error, GetCallerIdentity, and ConsoleLogin. That should be a good start.

EventBus Rules can send events based on a filter, so forwarding these events into an EventBus seems like a good idea. So so far the chain is: CloudTrail => S3 => Lambda => EventBus.

But how can I send these events to the member account? Well, an EventBus Rule, of course. So I created an EventBus in the member account.

Next issue: a CloudFormation stack can't create an EventBus Rule in a different region. Interestingly, it is possible to create cross-account but not cross-region. So I needed an EventBus in the target region as well and set up a Rule to forward events there.

Then the very last step is to set up a Rule to filter events for the IAM user(s) and set up a CloudWatch alarm that calls a Lambda that attaches the DenyAll policy to the user.

Since I wanted everything managed by CloudFormation I ended up with an enormous amount of stacks:

  • (mgmt acc us-east-1) CloudTrail + Lambda + EventBus
  • (mgmt acc us-east-1) EventBus Rule to forward events to the member account
  • (member acc us-east-1) EventBus to receive events from the mgmt account
  • (member acc us-east-1) EventBus Rule to forward events to the regional EventBus
  • (member acc eu-west-1) The target stack with the IAM users and an EventBus to receive events to

What makes it a particularly annoying experience is that there are so many small limitations that make a simpler solution impossible:

  • CloudTrail should support filtering by events so that the whole management account => member account part could be saved
  • Or: the default EventBus should receive all CloudTrail events not just ones for the current region
  • EventBus Rule should be allowed to be cross-region. That would have saved me one EventBus
  • EventBridge Pipes don't support SNS as a source and also it's not clear if that supports cross-region and cross-account pipes

I wrote about my frustrations in this article: https://advancedweb.hu/cloudtrails-horrible-developer-experience/.

Overall, I'm fairly happy with this solution, but I feel that it would be so much easier if AWS supported some basic features around CloudTrail.

January 6, 2025

Free PDF guide

Sign up to our newsletter and download the "Foreign key constraints in DynamoDB" guide.