Using Lambda and “aws:PrincipalOrgID” to centrally manage AWS Cloudwatch alarms at scale.

Using Lambda and “aws:PrincipalOrgID” to centrally manage AWS Cloudwatch alarms at scale.

When AWS announced the introduction of the aws:PricipalOrgID attribute in resource-based policies, it became a lot easier to secure cross-account access to resources within an AWS Organization. It also helped in making these resource policies low maintenance!

A practical application is shown below where multiple users and roles from separate accounts within the same AWS Organization access a single SNS topic in a central account. The Organization attribute accompanies the SNS messages so the receiving side can verify this.

Securing the SNS topic with a policy

Securing the central SNS topic with the new attribute is as simple as below policy. No need to specify every user or role in your organization!

{
    "Effect": "Allow",
    "Principal": "*",
    "Action": [
        "sns:Publish"
    ],
    "Resource": "arn:aws:sns:eu-west-1:123456789012:mytopic",
    "Condition": {
        "StringEquals": {
            "aws:PrincipalOrgID": [
                "o-123456"
            ]
        }
    }
}

Limitation of the aws:PrincipalOrgID attribute

Unfortunately, only users and roles send the aws:PrincipalOrgID attribute while publishing to an SNS topic which makes it impossible to centrally manage SNS notifications originating from resource-based services within the AWS environment. (eg. CloudWatch Alerts or AWS Budget notifications).

One solution for this is to send the notifications first through a local SNS topic and have the messages relayed by a small and simple Lambda function to the central SNS topic:

 

Since AWS Lambda Service assumes a role while invoking a function, the relaying of the SNS message is now accompanied again with the aws:PrincipalOrgID attribute!
The Lambda Function could be made as simple as below few Python lines of code:

import boto3
 

def lambda_handler(event, context):
    sns = boto3.client('sns')
    sns.publish(
        TopicArn="arn:aws:sns:eu-west-1:123456789012:mytopic",
        Message=event['Records'][0]['Sns']['Message'],
        Subject=event['Records'][0]['Sns']['Subject']
    )

Deploy this solution in an AWS Organization with StackSets

Since we want to build this for scale, we prepare two CloudFormation templates to set this up:

  • A template for the account which houses the central SNS topic
  • A template which can be rolled out via CloudFormation StackSets over multiple accounts, Organizational Units (OU) or the complete AWS Organization.

The central template prepares the topic:

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "Central SNS Notification Topic",
    "Parameters": {
        "CentralNotificationTopicName": {
            "Type": "String",
            "Default": "CentralNotificationTopic"
        },
        "AwsOrganizationId": {
            "Type": "String",
            "Default": "o-123456",
            "Description": "The AWS Organization Id"
        }
    },
    "Resources": {
        "CentralNotificationTopic": {
            "Type": "AWS::SNS::Topic",
            "Properties": {
                "TopicName": {
                    "Ref": "CentralNotificationTopicName"
                }
            }
        },
        "BudgetNotificationTopicPolicy": {
            "Type": "AWS::SNS::TopicPolicy",
            "Properties": {
                "PolicyDocument": {
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": "*",
                            "Action": [
                                "sns:Publish"
                            ],
                            "Resource": {
                                "Ref": "CentralNotificationTopic"
                            },
                            "Condition": {
                                "StringEquals": {
                                    "aws:PrincipalOrgID": [
                                        {
                                            "Ref": "AwsOrganizationId"
                                        }
                                    ]
                                }
                            }
                        }
                    ]
                },
                "Topics": [
                    {
                        "Ref": "CentralNotificationTopic"
                    }
                ]
            }
        }
    }
}




This template needs two parameters:

  • The name of the SNS topic
  • The Organizaion ID of the AWS environment

How to determine the AWS Organization ID

You can look up the Organization ID by switching in the AWS Console to “AWS Organizations” or by executing below AWS CLI command:

bas@saiph:~$ aws --profile <your-aws-profile> organizations describe-organization
{
    "Organization": {
        "Id": "o-123456",
        "Arn": "arn:aws:organizations::accountId:organization/o-123456",
        "FeatureSet": "ALL",
        "MasterAccountArn": "arn:aws:organizations:::masterAccountId:account/o-123456/:",
        "MasterAccountId": ":",
        "MasterAccountEmail": "address@example.com",
        "AvailablePolicyTypes": [
            {
                  ... enabled policies ...
            }
         ]
    }
}
bas@saiph:~$

The second template has a few additions which are needed or simplifies deployment and keep resource costs under control:

  • Explicitly define the AWS log group with a retention period of 30 days. If you let AWS Lambda implicitly create the CloudWatch log group, the retention will be indefinite and adds hidden cost.
  • Conveniently use the AWS Managed Policy “AWSLambdaBasicExecutionRole” which sets up the right permissions for the Lambda Function to operate
  • Appropriate permissions for the local SNS topic to allow the invocation of the relay Lambda
{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "Cloudwatch Alarms",
    "Parameters": {
        "CentralNotificationTopicArn": {
            "Type": "String",
            "Default": "arn:aws:sns:eu-central-1:1234567:CentralNotificationTopic",
            "Description": "The Central SNS topic Arn"
        }
    },
    "Resources": {
        "CloudwatchAlarmNotificationTopic": {
            "Type": "AWS::SNS::Topic",
            "Properties": {
                "Subscription": [
                    {
                        "Endpoint": {
                            "Fn::GetAtt": [
                                "CloudwatchAlarmNotificationLambda",
                                "Arn"
                            ]
                        },
                        "Protocol": "lambda"
                    }
                ]
            }
        },
        "CloudwatchAlarmNotificationTopicPolicy": {
            "Type": "AWS::SNS::TopicPolicy",
            "Properties": {
                "PolicyDocument": {
                    "Statement": [
                        {
                            "Sid": "CloudwatchAlarmPublish",
                            "Effect": "Allow",
                            "Principal": {
                                "Service": "cloudwatch.amazonaws.com"
                            },
                            "Action": [
                                "sns:Publish"
                            ],
                            "Resource": {
                                "Ref": "CloudwatchAlarmNotificationTopic"
                            }
                        }
                    ]
                },
                "Topics": [
                    {
                        "Ref": "CloudwatchAlarmNotificationTopic"
                    }
                ]
            }
        },
        "CloudwatchAlarmNotificationLogGroup": {
            "Type": "AWS::Logs::LogGroup",
            "Properties": {
                "LogGroupName": {
                    "Fn::Join": [
                        "",
                        [
                            "/aws/lambda/",
                            {
                                "Ref": "CloudwatchAlarmNotificationLambda"
                            }
                        ]
                    ]
                },
                "RetentionInDays": 30
            }
        },
        "CloudwatchAlarmNotificationLambda": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "Description": "Relay SNS notifications to a central SNS topic.",
                "Code": {
                    "ZipFile": {
                        "Fn::Join": [
                            "\n",
                            [
                                "import boto3",
                                "def lambda_handler(event, context):",
                                "    print(event)",
                                "    sns = boto3.client('sns')",
                                {
                                    "Fn::Join": [
                                        "",
                                        [
                                            "    sns.publish(TopicArn='",
                                            {
                                                "Ref": "CentralNotificationTopicArn"
                                            },
                                            "',"
                                        ]
                                    ]
                                },
                                "        Message=event['Records'][0]['Sns']['Message'],",
                                "        Subject=event['Records'][0]['Sns']['Subject']",
                                "    )"
                            ]
                        ]
                    }
                },
                "Handler": "index.lambda_handler",
                "Role": {
                    "Fn::GetAtt": [
                        "CloudwatchAlarmNotificationLambdaRole",
                        "Arn"
                    ]
                },
                "Runtime": "python3.6",
                "Timeout": 30,
                "MemorySize": 128
            }
        },
        "CloudwatchAlarmNotificationLambdaPermission": {
            "Type": "AWS::Lambda::Permission",
            "Properties": {
                "Action": "lambda:InvokeFunction",
                "FunctionName": {
                    "Ref": "CloudwatchAlarmNotificationLambda"
                },
                "Principal": "sns.amazonaws.com",
                "SourceArn": {
                    "Ref": "CloudwatchAlarmNotificationTopic"
                }
            }
        },
        "CloudwatchAlarmNotificationLambdaRole": {
            "Type": "AWS::IAM::Role",
            "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"
                ],
                "Policies": [
                    {
                        "PolicyName": "PublishSnsMessage",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Sid": "PublishSnsMessage",
                                    "Effect": "Allow",
                                    "Action": [
                                        "sns:Publish"
                                    ],
                                    "Resource": {
                                        "Ref": "CentralNotificationTopicArn"
                                    }
                                }
                            ]
                        }
                    }
                ]
            }
        }
    }
}

Visual representation of the Cloudformation Stack

Below is a visual representation of this Cloudformation template and the flow of the message:

The flow of an alert through the solution goes as following:

  1. A cloudwatch alarm publishes an event
  2. AWS SNS checks with the Topic Policy if publishing is allowed
  3. AWS SNS sends the message to AWS Lambda
  4. AWS Lambda verifies if this specific SNS topic is allowed to invoke the function
  5. After granting permission, the function is invoked with the message embedded in the event
  6. The Function assumes a role which allows the function to:
  7. Relay the message to the central topic
  8. And log the results to AWS Cloudwatch

The next step is to extend this cloudformation template and add specific Cloudwatch Alarms or Budget triggers which will be the topic of a later blog.

 

Partner at FourCo Specialising in Cloud and Big Data infrastructure.