AWS Event Driven Architecture For Security

Jose Adailson De Sousa Silva
AWS Tip
Published in
5 min readApr 11, 2024

--

One of the biggest challenges of the Cloud Security professional is to deal with services exposed to the internet without the proper security controls in the creation of the resource and or application after construction.

Following this scenario, I was challenged to create an alert monitoring on AWS to warn the Cloud Security team when a specific resource was created. Be it an ec2 Instance, Load Balancer, Elastic IP

We sat down with the AWS team that supports us and asked for some examples of how to do this somehow economically and using the AWS services we already use in our structure.

So we come to Event Driven Architecture.

To build this solution, we use services on AWS such as AWS EventBridge, Lambda, CloudWatch and SNS.

Elb notification

Ec2 Instance notification

Elastic IP notification

First we have to create the Lambda function that will process the events we want to capture and sends the event payload to the Cloud Watch Log Group, in Lambda we also send the notification to SNS that sent to registered people (I’m using email notification) In addition, the message that will be sent is formatted.

import json
import boto3
import time

cloudwatch_logs_client = boto3.client('logs')
sns_client = boto3.client('sns')

def lambda_handler(event, context):
log_event(event, context)

try:
event_source = event['source']
detail_type = event['detail-type']
event_time = event['time']
event_name = event['detail']['eventName']
instance_id_or_public_ip_or_dns_name = ''

if event_source == 'aws.ec2' and event_name == 'RunInstances':
instance_id = event['detail']['responseElements']['instancesSet']['items'][0]['instanceId']
message = f"Received EC2 instance creation event: {detail_type}, at: {event_time}, event_name: {event_name}, Instance ID: {instance_id}"
instance_id_or_public_ip_or_dns_name = f"Instance ID: {instance_id}"
elif event_source == 'aws.ec2' and event_name == 'AllocateAddress':
public_ip = event['detail']['responseElements']['publicIp']
message = f"Received Elastic IP allocation event: {detail_type}, at: {event_time}, event_name: {event_name}, Public IP: {public_ip}"
instance_id_or_public_ip_or_dns_name = f"Elastic IP: {public_ip}"
else:
dns_name = event['detail']['responseElements']['loadBalancers'][0]['dNSName']
message = f"Received ELB creation event from: {event_source} {detail_type}, at: {event_time}, event_name: {event_name}, Load Balancer DNS:{dns_name}"
instance_id_or_public_ip_or_dns_name = f"DNS Name: {dns_name}"

print(message)
sns_client.publish(
TopicArn='arn:aws:sns:us-east-1:123456789012:PublicIPNotificationTopic',
Message=json.dumps(message),
Subject=f"AWS Notification - Resource Created: {instance_id_or_public_ip_or_dns_name}"
)
except KeyError as e:
print(f"Error: {e}")
print(f"Event: {event}")

def log_event(event, context):
log_group_name = '/aws/lambda/Aws_External_Ip_Notified'
log_stream_name = context.log_stream_name

log_events = [{'timestamp': int(round(time.time() * 1000)), 'message': json.dumps(event)}]
response = cloudwatch_logs_client.put_log_events(
logGroupName=log_group_name,
logStreamName=log_stream_name,
logEvents=log_events
)

After that, we built our infrastructure using Terraform.

  provider "aws" {
region = "us-east-1"
}

resource "aws_cloudwatch_event_rule" "cloudtrail_events" {
name = "CloudTrailEventsRule"
description = "Rule to capture CloudTrail events for EC2 instance creation, Elastic IP allocation, and ELB creation"
event_pattern = <<EOF
{
"source": ["aws.ec2", "aws.elasticloadbalancing"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventSource": ["ec2.amazonaws.com", "elasticloadbalancing.amazonaws.com"],
"eventName": ["RunInstances", "AllocateAddress", "CreateLoadBalancer"]
}
}
EOF
}

resource "aws_cloudwatch_event_target" "lambda_target" {
rule = aws_cloudwatch_event_rule.cloudtrail_events.name
target_id = "LambdaTarget"
arn = aws_lambda_function.lambda_function.arn
}

data "archive_file" "lambda" {
type = "zip"
source_file = "aws_elastic_ip_logs.py"
output_path = "aws_elastic_ip_logs.zip"
}

resource "aws_lambda_function" "lambda_function" {
filename = "aws_elastic_ip_logs.zip"
function_name = "Aws_External_Ip_Notified"
role = aws_iam_role.lambda_role.arn
handler = "aws_elastic_ip_logs.lambda_handler"
runtime = "python3.11"
}

resource "aws_lambda_permission" "eventbridge_permission" {
statement_id = "AllowExecutionFromCloudWatch"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.lambda_function.arn
principal = "events.amazonaws.com"

source_arn = aws_cloudwatch_event_rule.cloudtrail_events.arn
}

resource "aws_sns_topic" "sns_topic" {
name = "PublicIPNotificationTopic"
}

resource "aws_sns_topic_subscription" "lambda_subscription" {
topic_arn = aws_sns_topic.sns_topic.arn
protocol = "lambda"
endpoint = aws_lambda_function.lambda_function.arn
}

resource "aws_sns_topic_subscription" "email_subscription" {
topic_arn = aws_sns_topic.sns_topic.arn
protocol = "email"
endpoint = "target@email.com"
}


resource "aws_iam_role" "lambda_role" {
name = "LambdaExecutionRole"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})

inline_policy {
name = "LambdaCloudWatchLogsPolicy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams"
]
Resource = "arn:aws:logs:*:*:*"
}
]
})
}
}

resource "aws_iam_policy" "sns_publish_policy" {
name = "SNSPublishPolicy"
description = "Allows publishing messages to the SNS topic"

policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sns:Publish",
"Resource": "${aws_sns_topic.sns_topic.arn}"
}
]
}
EOF
}

resource "aws_iam_role_policy_attachment" "sns_publish_attachment" {
role = aws_iam_role.lambda_role.name
policy_arn = aws_iam_policy.sns_publish_policy.arn
}

resource "aws_iam_policy_attachment" "lambda_role_attachment" {
name = "LambdaCloudWatchLogsPolicyAttachment"
roles = [aws_iam_role.lambda_role.name]
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_cloudwatch_log_group" "lambda_log_group" {
name = "/aws/lambda/Aws_External_Ip_Notified"
retention_in_days = 30
}

output "sns_topic_arn" {
value = aws_sns_topic.sns_topic.arn
}

Below is what each resource in the terraform code does.

CloudWatch Event Rule: Defines a CloudWatch Event Rule named “CloudTrailEventsRule”. It specifies an event pattern that captures CloudTrail events related to EC2 instance creation, Elastic IP allocation, and ELB creation.

CloudWatch Event Target: Associates a Lambda function as the target for the CloudWatch Event Rule defined earlier. The Lambda function will be triggered by events matching the rule.

Archive File Data Source: Creates a zip archive from the source file “aws_elastic_ip_logs.py”. This is likely the source code for the Lambda function.

Lambda Function: Defines a Lambda function named “Aws_External_Ip_Notified” using the zip archive created earlier. It specifies the runtime as Python 3.11 and sets up a role (aws_iam_role.lambda_role) for the Lambda function to execute.

Lambda Permission: Grants permission to CloudWatch Events to invoke the Lambda function. It ensures that the CloudWatch Event Rule can trigger the Lambda function.

SNS Topic: Creates an SNS topic named “PublicIPNotificationTopic” for sending notifications.

SNS Topic Subscriptions: Configures two subscriptions for the SNS topic: one for the Lambda function and one for an email address (target@email.com). This allows notifications to be sent to both.

IAM Role for Lambda: Defines an IAM role named “LambdaExecutionRole” for the Lambda function with permissions necessary for CloudWatch Logs and Lambda execution.

IAM Policy for SNS Publish: Creates an IAM policy that allows publishing messages to the SNS topic.

IAM Role Policy Attachment: Attaches the IAM policy for publishing to the SNS topic to the IAM role created for the Lambda function.

IAM Policy Attachment: Attaches the AWS managed policy “AWSLambdaBasicExecutionRole” to the Lambda execution role. This policy provides basic execution permissions for Lambda functions.

CloudWatch Log Group: Creates a CloudWatch log group for the Lambda function’s logs.

--

--

Passionate about football, travel, beer and technology. Not necessarily in that order!