AWS IAM Defense in Depth
In my post "AWS Security Assessments - Getting Access", I described how to use a CloudFormation Quick Create-Link to easily setup Cross-Account Trusts to access customer accounts when performing an AWS Security Assessment. Following that post a colleague posed the question if cross-account trusts could be used for phishing to gain access to an account. The short answer to that question is YES!!
However keeping in mind a defense-in-depth strategy and approach to IAM security in AWS, you should follow least privilege access in IAM, periodically review the IAM Credential Report, and turn on IAM Access Analyzer. Another step in the defense in depth approach to securing your account is to use CloudFormation to create a CloudWatch Alarm to respond to CloudTrail API IAM Calls. What that means in simple terms is you'll get an email alert anytime changes are made to IAM users including add, delete, or modify operations.
The power of Infrastructure as Code (IaC) tools like CloudFormation or Terraform comes from its ability to allow you to quickly provision deployments to your infrastructure in an error free repeatable process. This of course is particularly useful when you're working to secure your AWS environment.
How Does it Work?
It's important to note that some of these steps are optional depending on what controls you already have enabled and you can modify your CloudFormation script as needed. Here's what we'll be doing in a nutshell.
- Create a CloudTrail Trail if one doesn't already exist in your account. CloudTrail provides event history of your AWS account activity, including actions taken through the AWS Management Console, AWS SDKs, command line tools, and other AWS services, and coincidentally should be one of the first things you enable in your account
- Create an S3 bucket that your Trail will write to.
- Create a CloudWatch Log Group that your Trail will write to.
- Create an SNS topic that will forward your alarm to the specified email address.
- Create your CloudWatch Alarm and apply the Metrics to watch for.
Here's the CloudFormation template written in YAML that you'll want to deploy. You can also update it as needed to include any additional IAM Actions you want to monitor from the full AWS IAM API Reference List.
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudWatch Alarm that detects changes to IAM users and
groups and publishes change events to an SNS topic for notification.'
Resources:
CloudTrail:
Type: 'AWS::CloudTrail::Trail'
Properties:
TrailName: ManagementEventsTrail
IsLogging: true
EnableLogFileValidation: false
EventSelectors:
- IncludeManagementEvents: true
ReadWriteType: All
IsMultiRegionTrail: true
IncludeGlobalServiceEvents: true
S3BucketName:
Ref: S3BucketForCloudTrail
CloudWatchLogsLogGroupArn:
'Fn::GetAtt':
- CloudWatchLogGroup
- Arn
CloudWatchLogsRoleArn:
'Fn::GetAtt':
- IamRoleForCwLogs
- Arn
DependsOn: S3BucketPolicy
S3BucketForCloudTrail:
Type: 'AWS::S3::Bucket'
Properties:
# <CHANGE BELOW AS NEEDED>
BucketName: management-events-trail-bucket-name
S3BucketPolicy:
Type: 'AWS::S3::BucketPolicy'
Properties:
Bucket:
Ref: S3BucketForCloudTrail
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AWSCloudTrailBucketPermissionsCheck
Effect: Allow
Principal:
Service:
- cloudtrail.amazonaws.com
Action: 's3:GetBucketAcl'
Resource:
'Fn::GetAtt':
- S3BucketForCloudTrail
- Arn
- Sid: ' AWSConfigBucketDelivery'
Effect: Allow
Principal:
Service:
- cloudtrail.amazonaws.com
Action: 's3:PutObject'
Resource:
'Fn::Join':
- ''
- - 'Fn::GetAtt':
- S3BucketForCloudTrail
- Arn
- /AWSLogs/*
Condition:
StringEquals:
's3:x-amz-acl': bucket-owner-full-control
CloudWatchLogGroup:
Type: 'AWS::Logs::LogGroup'
Properties:
# <CHANGE BELOW AS NEEDED>
LogGroupName: CloudTrailLogs
IamRoleForCwLogs:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: ''
Effect: Allow
Principal:
Service: cloudtrail.amazonaws.com
Action: 'sts:AssumeRole'
Policies:
- PolicyName: allow-access-to-cw-logs
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: '*'
RoleName: CloudTrailLogs-to-CloudWatch
SnsTopic:
Type: 'AWS::SNS::Topic'
Properties:
Subscription:
# <CHANGE BELOW AS NEEDED>
- Endpoint: example@example.com
Protocol: email
TopicName: alarm-action
CloudWatchAlarm:
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmName: iam_user_changes
AlarmDescription: >-
A CloudWatch Alarm that triggers when changes are made to IAM users.
MetricName: IAMPolicyEventCount
Namespace: CloudTrailMetrics
Statistic: Sum
Period: '300'
EvaluationPeriods: '1'
Threshold: '1'
ComparisonOperator: GreaterThanOrEqualToThreshold
AlarmActions:
- Ref: SnsTopic
TreatMissingData: notBreaching
MetricFilter:
Type: 'AWS::Logs::MetricFilter'
Properties:
# <CHANGE BELOW AS NEEDED>
LogGroupName: CloudTrailLogs
# <CHANGE BELOW AS NEEDED>
FilterPattern: >-
{($.eventName=AddUserToGroup)||($.eventName=AttachGroupPolicy)||($.eventName=AttachRolePolicy)||($.eventName=AttachUserPolicy)||($.eventName=ChangePassword)||($.eventName=CreateAccessKey)||($.eventName=CreatePolicy)||($.eventName=CreateRole)||($.eventName=CreateUser)||($.eventName=CreateVirtualMFADevice)||($.eventName=PutGroupPolicy)||($.eventName=PutRolePolicy)||($.eventName=PutUserPolicy)||($.eventName=UpdateAccessKey)||($.eventName=UpdateGroup)||($.eventName=UpdateRole)||($.eventName=UpdateUser)}
MetricTransformations:
- MetricValue: '1'
MetricNamespace: CloudTrailMetrics
MetricName: IAMPolicyEventCount
DependsOn: CloudWatchLogGroup
Parameters: {}
Metadata: {}
Conditions: {}
Create the Stack
Now that our code is modified, its time to deploy it to CloudFormation. Start by creating a new Stack.
Browse to your template
Give it a name and create your stack
Confirm Your Subscription
You'll need check your email to confirm your SNS Subscription.
Take it for a Test Drive
Now all you need to do is modify an IAM user to make sure it works. It's also important to note that CloudWatch Alarms aren't instantaneous and there is a delay associated with them. In fact one if the controls we specified in the script was Period: '300' which means our CloudWatch Alarm will check every 5 minutes. If everything worked as planned you should have received an alert like this.