--- #==================================================================================================== # AWS CloudFormation template for establishing CIS AWS 1.1 benchmark governance rules # Download the benchmarks here: https://benchmarks.cisecurity.org/en-us/?route=downloads.form.awsfoundations.110 # # The controls are a combination of AWS Config Rules (both AWS-managed and custom), Amazon CloudWatch rules, and Amazon CloudWatch alarms. # Please note that these resources will incur costs in your account; please refer to the pricing model for each service. # # For example, an estimate in us-east-1: # Config Rules: 17 rules @ $2.00/rule/month = $34.00/month # CloudWatch Alarms: 6 alarms @ $0.10/alarm/month = $0.60/month # CloudWatch Metrics: 6 metrics @ $0.30/metric/month = $1.80/month # CloudWatch Logs: 17 logs @ $0.50/GB ingested = based on usage # Lambda: variable (first 1 million requests per month are free) # # The following preconditions must be met before the stack can be launched: # Precondition 1: Config must be running in the region where this template will be run. This is needed for Config Rules. # Precondition 2: CloudTrail must be delivering logs to CloudWatch Logs. This is needed for CloudWatch metrics and alarms. # Precondition 3: Lambda must be supported in the region where this template will be launched. See this page for region support: # https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/ #==================================================================================================== AWSTemplateFormatVersion: 2010-09-09 Description: Establishes a baseline set of security controls Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Profile Level Parameters: - ProfileLevel - Label: default: CloudWatch Rules and Alarms Parameters: - NotificationEmailAddressForCloudWatchAlarms ParameterLabels: ProfileLevel: default: Profile Level NotificationEmailAddressForCloudWatchAlarms: default: Notification Address #================================================== # Parameters #================================================== Parameters: ProfileLevel: Description: "Level 1 controls are baseline governance controls, whereas Level 2 controls represent redundant or stricter governance controls. See the control list here for guidance: https://benchmarks.cisecurity.org/en-us/?route=downloads.form.awsfoundations.110" Type: String Default: Level 2 AllowedValues: - Level 1 - Level 2 NotificationEmailAddressForCloudWatchAlarms: Description: Email address that will be subscribed to the SNS topic for CloudWatch alarms and rules (a subscription confirmation email will be sent). Type: String Default: "" #================================================== # Conditions #================================================== Conditions: IsLevel2: !Equals ["Level 2", !Ref "ProfileLevel"] #================================================== # Resources #================================================== Resources: #================================================== # Resources for EvaluateCisBenchmarkingPreconditions #================================================== MasterConfigRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: [lambda.amazonaws.com] Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess - arn:aws:iam::aws:policy/AWSCloudTrailReadOnlyAccess - arn:aws:iam::aws:policy/IAMReadOnlyAccess - arn:aws:iam::aws:policy/service-role/AWSConfigRulesExecutionRole - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: KmsReadOnly PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - kms:GetKeyRotationStatus - kms:ListKeys Resource: "*" - PolicyName: S3ReadOnly PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - s3:GetBucketAcl - s3:GetBucketLogging Resource: "*" FunctiontForEvaluateCisBenchmarkingPreconditions: Type: AWS::Lambda::Function DependsOn: - MasterConfigRole Properties: FunctionName: EvaluateCisBenchmarkingPreconditions Code: ZipFile: | #================================================================================================== # Function: EvaluateCisBenchmarkingPreconditions # Purpose: Evaluates preconditions for CIS benchmarking # # Precondition 1: Config must have an active recorder running. # This is needed for Config Rules. # Precondition 2: CloudTrail must be delivering logs to CloudWatch Logs # This is needed for CloudWatch metrics and alarms. #================================================================================================== import json import boto3 import cfnresponse def lambda_handler(event, context): response_status = cfnresponse.SUCCESS response_data = '' # Only execute in a custom CloudFormation resource creation event. if 'RequestType' in event and event['RequestType'] == 'Create': is_recording = False # Determine whether there is at least one configuration recorder recording. for recorder in boto3.client('config').describe_configuration_recorder_status()['ConfigurationRecordersStatus']: is_recording = is_recording or recorder['recording'] if not is_recording: response_status = cfnresponse.FAILED response_data = response_data + 'There is no active Config Recorder.' # Determine whether any of the trails are delivering logs to CloudWatch Logs (the trail and log must be in-region) is_delivering_logs = False for trail in boto3.client('cloudtrail').describe_trails(includeShadowTrails=False)['trailList']: if 'CloudWatchLogsLogGroupArn' in trail: is_delivering_logs = True break if not is_delivering_logs: response_status = cfnresponse.FAILED response_data = response_data + ' CloudTrail is not delivering logs to CloudWatch Logs.' cfnresponse.send(event, context, response_status, {"Response":response_data}, '') Description: Evaluates preconditions for CIS benchmarking Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 5 ResourceForEvaluateCisBenchmarkingPreconditions: Type: Custom::ResourceForEvaluateCisBenchmarkingPreconditions DependsOn: FunctiontForEvaluateCisBenchmarkingPreconditions Properties: ServiceToken: !GetAtt FunctiontForEvaluateCisBenchmarkingPreconditions.Arn #================================================== # Config Rules #================================================== #================================================== # CIS 1.5 Ensure IAM password policy requires at least one uppercase letter # CIS 1.6 Ensure IAM password policy require at least one lowercase letter # CIS 1.7 Ensure IAM password policy require at least one symbol # CIS 1.8 Ensure IAM password policy require at least one number # CIS 1.9 Ensure IAM password policy requires minimum length of 14 or greater # CIS 1.10 Ensure IAM password policy prevents password reuse # CIS 1.11 Ensure IAM password policy expires passwords within 90 days or less #================================================== ConfigRuleForIamPasswordPolicy: Type: AWS::Config::ConfigRule DependsOn: ResourceForEvaluateCisBenchmarkingPreconditions Properties: ConfigRuleName: IamPasswordPolicyMustMeetRequirements Description: Evaluates whether the account password policy for IAM users meets the specified requirements. Scope: ComplianceResourceTypes: - AWS::IAM::User InputParameters: RequireUppercaseCharacters: true RequireLowercaseCharacters: true RequireSymbols: true RequireNumbers: true MinimumPasswordLength: 14 PasswordReusePrevention: 24 MaxPasswordAge: 90 Source: Owner: AWS SourceIdentifier: IAM_PASSWORD_POLICY #================================================== # CIS 1.12 Ensure no root account access key exists # CIS 1.13 Ensure MFA is enabled for the "root" account # CIS 1.14 Ensure hardware MFA is enabled for the "root" account #================================================== FunctionForEvaluateRootAccountRule: Type: AWS::Lambda::Function Condition: IsLevel2 DependsOn: - MasterConfigRole - ResourceForEvaluateCisBenchmarkingPreconditions Properties: FunctionName: EvaluateRootAccount Code: ZipFile: | #================================================================================================== # Function: EvaluateRootAccountSecurityProperties # Purpose: Evaluates the root account for security properties #================================================================================================== import json import boto3 import datetime FIELD_ACCESS_KEY_1_ACTIVE = 8 FIELD_ACCESS_KEY_2_ACTIVE = 13 def lambda_handler(event, context): is_compliant = True annotation = '' invoking_event = json.loads(event['invokingEvent']) result_token = 'No token found.' if 'resultToken' in event: result_token = event['resultToken'] client = boto3.client('iam') # Determine whether the root account has MFA enabled. summary = client.get_account_summary()['SummaryMap'] if 'AccountMFAEnabled' in summary and summary['AccountMFAEnabled'] == 1: is_compliant = is_compliant and True else: is_compliant = is_compliant and False annotation = annotation + ' The root account does not have MFA enabled.' # Determine whether the root account uses hardware-based MFA. mfa_devices = client.list_virtual_mfa_devices()['VirtualMFADevices'] for mfa_device in mfa_devices: if not 'SerialNumber' in mfa_device: is_compliant = is_compliant and True else: is_compliant = is_compliant and False annotation = annotation + ' The root account does not have hardware-based MFA enabled.' # Determine whether the root account has active access keys. # The credential report will contain comma-separated values, so transform the users into a list. response = client.generate_credential_report() content = client.get_credential_report()['Content'] users = content.splitlines() # Look for the '' user value and determine whether acccess keys are active. for user in users: if '' in user: user_values = user.split(',') if user_values[FIELD_ACCESS_KEY_1_ACTIVE].lower() == 'false' and user_values[FIELD_ACCESS_KEY_2_ACTIVE].lower() == 'false': is_compliant = is_compliant and True else: is_compliant = is_compliant and False annotation = annotation + ' The root account has active access keys associated with it.' break config = boto3.client('config') config.put_evaluations( Evaluations=[ { 'ComplianceResourceType': 'AWS::::Account', 'ComplianceResourceId': 'Root', 'ComplianceType': 'COMPLIANT' if is_compliant else 'NON_COMPLIANT', 'Annotation': annotation, 'OrderingTimestamp': datetime.datetime.now(), }, ], ResultToken=result_token ) Description: Evaluates the security properties of the root account Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 10 EvaluateRootAccountFunctionPermission: Type: AWS::Lambda::Permission Condition: IsLevel2 DependsOn: FunctionForEvaluateRootAccountRule Properties: FunctionName: !GetAtt FunctionForEvaluateRootAccountRule.Arn Action: lambda:InvokeFunction Principal: config.amazonaws.com ConfigRuleForEvaluateRootAccount: Type: AWS::Config::ConfigRule DependsOn: - ResourceForEvaluateCisBenchmarkingPreconditions - EvaluateRootAccountFunctionPermission Condition: IsLevel2 Properties: ConfigRuleName: RootAccoutMustHaveMfaEnabled Description: Evaluates the security properties of the root account. Source: Owner: CUSTOM_LAMBDA SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification SourceIdentifier: !GetAtt FunctionForEvaluateRootAccountRule.Arn ConfigRuleForRequiredTags: Type: AWS::Config::ConfigRule DependsOn: ResourceForEvaluateCisBenchmarkingPreconditions Properties: ConfigRuleName: ResourcesMustBeTagged Description: Evaluates whether your resources have the tags that you specify. For example, you can check whether your EC2 instances have the 'CostCenter' tag. Separate multiple values with commas. Scope: ComplianceResourceTypes: - AWS::EC2::CustomerGateway - AWS::EC2::Instance - AWS::EC2::InternetGateway - AWS::EC2::NetworkAcl - AWS::EC2::NetworkInterface - AWS::EC2::RouteTable - AWS::EC2::SecurityGroup - AWS::EC2::Subnet - AWS::EC2::Volume - AWS::EC2::VPC - AWS::EC2::VPNConnection - AWS::EC2::VPNGateway - AWS::ACM::Certificate - AWS::RDS::DBInstance - AWS::RDS::DBSnapshot - AWS::RDS::DBSubnetGroup - AWS::RDS::EventSubscription InputParameters: tag1Key: CostCenter Source: Owner: AWS SourceIdentifier: REQUIRED_TAGS ConfigRuleForEncryptedVolumes: Type: AWS::Config::ConfigRule DependsOn: ResourceForEvaluateCisBenchmarkingPreconditions Properties: ConfigRuleName: VolumesMustBeEncrypted Description: Evaluates whether EBS volumes that are in an attached state are encrypted. Optionally, you can specify the ID of a KMS key to use to encrypt the volume. Scope: ComplianceResourceTypes: - AWS::EC2::Volume Source: Owner: AWS SourceIdentifier: ENCRYPTED_VOLUMES #================================================== # CIS 4.1 Ensure no security groups allow ingress from 0.0.0.0/0 to port 22 #================================================== ConfigRuleForRestrictedSsh: Type: AWS::Config::ConfigRule DependsOn: ResourceForEvaluateCisBenchmarkingPreconditions Properties: ConfigRuleName: SecurityGroupsMustRestrictSshTraffic Description: Evaluates whether security groups that are in use disallow unrestricted incoming SSH traffic. Scope: ComplianceResourceTypes: - AWS::EC2::SecurityGroup Source: Owner: AWS SourceIdentifier: INCOMING_SSH_DISABLED #================================================== # CIS 4.2 Ensure no security groups allow ingress from 0.0.0.0/0 to port 3389 #================================================== ConfigRuleForUnrestrictedPorts: Type: AWS::Config::ConfigRule DependsOn: ResourceForEvaluateCisBenchmarkingPreconditions Properties: ConfigRuleName: SecurityGroupsMustDisallowTcpTraffic Description: Evaluates whether security groups that are in use disallow unrestricted incoming TCP traffic to the specified ports. InputParameters: blockedPort1: 3389 Scope: ComplianceResourceTypes: - AWS::EC2::SecurityGroup Source: Owner: AWS SourceIdentifier: RESTRICTED_INCOMING_TRAFFIC #================================================== # CIS 4.3 Ensure VPC flow logging is enabled in all VPCs #================================================== FunctionForVpcFlowLogRule: Type: AWS::Lambda::Function DependsOn: - MasterConfigRole - ResourceForEvaluateCisBenchmarkingPreconditions Properties: FunctionName: EvaluateVpcFlowLogs Code: ZipFile: | #================================================================================================== # Function: EvaluateVpcFlowLogs # Purpose: Determines whether VPC Flow Logs are enabled in the region #================================================================================================== import boto3 import json def evaluate_compliance(config_item, vpc_id): if (config_item['resourceType'] != 'AWS::EC2::VPC'): return 'NOT_APPLICABLE' elif is_flow_logs_enabled(vpc_id): return 'COMPLIANT' else: return 'NON_COMPLIANT' def is_flow_logs_enabled(vpc_id): ec2 = boto3.client('ec2') response = ec2.describe_flow_logs( Filter=[ { 'Name': 'resource-id', 'Values': [vpc_id,] }, ], ) if response['FlowLogs']: return True def lambda_handler(event, context): invoking_event = json.loads(event['invokingEvent']) compliance_value = 'NOT_APPLICABLE' vpc_id = invoking_event['configurationItem']['resourceId'] compliance_value = evaluate_compliance(invoking_event['configurationItem'], vpc_id) config = boto3.client('config') response = config.put_evaluations( Evaluations=[ { 'ComplianceResourceType': invoking_event['configurationItem']['resourceType'], 'ComplianceResourceId': vpc_id, 'ComplianceType': compliance_value, 'OrderingTimestamp': invoking_event['configurationItem']['configurationItemCaptureTime'] }, ], ResultToken=event['resultToken']) Description: Evaluates whether VPC Flow Logs are enabled for the VPCs Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 10 ConfigPermissionToCallVpcFlowLogLambda: Type: AWS::Lambda::Permission DependsOn: FunctionForVpcFlowLogRule Properties: FunctionName: !GetAtt FunctionForVpcFlowLogRule.Arn Action: lambda:InvokeFunction Principal: config.amazonaws.com #================================================== # CIS 4.4 Ensure the default security group of every VPC restricts all traffic #================================================== FunctionForVpcDefaultSecurityGroupsRule: Type: AWS::Lambda::Function DependsOn: - MasterConfigRole - ResourceForEvaluateCisBenchmarkingPreconditions Properties: FunctionName: EvaluateVpcDefaultSecurityGroups Code: ZipFile: | #================================================================================================== # Function: EvaluateVpcDefaultSecurityGroups # Purpose: Evaluates whether VPC default security groups restrict all traffic #================================================================================================== import boto3 import json def lambda_handler(event, context): is_compliant = True invoking_event = json.loads(event['invokingEvent']) annotation = '' security_group_id = invoking_event['configurationItem']['resourceId'] security_group = boto3.client('ec2').describe_security_groups(GroupIds=[security_group_id])['SecurityGroups'] if security_group[0]['GroupName'] == 'default': if security_group[0]['IpPermissions']: annotation = annotation + 'The security group has ingress rules in place.' is_compliant = False if security_group[0]['IpPermissionsEgress']: annotation = annotation + ' The security group has egress rules in place.' is_compliant = False evaluations = [ { 'ComplianceResourceType': invoking_event['configurationItem']['resourceType'], 'ComplianceResourceId': security_group_id, 'ComplianceType': 'COMPLIANT' if is_compliant else 'NON_COMPLIANT', 'OrderingTimestamp': invoking_event['configurationItem']['configurationItemCaptureTime'] } ] if annotation: evaluations[0]['Annotation'] = annotation response = boto3.client('config').put_evaluations( Evaluations = evaluations, ResultToken = event['resultToken']) Description: Evaluates whether VPC default security groups restrict all traffic Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 10 ConfigPermissionToCallVpcDefaultSecurityGroupsLambda: Type: AWS::Lambda::Permission DependsOn: FunctionForVpcDefaultSecurityGroupsRule Properties: FunctionName: !GetAtt FunctionForVpcDefaultSecurityGroupsRule.Arn Action: lambda:InvokeFunction Principal: config.amazonaws.com ConfigRuleForVpcDefaultSecurityGroupss: Type: AWS::Config::ConfigRule Condition: IsLevel2 DependsOn: - FunctionForVpcDefaultSecurityGroupsRule - ConfigPermissionToCallVpcDefaultSecurityGroupsLambda Properties: ConfigRuleName: VpcDefaultSecurityGroupsMustRestrictAllTraffic Description: Evaluates whether VPC default security groups restrict all traffic Scope: ComplianceResourceTypes: - AWS::EC2::SecurityGroup Source: Owner: CUSTOM_LAMBDA SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification SourceIdentifier: !GetAtt FunctionForVpcDefaultSecurityGroupsRule.Arn ConfigRuleForVpcFlowLogs: Type: AWS::Config::ConfigRule DependsOn: - FunctionForVpcFlowLogRule - ConfigPermissionToCallVpcFlowLogLambda Properties: ConfigRuleName: VpcsMustHaveFlowLogs Description: Evaluates whether VPC Flow Logs are enabled. Scope: ComplianceResourceTypes: - AWS::EC2::VPC Source: Owner: CUSTOM_LAMBDA SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification SourceIdentifier: !GetAtt FunctionForVpcFlowLogRule.Arn #================================================== # CIS 1.2: Ensure multi-factor authentication (MFA) is enabled for all IAM users that have a console password #================================================== FunctionForRoleForMfaOnUsersRule: Type: AWS::Lambda::Function DependsOn: - MasterConfigRole - ResourceForEvaluateCisBenchmarkingPreconditions Properties: FunctionName: EvaluateUserMfaUsage Code: ZipFile: | #================================================================================================== # Function: EvaluateUserMfaUsage # Purpose: Determines whether IAM users use MFA #================================================================================================== import json import boto3 APPLICABLE_RESOURCES = ['AWS::IAM::User'] def evaluate_compliance(configuration_item): if configuration_item['resourceType'] not in APPLICABLE_RESOURCES: return 'NOT_APPLICABLE' user_name = configuration_item['resourceName'] iam = boto3.client('iam') mfa = iam.list_mfa_devices(UserName=user_name) # Only check MFA on User with passwords try: profile = iam.get_login_profile(UserName=user_name) except: print('No login profile exists for {}. This user will be skipped.'.format(user_name)) return 'NOT_APPLICABLE' if len(mfa['MFADevices']) > 0: return 'COMPLIANT' else: return 'NON_COMPLIANT' def lambda_handler(event, context): invoking_event = json.loads(event['invokingEvent']) configuration_item = invoking_event['configurationItem'] result_token = 'No token found.' if 'resultToken' in event: result_token = event['resultToken'] config = boto3.client('config') config.put_evaluations( Evaluations=[ { 'ComplianceResourceType': configuration_item['resourceType'], 'ComplianceResourceId': configuration_item['resourceId'], 'ComplianceType': evaluate_compliance(configuration_item), 'OrderingTimestamp': configuration_item['configurationItemCaptureTime'] }, ], ResultToken=result_token ) Description: Evaluates whether users have MFA enabled. Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 10 ConfigPermissionToCallMfaForUsersLambda: Type: AWS::Lambda::Permission DependsOn: FunctionForRoleForMfaOnUsersRule Properties: FunctionName: !GetAtt FunctionForRoleForMfaOnUsersRule.Arn Action: lambda:InvokeFunction Principal: config.amazonaws.com ConfigRuleForMfaForUsers: Type: AWS::Config::ConfigRule DependsOn: ConfigPermissionToCallMfaForUsersLambda Properties: ConfigRuleName: UsersMustHaveMfaEnabled Description: Evaluates whether MFA is enabled on users. Scope: ComplianceResourceTypes: - AWS::IAM::User Source: Owner: CUSTOM_LAMBDA SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification SourceIdentifier: !GetAtt FunctionForRoleForMfaOnUsersRule.Arn #================================================== # CIS 1.24 Ensure IAM policies that allow full "*:*" administrative privileges are not created #================================================== FunctionForEvaluatePolicyPermissionsRule: Type: AWS::Lambda::Function DependsOn: - MasterConfigRole - ResourceForEvaluateCisBenchmarkingPreconditions Properties: FunctionName: EvaluatePolicyPermissions Code: ZipFile: | #================================================================================================== # Function: EvaluatePolicyPermissions # Purpose: Evaluates policies for over permissiveness #================================================================================================== import boto3 import json import jmespath def evaluate_compliance(config_item, policy_arn): if (config_item['resourceType'] != 'AWS::IAM::Policy'): return 'NOT_APPLICABLE' return_value = 'COMPLIANT' client = boto3.client('iam') # Get the policy details. policy = client.get_policy(PolicyArn = policy_arn)['Policy'] # Get the latest policy version. policy_version = client.get_policy_version( PolicyArn = policy['Arn'], VersionId = policy['DefaultVersionId'] ) if jmespath.search('PolicyVersion.Document.Statement[?Effect == \'Allow\' && contains(Resource, \'*\') && contains (Action, \'*\')]', policy_version): return_value = 'NON_COMPLIANT' return return_value def lambda_handler(event, context): invoking_event = json.loads(event['invokingEvent']) policy_arn = invoking_event['configurationItem']['ARN'] compliance_value = evaluate_compliance(invoking_event['configurationItem'], policy_arn) config = boto3.client('config') response = config.put_evaluations( Evaluations=[ { 'ComplianceResourceType': invoking_event['configurationItem']['resourceType'], 'ComplianceResourceId': invoking_event['configurationItem']['resourceId'], 'ComplianceType': compliance_value, 'OrderingTimestamp': invoking_event['configurationItem']['configurationItemCaptureTime'] }, ], ResultToken=event['resultToken']) Description: Evaluates whether IAM policies contain *.* statements Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 10 ConfigPermissionToCallEvaluatePolicyPermissionsLambda: Type: AWS::Lambda::Permission DependsOn: FunctionForEvaluatePolicyPermissionsRule Properties: FunctionName: !GetAtt FunctionForEvaluatePolicyPermissionsRule.Arn Action: lambda:InvokeFunction Principal: config.amazonaws.com ConfigRuleForEvaluatePolicyPermissions: Type: AWS::Config::ConfigRule DependsOn: - FunctionForEvaluatePolicyPermissionsRule - ConfigPermissionToCallEvaluatePolicyPermissionsLambda Properties: ConfigRuleName: IamPoliciesMustNotContainStarStar Description: Evaluates whether IAM policies contain *.* statements Scope: ComplianceResourceTypes: - AWS::IAM::Policy Source: Owner: CUSTOM_LAMBDA SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification SourceIdentifier: !GetAtt FunctionForEvaluatePolicyPermissionsRule.Arn #================================================== # CIS 1.16 Ensure IAM policies are attached only to groups or roles #================================================== FunctionForEvaluateUserPolicyAssociationRule: Type: AWS::Lambda::Function DependsOn: - MasterConfigRole - ResourceForEvaluateCisBenchmarkingPreconditions Properties: FunctionName: EvaluateUserPolicyAssociations Description: Evaluates whether users have policies associated with them. Users should inherit permissions from groups instead. Code: ZipFile: | #================================================================================================== # Function: EvaluateUserPolicyAssociations # Purpose: Evaluates whether users have policies associated with them. Users should inherit # permissions from groups instead. #================================================================================================== import json import boto3 APPLICABLE_RESOURCES = ['AWS::IAM::User'] def evaluate_compliance(configuration_item): if configuration_item['resourceType'] not in APPLICABLE_RESOURCES: return 'NOT_APPLICABLE' user_name = configuration_item['resourceName'] iam = boto3.client('iam') if iam.list_user_policies(UserName=user_name)['PolicyNames'] \ or iam.list_attached_user_policies(UserName=user_name)['AttachedPolicies']: return 'NON_COMPLIANT' else: return 'COMPLIANT' def lambda_handler(event, context): invoking_event = json.loads(event['invokingEvent']) configuration_item = invoking_event['configurationItem'] result_token = 'No token found.' if 'resultToken' in event: result_token = event['resultToken'] config = boto3.client('config') config.put_evaluations( Evaluations=[ { 'ComplianceResourceType': configuration_item['resourceType'], 'ComplianceResourceId': configuration_item['resourceId'], 'ComplianceType': evaluate_compliance(configuration_item), 'OrderingTimestamp': configuration_item['configurationItemCaptureTime'] }, ], ResultToken=result_token ) Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 10 ConfigPermissionToCallEvaluateUserPolicyAssociationLambda: Type: AWS::Lambda::Permission DependsOn: FunctionForEvaluateUserPolicyAssociationRule Properties: FunctionName: !GetAtt FunctionForEvaluateUserPolicyAssociationRule.Arn Action: lambda:InvokeFunction Principal: config.amazonaws.com ConfigRuleForEvaluateUserPolicyAssociations: Type: AWS::Config::ConfigRule DependsOn: - FunctionForEvaluateUserPolicyAssociationRule - ConfigPermissionToCallEvaluateUserPolicyAssociationLambda Properties: ConfigRuleName: UsersMustNotHaveAssociatedPolicies Description: Evaluates whether users have policies associated with them. Users should inherit permissions from groups instead. Scope: ComplianceResourceTypes: - AWS::IAM::User Source: Owner: CUSTOM_LAMBDA SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification SourceIdentifier: !GetAtt FunctionForEvaluateUserPolicyAssociationRule.Arn #================================================== # CIS 2.1 Ensure CloudTrail is enabled in all regions # CIS 2.4 Ensure CloudTrail trails are integrated with CloudWatch Logs #================================================== FunctionForEvaluateCloudTrailRule: Type: AWS::Lambda::Function DependsOn: - MasterConfigRole - ResourceForEvaluateCisBenchmarkingPreconditions Properties: FunctionName: EvaluateCloudTrail Code: ZipFile: | #================================================================================================== # Function: EvaluateCloudTrail # Purpose: Evaluates whether CloudTrail has appropriate security properties. #================================================================================================== import json import boto3 import datetime import time def lambda_handler(event, context): is_compliant = True #default annotation = '' is_multi_region = True #default is_publicly_accessible = False current_region_trail = {} # List all trails, including "shadow" trails, which are trails in other regions that could # be capturing multi-regional events. client = boto3.client('cloudtrail') for trail in client.describe_trails()['trailList']: is_multi_region = is_multi_region or trail['IsMultiRegionTrail'] if trail['HomeRegion'] == context.invoked_function_arn.split(':')[3]: current_region_trail = trail # Enabled in all regions? if not is_multi_region: is_compliant = False annotation = annotation + ' CloudTrail is not enabled in all regions.' # Integration with CloudWatch Logs? if 'CloudWatchLogsLogGroupArn' in current_region_trail and not current_region_trail['CloudWatchLogsLogGroupArn']: is_compliant = False annotation = annotation + ' CloudTrail is not integrated with Cloudwatch Logs.' # CloudWatch Logs delivered within the last day? trail_details = client.get_trail_status(Name = current_region_trail['Name']) if 'LatestCloudWatchLogsDeliveryTime' in trail_details: # Determine whether the number of minutes since the last delivery time exceeds 24 hours. if ((int(time.time()) - int(trail_details['LatestCloudWatchLogsDeliveryTime'].strftime("%s"))) / 1440) > 24: is_compliant = False annotation = annotation + ' The latest CloudTrail log delivery exceeds 24 hours.' else: is_compliant = False annotation = annotation + ' There is no record of CloudTrail log delivery.' result_token = 'No token found.' if 'resultToken' in event: result_token = event['resultToken'] evaluations = [ { 'ComplianceResourceType': 'AWS::CloudTrail::Trail', 'ComplianceResourceId': current_region_trail['Name'], 'ComplianceType': 'COMPLIANT' if is_compliant else 'NON_COMPLIANT', 'OrderingTimestamp': datetime.datetime.now() } ] if annotation: evaluations[0]['Annotation'] = annotation config = boto3.client('config') config.put_evaluations( Evaluations = evaluations, ResultToken = result_token ) Description: Evaluates whether CloudTrail has appropriate security properties Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 10 ConfigPermissionToCallEvaluateCloudTrailLambda: Type: AWS::Lambda::Permission DependsOn: FunctionForEvaluateCloudTrailRule Properties: FunctionName: !GetAtt FunctionForEvaluateCloudTrailRule.Arn Action: lambda:InvokeFunction Principal: config.amazonaws.com ConfigRuleForEvaluateCloudTrail: Type: AWS::Config::ConfigRule DependsOn: - FunctionForEvaluateCloudTrailRule - ConfigPermissionToCallEvaluateCloudTrailLambda Properties: ConfigRuleName: CloudTrailMustBeActive Description: Evaluates whether CloudTrail is active and follows security principles Source: Owner: CUSTOM_LAMBDA SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification SourceIdentifier: !GetAtt FunctionForEvaluateCloudTrailRule.Arn #================================================== # CIS 2.3 Ensure the S3 bucket CloudTrail logs to is not publicly accessible # CIS 2.6 Ensure S3 bucket access logging is enabled on the CloudTrail S3 bucket #================================================== FunctionForEvaluateCloudTrailBucketRule: Type: AWS::Lambda::Function DependsOn: - MasterConfigRole - ResourceForEvaluateCisBenchmarkingPreconditions Properties: FunctionName: EvaluateCloudTrailBucket Code: ZipFile: | #================================================================================================== # Function: EvaluateCloudTrailBucket # Purpose: Evaluates whether the CloudTrail S3 bucket has appropriate security properties. #================================================================================================== import json import boto3 import datetime import time def lambda_handler(event, context): is_compliant = True #default annotation = '' is_publicly_accessible = False s3_bucket_name = '' # Get the trail for the current region. client = boto3.client('cloudtrail') for trail in client.describe_trails(includeShadowTrails = False)['trailList']: # CloudTrail S3 bucket not publicly accessible and is logged? if trail['S3BucketName']: s3_bucket_name = trail['S3BucketName'] client = boto3.client('s3') try: for grant in client.get_bucket_acl(Bucket = s3_bucket_name)['Grants']: if grant['Permission'] in ['READ','FULL_CONTROL'] \ and ('URI' in grant['Grantee'] \ and ('AuthenticatedUsers' in grant['Grantee']['URI'] or 'AllUsers' in grant['Grantee']['URI'])): # Bucket has an ACL that allows it to be publicly accessible. is_publicly_accessible = True if is_publicly_accessible: is_compliant = False annotation = annotation + ' The CloudTrail S3 bucket \'{}\' is publicly accessible.'.format(s3_bucket_name) # CloudTrail S3 bucket has logging enabled? if not client.get_bucket_logging(Bucket = s3_bucket_name): is_compliant = False annotation = annotation + ' The CloudTrail S3 bucket \'{}\' does not have logging enabled.'.format(s3_bucket_name) except E: is_compliant = False annotation = annotation + ' There was an error looking up CloudTrail S3 bucket \'{}\'.'.format(s3_bucket_name) else: annotation = annotation + ' CloudTrail is not integrated with S3.' result_token = 'No token found.' if 'resultToken' in event: result_token = event['resultToken'] evaluations = [ { 'ComplianceResourceType': 'AWS::S3::Bucket', 'ComplianceResourceId': s3_bucket_name, 'ComplianceType': 'COMPLIANT' if is_compliant else 'NON_COMPLIANT', 'OrderingTimestamp': datetime.datetime.now() } ] if annotation: evaluations[0]['Annotation'] = annotation config = boto3.client('config') config.put_evaluations( Evaluations = evaluations, ResultToken = result_token ) Description: Evaluates whether CloudTrail has appropriate security properties Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 10 ConfigPermissionToCallEvaluateCloudTrailBucketLambda: Type: AWS::Lambda::Permission DependsOn: FunctionForEvaluateCloudTrailBucketRule Properties: FunctionName: !GetAtt FunctionForEvaluateCloudTrailBucketRule.Arn Action: lambda:InvokeFunction Principal: config.amazonaws.com ConfigRuleForEvaluateCloudTrailBucket: Type: AWS::Config::ConfigRule DependsOn: - FunctionForEvaluateCloudTrailBucketRule - ConfigPermissionToCallEvaluateCloudTrailBucketLambda Properties: ConfigRuleName: CloudTrailBucketMustBeSecure Description: Evaluates whether the CloudTrail S3 bucket follows security principles Source: Owner: CUSTOM_LAMBDA SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification SourceIdentifier: !GetAtt FunctionForEvaluateCloudTrailBucketRule.Arn #================================================== # CIS 2.2 Ensure CloudTrail log file validation is enabled # CIS 2.7 Ensure CloudTrail logs are encrypted at rest using KMS CMKs #================================================== FunctionForEvaluateCloudTrailLogIntegrityRule: Type: AWS::Lambda::Function Condition: IsLevel2 DependsOn: - MasterConfigRole - ResourceForEvaluateCisBenchmarkingPreconditions Properties: FunctionName: EvaluateCloudTrailLogIntegrity Code: ZipFile: | #================================================================================================== # Function: EvaluateCloudTrailLogIntegrity # Purpose: Evaluates whether CloudTrail Logs are validated and encrypted #================================================================================================== import json import boto3 import datetime import time def lambda_handler(event, context): is_compliant = True #default annotation = '' current_region_trail = {} client = boto3.client('cloudtrail') for trail in client.describe_trails()['trailList']: if trail['HomeRegion'] == context.invoked_function_arn.split(':')[3]: current_region_trail = trail # Log file validation enabled? if not current_region_trail['LogFileValidationEnabled']: is_compliant = False annotation = annotation + ' CloudTrail log file validation is not enabled.' # Log files encrypted? if not 'KmsKeyId' in current_region_trail: is_compliant = False annotation = annotation + ' CloudTrail log files are not encrypted in S3.' result_token = 'No token found.' if 'resultToken' in event: result_token = event['resultToken'] evaluations = [ { 'ComplianceResourceType': 'AWS::CloudTrail::Trail', 'ComplianceResourceId': current_region_trail['Name'], 'ComplianceType': 'COMPLIANT' if is_compliant else 'NON_COMPLIANT', 'OrderingTimestamp': datetime.datetime.now() } ] if annotation: evaluations[0]['Annotation'] = annotation config = boto3.client('config') config.put_evaluations( Evaluations = evaluations, ResultToken = result_token ) Description: Evaluates whether CloudTrail has appropriate security properties Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 10 ConfigPermissionToCallEvaluateCloudTrailLogIntegrityLambda: Type: AWS::Lambda::Permission Condition: IsLevel2 DependsOn: FunctionForEvaluateCloudTrailLogIntegrityRule Properties: FunctionName: !GetAtt FunctionForEvaluateCloudTrailLogIntegrityRule.Arn Action: lambda:InvokeFunction Principal: config.amazonaws.com ConfigRuleForEvaluateCloudTrailLogIntegrity: Type: AWS::Config::ConfigRule Condition: IsLevel2 DependsOn: - FunctionForEvaluateCloudTrailLogIntegrityRule - ConfigPermissionToCallEvaluateCloudTrailLogIntegrityLambda Properties: ConfigRuleName: CloudTrailLogsMustBeValidatedAndEncrypted Description: Evaluates whether CloudTrail Logs are validated and encrypted Source: Owner: CUSTOM_LAMBDA SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification SourceIdentifier: !GetAtt FunctionForEvaluateCloudTrailLogIntegrityRule.Arn #================================================== # CIS 1.21 Ensure IAM instance roles are used for AWS resource access from instances #================================================== FunctionForInstanceRoleUseRule: Type: AWS::Lambda::Function Condition: IsLevel2 DependsOn: - MasterConfigRole - ResourceForEvaluateCisBenchmarkingPreconditions Properties: FunctionName: EvaluateInstanceRoleUse Code: ZipFile: | #================================================================================================== # Function: EvaluateInstanceRoleUse # Purpose: Evaluates whether instances use instance roles #================================================================================================== import boto3 import json def evaluate_compliance(config_item, instance_id): if (config_item['resourceType'] != 'AWS::EC2::Instance'): return 'NOT_APPLICABLE' reservations = boto3.client('ec2').describe_instances(InstanceIds=[instance_id])['Reservations'] if reservations and 'IamInstanceProfile' in reservations[0]['Instances'][0]: return 'COMPLIANT' else: return 'NON_COMPLIANT' def lambda_handler(event, context): invoking_event = json.loads(event['invokingEvent']) compliance_value = 'NOT_APPLICABLE' instance_id = invoking_event['configurationItem']['resourceId'] compliance_value = evaluate_compliance(invoking_event['configurationItem'], instance_id) config = boto3.client('config') response = config.put_evaluations( Evaluations=[ { 'ComplianceResourceType': invoking_event['configurationItem']['resourceType'], 'ComplianceResourceId': instance_id, 'ComplianceType': compliance_value, 'OrderingTimestamp': invoking_event['configurationItem']['configurationItemCaptureTime'] }, ], ResultToken=event['resultToken']) Description: Evaluates whether instances use instance roles Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 10 ConfigPermissionToCallInstanceRoleUseLambda: Type: AWS::Lambda::Permission Condition: IsLevel2 DependsOn: FunctionForInstanceRoleUseRule Properties: FunctionName: !GetAtt FunctionForInstanceRoleUseRule.Arn Action: lambda:InvokeFunction Principal: config.amazonaws.com ConfigRuleForInstanceRoleUses: Type: AWS::Config::ConfigRule Condition: IsLevel2 DependsOn: - FunctionForInstanceRoleUseRule - ConfigPermissionToCallInstanceRoleUseLambda Properties: ConfigRuleName: InstancesMustUseIamRoles Description: Evaluates whether instances use instance roles Scope: ComplianceResourceTypes: - AWS::EC2::Instance Source: Owner: CUSTOM_LAMBDA SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification SourceIdentifier: !GetAtt FunctionForInstanceRoleUseRule.Arn #================================================== # CIS 2.8 Ensure rotation for customer created CMKs is enabled #================================================== FunctionForEvaluateKeyRotationRule: Type: AWS::Lambda::Function Condition: IsLevel2 DependsOn: - MasterConfigRole - ResourceForEvaluateCisBenchmarkingPreconditions Properties: FunctionName: EvaluateKmsCustomerKeyRotation Code: ZipFile: | #================================================================================================== # Function: EvaluateKmsCustomerKeyRotation # Purpose: Evaluates whether customer-managed KMS keys are rotated #================================================================================================== import boto3 import json import datetime def lambda_handler(event, context): is_compliant = True result_token = 'No token found.' compliance_resource_type = 'N/A' if 'resultToken' in event: result_token = event['resultToken'] evaluations = [] kms_client = boto3.client('kms') config_client = boto3.client('config') # Get a list of key aliases. This will be used to discard AWS managed keys from rotation consideration. aws_managed_keys = [] for key in kms_client.list_aliases()['Aliases']: if 'TargetKeyId' in key and key['AliasName'].startswith('alias/aws'): aws_managed_keys.append(key['TargetKeyId']) for key in kms_client.list_keys()['Keys']: # Do not evaluate AWS-managed keys. if not key['KeyId'] in aws_managed_keys: try: is_compliant = kms_client.get_key_rotation_status(KeyId = key['KeyId'])['KeyRotationEnabled'] except: is_compliant = True evaluations.append( { 'ComplianceResourceType': 'AWS::KMS::Key', 'ComplianceResourceId': key['KeyId'], 'ComplianceType': 'COMPLIANT' if is_compliant else 'NON_COMPLIANT', 'OrderingTimestamp': datetime.datetime.now() } ) response = config_client.put_evaluations( Evaluations = evaluations, ResultToken = event['resultToken'] ) Description: Evaluates whether customer-managed KMS keys are rotated Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 10 ConfigPermissionToCallEvaluateKeyRotationLambda: Type: AWS::Lambda::Permission Condition: IsLevel2 DependsOn: FunctionForEvaluateKeyRotationRule Properties: FunctionName: !GetAtt FunctionForEvaluateKeyRotationRule.Arn Action: lambda:InvokeFunction Principal: config.amazonaws.com ConfigRuleForEvaluateKeyRotations: Type: AWS::Config::ConfigRule Condition: IsLevel2 DependsOn: - FunctionForEvaluateKeyRotationRule - ConfigPermissionToCallEvaluateKeyRotationLambda Properties: ConfigRuleName: KmsCustomerKeysMustBeRotated Description: Evaluates whether customer-managed KMS keys are rotated. Source: Owner: CUSTOM_LAMBDA SourceIdentifier: !GetAtt FunctionForEvaluateKeyRotationRule.Arn SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification - EventSource: aws.config MessageType: ScheduledNotification #================================================== # CIS 2.5 Ensure AWS Config is enabled in all regions #================================================== FunctionForEvaluateConfigInAllRegionsRule: Type: AWS::Lambda::Function DependsOn: - MasterConfigRole - ResourceForEvaluateCisBenchmarkingPreconditions Properties: FunctionName: EvaluateConfigInAllRegions Code: ZipFile: | #================================================================================================== # Function: EvaluateConfigInAllRegions # Purpose: Evaluates whether Config is enabled in all regions #================================================================================================== import boto3 import json import datetime def lambda_handler(event, context): is_compliant = True evaluations = [] annotation = [] result_token = 'No token found.' if 'resultToken' in event: result_token = event['resultToken'] # Get a list of regions. (Using EC2 in this way is a reliable and durable means of retrieving AWS regions.) regions = [region['RegionName'] for region in boto3.client('ec2').describe_regions()['Regions']] # Determine whether each region has an active configuration recorder and that at least one # region is recording global events (such as IAM). for region in regions: client = boto3.client('config', region_name = region) configuration_recorder_statuses = client.describe_configuration_recorder_status()['ConfigurationRecordersStatus'] if configuration_recorder_statuses and configuration_recorder_statuses[0]['recording']: # Now determine whether the active recorder is recording all resources in the region. configuration_recorders = client.describe_configuration_recorders()['ConfigurationRecorders'] if configuration_recorders and configuration_recorders[0]['recordingGroup']['allSupported']: evaluations.append(put_evaluation(region, True, '')) else: evaluations.append(put_evaluation(region, False, 'Config is not capturing all resources.')) else: evaluations.append(put_evaluation(region, False, 'Region does not have an active recorder.')) boto3.client('config').put_evaluations( Evaluations = evaluations, ResultToken = result_token ) def put_evaluation(region, is_compliant, annotation): evaluation = { 'ComplianceResourceType': 'AWS::Config::ConfigurationRecorder', 'ComplianceResourceId': region, 'ComplianceType': 'COMPLIANT' if is_compliant else 'NON_COMPLIANT', 'OrderingTimestamp': datetime.datetime.now() } if annotation: evaluation['Annotation'] = annotation return evaluation Description: Evaluates whether Config is enabled in all regions Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 120 EvaluateConfigInAllRegionsFunctionPermission: Type: AWS::Lambda::Permission DependsOn: FunctionForEvaluateConfigInAllRegionsRule Properties: FunctionName: !GetAtt FunctionForEvaluateConfigInAllRegionsRule.Arn Action: lambda:InvokeFunction Principal: config.amazonaws.com ConfigRuleForEvaluateConfigInAllRegions: Type: AWS::Config::ConfigRule DependsOn: - EvaluateConfigInAllRegionsFunctionPermission Properties: ConfigRuleName: ConfigMustBeEnabledInAllRegions Description: Evaluates whether Config is enabled in all regions. Source: Owner: CUSTOM_LAMBDA SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification - EventSource: aws.config MessageType: ScheduledNotification SourceIdentifier: !GetAtt FunctionForEvaluateConfigInAllRegionsRule.Arn #================================================== # CIS 4.5 Ensure routing tables for VPC peering are "least access" #================================================== FunctionForVpcPeeringRouteTablesRule: Type: AWS::Lambda::Function DependsOn: - MasterConfigRole - ResourceForEvaluateCisBenchmarkingPreconditions Properties: FunctionName: EvaluateVpcPeeringRouteTables Code: ZipFile: | #================================================================================================== # Function: EvaluateVpcPeeringRouteTables # Purpose: Evaluates whether VPC route tables are least access #================================================================================================== import boto3 import json def lambda_handler(event, context): is_compliant = True invoking_event = json.loads(event['invokingEvent']) annotation = '' route_table_id = invoking_event['configurationItem']['resourceId'] #print (json.dumps(boto3.client('ec2').describe_route_tables(RouteTableIds=[route_table_id]))) for route_table in boto3.client('ec2').describe_route_tables(RouteTableIds=[route_table_id])['RouteTables']: for route in route_table['Routes']: if 'VpcPeeringConnectionId' in route: if int(str(route['DestinationCidrBlock']).split("/", 1)[1]) < 24: is_compliant = False annotation = 'VPC peered route table has a large CIDR block destination.' evaluations = [ { 'ComplianceResourceType': invoking_event['configurationItem']['resourceType'], 'ComplianceResourceId': route_table_id, 'ComplianceType': 'COMPLIANT' if is_compliant else 'NON_COMPLIANT', 'OrderingTimestamp': invoking_event['configurationItem']['configurationItemCaptureTime'] } ] if annotation: evaluations[0]['Annotation'] = annotation response = boto3.client('config').put_evaluations( Evaluations = evaluations, ResultToken = event['resultToken']) Description: Evaluates whether VPC peered route tables are least access Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 10 ConfigPermissionToCallVpcPeeringRouteTablesLambda: Type: AWS::Lambda::Permission DependsOn: FunctionForVpcPeeringRouteTablesRule Properties: FunctionName: !GetAtt FunctionForVpcPeeringRouteTablesRule.Arn Action: lambda:InvokeFunction Principal: config.amazonaws.com ConfigRuleForVpcPeeringRouteTabless: Type: AWS::Config::ConfigRule Condition: IsLevel2 DependsOn: - FunctionForVpcPeeringRouteTablesRule - ConfigPermissionToCallVpcPeeringRouteTablesLambda Properties: ConfigRuleName: VpcPeeringRouteTablesMustBeLeastAccess Description: Evaluates whether VPC peered route tables are least access Scope: ComplianceResourceTypes: - AWS::EC2::RouteTable Source: Owner: CUSTOM_LAMBDA SourceDetails: - EventSource: aws.config MessageType: ConfigurationItemChangeNotification SourceIdentifier: !GetAtt FunctionForVpcPeeringRouteTablesRule.Arn #================================================== # CloudWatch Logs Metrics and Alarms #================================================== SnsTopicForCloudWatchEvents: Type: AWS::SNS::Topic DependsOn: ResourceForEvaluateCisBenchmarkingPreconditions Properties: TopicName: CloudWatchNotifications DisplayName: Broadcasts formatted CloudWatch events to subscribers Subscription: - Endpoint: !Ref NotificationEmailAddressForCloudWatchAlarms Protocol: email #================================================== # Resources for GetCloudTrailCloudWatchLog #================================================== GetCloudTrailCloudWatchLog: Type: AWS::Lambda::Function DependsOn: MasterConfigRole Properties: FunctionName: GetCloudTrailCloudWatchLog Code: ZipFile: | #================================================================================================== # Function: GetCloudTrailCloudWatchLog # Purpose: Returns the CloudWatch Log that is used by CloudTrail #================================================================================================== import boto3 import cfnresponse def lambda_handler(event, context): cloudwatch_log = '' response_data = {} if event['RequestType'] == 'Create': for trail in boto3.client('cloudtrail').describe_trails(includeShadowTrails=False)['trailList']: if 'CloudWatchLogsLogGroupArn' in trail: cloudwatch_log = trail['CloudWatchLogsLogGroupArn'].split(':')[6] break response_data['LogName'] = cloudwatch_log cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data, '') Description: Returns the CloudWatch Log that is used by CloudTrail Handler: index.lambda_handler MemorySize: 128 Role: !GetAtt MasterConfigRole.Arn Runtime: python3.8 Timeout: 5 ResourceForGetCloudTrailCloudWatchLog: Type: Custom::ResourceForGetCloudTrailCloudWatchLog DependsOn: GetCloudTrailCloudWatchLog Properties: ServiceToken: !GetAtt GetCloudTrailCloudWatchLog.Arn #================================================== # CIS 3.1 Ensure a log metric filter and alarm exist for unauthorized API calls #================================================== UnauthorizedAttemptsCloudWatchFilter: Type: AWS::Logs::MetricFilter DependsOn: - ResourceForEvaluateCisBenchmarkingPreconditions - ResourceForGetCloudTrailCloudWatchLog Properties: LogGroupName: !GetAtt ResourceForGetCloudTrailCloudWatchLog.LogName FilterPattern: "{ ($.errorCode = \"*UnauthorizedOperation\") || ($.errorCode = \"AccessDenied*\") }" MetricTransformations: - MetricNamespace: CloudTrailMetrics MetricName: UnauthorizedAttemptCount MetricValue: 1 UnauthorizedAttemptCloudWatchAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: Unauthorized Activity Attempt AlarmDescription: Multiple unauthorized actions or logins attempted AlarmActions: - !Ref SnsTopicForCloudWatchEvents MetricName: UnauthorizedAttemptCount Namespace: CloudTrailMetrics ComparisonOperator: GreaterThanOrEqualToThreshold EvaluationPeriods: 1 Period: 60 Statistic: Sum Threshold: 5 TreatMissingData: notBreaching #================================================== # CIS 1.1 Avoid the use of the "root" account # CIS 3.3 Ensure a log metric filter and alarm exist for usage of "root" account #================================================== IAMRootActivityCloudWatchMetric: Type: AWS::Logs::MetricFilter DependsOn: - ResourceForEvaluateCisBenchmarkingPreconditions - ResourceForGetCloudTrailCloudWatchLog Properties: LogGroupName: !GetAtt ResourceForGetCloudTrailCloudWatchLog.LogName FilterPattern: "{ $.userIdentity.type = \"Root\" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != \"AwsServiceEvent\" }" MetricTransformations: - MetricNamespace: CloudTrailMetrics MetricName: RootUserEventCount MetricValue: 1 IAMRootActivityCloudWatchAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: IAM Root Activity AlarmDescription: Root user activity detected AlarmActions: - !Ref SnsTopicForCloudWatchEvents MetricName: RootUserEventCount Namespace: CloudTrailMetrics ComparisonOperator: GreaterThanOrEqualToThreshold EvaluationPeriods: 1 Period: 60 Statistic: Sum Threshold: 1 TreatMissingData: notBreaching #================================================== # CIS 3.2 Ensure a log metric filter and alarm exist for Management Console sign-in without MFA #================================================== ConsoleSigninWithoutMfaCloudWatchMetric: Type: AWS::Logs::MetricFilter DependsOn: - ResourceForEvaluateCisBenchmarkingPreconditions - ResourceForGetCloudTrailCloudWatchLog Properties: LogGroupName: !GetAtt ResourceForGetCloudTrailCloudWatchLog.LogName FilterPattern: "{ ($.eventName = \"ConsoleLogin\") && ($.additionalEventData.MFAUsed != \"Yes\") && ($.responseElements.ConsoleLogin != \"Failure\") && ($.additionalEventData.SamlProviderArn NOT EXISTS) }" MetricTransformations: - MetricNamespace: CloudTrailMetrics MetricName: ConsoleSigninWithoutMFA MetricValue: 1 ConsoleSigninWithoutMFACloudWatchAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: Console Signin Without MFA AlarmDescription: Console signin without MFA AlarmActions: - !Ref SnsTopicForCloudWatchEvents MetricName: ConsoleSigninWithoutMFA Namespace: CloudTrailMetrics ComparisonOperator: GreaterThanOrEqualToThreshold EvaluationPeriods: 1 Period: 60 Statistic: Sum Threshold: 1 TreatMissingData: notBreaching #================================================== # CIS 3.6 Ensure a log metric filter and alarm exist for AWS Management Console authentication failures #================================================== ConsoleLoginFailureCloudWatchMetric: Type: AWS::Logs::MetricFilter Condition: IsLevel2 DependsOn: - ResourceForEvaluateCisBenchmarkingPreconditions - ResourceForGetCloudTrailCloudWatchLog Properties: LogGroupName: !GetAtt ResourceForGetCloudTrailCloudWatchLog.LogName FilterPattern: "{ ($.eventName = \"ConsoleLogin\") && ($.errorMessage = \"Failed authentication\") }" MetricTransformations: - MetricNamespace: CloudTrailMetrics MetricName: ConsoleLoginFailures MetricValue: 1 ConsoleLoginFailureCloudWatchAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: Console Login Failures AlarmDescription: Console login failures over a five-minute period AlarmActions: - !Ref SnsTopicForCloudWatchEvents MetricName: ConsoleLoginFailures Namespace: CloudTrailMetrics ComparisonOperator: GreaterThanOrEqualToThreshold EvaluationPeriods: 1 Period: 300 Statistic: Sum Threshold: 1 TreatMissingData: notBreaching #================================================== # 3.7 Ensure a log metric filter and alarm exist for disabling or scheduled deletion of customer created CMKs #================================================== KMSCustomerKeyDeletionCloudWatchMetric: Type: AWS::Logs::MetricFilter Condition: IsLevel2 DependsOn: - ResourceForEvaluateCisBenchmarkingPreconditions - ResourceForGetCloudTrailCloudWatchLog Properties: LogGroupName: !GetAtt ResourceForGetCloudTrailCloudWatchLog.LogName FilterPattern: "{ ($.eventSource = kms.amazonaws.com) && (($.eventName=DisableKey)||($.eventName=ScheduleKeyDeletion)) }" MetricTransformations: - MetricNamespace: CloudTrailMetrics MetricName: KMSCustomerKeyDeletion MetricValue: 1 KMSCustomerKeyDeletionCloudWatchAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: KMS Key Disabled or Scheduled for Deletion AlarmDescription: Disabling or scheduled deletion of customer-managed KMS keys AlarmActions: - !Ref SnsTopicForCloudWatchEvents MetricName: KMSCustomerKeyDeletion Namespace: CloudTrailMetrics ComparisonOperator: GreaterThanOrEqualToThreshold EvaluationPeriods: 1 Period: 60 Statistic: Sum Threshold: 1 TreatMissingData: notBreaching #================================================== # CloudWatch Event Rules #================================================== RoleForCloudWatchEvents: Type: AWS::IAM::Role DependsOn: SnsTopicForCloudWatchEvents Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: [lambda.amazonaws.com] Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: AllowSnsPublish PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: sns:Publish Resource: !Ref SnsTopicForCloudWatchEvents FunctionToFormatCloudWatchEvent: Type: AWS::Lambda::Function DependsOn: - RoleForCloudWatchEvents - SnsTopicForCloudWatchEvents Properties: FunctionName: FormatCloudWatchEvent Code: ZipFile: !Sub | #================================================================================================== # Function: process-cloudwatch-event # Purpose: Processes CloudWatch Event before publishing to SNS. #================================================================================================== import boto3 import json SNS_TOPIC_ARN = '${SnsTopicForCloudWatchEvents}' #================================================================================================== # Function handler #================================================================================================== def lambda_handler(event, context): response = boto3.client('sns').publish( TopicArn = SNS_TOPIC_ARN, Message = json.dumps(event, indent=4), Subject = 'NOTIFICATION {0}:{1}'.format(event['detail']['eventSource'], event['detail']['eventName']), MessageStructure = 'raw' ) Description: Formats a given CloudWatch Event to be published to an SNS topic Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt RoleForCloudWatchEvents.Arn Runtime: python3.8 Timeout: 5 LambdaPermissionForCloudTrailCloudWatchEventRules: Type: AWS::Lambda::Permission DependsOn: - FunctionToFormatCloudWatchEvent Properties: FunctionName: !GetAtt FunctionToFormatCloudWatchEvent.Arn Action: lambda:InvokeFunction Principal: events.amazonaws.com #================================================== # CIS 3.8 Ensure a log metric filter and alarm exist for S3 bucket policy changes #================================================== DetectS3BucketPolicyChanges: Type: AWS::Events::Rule Properties: Name: DetectS3BucketPolicyChanges Description: Publishes formatted S3 bucket policy change events to an SNS topic EventPattern: detail-type: - AWS API Call via CloudTrail detail: eventSource: - s3.amazonaws.com eventName: - PutBucketAcl - PutBucketPolicy - PutBucketCors - PutBucketLifecycle - PutBucketReplication - DeleteBucketPolicy - DeleteBucketCors - DeleteBucketLifecycle - DeleteBucketReplication State: ENABLED Targets: - Arn: !GetAtt FunctionToFormatCloudWatchEvent.Arn Id: TargetFunctionV1 #================================================== # CIS 3.9 Ensure a log metric filter and alarm exist for AWS Config configuration changes #================================================== DetectConfigChanges: Type: AWS::Events::Rule Condition: IsLevel2 Properties: Name: DetectConfigChanges Description: Publishes formatted Config change events to an SNS topic EventPattern: detail-type: - AWS API Call via CloudTrail detail: eventSource: - config.amazonaws.com eventName: - PutConfigurationRecorder - StopConfigurationRecorder - DeleteDeliveryChannel - PutDeliveryChannel State: ENABLED Targets: - Arn: !GetAtt FunctionToFormatCloudWatchEvent.Arn Id: TargetFunctionV1 #================================================== # KMS Key Use Detection #================================================== KmsKeyUseCloudWatchEventRule: Type: AWS::Events::Rule Properties: Name: DetectKmsKeyUsage Description: Publishes formatted KMS encryption events to an SNS topic EventPattern: detail-type: - AWS API Call via CloudTrail detail: eventSource: [kms.amazonaws.com] eventName: - Decrypt - Encrypt State: DISABLED Targets: - Arn: !GetAtt FunctionToFormatCloudWatchEvent.Arn Id: TargetFunctionV1 #================================================== # CIS 3.5 Ensure a log metric filter and alarm exist for CloudTrail configuration changes #================================================== CloudTrailCloudWatchEventRule: Type: AWS::Events::Rule Properties: Name: DetectCloudTrailChanges Description: Publishes formatted CloudTrail change events to an SNS topic EventPattern: detail-type: - AWS API Call via CloudTrail detail: eventSource: [cloudtrail.amazonaws.com] eventName: - StopLogging - DeleteTrail - UpdateTrail State: ENABLED Targets: - Arn: !GetAtt FunctionToFormatCloudWatchEvent.Arn Id: TargetFunctionV1 #================================================== # CIS 3.4 Ensure a log metric filter and alarm exist for IAM policy changes #================================================== IamPolicyChangesCloudWatchEventRule: Type: AWS::Events::Rule Properties: Name: DetectIamPolicyChanges Description: Publishes formatted IAM policy change events to an SNS topic EventPattern: detail-type: - AWS API Call via CloudTrail detail: eventSource: - iam.amazonaws.com eventName: - CreateAccessKey - DeleteAccessKey - DeleteRolePolicy - DeleteUserPolicy - PutGroupPolicy - PutRolePolicy - PutUserPolicy - CreatePolicy - DeletePolicy - CreatePolicyVersion - DeletePolicyVersion - AttachRolePolicy - DetachRolePolicy - AttachUserPolicy - DetachUserPolicy - AttachGroupPolicy - DetachGroupPolicy State: ENABLED Targets: - Arn: !GetAtt FunctionToFormatCloudWatchEvent.Arn Id: TargetFunctionV1 #================================================== # Billing Change Detection #================================================== BillingChangeCloudWatchEventRule: Type: AWS::Events::Rule Properties: Name: DetectBillingChangeEvents Description: Publishes formatted billing change events to an SNS topic EventPattern: detail-type: - AWS API Call via CloudTrail detail: eventSource: - aws-portal.amazonaws.com eventName: - ModifyAccount - ModifyBilling - ModifyPaymentMethods State: ENABLED Targets: - Arn: !GetAtt FunctionToFormatCloudWatchEvent.Arn Id: TargetFunctionV1 #================================================== # EC2 Termination Detection #================================================== Ec2TerminationCloudWatchEventRule: Type: AWS::Events::Rule Properties: Name: DetectEc2TerminationEvents Description: Publishes formatted EC2 termination events to an SNS topic EventPattern: detail-type: - AWS API Call via CloudTrail detail: eventSource: - ec2.amazonaws.com eventName: - TerminateInstances State: ENABLED Targets: - Arn: !GetAtt FunctionToFormatCloudWatchEvent.Arn Id: TargetFunctionV1 #================================================== # CIS 3.10 Ensure a log metric filter and alarm exist for security group changes #================================================== SecurityGroupChangesCloudWatchEventRule: Type: AWS::Events::Rule Condition: IsLevel2 Properties: Name: DetectSecurityGroupChanges Description: Publishes formatted security group change events to an SNS topic EventPattern: detail-type: - AWS API Call via CloudTrail detail: eventSource: - ec2.amazonaws.com eventName: - AuthorizeSecurityGroupIngress - AuthorizeSecurityGroupEgress - RevokeSecurityGroupIngress - RevokeSecurityGroupEgress - CreateSecurityGroup - DeleteSecurityGroup State: ENABLED Targets: - Arn: !GetAtt FunctionToFormatCloudWatchEvent.Arn Id: TargetFunctionV1 #================================================== # CIS 3.11 Ensure a log metric filter and alarm exist for changes to Network Access Control Lists (NACL) #================================================== NetworkAclChangesCloudWatchEventRule: Type: AWS::Events::Rule Condition: IsLevel2 Properties: Name: DetectNetworkAclChanges Description: Publishes formatted network ACL change events to an SNS topic EventPattern: detail-type: - AWS API Call via CloudTrail detail: eventSource: - ec2.amazonaws.com eventName: - CreateNetworkAcl - CreateNetworkAclEntry - DeleteNetworkAcl - DeleteNetworkAclEntry - ReplaceNetworkAclEntry - ReplaceNetworkAclAssociation State: ENABLED Targets: - Arn: !GetAtt FunctionToFormatCloudWatchEvent.Arn Id: TargetFunctionV1 #================================================== # CIS 3.12 Ensure a log metric filter and alarm exist for changes to network gateways # CIS 3.13 Ensure a log metric filter and alarm exist for route table changes # CIS 3.14 Ensure a log metric filter and alarm exist for VPC changes # CIS 3.15 Ensure appropriate subscribers to each SNS topic #================================================== NetworkChangeCloudWatchEventRule: Type: AWS::Events::Rule Properties: Name: DetectNetworkChangeEvents Description: Publishes formatted network change events to an SNS topic EventPattern: detail-type: - AWS API Call via CloudTrail detail: eventSource: - ec2.amazonaws.com eventName: - AttachInternetGateway - AssociateRouteTable - CreateCustomerGateway - CreateInternetGateway - CreateRoute - CreateRouteTable - DeleteCustomerGateway - DeleteInternetGateway - DeleteRoute - DeleteRouteTable - DeleteDhcpOptions - DetachInternetGateway - DisassociateRouteTable - ReplaceRoute - ReplaceRouteTableAssociation State: ENABLED Targets: - Arn: !GetAtt FunctionToFormatCloudWatchEvent.Arn Id: TargetFunctionV1 BillingChangesCloudWatchFilter: Type: AWS::Logs::MetricFilter DependsOn: - ResourceForEvaluateCisBenchmarkingPreconditions - ResourceForGetCloudTrailCloudWatchLog Properties: LogGroupName: !GetAtt ResourceForGetCloudTrailCloudWatchLog.LogName FilterPattern: "{ ($.eventName = ModifyAccount) || ($.eventName = ModifyBilling) || ($.eventName = ModifyPaymentMethods) }" MetricTransformations: - MetricNamespace: CloudTrailMetrics MetricName: BillingEventCount MetricValue: 1 BillingChangesCloudWatchAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: Billing Changes AlarmDescription: Alarms when changes are made to billing properties AlarmActions: - !Ref SnsTopicForCloudWatchEvents MetricName: BillingEventCount Namespace: CloudTrailMetrics ComparisonOperator: GreaterThanOrEqualToThreshold EvaluationPeriods: 1 Period: 60 Statistic: Sum Threshold: 1 TreatMissingData: notBreaching #================================================== # CIS 1.3 Ensure credentials unused for 90 days or greater are disabled # CIS 1.4 Ensure access keys are rotated every 90 days or less #================================================== RoleForDisableUnusedCredentialsFunction: Type: AWS::IAM::Role DependsOn: ResourceForEvaluateCisBenchmarkingPreconditions Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: [lambda.amazonaws.com] Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: DisableCredentials PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - iam:DeleteLoginProfile - iam:GetAccessKeyLastUsed - iam:GetLoginProfile - iam:DeleteLoginProfile - iam:ListAccessKeys - iam:ListUsers - iam:UpdateAccessKey Resource: "*" FunctionToDisableUnusedCredentials: Type: AWS::Lambda::Function DependsOn: RoleForDisableUnusedCredentialsFunction Properties: FunctionName: DisableUnusedCredentials Code: ZipFile: | #================================================================================================== # Function: DisableUnusedCredentials # Purpose: Disables unused credentials older than the given period. #================================================================================================== import boto3 import json import datetime from datetime import date DEFAULT_AGE_THRESHOLD_IN_DAYS = 90 #================================================================================================== # Function handler #================================================================================================== def lambda_handler(event, context): return_value = {} return_value['DeletedPasswords'] = [] return_value['DisabledAccessKeys'] = [] client = boto3.client('iam') now = date(datetime.date.today().year, datetime.date.today().month, datetime.date.today().day) # For each user, determine when: # (1) the user last logged in and # (2) when the user's access key were last used. for user in client.list_users()['Users']: # Users who have never logged in or who don't have a password won't have the 'PasswordLastUsed' property. if 'PasswordLastUsed' in user: password_last_used = date(user['PasswordLastUsed'].year, user['PasswordLastUsed'].month, user['PasswordLastUsed'].day) age = (now - password_last_used).days if age > DEFAULT_AGE_THRESHOLD_IN_DAYS: # Danger, Will Robinson! Disable the user's password (delete login profile). print('The user {0} has not logged in to the console in {1} days.'.format(user['UserName'], age)) print('DELETING password for {0}.'.format(user['UserName'])) try: if client.get_login_profile(UserName = user['UserName']): response = client.delete_login_profile(UserName = user['UserName']) return_value['DeletedPasswords'].append({'UserName': user['UserName'], 'PasswordLastUsed': str(user['PasswordLastUsed'])}) except: #No-op print('No login profile exists for {}. It may been already been deleted.'.format(user['UserName'])) # Next, determine when the user's access keys were last used. for access_key in client.list_access_keys(UserName = user['UserName'])['AccessKeyMetadata']: if access_key['Status'] == 'Active': response = client.get_access_key_last_used(AccessKeyId = access_key['AccessKeyId']) if 'LastUsedDate' in response['AccessKeyLastUsed']: access_key_last_used_date = response['AccessKeyLastUsed']['LastUsedDate'] access_key_last_used_date = date(access_key_last_used_date.year, access_key_last_used_date.month, access_key_last_used_date.day) age = (now - access_key_last_used_date).days if age > DEFAULT_AGE_THRESHOLD_IN_DAYS: # Disable the access key. print('The access key {0} has not been used in {1} days.'.format(access_key['AccessKeyId'], age)) print('DISABLING access key {0}.'.format(access_key['AccessKeyId'])) response = client.update_access_key( UserName = user['UserName'], AccessKeyId = access_key['AccessKeyId'], Status = 'Inactive') return_value['DisabledAccessKeys'].append({'AccessKeyId': access_key['AccessKeyId'], 'LastUsedDate': str(access_key_last_used_date)}) return return_value Description: Deletes unused passwords and disables unused access keys Handler: index.lambda_handler MemorySize: 1024 Role: !GetAtt RoleForDisableUnusedCredentialsFunction.Arn Runtime: python3.8 Timeout: 10 LambdaPermissionForDisableUnusedCredentials: Type: AWS::Lambda::Permission DependsOn: - FunctionToDisableUnusedCredentials Properties: FunctionName: !GetAtt FunctionToDisableUnusedCredentials.Arn Action: lambda:InvokeFunction Principal: events.amazonaws.com ScheduledRuleForDisableUnusedCredentials: Type: AWS::Events::Rule Properties: Name: DisableUnusedCredentials Description: Deletes unused passwords and disables unused access keys ScheduleExpression: rate(1 day) State: ENABLED Targets: - Arn: !GetAtt FunctionToDisableUnusedCredentials.Arn Id: TargetFunctionV1