├── outputs.tf ├── section-1_13.tf ├── section-1_10.tf ├── section-1_11.tf ├── section-1_14.tf ├── section-1_6.tf ├── section-1_7.tf ├── section-1_8.tf ├── section-1_9.tf ├── section-1_18.tf ├── section-2_2.tf ├── section-2_3.tf ├── .gitignore ├── templates ├── iam_lambda_assume_role_policy.json.tpl ├── lambda_root_account_check_policy.json.tpl ├── lambda_cloudtrail_status_check_policy.json.tpl ├── lambda_password_policy_check_policy.json.tpl ├── lambda_ec2_instances_iam_role_check_policy.json.tpl ├── lambda_access_key_age_check_policy.json.tpl ├── lambda_support_group_check_policy.json.tpl ├── lambda_user_policies_check_policy.json.tpl ├── lambda_mfa_check_policy.json.tpl ├── lambda_inactivity_check_policy.json.tpl ├── billing_s3_bucket_policy.json.tpl └── cloudtrail_kms_policy.json.tpl ├── .pre-commit-config.yaml ├── main.tf ├── section-1_17.tf ├── Makefile ├── files ├── root_account_check.py ├── ec2_instances_iam_role_check.py ├── support_group_check.py ├── cloudtrail_status_check.py ├── user_policies_check.py ├── password_policy_check.py ├── mfa_check.py ├── access_key_age_check.py └── inactivity_check.py ├── section-1_22.tf ├── section-1_2.tf ├── section-1_3.tf ├── section-1_16.tf ├── section-1_21.tf ├── section-1_12.tf ├── section-1_4.tf ├── README.md ├── variables.tf ├── section-1_5.tf ├── section-2_1.tf └── LICENSE /outputs.tf: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /section-1_13.tf: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /section-1_10.tf: -------------------------------------------------------------------------------- 1 | # in the same file with 1.5 2 | 3 | -------------------------------------------------------------------------------- /section-1_11.tf: -------------------------------------------------------------------------------- 1 | # in the same file with 1.5 2 | 3 | -------------------------------------------------------------------------------- /section-1_14.tf: -------------------------------------------------------------------------------- 1 | # in the same file with 1.12 2 | 3 | -------------------------------------------------------------------------------- /section-1_6.tf: -------------------------------------------------------------------------------- 1 | # in the same file with 1.5 2 | 3 | -------------------------------------------------------------------------------- /section-1_7.tf: -------------------------------------------------------------------------------- 1 | # in the same file with 1.5 2 | 3 | -------------------------------------------------------------------------------- /section-1_8.tf: -------------------------------------------------------------------------------- 1 | # in the same file with 1.5 2 | 3 | -------------------------------------------------------------------------------- /section-1_9.tf: -------------------------------------------------------------------------------- 1 | # in the same file with 1.5 2 | 3 | -------------------------------------------------------------------------------- /section-1_18.tf: -------------------------------------------------------------------------------- 1 | # needs to be discussed and planned 2 | 3 | -------------------------------------------------------------------------------- /section-2_2.tf: -------------------------------------------------------------------------------- 1 | # this section covered by the lambda function 2 | 3 | -------------------------------------------------------------------------------- /section-2_3.tf: -------------------------------------------------------------------------------- 1 | # this section covered by the lambda function 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.tfstate 2 | *.tfstate.backup 3 | *.tfstate.*.backup 4 | .terraform/ 5 | *.output 6 | *.plan 7 | *.tfplan 8 | -------------------------------------------------------------------------------- /templates/iam_lambda_assume_role_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Action": "sts:AssumeRole", 6 | "Principal": { 7 | "Service": "lambda.amazonaws.com" 8 | }, 9 | "Effect": "Allow", 10 | "Sid": "" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: https://github.com/pre-commit/pre-commit-hooks 2 | sha: v1.2.3 3 | hooks: 4 | - id: trailing-whitespace 5 | - id: autopep8-wrapper 6 | 7 | - repo: https://github.com/Lucas-C/pre-commit-hooks 8 | sha: v1.1.5 9 | hooks: 10 | - id: forbid-tabs 11 | 12 | - repo: https://github.com/kintoandar/pre-commit.git 13 | sha: v2.1.0 14 | hooks: 15 | - id: terraform_fmt 16 | -------------------------------------------------------------------------------- /templates/lambda_root_account_check_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "logs:CreateLogGroup", 8 | "logs:CreateLogStream", 9 | "logs:PutLogEvents" 10 | ], 11 | "Resource": "arn:aws:logs:*:*:*" 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "iam:GetAccountSummary" 17 | ], 18 | "Resource": "*" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /templates/lambda_cloudtrail_status_check_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "logs:CreateLogGroup", 8 | "logs:CreateLogStream", 9 | "logs:PutLogEvents" 10 | ], 11 | "Resource": "arn:aws:logs:*:*:*" 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "cloudtrail:DescribeTrails" 17 | ], 18 | "Resource": "*" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /templates/lambda_password_policy_check_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "logs:CreateLogGroup", 8 | "logs:CreateLogStream", 9 | "logs:PutLogEvents" 10 | ], 11 | "Resource": "arn:aws:logs:*:*:*" 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "iam:GetAccountPasswordPolicy" 17 | ], 18 | "Resource": "*" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /templates/lambda_ec2_instances_iam_role_check_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "logs:CreateLogGroup", 8 | "logs:CreateLogStream", 9 | "logs:PutLogEvents" 10 | ], 11 | "Resource": "arn:aws:logs:*:*:*" 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "ec2:DescribeRegions", 17 | "ec2:DescribeInstances" 18 | ], 19 | "Resource": "*" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /templates/lambda_access_key_age_check_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "logs:CreateLogGroup", 8 | "logs:CreateLogStream", 9 | "logs:PutLogEvents" 10 | ], 11 | "Resource": "arn:aws:logs:*:*:*" 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "iam:DeleteAccessKey", 17 | "iam:ListUsers", 18 | "iam:ListAccessKeys" 19 | ], 20 | "Resource": "*" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /templates/lambda_support_group_check_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "logs:CreateLogGroup", 8 | "logs:CreateLogStream", 9 | "logs:PutLogEvents" 10 | ], 11 | "Resource": "arn:aws:logs:*:*:*" 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "iam:ListEntitiesForPolicy", 17 | "iam:GetGroup", 18 | "iam:ListPolicies" 19 | ], 20 | "Resource": "*" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /templates/lambda_user_policies_check_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "logs:CreateLogGroup", 8 | "logs:CreateLogStream", 9 | "logs:PutLogEvents" 10 | ], 11 | "Resource": "arn:aws:logs:*:*:*" 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "iam:DetachUserPolicy", 17 | "iam:ListUsers", 18 | "iam:ListAttachedUserPolicies", 19 | "iam:ListUserPolicies" 20 | ], 21 | "Resource": "*" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /templates/lambda_mfa_check_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "logs:CreateLogGroup", 8 | "logs:CreateLogStream", 9 | "logs:PutLogEvents" 10 | ], 11 | "Resource": "arn:aws:logs:*:*:*" 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "iam:ListUsers", 17 | "iam:ListMFADevices", 18 | "iam:ListAccessKeys", 19 | "iam:DeleteLoginProfile", 20 | "iam:DeleteAccessKey" 21 | ], 22 | "Resource": "*" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /templates/lambda_inactivity_check_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "logs:CreateLogGroup", 8 | "logs:CreateLogStream", 9 | "logs:PutLogEvents" 10 | ], 11 | "Resource": "arn:aws:logs:*:*:*" 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "iam:GetUser", 17 | "iam:GetAccessKeyLastUsed", 18 | "iam:DeleteLoginProfile", 19 | "iam:ListAccessKeys", 20 | "iam:DeleteAccessKey", 21 | "iam:ListUsers" 22 | ], 23 | "Resource": "*" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /templates/billing_s3_bucket_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Id": "Policy", 3 | "Version": "2012-10-17", 4 | "Statement": [ 5 | { 6 | "Action": [ 7 | "s3:GetBucketAcl", 8 | "s3:GetBucketPolicy" 9 | ], 10 | "Effect": "Allow", 11 | "Resource": "arn:aws:s3:::${bucket_name}", 12 | "Principal": { 13 | "AWS": [ 14 | "${aws_billing_service_account_arn}" 15 | ] 16 | } 17 | }, 18 | { 19 | "Action": [ 20 | "s3:PutObject" 21 | ], 22 | "Effect": "Allow", 23 | "Resource": "arn:aws:s3:::${bucket_name}/*", 24 | "Principal": { 25 | "AWS": [ 26 | "${aws_billing_service_account_arn}" 27 | ] 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | # main.tf only contains shared resouces across the module for purpose even the best pracites says 2 | # keep roles as small as possible and have three files main,variables,outputs.tf 3 | # So, the motivation in here make the code easily readable. 4 | # You can open the CIS Benchmark and go step by step to verify or understand how 5 | # the every other section works. 6 | # Also, another aventage of this is easy to update the module when the benchmark 7 | # gets any updates 8 | # 9 | # So that, we decided to break down the module into files per section. 10 | 11 | # every lambda function uses this assume role policy 12 | data "template_file" "iam_lambda_assume_role_policy" { 13 | template = "${file("${path.module}/templates/iam_lambda_assume_role_policy.json.tpl")}" 14 | } 15 | -------------------------------------------------------------------------------- /section-1_17.tf: -------------------------------------------------------------------------------- 1 | data "aws_billing_service_account" "main" {} 2 | 3 | data "template_file" "billing_s3_bucket_policy" { 4 | template = "${file("${path.module}/templates/billing_s3_bucket_policy.json.tpl")}" 5 | 6 | vars { 7 | bucket_name = "${var.billing_s3_bucket_name != "" ? "${var.billing_s3_bucket_name}" : "${var.resource_name_prefix}-billing-logs"}" 8 | aws_billing_service_account_arn = "${data.aws_billing_service_account.main.arn}" 9 | } 10 | } 11 | 12 | resource "aws_s3_bucket" "billing_logs" { 13 | bucket = "${var.billing_s3_bucket_name != "" ? "${var.billing_s3_bucket_name}" : "${var.resource_name_prefix}-billing-logs"}" 14 | acl = "private" 15 | 16 | policy = "${var.billing_s3_bucket_policy != "" ? "${var.billing_s3_bucket_policy}" : "${data.template_file.billing_s3_bucket_policy.rendered}"}" 17 | } 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFILE_PATH:= $(abspath $(lastword $(MAKEFILE_LIST))) 2 | MAKEFILE_ROOT:= $(dir $(MAKEFILE_PATH)) 3 | 4 | TERRAFORM:=$(shell command -v terraform 2>/dev/null) 5 | PRE_COMMIT:=$(shell command -v pre-commit 2>/dev/null) 6 | 7 | ifndef TERRAFORM 8 | $(error Terraform is not installed Please download and install Terraform first - https://www.terraform.io/downloads.html) 9 | endif 10 | 11 | ifndef PRE_COMMIT 12 | $(error pre-commit is not installed. Please install pre-commit first - http://pre-commit.com/#install) 13 | endif 14 | 15 | .DEFAULT_GOAL := help 16 | .PHONY: help 17 | help: 18 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort -k2,2 19 | 20 | .PHONY: dev-init 21 | dev-init: ## Initiate development tools 22 | $(info ... pre-commit hooks installing) 23 | @$(PRE_COMMIT) install 24 | @$(TERRAFORM) init -backend=false -get=false -get-plugins=true 25 | -------------------------------------------------------------------------------- /files/root_account_check.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | 4 | def send_notifications(message): 5 | # TODO 6 | return True 7 | 8 | 9 | def lambda_handler(event, context): 10 | iam = boto3.client('iam') 11 | message_body = "" 12 | 13 | account_summary = iam.get_account_summary() 14 | 15 | if account_summary['SummaryMap']['AccountAccessKeysPresent'] != 0: 16 | notification = "Root account has an access key. It should be removed\n" 17 | print notification 18 | message_body += notification 19 | 20 | if account_summary['SummaryMap']['AccountMFAEnabled'] != 1: 21 | notification = "Root account does not have MFA set up\n" 22 | 23 | # TODO 24 | # There will be check if the root account's MFA device is a hardware oneself. 25 | # First, I need to have one that I can test while I develop 26 | 27 | if message_body: 28 | send_notifications(message_body) 29 | else: 30 | print 'Everything seems fine' 31 | 32 | # if __name__ == "__main__": 33 | # event = 1 34 | # context = 1 35 | # lambda_handler(event, context) 36 | -------------------------------------------------------------------------------- /files/ec2_instances_iam_role_check.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | 4 | 5 | def answer_no(x): return True if str(x).lower() in [ 6 | '0', 'no', 'false'] else False 7 | 8 | 9 | def answer_yes(x): return True if str(x).lower() in [ 10 | '1', 'yes', 'true'] else False 11 | 12 | 13 | def send_notifications(message): 14 | # TODO 15 | return True 16 | 17 | 18 | def process_regions(regions): 19 | message_body = '' 20 | 21 | for region, instances in regions.iteritems(): 22 | message_body = 'Region ' + region + \ 23 | " has following instances without an IAM Instance Profile\n" 24 | message_body += "\n".join(instances) 25 | 26 | print message_body 27 | 28 | if len(regions) > 0 and ('DRY_RUN' in os.environ and answer_no(os.environ['DRY_RUN'])): 29 | send_notifications(message_body) 30 | else: 31 | print "Nothing to do. Either everything is fine or DRY_RUN is active" 32 | 33 | 34 | def lambda_handler(event, context): 35 | regions_to_process = {} 36 | 37 | ec2 = boto3.client('ec2') 38 | 39 | regions = ec2.describe_regions() 40 | 41 | for region in regions['Regions']: 42 | 43 | print 'Processing region ' + region['RegionName'] 44 | 45 | # Create a new client for the region 46 | ec2_region = boto3.client('ec2', region_name=region['RegionName']) 47 | # Create a paginator to filter 48 | paginator = ec2_region.get_paginator('describe_instances') 49 | page_iterator = paginator.paginate() 50 | # Filter with JMESPath and find out instances without an IAM Instance profile 51 | filtered_iterator = page_iterator.search( 52 | 'Reservations[].Instances[].{InstanceId: InstanceId, InstanceProfileArn: IamInstanceProfile.Arn} | [?@.InstanceProfileArn == null].InstanceId') 53 | 54 | for instance in filtered_iterator: 55 | if region['RegionName'] not in regions_to_process: 56 | regions_to_process.setdefault(region['RegionName'], []) 57 | regions_to_process[region['RegionName']].append(instance) 58 | 59 | process_regions(regions_to_process) 60 | 61 | 62 | # if __name__ == "__main__": 63 | # event = 1 64 | # context = 1 65 | # lambda_handler(event, context) 66 | -------------------------------------------------------------------------------- /files/support_group_check.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | 4 | iam = boto3.client('iam') 5 | 6 | 7 | def answer_no(x): return True if str(x).lower() in [ 8 | '0', 'no', 'false'] else False 9 | 10 | 11 | def answer_yes(x): return True if str(x).lower() in [ 12 | '1', 'yes', 'true'] else False 13 | 14 | 15 | def send_notifications(message): 16 | # TODO: 17 | return True 18 | 19 | 20 | def if_policy_attached_to_any_group(arn): 21 | entities = iam.list_entities_for_policy( 22 | PolicyArn=arn, 23 | EntityFilter='Group', 24 | ) 25 | return entities['PolicyGroups'] 26 | 27 | 28 | def if_any_group_has_users(groups): 29 | for group in groups: 30 | group_detail = iam.get_group( 31 | GroupName=group['GroupName'], 32 | MaxItems=1, 33 | ) 34 | if len(group_detail['Users']) > 0: 35 | return True 36 | return False 37 | 38 | 39 | def lambda_handler(event, context): 40 | rc = 1 41 | message = "Checking if the AWSSupportAccess policy attached to any group" 42 | print message 43 | 44 | paginator = iam.get_paginator('list_policies') 45 | page_iterator = paginator.paginate() 46 | # Filter with JMESPath and find out instances without an IAM Instance profile 47 | filtered_iterator = page_iterator.search( 48 | 'Policies[?PolicyName == `AWSSupportAccess`].Arn') 49 | 50 | for arn in filtered_iterator: 51 | groups = if_policy_attached_to_any_group(arn) 52 | 53 | if len(groups) > 0: 54 | groups_has_user = if_any_group_has_users(groups) 55 | if groups_has_user: 56 | notification = 'Everthing is fine.' 57 | print notification 58 | message += notification 59 | rc = 0 60 | else: 61 | notification = 'None of the groups have user attached' 62 | print notification 63 | message += notification 64 | else: 65 | notification = 'AWSSupportAccess is not attached to any group' 66 | print notification 67 | message += notification + "\n" 68 | send_notifications(message) 69 | exit(rc) 70 | 71 | 72 | # if __name__ == "__main__": 73 | # event = 1 74 | # context = 1 75 | # lambda_handler(event, context) 76 | -------------------------------------------------------------------------------- /files/cloudtrail_status_check.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | 4 | 5 | def answer_no(x): return True if str(x).lower() in [ 6 | '0', 'no', 'false'] else False 7 | 8 | 9 | def answer_yes(x): return True if str(x).lower() in [ 10 | '1', 'yes', 'true'] else False 11 | 12 | 13 | def send_notifications(message): 14 | # TODO 15 | return True 16 | 17 | 18 | def is_bucket_not_public(bucket_name): 19 | s3 = boto3.client('s3') 20 | bucket_acl = s3.get_bucket_acl(Bucket=bucket_name) 21 | 22 | # If there is a permission attached with any value for AllUsers, 23 | # it means the bucket is public 24 | # We don't need to check if the permission any of 25 | # READ|WRITE|READ_ACP|WRITE_ACP|FULL_CONTROL 26 | for grantee in bucket_acl['Grants']: 27 | if grantee['Grantee']['Type'] == 'Group' \ 28 | and grantee['Grantee']['URI'] == 'http://acs.amazonaws.com/groups/global/AllUsers': 29 | return False 30 | return True 31 | 32 | 33 | def lambda_handler(event, context): 34 | rc = 1 35 | message_body = 'Chekcing trails' 36 | print message_body 37 | 38 | cloudtrail = boto3.client('cloudtrail') 39 | trails = cloudtrail.describe_trails() 40 | 41 | for trail in trails['trailList']: 42 | notification = 'Checking ' + trail['Name'] 43 | print notification 44 | message_body += notification + "\n" 45 | 46 | if trail['IsMultiRegionTrail'] \ 47 | and ('KmsKeyId' in trail and trail['KmsKeyId'] != '') \ 48 | and trail['IncludeGlobalServiceEvents'] \ 49 | and trail['LogFileValidationEnabled']: 50 | 51 | notification = trail['Name'] + ' is OK' 52 | print notification 53 | message_body += notification + "\n" 54 | rc = 0 55 | else: 56 | notification = trail['Name'] + \ 57 | ' does not match with the requirements' 58 | print notification 59 | message_body += notification + "\n" 60 | 61 | if not is_bucket_not_public(trail['S3BucketName']): 62 | rc = 1 63 | notification = trail['Name'] + \ 64 | "\'s bucket has public access." 65 | print notification 66 | message_body += notification + "\n" 67 | 68 | if rc == 1 and ('DRY_RUN' in os.environ and answer_no(os.environ['DRY_RUN'])): 69 | send_notifications(message_body) 70 | exit(rc) 71 | 72 | # if __name__ == "__main__": 73 | # event = 1 74 | # context = 1 75 | # lambda_handler(event, context) 76 | -------------------------------------------------------------------------------- /files/user_policies_check.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | 4 | iam = boto3.client('iam') 5 | 6 | 7 | def answer_no(x): return True if str(x).lower() in [ 8 | '0', 'no', 'false'] else False 9 | 10 | 11 | def answer_yes(x): return True if str(x).lower() in [ 12 | '1', 'yes', 'true'] else False 13 | 14 | 15 | def send_notifications(message): 16 | # TO DO 17 | return True 18 | 19 | 20 | def detach_policies(users): 21 | message_body = 'AGGRESSIVE is set to ' + os.environ['AGGRESSIVE'] \ 22 | if ('AGGRESSIVE' in os.environ and answer_yes(os.environ['AGGRESSIVE'])) \ 23 | else 'AGGRESSIVE mode is not active' 24 | print message_body 25 | 26 | for user, policies in users.iteritems(): 27 | notification = 'Processing ' + user 28 | print notification 29 | message_body += notification + "\n" 30 | for policy in policies: 31 | notification = policy['PolicyName'] + \ 32 | ' will be detached from the user' 33 | print notification 34 | message_body += notification + "\n" 35 | if ('DRY_RUN' not in os.environ or answer_no(os.environ['DRY_RUN'])) \ 36 | and ('AGGRESSIVE' in os.environ and answer_yes(os.environ['AGGRESSIVE'])): 37 | iam.detach_user_policy( 38 | UserName=user, PolicyArn=policy['PolicyArn']) 39 | else: 40 | notification = 'AGREESIVE is not active or DRY_RUN is enabled, so the policy is not removed' 41 | print notification 42 | message_body += notification + "\n" 43 | 44 | if len(users) > 0 and ('DRY_RUN' not in os.environ or answer_no(os.environ['DRY_RUN'])): 45 | send_notifications(message_body) 46 | else: 47 | print 'DRY_RUN is active and/or nothing to do' 48 | 49 | 50 | def lambda_handler(event, context): 51 | users = iam.list_users() 52 | user_policies = {} 53 | 54 | for user in users['Users']: 55 | attached_policy_list = iam.list_attached_user_policies( 56 | UserName=user['UserName']) 57 | user_policy_list = iam.list_user_policies(UserName=user['UserName']) 58 | 59 | if len(attached_policy_list['AttachedPolicies']) > 0 \ 60 | or len(user_policy_list['PolicyNames']) > 0: 61 | 62 | user_policies[user['UserName']] = attached_policy_list['AttachedPolicies'] + \ 63 | user_policy_list['PolicyNames'] 64 | detach_policies(user_policies) 65 | 66 | 67 | # if __name__ == "__main__": 68 | # event = 1 69 | # context = 1 70 | # lambda_handler(event, context) 71 | -------------------------------------------------------------------------------- /section-1_22.tf: -------------------------------------------------------------------------------- 1 | # Support group check and delete function 2 | ## IAM Policy 3 | data "template_file" "support_group_check_policy" { 4 | template = "${file("${path.module}/templates/lambda_support_group_check_policy.json.tpl")}" 5 | } 6 | 7 | resource "aws_iam_role" "support_group_check" { 8 | name = "${var.resource_name_prefix}-support-group-check" 9 | assume_role_policy = "${data.template_file.iam_lambda_assume_role_policy.rendered}" 10 | } 11 | 12 | resource "aws_iam_role_policy" "support_group_check" { 13 | name = "${var.resource_name_prefix}-lambda-support-group-check" 14 | role = "${aws_iam_role.support_group_check.id}" 15 | policy = "${data.template_file.support_group_check_policy.rendered}" 16 | } 17 | 18 | ## /IAM Policy 19 | 20 | ## Create the function 21 | data "archive_file" "support_group_check" { 22 | type = "zip" 23 | source_file = "${path.module}/files/support_group_check.py" 24 | output_path = "${var.temp_artifacts_dir}/support_group_check.zip" 25 | } 26 | 27 | resource "aws_lambda_function" "support_group_check" { 28 | filename = "${var.temp_artifacts_dir}/support_group_check.zip" 29 | function_name = "${var.resource_name_prefix}-support-group-check" 30 | role = "${aws_iam_role.support_group_check.arn}" 31 | handler = "support_group_check.lambda_handler" 32 | source_code_hash = "${data.archive_file.support_group_check.output_base64sha256}" 33 | runtime = "python2.7" 34 | timeout = "${var.lambda_timeout}" 35 | 36 | tags = "${var.tags}" 37 | } 38 | 39 | ## /Create the function 40 | 41 | ## Schedule the lambda function 42 | resource "aws_cloudwatch_event_rule" "support_group_check" { 43 | name = "${var.resource_name_prefix}-support-group-check" 44 | description = "remove expiring access keys" 45 | schedule_expression = "${var.lambda_cron_schedule}" 46 | } 47 | 48 | resource "aws_cloudwatch_event_target" "support_group_check" { 49 | rule = "${aws_cloudwatch_event_rule.support_group_check.name}" 50 | target_id = "${var.resource_name_prefix}-support-group-check" 51 | arn = "${aws_lambda_function.support_group_check.arn}" 52 | } 53 | 54 | resource "aws_lambda_permission" "support_group_check" { 55 | statement_id = "AllowExecutionFromCloudWatch" 56 | action = "lambda:InvokeFunction" 57 | function_name = "${aws_lambda_function.support_group_check.function_name}" 58 | principal = "events.amazonaws.com" 59 | source_arn = "${aws_cloudwatch_event_rule.support_group_check.arn}" 60 | } 61 | 62 | ## /Schedule the lambda function 63 | # /Support group check and delete function 64 | 65 | -------------------------------------------------------------------------------- /section-1_2.tf: -------------------------------------------------------------------------------- 1 | # MFA check and disable function 2 | ## IAM Policy 3 | data "template_file" "mfa_check_policy" { 4 | template = "${file("${path.module}/templates/lambda_mfa_check_policy.json.tpl")}" 5 | } 6 | 7 | resource "aws_iam_role" "mfa_check" { 8 | name = "${var.resource_name_prefix}-mfa-check" 9 | assume_role_policy = "${data.template_file.iam_lambda_assume_role_policy.rendered}" 10 | } 11 | 12 | resource "aws_iam_role_policy" "mfa_check" { 13 | name = "${var.resource_name_prefix}-lambda-mfa-check" 14 | role = "${aws_iam_role.mfa_check.id}" 15 | policy = "${data.template_file.mfa_check_policy.rendered}" 16 | } 17 | 18 | ## /IAM Policy 19 | 20 | ## Create the function 21 | data "archive_file" "mfa_check" { 22 | type = "zip" 23 | source_file = "${path.module}/files/mfa_check.py" 24 | output_path = "${var.temp_artifacts_dir}/mfa_check.zip" 25 | } 26 | 27 | resource "aws_lambda_function" "mfa_check" { 28 | filename = "${var.temp_artifacts_dir}/mfa_check.zip" 29 | function_name = "${var.resource_name_prefix}-mfa-check" 30 | role = "${aws_iam_role.mfa_check.arn}" 31 | handler = "mfa_check.lambda_handler" 32 | source_code_hash = "${data.archive_file.mfa_check.output_base64sha256}" 33 | runtime = "python2.7" 34 | timeout = "${var.lambda_timeout}" 35 | 36 | environment { 37 | variables = { 38 | DRY_RUN = "${var.lambda_dry_run}" 39 | AGGRESSIVE = "${var.lambda_aggressive}" 40 | IGNORE_IAM_USER_PREFIX = "${var.lambda_mfa_checker_user_prefix}" 41 | IGNORE_IAM_USER_SUFFIX = "${var.lambda_mfa_checker_user_suffix}" 42 | } 43 | } 44 | 45 | tags = "${var.tags}" 46 | } 47 | 48 | ## /Create the function 49 | 50 | ## Schedule the lambda function 51 | resource "aws_cloudwatch_event_rule" "mfa_check" { 52 | name = "${var.resource_name_prefix}-mfa-check" 53 | description = "disables users without MFA" 54 | schedule_expression = "${var.lambda_cron_schedule}" 55 | } 56 | 57 | resource "aws_cloudwatch_event_target" "mfa_check" { 58 | rule = "${aws_cloudwatch_event_rule.mfa_check.name}" 59 | target_id = "${var.resource_name_prefix}-mfa-check" 60 | arn = "${aws_lambda_function.mfa_check.arn}" 61 | } 62 | 63 | resource "aws_lambda_permission" "mfa_check" { 64 | statement_id = "AllowExecutionFromCloudWatch" 65 | action = "lambda:InvokeFunction" 66 | function_name = "${aws_lambda_function.mfa_check.function_name}" 67 | principal = "events.amazonaws.com" 68 | source_arn = "${aws_cloudwatch_event_rule.mfa_check.arn}" 69 | } 70 | 71 | ## /Schedule the lambda function 72 | # /MFA check and disable function 73 | 74 | -------------------------------------------------------------------------------- /templates/cloudtrail_kms_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Id": "Key policy created by CloudTrail", 4 | "Statement": [ 5 | { 6 | "Sid": "Enable IAM User Permissions", 7 | "Effect": "Allow", 8 | "Principal": { 9 | "AWS": [ 10 | "arn:aws:iam::${aws_account_id}:root" 11 | ] 12 | }, 13 | "Action": "kms:*", 14 | "Resource": "*" 15 | }, 16 | { 17 | "Sid": "Allow CloudTrail to encrypt logs", 18 | "Effect": "Allow", 19 | "Principal": { 20 | "Service": "cloudtrail.amazonaws.com" 21 | }, 22 | "Action": "kms:GenerateDataKey*", 23 | "Resource": "*", 24 | "Condition": { 25 | "StringLike": { 26 | "kms:EncryptionContext:aws:cloudtrail:arn": "arn:aws:cloudtrail:*:${aws_account_id}:trail/*" 27 | } 28 | } 29 | }, 30 | { 31 | "Sid": "Allow CloudTrail to describe key", 32 | "Effect": "Allow", 33 | "Principal": { 34 | "Service": "cloudtrail.amazonaws.com" 35 | }, 36 | "Action": "kms:DescribeKey", 37 | "Resource": "*" 38 | }, 39 | { 40 | "Sid": "Allow principals in the account to decrypt log files", 41 | "Effect": "Allow", 42 | "Principal": { 43 | "AWS": "*" 44 | }, 45 | "Action": [ 46 | "kms:Decrypt", 47 | "kms:ReEncryptFrom" 48 | ], 49 | "Resource": "*", 50 | "Condition": { 51 | "StringEquals": { 52 | "kms:CallerAccount": "${aws_account_id}" 53 | }, 54 | "StringLike": { 55 | "kms:EncryptionContext:aws:cloudtrail:arn": "arn:aws:cloudtrail:*:${aws_account_id}:trail/*" 56 | } 57 | } 58 | }, 59 | { 60 | "Sid": "Allow alias creation during setup", 61 | "Effect": "Allow", 62 | "Principal": { 63 | "AWS": "*" 64 | }, 65 | "Action": "kms:CreateAlias", 66 | "Resource": "*", 67 | "Condition": { 68 | "StringEquals": { 69 | "kms:CallerAccount": "${aws_account_id}", 70 | "kms:ViaService": "ec2.eu-west-2.amazonaws.com" 71 | } 72 | } 73 | }, 74 | { 75 | "Sid": "Enable cross account log decryption", 76 | "Effect": "Allow", 77 | "Principal": { 78 | "AWS": "*" 79 | }, 80 | "Action": [ 81 | "kms:Decrypt", 82 | "kms:ReEncryptFrom" 83 | ], 84 | "Resource": "*", 85 | "Condition": { 86 | "StringEquals": { 87 | "kms:CallerAccount": "${aws_account_id}" 88 | }, 89 | "StringLike": { 90 | "kms:EncryptionContext:aws:cloudtrail:arn": "arn:aws:cloudtrail:*:${aws_account_id}:trail/*" 91 | } 92 | } 93 | } 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /section-1_3.tf: -------------------------------------------------------------------------------- 1 | # Inactivity check and disable function 2 | ## IAM Policy 3 | data "template_file" "inactivity_check_policy" { 4 | template = "${file("${path.module}/templates/lambda_inactivity_check_policy.json.tpl")}" 5 | } 6 | 7 | resource "aws_iam_role" "inactivity_check" { 8 | name = "${var.resource_name_prefix}-inactivity-check" 9 | assume_role_policy = "${data.template_file.iam_lambda_assume_role_policy.rendered}" 10 | } 11 | 12 | resource "aws_iam_role_policy" "inactivity_check" { 13 | name = "${var.resource_name_prefix}-lambda-inactivity-check" 14 | role = "${aws_iam_role.inactivity_check.id}" 15 | policy = "${data.template_file.inactivity_check_policy.rendered}" 16 | } 17 | 18 | ## /IAM Policy 19 | 20 | ## Create the function 21 | data "archive_file" "inactivity_check" { 22 | type = "zip" 23 | source_file = "${path.module}/files/inactivity_check.py" 24 | output_path = "${var.temp_artifacts_dir}/inactivity_check.zip" 25 | } 26 | 27 | resource "aws_lambda_function" "inactivity_check" { 28 | filename = "${var.temp_artifacts_dir}/inactivity_check.zip" 29 | function_name = "${var.resource_name_prefix}-inactivity-check" 30 | role = "${aws_iam_role.inactivity_check.arn}" 31 | handler = "inactivity_check.lambda_handler" 32 | source_code_hash = "${data.archive_file.inactivity_check.output_base64sha256}" 33 | runtime = "python2.7" 34 | timeout = "${var.lambda_timeout}" 35 | 36 | environment { 37 | variables = { 38 | DRY_RUN = "${var.lambda_dry_run}" 39 | AGGRESSIVE = "${var.lambda_aggressive}" 40 | INACTIVITY_LIMIT = "${var.lambda_user_inactivity_limit}" 41 | IGNORE_IAM_USER_PREFIX = "${var.lambda_mfa_checker_user_prefix}" 42 | IGNORE_IAM_USER_SUFFIX = "${var.lambda_mfa_checker_user_suffix}" 43 | } 44 | } 45 | 46 | tags = "${var.tags}" 47 | } 48 | 49 | ## /Create the function 50 | 51 | ## Schedule the lambda function 52 | resource "aws_cloudwatch_event_rule" "inactivity_check" { 53 | name = "${var.resource_name_prefix}-inactivity-check" 54 | description = "disables inactive users" 55 | schedule_expression = "${var.lambda_cron_schedule}" 56 | } 57 | 58 | resource "aws_cloudwatch_event_target" "inactivity_check" { 59 | rule = "${aws_cloudwatch_event_rule.inactivity_check.name}" 60 | target_id = "${var.resource_name_prefix}-inactivity-check" 61 | arn = "${aws_lambda_function.inactivity_check.arn}" 62 | } 63 | 64 | resource "aws_lambda_permission" "inactivity_check" { 65 | statement_id = "AllowExecutionFromCloudWatch" 66 | action = "lambda:InvokeFunction" 67 | function_name = "${aws_lambda_function.inactivity_check.function_name}" 68 | principal = "events.amazonaws.com" 69 | source_arn = "${aws_cloudwatch_event_rule.inactivity_check.arn}" 70 | } 71 | 72 | ## /Schedule the lambda function 73 | # /MFA check and disable function 74 | 75 | -------------------------------------------------------------------------------- /section-1_16.tf: -------------------------------------------------------------------------------- 1 | # AccessKey age check and delete function 2 | ## IAM Policy 3 | data "template_file" "user_policies_check_policy" { 4 | template = "${file("${path.module}/templates/lambda_user_policies_check_policy.json.tpl")}" 5 | } 6 | 7 | resource "aws_iam_role" "user_policies_check" { 8 | name = "${var.resource_name_prefix}-user-policies-check" 9 | assume_role_policy = "${data.template_file.iam_lambda_assume_role_policy.rendered}" 10 | } 11 | 12 | resource "aws_iam_role_policy" "user_policies_check" { 13 | name = "${var.resource_name_prefix}-lambda-user-policies-check" 14 | role = "${aws_iam_role.user_policies_check.id}" 15 | policy = "${data.template_file.user_policies_check_policy.rendered}" 16 | } 17 | 18 | ## /IAM Policy 19 | 20 | ## Create the function 21 | data "archive_file" "user_policies_check" { 22 | type = "zip" 23 | source_file = "${path.module}/files/user_policies_check.py" 24 | output_path = "${var.temp_artifacts_dir}/user_policies_check.zip" 25 | } 26 | 27 | resource "aws_lambda_function" "user_policies_check" { 28 | filename = "${var.temp_artifacts_dir}/user_policies_check.zip" 29 | function_name = "${var.resource_name_prefix}-user-policies-check" 30 | role = "${aws_iam_role.user_policies_check.arn}" 31 | handler = "user_policies_check.lambda_handler" 32 | source_code_hash = "${data.archive_file.user_policies_check.output_base64sha256}" 33 | runtime = "python2.7" 34 | timeout = "${var.lambda_timeout}" 35 | 36 | environment { 37 | variables = { 38 | DRY_RUN = "${var.lambda_dry_run}" 39 | AGGRESSIVE = "${var.lambda_aggressive}" 40 | IGNORE_IAM_USER_PREFIX = "${var.lambda_mfa_checker_user_prefix}" 41 | IGNORE_IAM_USER_SUFFIX = "${var.lambda_mfa_checker_user_suffix}" 42 | } 43 | } 44 | 45 | tags = "${var.tags}" 46 | } 47 | 48 | ## /Create the function 49 | 50 | ## Schedule the lambda function 51 | resource "aws_cloudwatch_event_rule" "user_policies_check" { 52 | name = "${var.resource_name_prefix}-user-policies-check" 53 | description = "remove expiring access keys" 54 | schedule_expression = "${var.lambda_cron_schedule}" 55 | } 56 | 57 | resource "aws_cloudwatch_event_target" "user_policies_check" { 58 | rule = "${aws_cloudwatch_event_rule.user_policies_check.name}" 59 | target_id = "${var.resource_name_prefix}-user-policies-check" 60 | arn = "${aws_lambda_function.user_policies_check.arn}" 61 | } 62 | 63 | resource "aws_lambda_permission" "user_policies_check" { 64 | statement_id = "AllowExecutionFromCloudWatch" 65 | action = "lambda:InvokeFunction" 66 | function_name = "${aws_lambda_function.user_policies_check.function_name}" 67 | principal = "events.amazonaws.com" 68 | source_arn = "${aws_cloudwatch_event_rule.user_policies_check.arn}" 69 | } 70 | 71 | ## /Schedule the lambda function 72 | # /AccessKey age check and delete function 73 | 74 | -------------------------------------------------------------------------------- /section-1_21.tf: -------------------------------------------------------------------------------- 1 | # IAM Instance Profile check for instances 2 | ## IAM Policy 3 | data "template_file" "ec2_instances_iam_role_check_policy" { 4 | template = "${file("${path.module}/templates/lambda_ec2_instances_iam_role_check_policy.json.tpl")}" 5 | } 6 | 7 | resource "aws_iam_role" "ec2_instances_iam_role_check" { 8 | name = "${var.resource_name_prefix}-ec2-instances-iam-role-check" 9 | assume_role_policy = "${data.template_file.iam_lambda_assume_role_policy.rendered}" 10 | } 11 | 12 | resource "aws_iam_role_policy" "ec2_instances_iam_role_check" { 13 | name = "${var.resource_name_prefix}-lambda-ec2-instances-iam-role-check" 14 | role = "${aws_iam_role.ec2_instances_iam_role_check.id}" 15 | policy = "${data.template_file.ec2_instances_iam_role_check_policy.rendered}" 16 | } 17 | 18 | ## /IAM Policy 19 | 20 | ## Create the function 21 | data "archive_file" "ec2_instances_iam_role_check" { 22 | type = "zip" 23 | source_file = "${path.module}/files/ec2_instances_iam_role_check.py" 24 | output_path = "${var.temp_artifacts_dir}/ec2_instances_iam_role_check.zip" 25 | } 26 | 27 | resource "aws_lambda_function" "ec2_instances_iam_role_check" { 28 | filename = "${var.temp_artifacts_dir}/ec2_instances_iam_role_check.zip" 29 | function_name = "${var.resource_name_prefix}-ec2-instances-iam-role-check" 30 | role = "${aws_iam_role.ec2_instances_iam_role_check.arn}" 31 | handler = "ec2_instances_iam_role_check.lambda_handler" 32 | source_code_hash = "${data.archive_file.ec2_instances_iam_role_check.output_base64sha256}" 33 | runtime = "python2.7" 34 | timeout = "${var.lambda_timeout}" 35 | 36 | environment { 37 | variables = { 38 | DRY_RUN = "${var.lambda_dry_run}" 39 | } 40 | } 41 | 42 | tags = "${var.tags}" 43 | } 44 | 45 | ## /Create the function 46 | 47 | ## Schedule the lambda function 48 | resource "aws_cloudwatch_event_rule" "ec2_instances_iam_role_check" { 49 | name = "${var.resource_name_prefix}-ec2-instances-iam-role-check" 50 | description = "remove expiring access keys" 51 | schedule_expression = "${var.lambda_cron_schedule}" 52 | } 53 | 54 | resource "aws_cloudwatch_event_target" "ec2_instances_iam_role_check" { 55 | rule = "${aws_cloudwatch_event_rule.ec2_instances_iam_role_check.name}" 56 | target_id = "${var.resource_name_prefix}-ec2-instances-iam-role-check" 57 | arn = "${aws_lambda_function.ec2_instances_iam_role_check.arn}" 58 | } 59 | 60 | resource "aws_lambda_permission" "ec2_instances_iam_role_check" { 61 | statement_id = "AllowExecutionFromCloudWatch" 62 | action = "lambda:InvokeFunction" 63 | function_name = "${aws_lambda_function.ec2_instances_iam_role_check.function_name}" 64 | principal = "events.amazonaws.com" 65 | source_arn = "${aws_cloudwatch_event_rule.ec2_instances_iam_role_check.arn}" 66 | } 67 | 68 | ## /Schedule the lambda function 69 | # /IAM Instance Profile check for instances 70 | 71 | -------------------------------------------------------------------------------- /section-1_12.tf: -------------------------------------------------------------------------------- 1 | # Inactivity check and disable function 2 | ## IAM Policy 3 | data "template_file" "root_account_check_policy" { 4 | template = "${file("${path.module}/templates/lambda_root_account_check_policy.json.tpl")}" 5 | } 6 | 7 | resource "aws_iam_role" "root_account_check" { 8 | name = "${var.resource_name_prefix}-root-account-check" 9 | assume_role_policy = "${data.template_file.iam_lambda_assume_role_policy.rendered}" 10 | } 11 | 12 | resource "aws_iam_role_policy" "root_account_check" { 13 | name = "${var.resource_name_prefix}-lambda-root-account-check" 14 | role = "${aws_iam_role.root_account_check.id}" 15 | policy = "${data.template_file.root_account_check_policy.rendered}" 16 | } 17 | 18 | ## /IAM Policy 19 | 20 | ## Create the function 21 | data "archive_file" "root_account_check" { 22 | type = "zip" 23 | source_file = "${path.module}/files/root_account_check.py" 24 | output_path = "${var.temp_artifacts_dir}/root_account_check.zip" 25 | } 26 | 27 | resource "aws_lambda_function" "root_account_check" { 28 | filename = "${var.temp_artifacts_dir}/root_account_check.zip" 29 | function_name = "${var.resource_name_prefix}-root-account-check" 30 | role = "${aws_iam_role.root_account_check.arn}" 31 | handler = "root_account_check.lambda_handler" 32 | source_code_hash = "${data.archive_file.root_account_check.output_base64sha256}" 33 | runtime = "python2.7" 34 | timeout = "${var.lambda_timeout}" 35 | 36 | environment { 37 | variables = { 38 | DRY_RUN = "${var.lambda_dry_run}" 39 | AGGRESSIVE = "${var.lambda_aggressive}" 40 | INACTIVITY_LIMIT = "${var.lambda_user_inactivity_limit}" 41 | IGNORE_IAM_USER_PREFIX = "${var.lambda_mfa_checker_user_prefix}" 42 | IGNORE_IAM_USER_SUFFIX = "${var.lambda_mfa_checker_user_suffix}" 43 | } 44 | } 45 | 46 | tags = "${var.tags}" 47 | } 48 | 49 | ## /Create the function 50 | 51 | ## Schedule the lambda function 52 | resource "aws_cloudwatch_event_rule" "root_account_check" { 53 | name = "${var.resource_name_prefix}-root-account-check" 54 | description = "disables inactive users" 55 | schedule_expression = "${var.lambda_cron_schedule}" 56 | } 57 | 58 | resource "aws_cloudwatch_event_target" "root_account_check" { 59 | rule = "${aws_cloudwatch_event_rule.root_account_check.name}" 60 | target_id = "${var.resource_name_prefix}-root-account-check" 61 | arn = "${aws_lambda_function.root_account_check.arn}" 62 | } 63 | 64 | resource "aws_lambda_permission" "root_account_check" { 65 | statement_id = "AllowExecutionFromCloudWatch" 66 | action = "lambda:InvokeFunction" 67 | function_name = "${aws_lambda_function.root_account_check.function_name}" 68 | principal = "events.amazonaws.com" 69 | source_arn = "${aws_cloudwatch_event_rule.root_account_check.arn}" 70 | } 71 | 72 | ## /Schedule the lambda function 73 | # /MFA check and disable function 74 | 75 | -------------------------------------------------------------------------------- /section-1_4.tf: -------------------------------------------------------------------------------- 1 | # AccessKey age check and delete function 2 | ## IAM Policy 3 | data "template_file" "access_key_age_check_policy" { 4 | template = "${file("${path.module}/templates/lambda_access_key_age_check_policy.json.tpl")}" 5 | } 6 | 7 | resource "aws_iam_role" "access_key_age_check" { 8 | name = "${var.resource_name_prefix}-access-key-age-check" 9 | assume_role_policy = "${data.template_file.iam_lambda_assume_role_policy.rendered}" 10 | } 11 | 12 | resource "aws_iam_role_policy" "access_key_age_check" { 13 | name = "${var.resource_name_prefix}-lambda-access-key-age-check" 14 | role = "${aws_iam_role.access_key_age_check.id}" 15 | policy = "${data.template_file.access_key_age_check_policy.rendered}" 16 | } 17 | 18 | ## /IAM Policy 19 | 20 | ## Create the function 21 | data "archive_file" "access_key_age_check" { 22 | type = "zip" 23 | source_file = "${path.module}/files/access_key_age_check.py" 24 | output_path = "${var.temp_artifacts_dir}/access_key_age_check.zip" 25 | } 26 | 27 | resource "aws_lambda_function" "access_key_age_check" { 28 | filename = "${var.temp_artifacts_dir}/access_key_age_check.zip" 29 | function_name = "${var.resource_name_prefix}-access-key-age-check" 30 | role = "${aws_iam_role.access_key_age_check.arn}" 31 | handler = "access_key_age_check.lambda_handler" 32 | source_code_hash = "${data.archive_file.access_key_age_check.output_base64sha256}" 33 | runtime = "python2.7" 34 | timeout = "${var.lambda_timeout}" 35 | 36 | environment { 37 | variables = { 38 | DRY_RUN = "${var.lambda_dry_run}" 39 | AGGRESSIVE = "${var.lambda_aggressive}" 40 | KEY_AGE_MAX = "${var.lambda_access_key_age_max}" 41 | KEY_AGE_NOTIFY = "${var.lambda_access_key_age_notify}" 42 | IGNORE_IAM_USER_PREFIX = "${var.lambda_mfa_checker_user_prefix}" 43 | IGNORE_IAM_USER_SUFFIX = "${var.lambda_mfa_checker_user_suffix}" 44 | } 45 | } 46 | 47 | tags = "${var.tags}" 48 | } 49 | 50 | ## /Create the function 51 | 52 | ## Schedule the lambda function 53 | resource "aws_cloudwatch_event_rule" "access_key_age_check" { 54 | name = "${var.resource_name_prefix}-access-key-age-check" 55 | description = "remove expiring access keys" 56 | schedule_expression = "${var.lambda_cron_schedule}" 57 | } 58 | 59 | resource "aws_cloudwatch_event_target" "access_key_age_check" { 60 | rule = "${aws_cloudwatch_event_rule.access_key_age_check.name}" 61 | target_id = "${var.resource_name_prefix}-access-key-age-check" 62 | arn = "${aws_lambda_function.access_key_age_check.arn}" 63 | } 64 | 65 | resource "aws_lambda_permission" "access_key_age_check" { 66 | statement_id = "AllowExecutionFromCloudWatch" 67 | action = "lambda:InvokeFunction" 68 | function_name = "${aws_lambda_function.access_key_age_check.function_name}" 69 | principal = "events.amazonaws.com" 70 | source_arn = "${aws_cloudwatch_event_rule.access_key_age_check.arn}" 71 | } 72 | 73 | ## /Schedule the lambda function 74 | # /AccessKey age check and delete function 75 | 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-aws-cis-fundamentals 2 | 3 | This Terraform module helps to setup an AWS account with the requirements of CIS Amazon Web Services Foundations Benchmark v1.1.0 4 | 5 | 1. Identity and Access Management 6 | 1. *Avoid the use of the "root" account (Scored)* - **Cannot be codified** 7 | 2. Ensure multi-factor authentication (MFA) is enabled for all IAM users that have a console password (Scored) 8 | 3. Ensure credentials unused for 90 days or greater are disabled (Scored) 9 | 4. Ensure access keys are rotated every 90 days or less (Scored) 10 | 5. Ensure IAM password policy requires at least one uppercase letter (Scored) 11 | 6. Ensure IAM password policy require at least one lowercase letter (Scored) 12 | 7. Ensure IAM password policy require at least one symbol (Scored) 13 | 8. Ensure IAM password policy require at least one number (Scored) 14 | 9. Ensure IAM password policy requires minimum length of 14 or greater (Scored) 15 | 10. Ensure IAM password policy prevents password reuse (Scored) 16 | 11. Ensure IAM password policy expires passwords within 90 days or less (Scored) 17 | 12. Ensure no root account access key exists (Scored) 18 | 13. Ensure MFA is enabled for the "root" account (Scored) 19 | 14. *TODO*: Ensure hardware MFA is enabled for the "root" account (Scored) 20 | 15. *Ensure security questions are registered in the AWS account (Not Scored)* **Cannot be codified** 21 | 16. Ensure IAM policies are attached only to groups or roles (Scored) 22 | 17. Enable detailed billing (Scored) **[Manual intervention 1](#action-1)** 23 | 18. *TODO*: Ensure IAM Master and IAM Manager roles are active (Scored). 24 | 19. Maintain current contact details (Scored) **Cannot be codified** **[Manual intervention 2](#action-2)** 25 | 20. Ensure security contact information is registered (Scored) **Cannot be codified** **[Manual intervention 2](#action-2)** 26 | 21. Ensure IAM instance roles are used for AWS resource access from instances (Not Scored) 27 | 22. Ensure a support role has been created to manage incidents with AWS Support (Scored) 28 | 2. Logging 29 | 1. Ensure CloudTrail is enabled in all regions (Scored) 30 | 2. Ensure CloudTrail log file validation is enabled (Scored) 31 | 3. Ensure the S3 bucket CloudTrail logs to is not publicly accessible (Scored) 32 | 33 | 34 | # List of manual interventions 35 | ##### Action 1 36 | AWS API does not support to set up billing reports and the section 1.17 only creates the necessary bucket. The rest should be taken care of manually. 37 | 38 | After applying Terraform, a privileged user needs to take following actions 39 | 1. Open https://console.aws.amazon.com/billing/home?#/preference 40 | 2. Enable **Receive Billing Reports** 41 | 3. Type the name of the bucket you've created in section 1.17 into the textbox. 42 | 4. Click **Verify** 43 | 5. Click **Save preferences** 44 | 45 | ##### Action 2 46 | AWS API does not support this action, so needs to be completed manually by the root user. 47 | To find out what needs to be done, please visit https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/manage-account-payment.html#contact-info 48 | -------------------------------------------------------------------------------- /files/password_policy_check.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | 4 | 5 | def send_notifications(message): 6 | # TODO 7 | return True 8 | 9 | 10 | def lambda_handler(event, context): 11 | iam = boto3.client('iam') 12 | message_body = "" 13 | 14 | try: 15 | policy = iam.get_account_password_policy() 16 | except: 17 | message_body = 'Account has no password policy' 18 | print message_body 19 | 20 | require_uppercase_characters = bool( 21 | os.environ['REQUIRE_UPPERCASE_CHARACTERS']) if 'REQUIRE_UPPERCASE_CHARACTERS' in os.environ else True 22 | require_lowercase_characters = bool( 23 | os.environ['REQUIRE_LOWERCASE_CHARACTERS']) if 'REQUIRE_LOWERCASE_CHARACTERS' in os.environ else True 24 | require_symbols = bool( 25 | os.environ['REQUIRE_SYMBOLS']) if 'REQUIRE_SYMBOLS' in os.environ else True 26 | require_numbers = bool( 27 | os.environ['REQUIRE_NUMBERS']) if 'REQUIRE_NUMBERS' in os.environ else True 28 | minimum_password_length = int( 29 | os.environ['MINIMUM_PASSWORD_LENGTH']) if 'MINIMUM_PASSWORD_LENGTH' in os.environ else 14 30 | password_reuse_prevention = int( 31 | os.environ['PASSWORD_REUSE_PREVENTION']) if 'PASSWORD_REUSE_PREVENTION' in os.environ else 24 32 | max_password_age = int( 33 | os.environ['MAX_PASSWORD_AGE']) if 'MAX_PASSWORD_AGE' in os.environ else 90 34 | allow_users_to_change_password = bool( 35 | os.environ['ALLOW_USERS_TO_CHANGE_PASSWORD']) if 'ALLOW_USERS_TO_CHANGE_PASSWORD' in os.environ else True 36 | hard_expiry = bool(os.environ['HARD_EXPIRY'] 37 | ) if 'HARD_EXPIRY' in os.environ else True 38 | 39 | if not message_body: 40 | if policy['PasswordPolicy']['RequireUppercaseCharacters'] != require_uppercase_characters: 41 | message_body += "Require an uppercase letter has been set incorrectly\n" 42 | 43 | if policy['PasswordPolicy']['RequireLowercaseCharacters'] != require_lowercase_characters: 44 | message_body += "Require an lowercase letter has been set incorrectly\n" 45 | 46 | if policy['PasswordPolicy']['RequireSymbols'] != require_symbols: 47 | message_body += "Require a symbol has been set incorrectly\n" 48 | 49 | if policy['PasswordPolicy']['RequireNumbers'] != require_numbers: 50 | message_body += "Require a number has been set incorrectly\n" 51 | 52 | if policy['PasswordPolicy']['MinimumPasswordLength'] != minimum_password_length: 53 | message_body += "Minimum password length has been set incorrectly\n" 54 | 55 | if policy['PasswordPolicy']['MaxPasswordAge'] != max_password_age: 56 | message_body += "Maximum password age has been set incorrectly\n" 57 | 58 | if policy['PasswordPolicy']['AllowUsersToChangePassword'] != allow_users_to_change_password: 59 | message_body += "Allow users to change password has been set incorrectly\n" 60 | 61 | if policy['PasswordPolicy']['HardExpiry'] != hard_expiry: 62 | message_body += "Hard password expiry has been set incorrectly\n" 63 | 64 | if message_body: 65 | send_notifications(message_body) 66 | else: 67 | print 'Everything seems fine' 68 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "resource_name_prefix" { 2 | description = "All the resources will be prefixed with this varible" 3 | default = "aws-cis" 4 | } 5 | 6 | variable "tags" { 7 | description = "Hash of tags will be used in all resources" 8 | default = {} 9 | } 10 | 11 | variable "lambda_timeout" { 12 | description = "Default timeout of lambda fucntions" 13 | default = 180 14 | } 15 | 16 | variable "lambda_dry_run" { 17 | description = "Sets DRY_RUN environment variable for all lambda functions" 18 | default = false 19 | } 20 | 21 | variable "lambda_aggressive" { 22 | description = "Sets AGGRESSIVE mode as true for lambda fucntions" 23 | default = true 24 | } 25 | 26 | variable "lambda_mfa_checker_user_prefix" { 27 | description = "Comma separated list of prefixes that mfa checker lambda helper will ignore" 28 | default = "" 29 | } 30 | 31 | variable "lambda_mfa_checker_user_suffix" { 32 | description = "Comma separated list of suffixes that mfa checker lambda helper will ignore" 33 | default = "" 34 | } 35 | 36 | variable "lambda_user_inactivity_limit" { 37 | description = "Disable inactive users more than N days" 38 | default = 90 39 | } 40 | 41 | variable "lambda_access_key_age_max" { 42 | description = "Expire access keys after N days" 43 | default = 90 44 | } 45 | 46 | variable "lambda_access_key_age_notify" { 47 | description = "Start to send notifications for expiring keys N before" 48 | default = 7 49 | } 50 | 51 | variable "lambda_cron_schedule" { 52 | description = "Default Cron schedule for lambda helpers" 53 | default = "cron(0 6 * * ? *)" 54 | } 55 | 56 | variable "temp_artifacts_dir" { 57 | description = "The path for creating the zip file" 58 | default = "/tmp/terraform-aws-cis-fundatentals/artifacts" 59 | } 60 | 61 | variable "iam_require_uppercase_characters" { 62 | description = "Require at least one uppercase letter in passwords" 63 | default = true 64 | } 65 | 66 | variable "iam_require_lowercase_characters" { 67 | description = "Require at least one lowercase letter in passwords" 68 | default = true 69 | } 70 | 71 | variable "iam_require_symbols" { 72 | description = "Require at least one symbol in passwords" 73 | default = true 74 | } 75 | 76 | variable "iam_require_numbers" { 77 | description = "Require at least one number in passwords" 78 | default = true 79 | } 80 | 81 | variable "iam_minimum_password_length" { 82 | description = "Require minimum lenght of password" 83 | default = 14 84 | } 85 | 86 | variable "iam_password_reuse_prevention" { 87 | description = "Prevent password reuse N times" 88 | default = 24 89 | } 90 | 91 | variable "iam_max_password_age" { 92 | description = "Passwords expire in N days" 93 | default = 90 94 | } 95 | 96 | variable "iam_allow_users_to_change_password" { 97 | description = "Can users change their own password" 98 | default = true 99 | } 100 | 101 | variable "iam_hard_expiry" { 102 | description = "Everyone needs hard reset for expired passwords" 103 | default = true 104 | } 105 | 106 | variable "billing_s3_bucket_name" { 107 | description = "S3 bucket name for billing logs" 108 | default = "" 109 | } 110 | 111 | variable "billing_s3_bucket_policy" { 112 | description = "Custom S3 bucket policy for billing logs. The default policy will be used if not defined" 113 | default = "" 114 | } 115 | 116 | # The default policy will be used if this left empty 117 | variable "cloudtrail_kms_policy" { 118 | description = "KMS policy for Cloudtrail logs." 119 | default = "" 120 | } 121 | 122 | # "ReadOnly", "WriteOnly", "All". 123 | variable "clodtrail_event_selector_type" { 124 | description = "Log type for event selectors" 125 | default = "All" 126 | } 127 | 128 | variable "cloudtrail_s3_bucket_name" { 129 | description = "S3 bucket name for CloudTrail logs" 130 | } 131 | -------------------------------------------------------------------------------- /files/mfa_check.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | import datetime 4 | 5 | 6 | def answer_no(x): return True if str(x).lower() in [ 7 | '0', 'no', 'false'] else False 8 | 9 | 10 | def answer_yes(x): return True if str(x).lower() in [ 11 | '1', 'yes', 'true'] else False 12 | 13 | 14 | def mask_key(key): 15 | masked = "" 16 | if type(key) is str: 17 | masked = key[:5] + '*' * (len(key) - 9) + key[-4:] 18 | elif type(key) is list: 19 | for i in key: 20 | masked = masked + ',' + mask_key(i) 21 | return masked 22 | 23 | 24 | def send_notifications(message): 25 | # TO DO 26 | return True 27 | 28 | 29 | def disable_users(users): 30 | iam = boto3.client('iam') 31 | 32 | message_body = 'AGGRESSIVE is set to ' + os.environ['AGGRESSIVE'] \ 33 | if ('AGGRESSIVE' in os.environ and answer_yes(os.environ['AGGRESSIVE'])) else "" 34 | print message_body 35 | 36 | for user in users: 37 | if 'DRY_RUN' not in os.environ or answer_no(os.environ['DRY_RUN']): 38 | if 'AGGRESSIVE' in os.environ and answer_yes(os.environ['AGGRESSIVE']): 39 | 40 | user_notification = 'Deleting ' + user + "'s login profile" 41 | print user_notification 42 | message_body = "\n" + user_notification 43 | 44 | try: 45 | iam.delete_login_profile(UserName=user) 46 | except Exception, e: 47 | print 'Skipping User ' + user + ' has no login profile' 48 | 49 | try: 50 | access_keys = iam.list_access_keys(UserName=user) 51 | except Exception, e: 52 | print 'Skipping. User ' + user + ' has no access key' 53 | 54 | if 'AccessKeyMetadata' in access_keys: 55 | for key in access_keys['AccessKeyMetadata']: 56 | key_notification = 'User ' + user + "'s key " + \ 57 | mask_key(key['AccessKeyId']) + ' will be deleted' 58 | print key_notification 59 | message_body = "\n" + key_notification 60 | iam.delete_access_key( 61 | UserName=key['UserName'], AccessKeyId=key['AccessKeyId']) 62 | else: 63 | no_action = "DRY_RUN has been set and the %s is not disabled" % user 64 | print no_action 65 | message_body = message_body + "\n" + no_action 66 | 67 | if len(users) > 0 and ('DRY_RUN' in os.environ and answer_no(os.environ['DRY_RUN'])): 68 | send_notifications(message_body) 69 | 70 | 71 | def lambda_handler(event, context): 72 | iam = boto3.client('iam') 73 | users = iam.list_users() 74 | non_mfa_users = [] 75 | prefix_list = os.environ['IGNORE_IAM_USER_PREFIX'].split( 76 | ',') if 'IGNORE_IAM_USER_PREFIX' in os.environ else [] 77 | suffix_list = os.environ['IGNORE_IAM_USER_SUFFIX'].split( 78 | ',') if 'IGNORE_IAM_USER_SUFFIX' in os.environ else [] 79 | 80 | def is_user_ignored(prefix_list, suffix_list, name): 81 | for prefix in prefix_list: 82 | if name.startswith(prefix): 83 | return True 84 | for prefix in suffix_list: 85 | if name.endswith(prefix): 86 | return True 87 | return False 88 | 89 | for user in users['Users']: 90 | print('Processing ' + user['UserName']) 91 | mfa = iam.list_mfa_devices(UserName=user['UserName']) 92 | 93 | if not mfa['MFADevices'] and not is_user_ignored(prefix_list, suffix_list, user['UserName']): 94 | non_mfa_users.append(user['UserName']) 95 | 96 | print "The following users do not have MFA set up, so should be removed." 97 | print non_mfa_users 98 | disable_users(non_mfa_users) 99 | 100 | # if __name__ == "__main__": 101 | # event = 1 102 | # context = 1 103 | # lambda_handler(event, context) 104 | -------------------------------------------------------------------------------- /section-1_5.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_account_password_policy" "cis" { 2 | # 1.5 3 | require_uppercase_characters = "${var.iam_require_uppercase_characters}" 4 | 5 | # 1.6 6 | require_lowercase_characters = "${var.iam_require_lowercase_characters}" 7 | 8 | # 1.7 9 | require_symbols = "${var.iam_require_symbols}" 10 | 11 | # 1.8 12 | require_numbers = "${var.iam_require_numbers}" 13 | 14 | # 1.9 15 | minimum_password_length = "${var.iam_minimum_password_length}" 16 | 17 | # 1.10 18 | password_reuse_prevention = "${var.iam_password_reuse_prevention}" 19 | 20 | # 1.11 21 | max_password_age = "${var.iam_max_password_age}" 22 | 23 | allow_users_to_change_password = "${var.iam_allow_users_to_change_password}" 24 | 25 | hard_expiry = "${var.iam_hard_expiry}" 26 | } 27 | 28 | # Password policy check function 29 | ## IAM Policy 30 | data "template_file" "password_policy_check_policy" { 31 | template = "${file("${path.module}/templates/lambda_password_policy_check_policy.json.tpl")}" 32 | } 33 | 34 | resource "aws_iam_role" "password_policy_check" { 35 | name = "${var.resource_name_prefix}-password-policy-check" 36 | assume_role_policy = "${data.template_file.iam_lambda_assume_role_policy.rendered}" 37 | } 38 | 39 | resource "aws_iam_role_policy" "password_policy_check" { 40 | name = "${var.resource_name_prefix}-lambda-password-policy-check" 41 | role = "${aws_iam_role.password_policy_check.id}" 42 | policy = "${data.template_file.password_policy_check_policy.rendered}" 43 | } 44 | 45 | ## /IAM Policy 46 | 47 | ## Create the function 48 | data "archive_file" "password_policy_check" { 49 | type = "zip" 50 | source_file = "${path.module}/files/password_policy_check.py" 51 | output_path = "${var.temp_artifacts_dir}/password_policy_check.zip" 52 | } 53 | 54 | resource "aws_lambda_function" "password_policy_check" { 55 | filename = "${var.temp_artifacts_dir}/password_policy_check.zip" 56 | function_name = "${var.resource_name_prefix}-password-policy-check" 57 | role = "${aws_iam_role.password_policy_check.arn}" 58 | handler = "password_policy_check.lambda_handler" 59 | source_code_hash = "${data.archive_file.password_policy_check.output_base64sha256}" 60 | runtime = "python2.7" 61 | timeout = "${var.lambda_timeout}" 62 | 63 | environment { 64 | variables = { 65 | REQUIRE_UPPERCASE_CHARACTERS = "${var.iam_require_uppercase_characters}" 66 | REQUIRE_LOWERCASE_CHARACTERS = "${var.iam_require_lowercase_characters}" 67 | REQUIRE_SYMBOLS = "${var.iam_require_symbols}" 68 | REQUIRE_NUMBERS = "${var.iam_require_numbers}" 69 | MINIMUM_PASSWORD_LENGTH = "${var.iam_minimum_password_length}" 70 | PASSWORD_REUSE_PREVENTION = "${var.iam_password_reuse_prevention}" 71 | MAX_PASSWORD_AGE = "${var.iam_max_password_age}" 72 | ALLOW_USERS_TO_CHANGE_PASSWORD = "${var.iam_allow_users_to_change_password}" 73 | HARD_EXPIRY = "${var.iam_hard_expiry}" 74 | } 75 | } 76 | 77 | tags = "${var.tags}" 78 | } 79 | 80 | ## /Create the function 81 | 82 | ## Schedule the lambda function 83 | resource "aws_cloudwatch_event_rule" "password_policy_check" { 84 | name = "${var.resource_name_prefix}-password-policy-check" 85 | description = "Check if password policy is in desired state" 86 | schedule_expression = "${var.lambda_cron_schedule}" 87 | } 88 | 89 | resource "aws_cloudwatch_event_target" "password_policy_check" { 90 | rule = "${aws_cloudwatch_event_rule.password_policy_check.name}" 91 | target_id = "${var.resource_name_prefix}-password-policy-check" 92 | arn = "${aws_lambda_function.password_policy_check.arn}" 93 | } 94 | 95 | resource "aws_lambda_permission" "password_policy_check" { 96 | statement_id = "AllowExecutionFromCloudWatch" 97 | action = "lambda:InvokeFunction" 98 | function_name = "${aws_lambda_function.password_policy_check.function_name}" 99 | principal = "events.amazonaws.com" 100 | source_arn = "${aws_cloudwatch_event_rule.password_policy_check.arn}" 101 | } 102 | 103 | ## /Schedule the lambda function 104 | # /Password policy check function 105 | 106 | -------------------------------------------------------------------------------- /section-2_1.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | data "template_file" "cloudtrail_kms" { 4 | template = "${file("${path.module}/templates/cloudtrail_kms_policy.json.tpl")}" 5 | 6 | vars { 7 | aws_account_id = "${data.aws_caller_identity.current.account_id}" 8 | } 9 | } 10 | 11 | resource "aws_kms_key" "cloudtrail" { 12 | description = "Encrypt/Decrypt cloudtrail logs" 13 | deletion_window_in_days = 30 14 | is_enabled = true 15 | enable_key_rotation = true 16 | 17 | policy = "${var.cloudtrail_kms_policy != "" ? "${var.cloudtrail_kms_policy}" : "${data.template_file.cloudtrail_kms.rendered}"}" 18 | 19 | tags = "${var.tags}" 20 | } 21 | 22 | resource "aws_kms_alias" "cloudtrail" { 23 | name = "alias/${var.resource_name_prefix}-cloudtrail" 24 | target_key_id = "${aws_kms_key.cloudtrail.key_id}" 25 | } 26 | 27 | resource "aws_cloudtrail" "cloudtrail" { 28 | name = "${var.resource_name_prefix}-trail" 29 | s3_bucket_name = "${var.cloudtrail_s3_bucket_name}" 30 | is_multi_region_trail = true 31 | include_global_service_events = true 32 | enable_log_file_validation = true 33 | kms_key_id = "${aws_kms_key.cloudtrail.arn}" 34 | 35 | event_selector { 36 | read_write_type = "${var.clodtrail_event_selector_type}" 37 | include_management_events = true 38 | 39 | data_resource { 40 | type = "AWS::S3::Object" 41 | values = ["arn:aws:s3"] 42 | } 43 | 44 | data_resource { 45 | type = "AWS::Lambda::Function" 46 | values = ["arn:aws:lambda"] 47 | } 48 | } 49 | 50 | tags = "${var.tags}" 51 | } 52 | 53 | # CloudTrail check 54 | ## IAM Policy 55 | data "template_file" "cloudtrail_status_check_policy" { 56 | template = "${file("${path.module}/templates/lambda_cloudtrail_status_check_policy.json.tpl")}" 57 | } 58 | 59 | resource "aws_iam_role" "cloudtrail_status_check" { 60 | name = "${var.resource_name_prefix}-cloudtrail-status-check" 61 | assume_role_policy = "${data.template_file.iam_lambda_assume_role_policy.rendered}" 62 | } 63 | 64 | resource "aws_iam_role_policy" "cloudtrail_status_check" { 65 | name = "${var.resource_name_prefix}-lambda-cloudtrail-status-check" 66 | role = "${aws_iam_role.cloudtrail_status_check.id}" 67 | policy = "${data.template_file.cloudtrail_status_check_policy.rendered}" 68 | } 69 | 70 | ## /IAM Policy 71 | 72 | ## Create the function 73 | data "archive_file" "cloudtrail_status_check" { 74 | type = "zip" 75 | source_file = "${path.module}/files/cloudtrail_status_check.py" 76 | output_path = "${var.temp_artifacts_dir}/cloudtrail_status_check.zip" 77 | } 78 | 79 | resource "aws_lambda_function" "cloudtrail_status_check" { 80 | filename = "${var.temp_artifacts_dir}/cloudtrail_status_check.zip" 81 | function_name = "${var.resource_name_prefix}-cloudtrail-status-check" 82 | role = "${aws_iam_role.cloudtrail_status_check.arn}" 83 | handler = "cloudtrail_status_check.lambda_handler" 84 | source_code_hash = "${data.archive_file.cloudtrail_status_check.output_base64sha256}" 85 | runtime = "python2.7" 86 | timeout = "${var.lambda_timeout}" 87 | 88 | tags = "${var.tags}" 89 | } 90 | 91 | ## /Create the function 92 | 93 | ## Schedule the lambda function 94 | resource "aws_cloudwatch_event_rule" "cloudtrail_status_check" { 95 | name = "${var.resource_name_prefix}-cloudtrail-status-check" 96 | description = "remove expiring access keys" 97 | schedule_expression = "${var.lambda_cron_schedule}" 98 | } 99 | 100 | resource "aws_cloudwatch_event_target" "cloudtrail_status_check" { 101 | rule = "${aws_cloudwatch_event_rule.cloudtrail_status_check.name}" 102 | target_id = "${var.resource_name_prefix}-cloudtrail-status-check" 103 | arn = "${aws_lambda_function.cloudtrail_status_check.arn}" 104 | } 105 | 106 | resource "aws_lambda_permission" "cloudtrail_status_check" { 107 | statement_id = "AllowExecutionFromCloudWatch" 108 | action = "lambda:InvokeFunction" 109 | function_name = "${aws_lambda_function.cloudtrail_status_check.function_name}" 110 | principal = "events.amazonaws.com" 111 | source_arn = "${aws_cloudwatch_event_rule.cloudtrail_status_check.arn}" 112 | } 113 | 114 | ## /Schedule the lambda function 115 | # /# CloudTrail check 116 | 117 | -------------------------------------------------------------------------------- /files/access_key_age_check.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | import datetime 4 | 5 | iam = boto3.client('iam') 6 | 7 | 8 | def answer_no(x): return True if str(x).lower() in [ 9 | '0', 'no', 'false'] else False 10 | 11 | 12 | def answer_yes(x): return True if str(x).lower() in [ 13 | '1', 'yes', 'true'] else False 14 | 15 | 16 | def mask_key(key): 17 | masked = "" 18 | if type(key) is str: 19 | masked = key[:5] + '*' * (len(key) - 10) + key[-5:] 20 | elif type(key) is list: 21 | for i in key: 22 | masked = masked + mask_key(i) 23 | return masked 24 | 25 | 26 | def send_notifications(message): 27 | # TODO 28 | return True 29 | 30 | 31 | def process_expring_keys(users): 32 | message_body = 'AGGRESSIVE is set to ' + os.environ['AGGRESSIVE'] \ 33 | if ('AGGRESSIVE' in os.environ and answer_yes(os.environ['AGGRESSIVE'])) \ 34 | else "AGGRESSIVE mode is not active" 35 | print message_body 36 | 37 | key_age_max = int( 38 | os.environ['KEY_AGE_MAX']) if 'KEY_AGE_MAX' in os.environ else 90 39 | key_age_notify = int( 40 | os.environ['KEY_AGE_NOTIFY']) if 'KEY_AGE_NOTIFY' in os.environ else 7 41 | 42 | notification = 'Notify: ' + \ 43 | str(key_age_notify) + ', Expire: ' + str(key_age_max) 44 | print notification 45 | message_body += "\n" + notification 46 | 47 | for user, access_key in users.iteritems(): 48 | for key_id, expirity in access_key.iteritems(): 49 | if expirity >= key_age_max: 50 | if 'AGGRESSIVE' in os.environ and answer_yes(os.environ['AGGRESSIVE']): 51 | notification = 'Deleting ' + user + "'s key " + \ 52 | mask_key(key_id) + ' due to expiration' 53 | print notification 54 | message_body += "\n" + notification 55 | iam.delete_access_key(UserName=user, AccessKeyId=key_id) 56 | else: 57 | notification = user + "'s key " + \ 58 | mask_key(key_id) + ' ' + str(expirity) + \ 59 | ' old, so should be deleted' 60 | print notification 61 | message_body += "\n" + notification 62 | elif expirity < key_age_max and expirity >= key_age_max - key_age_notify: 63 | notification = user + "'s key " + mask_key(key_id) \ 64 | + ' will be expiring in ' + str(expirity) + ' days.' \ 65 | + 'It should be rotated soon' 66 | print notification 67 | message_body += "\n" + notification 68 | 69 | if len(users) > 0 and ('DRY_RUN' in os.environ and answer_no(os.environ['DRY_RUN'])): 70 | send_notifications(message_body) 71 | else: 72 | print "Nothing to do. Either there is no user to process or DRY_RUN is active" 73 | 74 | 75 | def lambda_handler(event, context): 76 | users = iam.list_users() 77 | 78 | prefix_list = os.environ['IGNORE_IAM_USER_PREFIX'].split( 79 | ',') if 'IGNORE_IAM_USER_PREFIX' in os.environ else [] 80 | suffix_list = os.environ['IGNORE_IAM_USER_SUFFIX'].split( 81 | ',') if 'IGNORE_IAM_USER_SUFFIX' in os.environ else [] 82 | 83 | def is_user_ignored(prefix_list, suffix_list, name): 84 | for prefix in prefix_list: 85 | if name.startswith(prefix): 86 | return True 87 | for prefix in suffix_list: 88 | if name.endswith(prefix): 89 | return True 90 | return False 91 | 92 | keys_to_process = {} 93 | 94 | for user in users['Users']: 95 | if not is_user_ignored(prefix_list, suffix_list, user['UserName']): 96 | print 'Processing ' + user['UserName'] 97 | access_keys = iam.list_access_keys(UserName=user['UserName']) 98 | 99 | for key in access_keys['AccessKeyMetadata']: 100 | today = datetime.datetime.utcnow().replace( 101 | tzinfo=key['CreateDate'].tzinfo) 102 | delta = today - key['CreateDate'] 103 | if key['UserName'] not in keys_to_process: 104 | keys_to_process[key['UserName']] = {} 105 | keys_to_process[key['UserName']].setdefault( 106 | key['AccessKeyId'], delta.days) 107 | 108 | process_expring_keys(keys_to_process) 109 | 110 | # if __name__ == "__main__": 111 | # event = 1 112 | # context = 1 113 | # lambda_handler(event, context) 114 | -------------------------------------------------------------------------------- /files/inactivity_check.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | import datetime 4 | 5 | iam = boto3.client('iam') 6 | inactivity_limit = int( 7 | os.environ['INACTIVITY_LIMIT']) if 'INACTIVITY_LIMIT' in os.environ else 90 8 | 9 | 10 | def answer_no(x): return True if str(x).lower() in [ 11 | '0', 'no', 'false'] else False 12 | 13 | 14 | def answer_yes(x): return True if str(x).lower() in [ 15 | '1', 'yes', 'true'] else False 16 | 17 | 18 | def mask_key(key): 19 | masked = "" 20 | if type(key) is str: 21 | masked = key[:5] + '*' * (len(key) - 9) + key[-4:] 22 | elif type(key) is list: 23 | for i in key: 24 | masked = masked + ',' + mask_key(i) 25 | return masked 26 | 27 | 28 | def send_notifications(message): 29 | # TODO 30 | return True 31 | 32 | 33 | def is_user_logged_in(user_name): 34 | user_details = iam.get_user(UserName=user_name) 35 | 36 | if 'PasswordLastUsed' in user_details['User']: 37 | last_login = user_details['User']['PasswordLastUsed'] 38 | else: 39 | last_login = user_details['User']['CreateDate'] 40 | 41 | today = datetime.datetime.utcnow().replace(tzinfo=last_login.tzinfo) 42 | last_login_delta = today - last_login 43 | 44 | if last_login_delta.days > inactivity_limit: 45 | return False 46 | 47 | return True 48 | 49 | 50 | def is_access_key_used(access_key_id): 51 | access_key_last_used = iam.get_access_key_last_used( 52 | AccessKeyId=access_key_id) 53 | # if the key is never used, there is no LastUsedDate item 54 | if 'LastUsedDate' in access_key_last_used['AccessKeyLastUsed']: 55 | last_used = access_key_last_used['AccessKeyLastUsed']['LastUsedDate'] 56 | today = datetime.datetime.utcnow().replace(tzinfo=last_used.tzinfo) 57 | access_key_last_used_delta = today - last_used 58 | if access_key_last_used_delta.days > inactivity_limit: 59 | return False 60 | return True 61 | 62 | 63 | def delete_credentials(users): 64 | message_body = 'AGGRESSIVE is set to ' + os.environ['AGGRESSIVE'] \ 65 | if ('AGGRESSIVE' in os.environ and answer_yes(os.environ['AGGRESSIVE'])) \ 66 | else "AGGRESSIVE mode is not active" 67 | print message_body 68 | 69 | for user in users: 70 | if 'DRY_RUN' not in os.environ or answer_no(os.environ['DRY_RUN']): 71 | if 'AGGRESSIVE' in os.environ and answer_yes(os.environ['AGGRESSIVE']): 72 | user_notification = 'Deleting ' + user + "'s login profile" 73 | print user_notification 74 | message_body += "\n" + user_notification 75 | 76 | try: 77 | iam.delete_login_profile(UserName=user) 78 | except Exception, e: 79 | print 'Skipping User ' + user + ' has no login profile' 80 | 81 | try: 82 | access_keys = iam.list_access_keys(UserName=user) 83 | except Exception, e: 84 | print 'Skipping. User ' + user + ' has no access key' 85 | 86 | if 'AccessKeyMetadata' in access_keys: 87 | for key in access_keys['AccessKeyMetadata']: 88 | key_notification = 'User ' + user + "'s key " + \ 89 | mask_key(key['AccessKeyId']) + ' will be deleted' 90 | print key_notification 91 | message_body = "\n" + key_notification 92 | iam.delete_access_key( 93 | UserName=key['UserName'], AccessKeyId=key['AccessKeyId']) 94 | else: 95 | user_notification = user + ' is not an active user, so should be disabled' 96 | print user_notification 97 | message_body += "\n" + user_notification 98 | else: 99 | no_action = "DRY_RUN has been set and the %s is not disabled" % user 100 | print no_action 101 | message_body += "\n" + no_action 102 | 103 | if len(users) > 0 and ('DRY_RUN' in os.environ and answer_no(os.environ['DRY_RUN'])): 104 | send_notifications(message_body) 105 | else: 106 | print "Nothing to do" 107 | 108 | 109 | def lambda_handler(event, context): 110 | print 'Inactivity Limit ' + str(inactivity_limit) 111 | inactive_users = [] 112 | 113 | prefix_list = os.environ['IGNORE_IAM_USER_PREFIX'].split( 114 | ',') if 'IGNORE_IAM_USER_PREFIX' in os.environ else [] 115 | suffix_list = os.environ['IGNORE_IAM_USER_SUFFIX'].split( 116 | ',') if 'IGNORE_IAM_USER_SUFFIX' in os.environ else [] 117 | 118 | def is_user_ignored(prefix_list, suffix_list, name): 119 | for prefix in prefix_list: 120 | if name.startswith(prefix): 121 | return True 122 | for prefix in suffix_list: 123 | if name.endswith(prefix): 124 | return True 125 | return False 126 | 127 | users = iam.list_users() 128 | 129 | for user in users['Users']: 130 | if not is_user_ignored(prefix_list, suffix_list, user['UserName']): 131 | any_key_used = False 132 | 133 | logged_in = is_user_logged_in(user['UserName']) 134 | 135 | access_keys = iam.list_access_keys(UserName=user['UserName']) 136 | for key in access_keys['AccessKeyMetadata']: 137 | if is_access_key_used(key['AccessKeyId']): 138 | any_key_used = True 139 | 140 | if not logged_in and not any_key_used: 141 | inactive_users.append(user['UserName']) 142 | delete_credentials(inactive_users) 143 | 144 | # if __name__ == "__main__": 145 | # event = 1 146 | # context = 1 147 | # lambda_handler(event, context) 148 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------