├── .gitignore ├── LICENSE ├── README.md ├── examples └── all-alarms.tf └── terraform ├── alarms ├── root_activity │ ├── main.tf │ └── variables.tf ├── unauthorized_activity │ ├── main.tf │ └── variables.tf └── unexpected_ip_access │ ├── main.tf │ └── variables.tf ├── main.tf └── variables.tf /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.tfstate* 3 | *.tfvars 4 | .terraform 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Crown Copyright 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudWatch Metrics and Alarms for CloudTrail logs # 2 | 3 | _Work in progress:_ We're currently investigating the best ways to monitor our AWS usage. This is one option we're testing out. 4 | 5 | Terraform to setup CloudTrail for sending logs into CloudWatch. Creates metrics and alarms in CloudWatch which trigger notifications to be sent via SNS. 6 | 7 | ## Alarms ## 8 | * *unexpected-ip-access:* Triggered by API calls made from IP addresses not set in `var.admin_whitelist`. 9 | * *root-activity* Triggered by API calls made by the *Root* user account. 10 | * *unauthorized* Triggered by failed logins from the AWS console. Or API calls which are now allowed for that credential. 11 | 12 | ## Usage ## 13 | 14 | Example: [all-alarms.tf](examples/all-alarms.tf) sets up cloudtrail and enables the alarms provided here. 15 | 16 | When used as a module the `cloudtrail_s3_bucket_name` should be an existing S3 bucket name which may [be managed in a separate account](http://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-receive-logs-from-multiple-accounts.html). 17 | 18 | Configure CloudTrail, CloudWatch and SNS: 19 | 20 | ```hcl 21 | module "aws-security-alarms" { 22 | source = "github.com/alphagov/aws-security-alarms//terraform" 23 | cloudtrail_s3_bucket_name = "some-bucket-to-store-cloudtrail-logs-in" 24 | cloudtrail_s3_bucket_prefix = "bucket-sub-directory" 25 | } 26 | ``` 27 | 28 | Enable individual alarms: 29 | 30 | ```hcl 31 | module "unexpected-ip-access" { 32 | source = "github.com/alphagov/aws-security-alarms//terraform/alarms/unexpected_ip_access" 33 | cloudtrail_log_group = "${module.aws-security-alarms.cloudtrail_log_group}" 34 | alarm_actions = ["${module.aws-security-alarms.security-alerts-topic}"] 35 | environment_name = "test" 36 | } 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /examples/all-alarms.tf: -------------------------------------------------------------------------------- 1 | variable "environment_name" { 2 | type = "string" 3 | } 4 | 5 | variable "cloudtrail_s3_bucket_name" { 6 | type = "string" 7 | } 8 | 9 | variable "cloudtrail_s3_bucket_prefix" { 10 | type = "string" 11 | } 12 | 13 | module "aws_security_alarms" { 14 | source = "../terraform" 15 | cloudtrail_s3_bucket_name = "${var.cloudtrail_s3_bucket_name}" 16 | cloudtrail_s3_bucket_prefix = "${var.cloudtrail_s3_bucket_prefix}" 17 | } 18 | 19 | module "unexpected_ip_access" { 20 | source = "../terraform/alarms/unexpected_ip_access" 21 | cloudtrail_log_group = "${module.aws_security_alarms.cloudtrail_log_group}" 22 | alarm_actions = ["${module.aws_security_alarms.security_alerts_topic}"] 23 | environment_name = "${var.environment_name}" 24 | } 25 | 26 | module "root_activity" { 27 | source = "../terraform/alarms/root_activity" 28 | cloudtrail_log_group = "${module.aws_security_alarms.cloudtrail_log_group}" 29 | alarm_actions = ["${module.aws_security_alarms.security_alerts_topic}"] 30 | environment_name = "${var.environment_name}" 31 | } 32 | 33 | module "unauthorized_activity" { 34 | source = "../terraform/alarms/unauthorized_activity" 35 | cloudtrail_log_group = "${module.aws_security_alarms.cloudtrail_log_group}" 36 | alarm_actions = ["${module.aws_security_alarms.security_alerts_topic}"] 37 | environment_name = "${var.environment_name}" 38 | } 39 | -------------------------------------------------------------------------------- /terraform/alarms/root_activity/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_log_metric_filter" "root-activity" { 2 | name = "root-user-activity" 3 | pattern = "{$.userIdentity.type = Root && $.sourceIPAddress != *.amazonaws.com}" 4 | log_group_name = "${var.cloudtrail_log_group}" 5 | 6 | metric_transformation { 7 | name = "RootAccessEventCount" 8 | namespace = "CloudTrailMetrics" 9 | value = "1" 10 | } 11 | } 12 | 13 | resource "aws_cloudwatch_metric_alarm" "root-activity-alarm" { 14 | alarm_name = "${var.environment_name}.root-activity-alarm" 15 | comparison_operator = "GreaterThanOrEqualToThreshold" 16 | evaluation_periods = "1" 17 | metric_name = "RootAccessEventCount" 18 | namespace = "CloudTrailMetrics" 19 | period = "300" 20 | statistic = "Sum" 21 | threshold = "1" 22 | alarm_description = "Alarms on Root user activity." 23 | insufficient_data_actions = [] 24 | alarm_actions = ["${var.alarm_actions}"] 25 | } 26 | -------------------------------------------------------------------------------- /terraform/alarms/root_activity/variables.tf: -------------------------------------------------------------------------------- 1 | variable "environment_name" { 2 | type = "string" 3 | description = "Interpolated into the alarm names so it's visible in the Subject line of notifications from SNS. Should be set to something like 'servicename-env' e.g. 'secops-staging'" 4 | } 5 | 6 | variable "cloudtrail_log_group" { 7 | type = "string" 8 | } 9 | 10 | variable "alarm_actions" { 11 | type = "list" 12 | } 13 | -------------------------------------------------------------------------------- /terraform/alarms/unauthorized_activity/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_log_metric_filter" "unauthorized" { 2 | name = "unauthorized-activity" 3 | pattern = "{($.eventName = ConsoleLogin && $.errorMessage = Failed*) || $.errorCode = *UnauthorizedOperation || $.errorCode = AccessDenied*}" 4 | log_group_name = "${var.cloudtrail_log_group}" 5 | 6 | metric_transformation { 7 | name = "UnauthorizedAccessEventCount" 8 | namespace = "CloudTrailMetrics" 9 | value = "1" 10 | } 11 | } 12 | 13 | resource "aws_cloudwatch_metric_alarm" "unauthorized-activity-alarm" { 14 | alarm_name = "${var.environment_name}.unauthorized-activity-alarm" 15 | comparison_operator = "GreaterThanOrEqualToThreshold" 16 | evaluation_periods = "1" 17 | metric_name = "UnauthorizedAccessEventCount" 18 | namespace = "CloudTrailMetrics" 19 | period = "300" 20 | statistic = "Sum" 21 | threshold = "1" 22 | alarm_description = "Alarms on unauthorized user activity." 23 | insufficient_data_actions = [] 24 | alarm_actions = ["${var.alarm_actions}"] 25 | } 26 | -------------------------------------------------------------------------------- /terraform/alarms/unauthorized_activity/variables.tf: -------------------------------------------------------------------------------- 1 | variable "environment_name" { 2 | type = "string" 3 | description = "Interpolated into the alarm names so it's visible in the Subject line of notifications from SNS. Should be set to something like 'servicename-env' e.g. 'secops-staging'" 4 | } 5 | 6 | variable "cloudtrail_log_group" { 7 | type = "string" 8 | } 9 | 10 | variable "alarm_actions" { 11 | type = "list" 12 | } 13 | -------------------------------------------------------------------------------- /terraform/alarms/unexpected_ip_access/main.tf: -------------------------------------------------------------------------------- 1 | data "template_file" "ip_rule" { 2 | template = "{$.userIdentity.type = IAMUser && $.sourceIPAddress != *amazonaws.com && $.sourceIPAddress != AWS* && $.sourceIPAddress != $${ips} }" 3 | vars { 4 | ips = "${join(" && $.sourceIPAddress != ", var.admin_whitelist)}" 5 | } 6 | } 7 | 8 | resource "aws_cloudwatch_log_metric_filter" "unexpected-ip-access" { 9 | name = "${var.environment_name}.unexpected-ip-access" 10 | pattern = "${data.template_file.ip_rule.rendered}" 11 | log_group_name = "${var.cloudtrail_log_group}" 12 | 13 | metric_transformation { 14 | name = "UnexpectedIPAccessEventCount" 15 | namespace = "CloudTrailMetrics" 16 | value = "1" 17 | } 18 | } 19 | 20 | resource "aws_cloudwatch_metric_alarm" "unexpected-ip-access-alarm" { 21 | alarm_name = "${var.environment_name}.unexpected-ip-access-alarm" 22 | comparison_operator = "GreaterThanOrEqualToThreshold" 23 | evaluation_periods = "1" 24 | metric_name = "UnexpectedIPAccessEventCount" 25 | namespace = "CloudTrailMetrics" 26 | period = "300" 27 | statistic = "Sum" 28 | threshold = "1" 29 | alarm_description = "Alarms on access from unexpected IP addresses" 30 | insufficient_data_actions = [] 31 | alarm_actions = ["${var.alarm_actions}"] 32 | } 33 | -------------------------------------------------------------------------------- /terraform/alarms/unexpected_ip_access/variables.tf: -------------------------------------------------------------------------------- 1 | variable "environment_name" { 2 | type = "string" 3 | description = "Interpolated into the alarm names so it's visible in the Subject line of notifications from SNS. Should be set to something like 'servicename-env' e.g. 'secops-staging'" 4 | } 5 | 6 | variable "cloudtrail_log_group" { 7 | type = "string" 8 | } 9 | 10 | # See https://sites.google.com/a/digital.cabinet-office.gov.uk/gds-internal-it/news/aviationhouse-sourceipaddresses for details. 11 | variable "admin_whitelist" { 12 | type = "list" 13 | default = [ 14 | "80.194.77.90", 15 | "80.194.77.100", 16 | "85.133.67.244", 17 | "93.89.81.78", 18 | "213.86.153.212", 19 | "213.86.153.213", 20 | "213.86.153.214", 21 | "213.86.153.235", 22 | "213.86.153.236", 23 | "213.86.153.237" 24 | ] 25 | } 26 | 27 | variable "alarm_actions" { 28 | type = "list" 29 | } 30 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_sns_topic" "security_alerts" { 2 | name = "security-alerts-topic" 3 | } 4 | 5 | resource "aws_cloudtrail" "cloudtrail" { 6 | name = "CloudTrail-all-regions" 7 | s3_bucket_name = "${var.cloudtrail_s3_bucket_name}" 8 | s3_key_prefix = "${var.cloudtrail_s3_bucket_prefix}" 9 | include_global_service_events = true 10 | is_multi_region_trail = true 11 | cloud_watch_logs_role_arn = "${aws_iam_role.cloud_watch_logs_role.arn}" 12 | cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.cloudtrail_log_group.arn}" 13 | enable_log_file_validation = true 14 | } 15 | 16 | resource "aws_cloudwatch_log_group" "cloudtrail_log_group" { 17 | name = "CloudTrail/LogGroup" 18 | retention_in_days = 90 19 | } 20 | 21 | resource "aws_iam_role" "cloud_watch_logs_role" { 22 | name = "CloudWatch-for-CloudTrail" 23 | assume_role_policy = <