terraform-aws-cloudtrail-to-slack
terraform-aws-cloudtrail-to-slack copied to clipboard
Parse AWS CloudTrail events and send alerts to Slack for events that match pre-configured rules
- Terraform module to deploy lambda that sends notifications about AWS CloudTrail events to Slack
- Why this module?
- Example message
- Delivery delays
- Examples
- Module deployment with the default ruleset
- Separating notifications to different Slack channels
- Module deployment with the default ruleset and different slack channels for different accounts
- Tracking certain event types
- User defined rules to match events
- Module deployment with user-defined rules, list of events to track, and default rule sets
- Catch SSM Session events for the "111111111" account
- Ignore rules.
- Ignore events from the account "111111111".
- About rules and how they are applied
- Default rules
- Ignore rules
- Terraform specs
- Requirements
- Providers
- Modules
- Resources
- Inputs
- Outputs
- License
Terraform module to deploy lambda that sends notifications about AWS CloudTrail events to Slack
Why this module?
This module allows you to get notifications about:
- actions performed by root account (According to AWS best practices, you should use root account as little as possible and use SSO or IAM users)
- API calls that failed due to lack of permissions to do so (could be an indication of compromise or misconfiguration of your services/applications)
- console logins without MFA (Always use MFA for you IAM users or SSO)
- track a list of events that you might consider sensitive. Think IAM changes, network changes, data storage (S3, DBs) access changes. Though we recommend keeping that to a minimum to avoid alert fatigue
- define sophisticated rules to track user-defined conditions that are not covered by default rules (see examples below)
- send notifications to different Slack channels based on event account id
Example message

Delivery delays
The current implementation built upon parsing of S3 notifications, and thus you should expect a 5 to 10 min lag between action and event notification in Slack. If you do not get a notification at all - check CloudWatch logs for the lambda to see if there is any issue with provided filters.
Examples
Module deployment with the default ruleset
# we recomend storing hook url in SSM Parameter store and not commit it to the repo
data "aws_ssm_parameter" "hook" {
name = "/cloudtrail-to-slack/hook"
}
module "cloudtrail_to_slack" {
source = "fivexl/cloudtrail-to-slack/aws"
version = "2.0.0"
default_slack_hook_url = data.aws_ssm_parameter.hook.value
cloudtrail_logs_s3_bucket_name = aws_s3_bucket.cloudtrail.id
}
resource "aws_cloudtrail" "main" {
name = "main"
s3_bucket_name = aws_s3_bucket.cloudtrail.id
...
}
resource "aws_s3_bucket" "cloudtrail" {
....
}
Separating notifications to different Slack channels
Module deployment with the default ruleset and different slack channels for different accounts
# we recomend storing hook url in SSM Parameter store and not commit it to the repo
data "aws_ssm_parameter" "default_hook" {
name = "/cloudtrail-to-slack/default_hook"
}
data "aws_ssm_parameter" "dev_hook" {
name = "/cloudtrail-to-slack/dev_hook"
}
data "aws_ssm_parameter" "prod_hook" {
name = "/cloudtrail-to-slack/prod_hook"
}
module "cloudtrail_to_slack" {
source = "fivexl/cloudtrail-to-slack/aws"
version = "2.0.0"
default_slack_hook_url = data.aws_ssm_parameter.default_hook.value
configuration = [
{
"accounts": ["123456789"],
"slack_hook_url": data.aws_ssm_parameter.dev_hook.value
},
{
"accounts": ["987654321"],
"slack_hook_url": data.aws_ssm_parameter.prod_hook.value
}
]
cloudtrail_logs_s3_bucket_name = aws_s3_bucket.cloudtrail.id
}
resource "aws_cloudtrail" "main" {
name = "main"
s3_bucket_name = aws_s3_bucket.cloudtrail.id
...
}
resource "aws_s3_bucket" "cloudtrail" {
....
}
Tracking certain event types
Module deployment with the list of events to track and default rule sets
# we recomend storing hook url in SSM Parameter store and not commit it to the repo
data "aws_ssm_parameter" "hook" {
name = "/cloudtrail-to-slack/hook"
}
locals {
# CloudTrail events
cloudtrail = "DeleteTrail,StopLogging,UpdateTrail"
# EC2 Instance connect and EC2 events
ec2 = "SendSSHPublicKey"
# Config
config = "DeleteConfigRule,DeleteConfigurationRecorder,DeleteDeliveryChannel,DeleteEvaluationResults"
# All events
events_to_track = "${local.cloudtrail},${local.ec2},${local.config}"
}
module "cloudtrail_to_slack" {
source = "fivexl/cloudtrail-to-slack/aws"
version = "2.0.0"
default_slack_hook_url = data.aws_ssm_parameter.hook.value
cloudtrail_logs_s3_bucket_name = aws_s3_bucket.cloudtrail.id
events_to_track = local.events_to_track
}
resource "aws_cloudtrail" "main" {
name = "main"
s3_bucket_name = aws_s3_bucket.cloudtrail.id
...
}
resource "aws_s3_bucket" "cloudtrail" {
....
}
User defined rules to match events
Module deployment with user-defined rules, list of events to track, and default rule sets
# we recomend storing hook url in SSM Parameter store and not commit it to the repo
data "aws_ssm_parameter" "hook" {
name = "/cloudtrail-to-slack/hook"
}
module "cloudtrail_to_slack" {
source = "fivexl/cloudtrail-to-slack/aws"
version = "2.0.0"
default_slack_hook_url = data.aws_ssm_parameter.hook.value
cloudtrail_logs_s3_bucket_name = aws_s3_bucket.cloudtrail.id
rules = "'errorCode' in event and event['errorCode'] == 'UnauthorizedOperation','userIdentity.type' in event and event['userIdentity.type'] == 'Root'"
events_to_track = "CreateUser,StartInstances"
}
Catch SSM Session events for the "111111111" account
# Important! User defined rules should not contain comas since they are passed to lambda as coma separated string
locals {
cloudtrail_rules = [
"'userIdentity.accountId' in event and event['userIdentity.accountId'] == '11111111111' and event['eventSource'] == 'ssm.amazonaws.com' and event['eventName'].endswith(('Session'))",
]
}
# we recomend storing hook url in SSM Parameter store and not commit it to the repo
data "aws_ssm_parameter" "hook" {
name = "/cloudtrail-to-slack/hook"
}
module "cloudtrail_to_slack" {
source = "fivexl/cloudtrail-to-slack/aws"
version = "2.0.0"
default_slack_hook_url = data.aws_ssm_parameter.hook.value
cloudtrail_logs_s3_bucket_name = aws_s3_bucket.cloudtrail.id
rules = join(",", local.cloudtrail_rules)
}
Using a custom separator for complex rules containing commas
locals {
cloudtrail_rules = [
...
]
custom_separator = "%"
}
module "cloudtrail_to_slack" {
...
rules = join(local.custom_separator, local.cloudtrail_rules)
rules_separator = local.custom_separator
}
Ignore rules.
Ignore events from the account "111111111".
Note! We do recomend fixing alerts instead of ignoring them. But if there is no way you can fix it then there is a way to suppress events by providing ignore rules
# Important! User defined rules should not contain comas since they are passed to lambda as coma separated string
locals {
cloudtrail_ignore_rules = [
"'userIdentity.accountId' in event and event['userIdentity.accountId'] == '11111111111'",
]
}
# we recomend storing hook url in SSM Parameter store and not commit it to the repo
data "aws_ssm_parameter" "hook" {
name = "/cloudtrail-to-slack/hook"
}
module "cloudtrail_to_slack" {
source = "fivexl/cloudtrail-to-slack/aws"
version = "2.3.0"
default_slack_hook_url = data.aws_ssm_parameter.hook.value
cloudtrail_logs_s3_bucket_name = aws_s3_bucket.cloudtrail.id
ignore_rules = join(",", local.cloudtrail_ignore_rules)
}
About rules and how they are applied
This module comes with a set of predefined rules (default rules) that users can take advantage of.
Rules are python strings that are evaluated in the runtime and should return the bool value.
CloudTrail event (see format here) is flattened before processing and should be referenced as event variable
So, for instance, to access ARN from the event below, you should use the notation userIdentity.arn
{
"eventVersion": "1.05",
"userIdentity": {
"type": "IAMUser",
"principalId": "XXXXXXXXXXX",
"arn": "arn:aws:iam::XXXXXXXXXXX:user/xxxxxxxx",
"accountId": "XXXXXXXXXXX",
"userName": "xxxxxxxx"
},
"eventTime": "2019-07-03T16:14:51Z",
"eventSource": "signin.amazonaws.com",
"eventName": "ConsoleLogin",
"awsRegion": "us-east-1",
"sourceIPAddress": "83.41.208.104",
"userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0",
"requestParameters": null,
"responseElements": {
"ConsoleLogin": "Success"
},
"additionalEventData": {
"LoginTo": "https://console.aws.amazon.com/ec2/v2/home?XXXXXXXXXXX",
"MobileVersion": "No",
"MFAUsed": "No"
},
"eventID": "0e4d136e-25d4-4d92-b2b2-8a9fe1e3f1af",
"eventType": "AwsConsoleSignIn",
"recipientAccountId": "XXXXXXXXXXX"
}
Default rules
# Notify if someone logged in without MFA but skip notification for SSO logins
default_rules.append('event["eventName"] == "ConsoleLogin" ' +
'and event["additionalEventData.MFAUsed"] != "Yes" ' +
'and "assumed-role/AWSReservedSSO" not in event.get("userIdentity.arn", "")')
# Notify if someone is trying to do something they not supposed to be doing but do not notify
# about not logged in actions since there are a lot of scans for open buckets that generate noise
# This is useful to discover any misconfigurations in your account. Time to time services will try
# to do something but fail due to IAM permissions and those errors are very hard to find using
# other means
default_rules.append('event.get("errorCode", "") == "UnauthorizedOperation"')
default_rules.append('event.get("errorCode", "") == "AccessDenied" ' +
'and (event.get("userIdentity.accountId", "") != "ANONYMOUS_PRINCIPAL")')
# Notify about all non-read actions done by root
default_rules.append('event.get("userIdentity.type", "") == "Root" ' +
'and not event["eventName"].startswith(("Get", "List", "Describe", "Head"))')
Ignore rules
User can also provide ignore rules. Ignore rules have the same syntax as a default and user defined rules mentioned above. But instead of generating message to Slack on match those rules will cause lambda to ignore an event. Ignore rules tested before default and user defined rules which means that if even is ignored by ignore rules it will not be tested with any other rules.
Terraform specs
Requirements
| Name | Version |
|---|---|
| terraform | >= 0.12.31 |
| aws | >= 3.43 |
Providers
| Name | Version |
|---|---|
| aws | 3.62.0 |
Modules
| Name | Source | Version |
|---|---|---|
| lambda | terraform-aws-modules/lambda/aws | 2.25.0 |
Resources
| Name | Type |
|---|---|
| aws_lambda_permission.s3 | resource |
| aws_s3_bucket_notification.bucket_notification | resource |
| aws_caller_identity.current | data source |
| aws_iam_policy_document.s3 | data source |
| aws_kms_key.cloudtrail | data source |
| aws_s3_bucket.cloudtrail | data source |
Inputs
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| cloudtrail_logs_kms_key_id | Alias, key id or key arn of the KMS Key that used for CloudTrail events | string |
"" |
no |
| cloudtrail_logs_s3_bucket_name | Name of the CloudWatch log s3 bucket that contains CloudTrail events | string |
n/a | yes |
| configuration | Allows to configure slack web hook url per account(s) so you can separate events from different accounts to different channels. Useful in context of AWS organization | list(object({ |
null |
no |
| dead_letter_target_arn | The ARN of an SNS topic or SQS queue to notify when an invocation fails. | string |
null |
no |
| default_slack_hook_url | Slack incoming webhook URL to be used if AWS account id does not match any account id from configuration variable | string |
n/a | yes |
| events_to_track | Comma-separated list events to track and report | string |
"" |
no |
| function_name | Lambda function name | string |
"fivexl-cloudtrail-to-slack" |
no |
| ignore_rules | Comma-separated list of rules to ignore events if you need to suppress something. Will be applied before rules and default_rules | string |
"" |
no |
| lambda_logs_retention_in_days | Controls for how long to keep lambda logs. | number |
30 |
no |
| lambda_timeout_seconds | Controls lambda timeout setting. | number |
30 |
no |
| rules | Comma-separated list of rules to track events if just event name is not enough | string |
"" |
no |
| tags | Tags to attach to resources | map(string) |
{} |
no |
| use_default_rules | Should default rules be used | bool |
true |
no |
| rules_separator | Custom rules separator. Must be defined if there are commas in the rules | string |
"," |
no |
Outputs
| Name | Description |
|---|---|
| lambda_function_arn | The ARN of the Lambda Function |
License
Apache 2 Licensed. See LICENSE for full details.
