diff --git a/README.md b/README.md index 1d3c2dc..4400424 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,351 @@ # aws-iam-activity-alerts -CloudFormation template for monitoring IAM user creation and access key activities with real-time alerts via email (SNS) and Slack. + +CloudFormation template for monitoring IAM user creation and access key activities with real-time alerts via email (SNS) and/or Slack. + +## Overview + +This template sets up automated alerts for critical IAM events: +- **CreateUser** - New IAM user creation +- **CreateAccessKey** - Access key generation +- **CreateLoginProfile** - Console access enablement + +Notifications are delivered via email (SNS) and/or Slack, enabling security teams to monitor unauthorized or unexpected IAM activity in real time. + +## Use Cases + +- **Security Monitoring** - Detect unauthorized IAM user creation in real-time +- **Compliance** - Maintain audit trail for IAM changes +- **Team Awareness** - Keep security teams informed of IAM activities +- **Multi-Account Deployment** - Standardize alerting across AWS Organization + +## Files + +| File | Description | +|------|-------------| +| `iam-alerts-cloudformation.yaml` | Main CloudFormation template with EventBridge, SNS, and Lambda | +| `deploy.sh` | Interactive deployment script with validation | +| `parameters-example.json` | Sample parameters file for easy deployment | + +## Prerequisites + +### 1. AWS CLI + +Install the AWS CLI if you haven't already: + +- **macOS**: `brew install awscli` +- **Linux**: Follow the [official install guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) +- **Windows**: Use the MSI installer from the link above + +Verify it's installed: +```bash +aws --version +``` + +### 2. AWS Credentials + +You need valid AWS credentials with permission to create CloudFormation stacks, IAM roles, EventBridge rules, SNS topics, and Lambda functions. + +**Standard credentials** (long-lived access keys): +```bash +aws configure +# Enter your AWS Access Key ID, Secret Access Key, region, and output format +``` + +**Temporary credentials** (AWS SSO, MFA, or assumed roles): + +`aws configure` does not prompt for a session token. If you're using temporary credentials (e.g. copied from the AWS SSO console), paste all three lines individually: +```bash +aws configure set aws_access_key_id YOUR_ACCESS_KEY_ID +aws configure set aws_secret_access_key YOUR_SECRET_ACCESS_KEY +aws configure set aws_session_token YOUR_SESSION_TOKEN +aws configure set region us-east-1 +``` + +Alternatively, the `deploy.sh` script has a built-in credential setup step — you can paste the full credentials block (all three `key=value` lines at once) when prompted. + +Verify your credentials are working: +```bash +aws sts get-caller-identity +``` +You should see your account ID and IAM principal returned. If this fails, your credentials are invalid or expired. + +### 3. CloudTrail + +This solution relies on AWS CloudTrail to capture IAM API calls. Ensure a trail exists in your account that logs **management events**: + +```bash +aws cloudtrail describe-trails --query 'trailList[*].[name,IsMultiRegionTrail,HomeRegion]' --output table +``` + +If no trail exists, create one in the AWS Console under **CloudTrail > Create trail** before deploying. + +### 4. (Optional) Slack Incoming Webhook URL + +Required only if you want Slack notifications. See [Setting Up Slack Webhooks](#setting-up-slack-webhooks) below. + +--- + +## Quick Start + +### Interactive Script (Recommended) + +The easiest way to deploy is using the interactive script: + +```bash +./deploy.sh +``` + +The script will walk you through the following steps: + +1. **AWS credentials** — checks if you're already authenticated. If not, prompts you to paste your credentials block. +2. **CloudTrail check** — verifies a multi-region trail is active. +3. **Stack name** — defaults to `iam-activity-alerts`, press Enter to accept. +4. **AWS region** — defaults to your currently configured region (e.g. `us-east-1`). +5. **Notification method** — choose one: + - `1` — Email only (enter your email address) + - `2` — Slack only (enter your webhook URL) + - `3` — Both email and Slack +6. **Excluded usernames** — comma-separated list of IAM usernames that should NOT trigger alerts (e.g. automated service accounts). Leave empty to alert on all users. +7. **Alert rule name** — defaults to `iam-user-creation-alert`. +8. **Confirmation** — review the summary and type `yes` to deploy. + +The script waits for the stack to finish deploying and prints the stack outputs when complete. + +### AWS CLI + +Email notifications: +```bash +aws cloudformation create-stack \ + --stack-name iam-activity-alerts \ + --template-body file://iam-alerts-cloudformation.yaml \ + --parameters \ + ParameterKey=NotificationEmail,ParameterValue=security@example.com \ + ParameterKey=ExcludedUserNames,ParameterValue="service-account1,service-account2" \ + --capabilities CAPABILITY_IAM +``` + +Slack notifications: +```bash +aws cloudformation create-stack \ + --stack-name iam-activity-alerts \ + --template-body file://iam-alerts-cloudformation.yaml \ + --parameters \ + ParameterKey=SlackWebhookUrl,ParameterValue=https://hooks.slack.com/services/YOUR/WEBHOOK/URL \ + ParameterKey=ExcludedUserNames,ParameterValue="service-account1,service-account2" \ + --capabilities CAPABILITY_IAM +``` + +Both email and Slack: +```bash +aws cloudformation create-stack \ + --stack-name iam-activity-alerts \ + --template-body file://iam-alerts-cloudformation.yaml \ + --parameters \ + ParameterKey=NotificationEmail,ParameterValue=security@example.com \ + ParameterKey=SlackWebhookUrl,ParameterValue=https://hooks.slack.com/services/YOUR/WEBHOOK/URL \ + ParameterKey=ExcludedUserNames,ParameterValue="service-account1,service-account2" \ + --capabilities CAPABILITY_IAM +``` + +### Parameters File + +Copy `parameters-example.json`, fill in your values, then: + +```bash +aws cloudformation create-stack \ + --stack-name iam-activity-alerts \ + --template-body file://iam-alerts-cloudformation.yaml \ + --parameters file://parameters.json \ + --capabilities CAPABILITY_IAM +``` + +## Configuration Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `NotificationEmail` | Email for SNS alerts | (empty) | +| `SlackWebhookUrl` | Slack webhook URL | (empty) | +| `ExcludedUserNames` | Comma-separated usernames to exclude | (empty) | +| `AlertRuleName` | EventBridge rule name | `iam-user-creation-alert` | + +## Architecture + +1. **EventBridge Rule** - Monitors CloudTrail for IAM events +2. **SNS Topic** (optional) - Email notifications with formatted messages +3. **Lambda Function** (optional) - Slack integration with color-coded alerts +4. **IAM Roles** - Minimal permissions for Lambda execution + +## Setting Up Slack Webhooks + +1. Visit [https://api.slack.com/apps](https://api.slack.com/apps) and click "Create New App" > "From scratch" +2. Under "Incoming Webhooks", toggle "Activate Incoming Webhooks" to ON +3. Click "Add New Webhook to Workspace", select your channel (e.g., `#security-alerts`), and click "Allow" +4. Copy the webhook URL (format: `https://hooks.slack.com/services/T.../B.../xxx`) + +Test it: +```bash +curl -X POST -H 'Content-type: application/json' \ + --data '{"text":"IAM Alerts test message"}' \ + https://hooks.slack.com/services/YOUR/WEBHOOK/URL +``` + +## Email Subscription Confirmation + +After deploying with an email address, AWS sends a confirmation email from `no-reply@sns.amazonaws.com`. You must click the confirmation link within 3 days before alerts will be delivered. + +Verify subscription status: +```bash +aws sns list-subscriptions-by-topic \ + --topic-arn $(aws cloudformation describe-stacks \ + --stack-name iam-activity-alerts \ + --query 'Stacks[0].Outputs[?OutputKey==`SNSTopicArn`].OutputValue' \ + --output text) +``` + +## Alert Information + +Each alert includes: +- Event type and timestamp +- Target username +- AWS Account ID and region +- Principal who performed the action (ARN) +- Source IP address + +## Testing + +```bash +# Create a test user (will trigger alert) +aws iam create-user --user-name test-alert-user + +# Wait 1-2 minutes for the alert, then clean up +aws iam delete-user --user-name test-alert-user +``` + +Ensure `test-alert-user` is not in your excluded users list. + +## Sharing Across Teams + +**Option A: Share via S3 presigned URL** +```bash +# Upload to a private bucket +aws s3 cp iam-alerts-cloudformation.yaml s3://your-templates-bucket/ + +# Generate a time-limited presigned URL (valid 7 days) +aws s3 presign s3://your-templates-bucket/iam-alerts-cloudformation.yaml \ + --expires-in 604800 +``` + +**Option B: CloudFormation StackSets** for organization-wide deployment — see [AWS StackSets documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html). + +**Option C: Fork or clone this repository** — all files are self-contained. + +## Security Notes + +- **Slack webhook URL**: Marked `NoEcho` to prevent display in the CloudFormation console and CLI output. However, `NoEcho` does not prevent access via `lambda:GetFunctionConfiguration` — anyone with that permission can read the environment variable. For higher-security environments, store the webhook URL in [SSM Parameter Store (SecureString)](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html) or [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html) and retrieve it at Lambda runtime instead. +- Lambda has minimal IAM permissions (CloudWatch Logs only) +- SNS topic policy restricts publishing to EventBridge only +- No credentials stored or transmitted + +## Troubleshooting + +### No email alerts received + +1. Check subscription status (see [Email Subscription Confirmation](#email-subscription-confirmation) above) +2. Check spam/junk folder — add `no-reply@sns.amazonaws.com` to contacts +3. Verify email address in stack parameters: + ```bash + aws cloudformation describe-stacks \ + --stack-name iam-activity-alerts \ + --query 'Stacks[0].Parameters[?ParameterKey==`NotificationEmail`].ParameterValue' \ + --output text + ``` + +### No Slack alerts received + +1. Test the webhook directly: + ```bash + curl -X POST -H 'Content-type: application/json' \ + --data '{"text":"Test message"}' YOUR_WEBHOOK_URL + ``` +2. Check Lambda logs: + ```bash + aws logs tail /aws/lambda/iam-user-creation-alert-slack-notifier --follow + ``` + +### No alerts at all + +1. Verify CloudTrail is enabled and logging management events: + ```bash + aws cloudtrail get-trail-status --name YOUR_TRAIL_NAME + ``` +2. Check EventBridge rule is enabled: + ```bash + aws events describe-rule --name iam-user-creation-alert + ``` + Look for `"State": "ENABLED"`. +3. Verify targets: + ```bash + aws events list-targets-by-rule --rule iam-user-creation-alert + ``` + +### Excluded users still triggering alerts + +Check the parameter value (case-sensitive): +```bash +aws cloudformation describe-stacks \ + --stack-name iam-activity-alerts \ + --query 'Stacks[0].Parameters[?ParameterKey==`ExcludedUserNames`].ParameterValue' \ + --output text +``` + +## Customization + +### Add more excluded users + +```bash +aws cloudformation update-stack \ + --stack-name iam-activity-alerts \ + --use-previous-template \ + --parameters \ + ParameterKey=ExcludedUserNames,ParameterValue="user1,user2,user3" \ + ParameterKey=NotificationEmail,UsePreviousValue=true \ + ParameterKey=SlackWebhookUrl,UsePreviousValue=true \ + ParameterKey=AlertRuleName,UsePreviousValue=true +``` + +### Monitor additional IAM events + +Edit the `eventName` list in `iam-alerts-cloudformation.yaml`: + +```yaml +eventName: + - CreateUser + - CreateAccessKey + - CreateLoginProfile + - DeleteUser # add as needed + - DeleteAccessKey # add as needed +``` + +## Cleanup + +Use the interactive teardown script to remove all deployed resources: + +```bash +./teardown.sh +``` + +The script lists all stack resources before asking for confirmation, then waits for the deletion to complete. + +Alternatively, delete directly with the AWS CLI: +```bash +aws cloudformation delete-stack --stack-name iam-activity-alerts --region us-east-1 +``` + +## Cost + +Expected: **< $1/month** for typical usage + +- EventBridge: No charge for rules; minimal per-event cost +- Lambda (Slack): Free tier eligible +- SNS (email): First 1,000 emails/month free +- CloudWatch Logs: Minimal charges diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..30508bb --- /dev/null +++ b/deploy.sh @@ -0,0 +1,397 @@ +#!/bin/bash + +# IAM Activity Alerts - CloudFormation Deployment Script +# This script helps deploy the IAM alerts stack to your AWS account + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Default values +STACK_NAME="iam-activity-alerts" +TEMPLATE_FILE="iam-alerts-cloudformation.yaml" +REGION="" + +# Function to print colored output +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to check if AWS CLI is installed +check_aws_cli() { + if ! command -v aws &> /dev/null; then + print_error "AWS CLI is not installed. Please install it first." + echo "Visit: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" + exit 1 + fi + print_info "AWS CLI found: $(aws --version)" +} + +# Function to configure AWS credentials +configure_aws_credentials() { + echo "" + print_info "Checking AWS credentials..." + + # Check if credentials are already configured + if aws sts get-caller-identity &>/dev/null; then + local identity + identity=$(aws sts get-caller-identity --query 'Arn' --output text 2>/dev/null) + print_info "Already authenticated as: ${identity}" + read -r -p "Do you want to reconfigure credentials? (yes/no) [no]: " reconfigure + if [ "${reconfigure}" != "yes" ] && [ "${reconfigure}" != "y" ]; then + return + fi + else + print_warning "No valid AWS credentials found. Please configure them now." + fi + + echo "" + echo "Paste your AWS credentials block below, then press Enter twice:" + echo "(e.g. from AWS SSO: aws_access_key_id=... / aws_secret_access_key=... / aws_session_token=...)" + echo "" + + aws_access_key_id="" + aws_secret_access_key="" + aws_session_token="" + + while IFS= read -r line; do + [[ -z "$line" ]] && break + key="${line%%=*}" + value="${line#*=}" + case "$key" in + aws_access_key_id) aws_access_key_id="$value" ;; + aws_secret_access_key) aws_secret_access_key="$value" ;; + aws_session_token) aws_session_token="$value" ;; + esac + done + + if [ -z "$REGION" ]; then + REGION=$(get_configured_region) + fi + read -r -p "AWS Region [${REGION}]: " input + REGION=${input:-$REGION} + + export AWS_ACCESS_KEY_ID="$aws_access_key_id" + export AWS_SECRET_ACCESS_KEY="$aws_secret_access_key" + if [ -n "$aws_session_token" ]; then + export AWS_SESSION_TOKEN="$aws_session_token" + print_info "Session token configured." + fi + + # Verify credentials work + if aws sts get-caller-identity &>/dev/null; then + local identity + identity=$(aws sts get-caller-identity --query 'Arn' --output text 2>/dev/null) + print_info "Successfully authenticated as: ${identity}" + else + print_error "Credential verification failed. Please check your credentials and try again." + exit 1 + fi +} + +# Function to check if CloudTrail is enabled +check_cloudtrail() { + print_info "Checking if CloudTrail is enabled..." + local trails + trails=$(aws cloudtrail describe-trails --query 'trailList[?IsMultiRegionTrail==`true`]' --output json 2>/dev/null) + + if [ "$trails" == "[]" ]; then + print_warning "No multi-region CloudTrail found. The alerts require CloudTrail to be enabled." + print_warning "Please ensure CloudTrail is configured to log management events." + else + print_info "CloudTrail is enabled." + fi +} + +# Function to get the configured AWS region, falling back to us-east-1 if invalid +get_configured_region() { + local region + region=$(aws configure get region 2>/dev/null) + if [[ "$region" =~ ^[a-z]{2}(-[a-z]+)+-[0-9]+$ ]]; then + echo "$region" + else + echo "us-east-1" + fi +} + +# Function to validate email +validate_email() { + local email=$1 + if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then + return 0 + else + return 1 + fi +} + +# Function to get user input +get_parameters() { + echo "" + echo "==========================================" + echo "IAM Activity Alerts - Configuration" + echo "==========================================" + echo "" + + # Stack name + read -r -p "Stack name [${STACK_NAME}]: " input + STACK_NAME=${input:-$STACK_NAME} + + # AWS Region + if [ -z "$REGION" ]; then + REGION=$(get_configured_region) + fi + read -r -p "AWS Region [${REGION}]: " input + REGION=${input:-$REGION} + + # Notification method + echo "" + echo "Choose notification method:" + echo "1) Email only" + echo "2) Slack only" + echo "3) Both email and Slack" + read -r -p "Selection [1-3]: " notification_choice + + EMAIL="" + WEBHOOK="" + + case $notification_choice in + 1) + while true; do + read -r -p "Email address: " EMAIL + if validate_email "$EMAIL"; then + break + else + print_error "Invalid email format. Please try again." + fi + done + ;; + 2) + while true; do + read -r -p "Slack Webhook URL: " WEBHOOK + if [[ "$WEBHOOK" =~ ^https://hooks\.slack\.com/services/ ]]; then + break + else + print_error "Invalid Slack webhook URL. Must start with https://hooks.slack.com/services/" + fi + done + ;; + 3) + while true; do + read -r -p "Email address: " EMAIL + if validate_email "$EMAIL"; then + break + else + print_error "Invalid email format. Please try again." + fi + done + while true; do + read -r -p "Slack Webhook URL: " WEBHOOK + if [[ "$WEBHOOK" =~ ^https://hooks\.slack\.com/services/ ]]; then + break + else + print_error "Invalid Slack webhook URL. Must start with https://hooks.slack.com/services/" + fi + done + ;; + *) + print_error "Invalid selection" + exit 1 + ;; + esac + + # Excluded usernames + echo "" + read -r -p "Excluded usernames (comma-separated, leave empty for none): " EXCLUDED_USERS + + # Alert rule name + read -r -p "Alert rule name [iam-user-creation-alert]: " input + ALERT_RULE_NAME=${input:-iam-user-creation-alert} +} + +# Function to build parameters array +build_parameters() { + PARAMS=( + "ParameterKey=ExcludedUserNames,ParameterValue=${EXCLUDED_USERS}" + "ParameterKey=AlertRuleName,ParameterValue=${ALERT_RULE_NAME}" + ) + + if [ -n "$EMAIL" ]; then + PARAMS+=("ParameterKey=NotificationEmail,ParameterValue=${EMAIL}") + else + PARAMS+=("ParameterKey=NotificationEmail,ParameterValue=") + fi + + if [ -n "$WEBHOOK" ]; then + PARAMS+=("ParameterKey=SlackWebhookUrl,ParameterValue=${WEBHOOK}") + else + PARAMS+=("ParameterKey=SlackWebhookUrl,ParameterValue=") + fi +} + +# Function to deploy stack +deploy_stack() { + print_info "Deploying CloudFormation stack: ${STACK_NAME}" + print_info "Region: ${REGION}" + + # Check if stack exists + if aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$REGION" &>/dev/null; then + print_warning "Stack already exists. Updating..." + OPERATION="update-stack" + else + print_info "Creating new stack..." + OPERATION="create-stack" + fi + + # Deploy using array to avoid eval/injection risk + aws cloudformation "$OPERATION" \ + --stack-name "$STACK_NAME" \ + --template-body file://"$TEMPLATE_FILE" \ + --parameters "${PARAMS[@]}" \ + --capabilities CAPABILITY_IAM \ + --region "$REGION" + + print_info "Stack deployment initiated successfully!" + print_info "Waiting for stack to complete..." + + if [ "$OPERATION" == "create-stack" ]; then + aws cloudformation wait stack-create-complete --stack-name "$STACK_NAME" --region "$REGION" + else + aws cloudformation wait stack-update-complete --stack-name "$STACK_NAME" --region "$REGION" 2>/dev/null + STATUS=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$REGION" \ + --query 'Stacks[0].StackStatus' --output text) + if [[ "$STATUS" != "UPDATE_COMPLETE" ]]; then + print_error "Stack update failed. Status: ${STATUS}" + exit 1 + fi + fi + + print_info "Stack deployment completed!" + + if [ -n "$EMAIL" ]; then + echo "" + print_warning "IMPORTANT: Check your email and confirm the SNS subscription!" + fi + + echo "" + print_info "Stack outputs:" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --region "$REGION" \ + --query 'Stacks[0].Outputs' \ + --output table +} + +# Function to show summary +show_summary() { + echo "" + echo "==========================================" + echo "Deployment Summary" + echo "==========================================" + echo "Stack Name: ${STACK_NAME}" + echo "Region: ${REGION}" + echo "Email: ${EMAIL:-Not configured}" + echo "Slack: ${WEBHOOK:+Configured}${WEBHOOK:-Not configured}" + echo "Excluded Users: ${EXCLUDED_USERS:-None}" + echo "Alert Rule: ${ALERT_RULE_NAME}" + echo "==========================================" + echo "" + read -r -p "Proceed with deployment? (yes/no): " confirm + + if [ "$confirm" != "yes" ] && [ "$confirm" != "y" ]; then + print_warning "Deployment cancelled." + exit 0 + fi +} + +# Main execution +main() { + echo "" + echo "╔════════════════════════════════════════╗" + echo "║ IAM Activity Alerts Deployment ║" + echo "║ CloudFormation Stack Setup ║" + echo "╚════════════════════════════════════════╝" + echo "" + + check_aws_cli + configure_aws_credentials + check_cloudtrail + get_parameters + build_parameters + show_summary + deploy_stack + + echo "" + print_info "Deployment complete!" + echo "" + echo "==========================================" + echo "Next Steps" + echo "==========================================" + echo "" + if [ -n "$EMAIL" ]; then + echo "1. Confirm your SNS email subscription" + echo " AWS sent a confirmation email to: ${EMAIL}" + echo " You MUST click the confirmation link before alerts will be delivered." + echo "" + fi + echo "2. Verify the alert pipeline works end-to-end" + echo " Each command below triggers a monitored IAM event, which flows through:" + echo " CloudTrail -> EventBridge rule -> Lambda -> SNS/Slack notification" + echo "" + echo " --- Test 1: CreateUser ---" + echo " Triggers an alert when a new IAM user is created." + echo " aws iam create-user --user-name test-alert-user" + echo "" + echo " --- Test 2: CreateAccessKey ---" + echo " Triggers an alert when programmatic credentials are issued for a user." + echo " aws iam create-access-key --user-name test-alert-user" + echo "" + echo " --- Test 3: CreateLoginProfile ---" + echo " Triggers an alert when console (password) access is enabled for a user." + echo " aws iam create-login-profile --user-name test-alert-user --password 'TempPass123!' --no-password-reset-required" + echo "" + echo " --- Cleanup (run after testing) ---" + echo " Delete the access key (get the key ID from the CreateAccessKey output):" + echo " aws iam delete-access-key --user-name test-alert-user --access-key-id " + echo " Delete the login profile:" + echo " aws iam delete-login-profile --user-name test-alert-user" + echo " Delete the test user:" + echo " aws iam delete-user --user-name test-alert-user" + echo "" + echo "3. Check CloudWatch Logs if you don't receive an alert" + echo " The Lambda function logs to:" + echo " /aws/lambda/${ALERT_RULE_NAME}" + echo "" + echo " View recent logs:" + echo " aws logs tail /aws/lambda/${ALERT_RULE_NAME} --follow --region ${REGION}" + echo "" + echo "==========================================" + echo "Stack Management" + echo "==========================================" + echo "" + echo "View stack status:" + echo " aws cloudformation describe-stacks --stack-name ${STACK_NAME} --region ${REGION} --query 'Stacks[0].StackStatus'" + echo "" + echo "Tear down all resources:" + echo " Use the included teardown script for a guided removal (recommended):" + echo " ./teardown.sh" + echo "" + echo " Or manually via AWS CLI:" + echo " aws cloudformation delete-stack --stack-name ${STACK_NAME} --region ${REGION}" + echo " aws cloudformation wait stack-delete-complete --stack-name ${STACK_NAME} --region ${REGION}" + echo "" +} + +# Run main function +main diff --git a/iam-alerts-cloudformation.yaml b/iam-alerts-cloudformation.yaml new file mode 100644 index 0000000..464c42b --- /dev/null +++ b/iam-alerts-cloudformation.yaml @@ -0,0 +1,290 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'IAM Activity Monitoring - Alerts for CreateUser, CreateAccessKey, and CreateLoginProfile events' + +Parameters: + NotificationEmail: + Type: String + Description: Email address to receive IAM activity alerts (leave empty if using Slack only) + Default: '' + + SlackWebhookUrl: + Type: String + Description: > + Slack webhook URL for notifications (leave empty if using email only). + NOTE: NoEcho prevents display in the console, but the value is still readable + via lambda:GetFunctionConfiguration. For higher-security environments, store + this in SSM Parameter Store (SecureString) or Secrets Manager and retrieve + it at Lambda runtime instead. + Default: '' + NoEcho: true + + ExcludedUserNames: + Type: CommaDelimitedList + Description: Comma-separated list of usernames to exclude from alerts (e.g. service accounts) + Default: '' + + AlertRuleName: + Type: String + Description: Name for the EventBridge rule + Default: 'iam-user-creation-alert' + +Conditions: + UseEmail: !Not [!Equals [!Ref NotificationEmail, '']] + UseSlack: !Not [!Equals [!Ref SlackWebhookUrl, '']] + HasNotifications: !Or [!Condition UseEmail, !Condition UseSlack] + +Rules: + AtLeastOneNotification: + Assertions: + - Assert: !Or + - !Not [!Equals [!Ref NotificationEmail, '']] + - !Not [!Equals [!Ref SlackWebhookUrl, '']] + AssertDescription: At least one notification method (email or Slack) must be configured + +Resources: + # SNS Topic for email notifications + IAMAlertTopic: + Type: AWS::SNS::Topic + Condition: UseEmail + Properties: + TopicName: !Sub '${AlertRuleName}-topic' + DisplayName: IAM Activity Alerts + Subscription: + - Endpoint: !Ref NotificationEmail + Protocol: email + + # SNS Topic Policy + IAMAlertTopicPolicy: + Type: AWS::SNS::TopicPolicy + Condition: UseEmail + Properties: + Topics: + - !Ref IAMAlertTopic + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: AllowEventBridgeToPublish + Effect: Allow + Principal: + Service: events.amazonaws.com + Action: 'sns:Publish' + Resource: !Ref IAMAlertTopic + + # Lambda function for Slack notifications + SlackNotificationFunction: + Type: AWS::Lambda::Function + Condition: UseSlack + Properties: + FunctionName: !Sub '${AlertRuleName}-slack-notifier' + Runtime: python3.12 + Handler: index.lambda_handler + Role: !GetAtt SlackNotificationFunctionRole.Arn + Timeout: 30 + Environment: + Variables: + SLACK_WEBHOOK_URL: !Ref SlackWebhookUrl + Code: + ZipFile: | + import json + import logging + import os + import urllib3 + from datetime import datetime + + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + http = urllib3.PoolManager() + + def lambda_handler(event, context): + webhook_url = os.environ['SLACK_WEBHOOK_URL'] + + if not webhook_url.startswith('https://hooks.slack.com/'): + logger.error('Invalid webhook URL - must start with https://hooks.slack.com/') + raise RuntimeError('Invalid webhook URL') + + # Parse the EventBridge event + detail = event.get('detail', {}) + event_name = detail.get('eventName', 'Unknown') + user_name = detail.get('requestParameters', {}).get('userName', 'Unknown') + event_time = detail.get('eventTime', 'Unknown') + source_ip = detail.get('sourceIPAddress', 'Unknown') + user_identity = detail.get('userIdentity', {}) + principal = user_identity.get('principalId', 'Unknown') + arn = user_identity.get('arn', 'Unknown') + account_id = event.get('account', 'Unknown') + region = event.get('region', 'Unknown') + + # Determine color and emoji based on event type + color_map = { + 'CreateUser': '#FF9900', + 'CreateAccessKey': '#FF6600', + 'CreateLoginProfile': '#FF3300' + } + + emoji_map = { + 'CreateUser': ':bust_in_silhouette:', + 'CreateAccessKey': ':key:', + 'CreateLoginProfile': ':closed_lock_with_key:' + } + + color = color_map.get(event_name, '#FF0000') + emoji = emoji_map.get(event_name, ':warning:') + + # Build Slack message + slack_message = { + 'text': f'{emoji} IAM Activity Alert: {event_name}', + 'attachments': [ + { + 'color': color, + 'title': f'{event_name} Event Detected', + 'fields': [ + { + 'title': 'Target User', + 'value': f'`{user_name}`', + 'short': True + }, + { + 'title': 'Event Time', + 'value': f'`{event_time}`', + 'short': True + }, + { + 'title': 'Account ID', + 'value': f'`{account_id}`', + 'short': True + }, + { + 'title': 'Region', + 'value': f'`{region}`', + 'short': True + }, + { + 'title': 'Source IP', + 'value': f'`{source_ip}`', + 'short': True + }, + { + 'title': 'Principal', + 'value': f'`{principal}`', + 'short': True + }, + { + 'title': 'Principal ARN', + 'value': f'`{arn}`', + 'short': False + } + ], + 'footer': 'AWS CloudTrail', + 'ts': int(datetime.now().timestamp()) + } + ] + } + + # Send to Slack + try: + encoded_data = json.dumps(slack_message).encode('utf-8') + response = http.request( + 'POST', + webhook_url, + body=encoded_data, + headers={'Content-Type': 'application/json'} + ) + if response.status != 200: + msg = f'Slack webhook returned HTTP {response.status}: {response.data.decode("utf-8")}' + logger.error(msg) + raise RuntimeError(msg) + except Exception as e: + logger.exception('Failed to send Slack notification: %s', str(e)) + raise + + return { + 'statusCode': 200, + 'body': json.dumps('Notification sent to Slack') + } + + # IAM Role for Lambda + SlackNotificationFunctionRole: + Type: AWS::IAM::Role + Condition: UseSlack + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: 'sts:AssumeRole' + ManagedPolicyArns: + - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' + + # Permission for EventBridge to invoke Lambda + SlackNotificationFunctionPermission: + Type: AWS::Lambda::Permission + Condition: UseSlack + Properties: + FunctionName: !Ref SlackNotificationFunction + Action: 'lambda:InvokeFunction' + Principal: events.amazonaws.com + SourceArn: !GetAtt IAMEventRule.Arn + + # EventBridge Rule + IAMEventRule: + Type: AWS::Events::Rule + Properties: + Name: !Ref AlertRuleName + Description: Alert on IAM user creation and access key/login profile creation + State: ENABLED + EventPattern: + source: + - aws.iam + detail-type: + - AWS API Call via CloudTrail + detail: + eventSource: + - iam.amazonaws.com + eventName: + - CreateUser + - CreateAccessKey + - CreateLoginProfile + requestParameters: + userName: + - anything-but: !Ref ExcludedUserNames + Targets: + - !If + - UseEmail + - Arn: !Ref IAMAlertTopic + Id: email-target + InputTransformer: + InputPathsMap: + eventName: $.detail.eventName + userName: $.detail.requestParameters.userName + eventTime: $.detail.eventTime + sourceIP: $.detail.sourceIPAddress + principalId: $.detail.userIdentity.principalId + arn: $.detail.userIdentity.arn + accountId: $.account + region: $.region + InputTemplate: >- + "IAM Activity Alert\n==================\n\nEvent: \nTarget User: \nTime: \nAccount ID: \nRegion: \n\nPerformed by:\nPrincipal: \nARN: \nSource IP: \n\nThis alert was triggered because a new IAM user, access key, or login profile was created." + - !Ref AWS::NoValue + - !If + - UseSlack + - Arn: !GetAtt SlackNotificationFunction.Arn + Id: slack-target + - !Ref AWS::NoValue + +Outputs: + EventRuleName: + Description: Name of the EventBridge rule + Value: !Ref IAMEventRule + + SNSTopicArn: + Condition: UseEmail + Description: ARN of the SNS topic for email notifications + Value: !Ref IAMAlertTopic + + LambdaFunctionArn: + Condition: UseSlack + Description: ARN of the Lambda function for Slack notifications + Value: !GetAtt SlackNotificationFunction.Arn diff --git a/parameters-example.json b/parameters-example.json new file mode 100644 index 0000000..6686a7d --- /dev/null +++ b/parameters-example.json @@ -0,0 +1,18 @@ +[ + { + "ParameterKey": "NotificationEmail", + "ParameterValue": "security-team@example.com" + }, + { + "ParameterKey": "SlackWebhookUrl", + "ParameterValue": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL" + }, + { + "ParameterKey": "ExcludedUserNames", + "ParameterValue": "service-account1,service-account2" + }, + { + "ParameterKey": "AlertRuleName", + "ParameterValue": "iam-user-creation-alert" + } +] diff --git a/teardown.sh b/teardown.sh new file mode 100755 index 0000000..d496502 --- /dev/null +++ b/teardown.sh @@ -0,0 +1,194 @@ +#!/bin/bash + +# IAM Activity Alerts - CloudFormation Teardown Script +# This script removes the IAM alerts stack and all associated AWS resources + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Default values +STACK_NAME="iam-activity-alerts" +REGION="" +LAMBDA_FUNCTION_NAME="" + +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to get the configured AWS region, falling back to us-east-1 if invalid +get_configured_region() { + local region + region=$(aws configure get region 2>/dev/null) + if [[ "$region" =~ ^[a-z]{2}(-[a-z]+)+-[0-9]+$ ]]; then + echo "$region" + else + echo "us-east-1" + fi +} + +check_aws_cli() { + if ! command -v aws &> /dev/null; then + print_error "AWS CLI is not installed." + exit 1 + fi + if ! aws sts get-caller-identity &>/dev/null; then + print_error "No valid AWS credentials found. Please configure them first." + exit 1 + fi +} + +get_parameters() { + echo "" + echo "==========================================" + echo "IAM Activity Alerts - Teardown" + echo "==========================================" + echo "" + + read -r -p "Stack name [${STACK_NAME}]: " input + STACK_NAME=${input:-$STACK_NAME} + + if [ -z "$REGION" ]; then + REGION=$(get_configured_region) + fi + read -r -p "AWS Region [${REGION}]: " input + REGION=${input:-$REGION} +} + +delete_stack() { + # Check the stack exists + if ! aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$REGION" &>/dev/null; then + print_warning "Stack '${STACK_NAME}' not found in region '${REGION}'. Nothing to delete." + exit 0 + fi + + echo "" + print_warning "This will permanently delete the following CloudFormation stack and all its resources:" + echo " Stack: ${STACK_NAME}" + echo " Region: ${REGION}" + echo "" + print_info "Resources that will be removed:" + aws cloudformation list-stack-resources \ + --stack-name "$STACK_NAME" \ + --region "$REGION" \ + --query 'StackResourceSummaries[*].[ResourceType,LogicalResourceId,ResourceStatus]' \ + --output table 2>/dev/null || true + + # Capture the Lambda function name before deletion so we can clean up its log group later + LAMBDA_FUNCTION_NAME=$(aws cloudformation list-stack-resources \ + --stack-name "$STACK_NAME" \ + --region "$REGION" \ + --query "StackResourceSummaries[?ResourceType=='AWS::Lambda::Function'].PhysicalResourceId" \ + --output text 2>/dev/null || echo "") + + echo "" + read -r -p "Are you sure you want to delete this stack? (yes/no): " confirm + if [ "$confirm" != "yes" ] && [ "$confirm" != "y" ]; then + print_warning "Teardown cancelled." + exit 0 + fi + + print_info "Deleting CloudFormation stack: ${STACK_NAME}..." + aws cloudformation delete-stack \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + + print_info "Waiting for stack deletion to complete..." + if aws cloudformation wait stack-delete-complete \ + --stack-name "$STACK_NAME" \ + --region "$REGION" 2>/dev/null; then + print_info "Stack '${STACK_NAME}' deleted successfully." + else + print_error "Stack deletion may have failed. Check the CloudFormation console for details." + exit 1 + fi +} + +verify_deletion() { + echo "" + print_info "Verifying all resources have been removed..." + local all_clean=true + + # 1. Confirm the CloudFormation stack is gone + if aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$REGION" &>/dev/null; then + print_error "CloudFormation stack '${STACK_NAME}' still exists. Check the console for stuck resources." + all_clean=false + else + print_info "CloudFormation stack removed." + fi + + # 2. Check for leftover CloudWatch Log Groups (Lambda creates these at runtime; they are not owned by the stack) + local log_groups + if [ -n "$LAMBDA_FUNCTION_NAME" ]; then + log_groups=$(aws logs describe-log-groups \ + --log-group-name-prefix "/aws/lambda/${LAMBDA_FUNCTION_NAME}" \ + --region "$REGION" \ + --query "logGroups[].logGroupName" \ + --output text 2>/dev/null) + else + log_groups=$(aws logs describe-log-groups \ + --log-group-name-prefix "/aws/lambda/" \ + --region "$REGION" \ + --query "logGroups[?contains(logGroupName, '${STACK_NAME}')].logGroupName" \ + --output text 2>/dev/null) + fi + + if [ -n "$log_groups" ]; then + print_warning "The following CloudWatch Log Groups were not removed (Lambda creates these outside of CloudFormation):" + for lg in $log_groups; do + echo " ${lg}" + done + echo "" + read -r -p "Delete these log groups? (yes/no) [no]: " delete_logs + if [ "$delete_logs" == "yes" ] || [ "$delete_logs" == "y" ]; then + for lg in $log_groups; do + aws logs delete-log-group --log-group-name "$lg" --region "$REGION" + print_info "Deleted log group: ${lg}" + done + else + print_warning "Log groups left in place. You can delete them manually in the CloudWatch console." + fi + else + print_info "No leftover CloudWatch Log Groups found." + fi + + # 3. Summary + echo "" + if [ "$all_clean" = true ]; then + print_info "All stack resources have been removed successfully." + else + print_warning "Some resources may still exist. Review the output above." + fi +} + +main() { + echo "" + echo "╔════════════════════════════════════════╗" + echo "║ IAM Activity Alerts Teardown ║" + echo "║ CloudFormation Stack Removal ║" + echo "╚════════════════════════════════════════╝" + echo "" + + check_aws_cli + get_parameters + delete_stack + verify_deletion + + echo "" + print_info "Teardown complete." + echo "" +} + +main