├── .gitignore ├── LICENSE ├── README.md ├── alerting.tf ├── cloudtrail.tf ├── config.tf ├── output.tf ├── password_policy.tf ├── s3.tf ├── variables.tf └── version.tf /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | 11 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 12 | # .tfvars files are managed as part of configuration and so should be included in 13 | # version control. 14 | # 15 | # example.tfvars 16 | 17 | # Ignore override files as they are usually used to override resources locally and so 18 | # are not checked in 19 | override.tf 20 | override.tf.json 21 | *_override.tf 22 | *_override.tf.json 23 | 24 | # Include override files you do wish to add to version control using negated pattern 25 | # 26 | # !example_override.tf 27 | 28 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 29 | # example: *tfplan* 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 in4it 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-aws-cis-controls 2 | AWS CIS Controls module for terraform 3 | 4 | https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html 5 | 6 | ### Controls covered: 7 | - 1.1 Avoid the use of the "root" account 8 | - 1.5 Ensure IAM password policy requires at least one uppercase letter 9 | - 1.6 Ensure IAM password policy requires at least one lowercase letter 10 | - 1.7 Ensure IAM password policy requires at least one symbol 11 | - 1.8 Ensure IAM password policy requires at least one number 12 | - 1.9 Ensure IAM password policy requires a minimum length of 14 or greater 1.9 13 | - 1.10 Ensure IAM password policy prevents password reuse 14 | - 1.11 Ensure IAM password policy expires passwords within 90 days or less 15 | - 2.1 Ensure CloudTrail is enabled in all Regions 16 | - 2.2 Ensure CloudTrail log file validation is enabled 17 | - 2.3 Ensure the S3 bucket CloudTrail logs to is not publicly accessible 18 | - 2.4 Ensure CloudTrail trails are integrated with Amazon CloudWatch Logs 19 | - 2.5 Ensure AWS Config is enabled 20 | - 2.6 Ensure S3 bucket access logging is enabled on the CloudTrail S3 bucket 21 | - 2.7 Ensure CloudTrail logs are encrypted at rest using AWS KMS CMKs 22 | - 3.1 Ensure a log metric filter and alarm exist for unauthorized API calls 23 | - 3.2 Ensure a log metric filter and alarm exist for AWS Management Console sign-in without MFA 24 | - 3.3 Ensure a log metric filter and alarm exist for usage of "root" account 25 | - 3.4 Ensure a log metric filter and alarm exist for IAM policy changes 26 | - 3.5 Ensure a log metric filter and alarm exist for CloudTrail configuration changes 27 | - 3.6 Ensure a log metric filter and alarm exist for AWS Management Console authentication failures 28 | - 3.7 Ensure a log metric filter and alarm exist for disabling or scheduled deletion of customer created CMKs 29 | - 3.8 Ensure a log metric filter and alarm exist for S3 bucket policy changes 30 | - 3.9 Ensure a log metric filter and alarm exist for AWS Config configuration changes 31 | - 3.10 Ensure a log metric filter and alarm exist for security group changes 32 | - 3.11 Ensure a log metric filter and alarm exist for changes to Network Access Control Lists (NACL) 33 | - 3.12 Ensure a log metric filter and alarm exist for changes to network gateways 34 | - 3.13 Ensure a log metric filter and alarm exist for route table changes 35 | - 3.14 Ensure a log metric filter and alarm exist for VPC changes 36 | 37 | 38 | 39 | ## Inputs 40 | 41 | | Name | Description | Type | Default | Required | 42 | |------|-------------|:----:|:-----:|:-----:| 43 | | sns\_arn | CIS notifications SNS arn | string | `"individual"` | yes | 44 | | s3\_enabled | Enable S3 Bucket setup for audit logs and CloudTrail | | `"true"` | no | 45 | | audit\_log\_bucket\_custom\_policy\_json | Override policy for the audit log bucket. | string | `""` | no | 46 | | config\_enabled | Enable AWS config setup | string | `"true"` | no | 47 | | include\_global\_resource\_types | AWS Config include global resource types | string | `"true"` | no | 48 | | cloudtrail\_log\_group\_name | CloudTrail LogGroup name | string | `""` | yes | 49 | | cloudtrail\_event\_selector\_type | CloudTrail event selector | string | `"ALL"` | no | 50 | | aws\_account\_id | AWS account ID | string | `""` | yes | 51 | | region | AWS Region | string | `""` | yes | 52 | | cloudtrail\_kms\_policy | Override policy for the CloudTrail KMS | string | `""` | no | 53 | | alerting\_enabled | Enable CloudWatch alarms | string | `"true"` | no | 54 | | alarm\_namespace | CloudWatch alarm namespace | string |`"CISBenchmark"` | no | 55 | -------------------------------------------------------------------------------- /alerting.tf: -------------------------------------------------------------------------------- 1 | #https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html 2 | 3 | # 1.1 – Avoid the use of the "root" account 4 | resource "aws_cloudwatch_log_metric_filter" "aws_cis_1_1_avoid_the_use_of_root_account" { 5 | count = var.alerting_enabled ? 1 : 0 6 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 7 | name = "RootAccountUsage" 8 | pattern = "{ $.userIdentity.type=\"Root\" && $.userIdentity.invokedBy NOT EXISTS && $.eventType !=\"AwsServiceEvent\" }" 9 | metric_transformation { 10 | name = "RootAccountUsage" 11 | namespace = var.alarm_namespace 12 | value = "1" 13 | } 14 | } 15 | 16 | resource "aws_cloudwatch_metric_alarm" "aws_cis_1_1_avoid_the_use_of_root_account" { 17 | count = var.alerting_enabled ? 1 : 0 18 | alarm_description = "Monitoring for root account logins will provide visibility into the use of a fully privileged account and an opportunity to reduce the use of it." 19 | alarm_name = "CIS-1.1-RootAccountUsage" 20 | comparison_operator = "GreaterThanOrEqualToThreshold" 21 | datapoints_to_alarm = 1 22 | evaluation_periods = 1 23 | insufficient_data_actions = [] 24 | metric_name = aws_cloudwatch_log_metric_filter.aws_cis_1_1_avoid_the_use_of_root_account[0].id 25 | namespace = var.alarm_namespace 26 | ok_actions = [] 27 | period = 300 28 | statistic = "Sum" 29 | alarm_actions = [var.sns_arn] 30 | threshold = 1 31 | treat_missing_data = "notBreaching" 32 | tags = var.tags 33 | } 34 | 35 | # 3.1 – Ensure a log metric filter and alarm exist for unauthorized API calls 36 | resource "aws_cloudwatch_log_metric_filter" "unauthorized_api_calls" { 37 | count = var.alerting_enabled ? 1 : 0 38 | name = "UnauthorizedAPICalls" 39 | pattern = "{ ($.errorCode = \"*UnauthorizedOperation\") || ($.errorCode = \"AccessDenied*\") }" 40 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 41 | metric_transformation { 42 | name = "UnauthorizedAPICalls" 43 | namespace = "CISBenchmark" 44 | value = "1" 45 | } 46 | } 47 | 48 | resource "aws_cloudwatch_metric_alarm" "unauthorized_api_calls" { 49 | count = var.alerting_enabled ? 1 : 0 50 | alarm_name = "CIS-3.1-UnauthorizedAPICalls" 51 | comparison_operator = "GreaterThanOrEqualToThreshold" 52 | evaluation_periods = "1" 53 | metric_name = aws_cloudwatch_log_metric_filter.unauthorized_api_calls[0].id 54 | namespace = var.alarm_namespace 55 | period = "300" 56 | statistic = "Sum" 57 | threshold = "1" 58 | alarm_description = "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity." 59 | alarm_actions = [var.sns_arn] 60 | treat_missing_data = "notBreaching" 61 | insufficient_data_actions = [] 62 | tags = var.tags 63 | } 64 | 65 | # 3.2 – Ensure a log metric filter and alarm exist for AWS Management Console sign-in without MFA 66 | resource "aws_cloudwatch_log_metric_filter" "no_mfa_console_signin" { 67 | count = var.alerting_enabled ? 1 : 0 68 | name = "NoMFAConsoleSignin" 69 | pattern = "{ ($.eventName = \"ConsoleLogin\") && ($.additionalEventData.MFAUsed != \"Yes\") }" 70 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 71 | metric_transformation { 72 | name = "NoMFAConsoleSignin" 73 | namespace = var.alarm_namespace 74 | value = "1" 75 | } 76 | } 77 | 78 | resource "aws_cloudwatch_metric_alarm" "no_mfa_console_signin" { 79 | count = var.alerting_enabled ? 1 : 0 80 | alarm_name = "CIS-3.2-ConsoleSigninWithoutMFA" 81 | comparison_operator = "GreaterThanOrEqualToThreshold" 82 | evaluation_periods = "1" 83 | metric_name = aws_cloudwatch_log_metric_filter.no_mfa_console_signin[0].id 84 | namespace = var.alarm_namespace 85 | period = "300" 86 | statistic = "Sum" 87 | threshold = "1" 88 | alarm_description = "Monitoring for single-factor console logins will increase visibility into accounts that are not protected by MFA." 89 | alarm_actions = [var.sns_arn] 90 | treat_missing_data = "notBreaching" 91 | insufficient_data_actions = [] 92 | tags = var.tags 93 | } 94 | 95 | # 3.3 – Ensure a log metric filter and alarm exist for usage of "root" account 96 | resource "aws_cloudwatch_log_metric_filter" "root_usage" { 97 | count = var.alerting_enabled ? 1 : 0 98 | name = "RootUsage" 99 | pattern = "{ $.userIdentity.type = \"Root\" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != \"AwsServiceEvent\" }" 100 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 101 | metric_transformation { 102 | name = "RootUsage" 103 | namespace = var.alarm_namespace 104 | value = "1" 105 | } 106 | } 107 | 108 | resource "aws_cloudwatch_metric_alarm" "root_usage" { 109 | count = var.alerting_enabled ? 1 : 0 110 | alarm_name = "CIS-3.3-RootAccountUsage" 111 | comparison_operator = "GreaterThanOrEqualToThreshold" 112 | evaluation_periods = "1" 113 | metric_name = aws_cloudwatch_log_metric_filter.root_usage[0].id 114 | namespace = var.alarm_namespace 115 | period = "300" 116 | statistic = "Sum" 117 | threshold = "1" 118 | alarm_description = "Monitoring for root account logins will provide visibility into the use of a fully privileged account and an opportunity to reduce the use of it." 119 | alarm_actions = [var.sns_arn] 120 | treat_missing_data = "notBreaching" 121 | insufficient_data_actions = [] 122 | tags = var.tags 123 | } 124 | 125 | # 3.4 – Ensure a log metric filter and alarm exist for IAM policy changes 126 | resource "aws_cloudwatch_log_metric_filter" "iam_changes" { 127 | count = var.alerting_enabled ? 1 : 0 128 | name = "IAMChanges" 129 | pattern = "{($.eventName=DeleteGroupPolicy)||($.eventName=DeleteRolePolicy)||($.eventName=DeleteUserPolicy)||($.eventName=PutGroupPolicy)||($.eventName=PutRolePolicy)||($.eventName=PutUserPolicy)||($.eventName=CreatePolicy)||($.eventName=DeletePolicy)||($.eventName=CreatePolicyVersion)||($.eventName=DeletePolicyVersion)||($.eventName=AttachRolePolicy)||($.eventName=DetachRolePolicy)||($.eventName=AttachUserPolicy)||($.eventName=DetachUserPolicy)||($.eventName=AttachGroupPolicy)||($.eventName=DetachGroupPolicy)}" 130 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 131 | metric_transformation { 132 | name = "IAMChanges" 133 | namespace = var.alarm_namespace 134 | value = "1" 135 | } 136 | } 137 | 138 | resource "aws_cloudwatch_metric_alarm" "iam_changes" { 139 | count = var.alerting_enabled ? 1 : 0 140 | alarm_name = "CIS-3.4-IAMPolicyChanges" 141 | comparison_operator = "GreaterThanOrEqualToThreshold" 142 | evaluation_periods = "1" 143 | metric_name = aws_cloudwatch_log_metric_filter.iam_changes[0].id 144 | namespace = var.alarm_namespace 145 | period = "300" 146 | statistic = "Sum" 147 | threshold = "1" 148 | alarm_description = "Monitoring changes to IAM policies will help ensure authentication and authorization controls remain intact." 149 | alarm_actions = [var.sns_arn] 150 | treat_missing_data = "notBreaching" 151 | insufficient_data_actions = [] 152 | tags = var.tags 153 | } 154 | 155 | # 3.5 – Ensure a log metric filter and alarm exist for CloudTrail configuration changes 156 | resource "aws_cloudwatch_log_metric_filter" "cloudtrail_cfg_changes" { 157 | count = var.alerting_enabled ? 1 : 0 158 | name = "CloudTrailCfgChanges" 159 | pattern = "{ ($.eventName = CreateTrail) || ($.eventName = UpdateTrail) || ($.eventName = DeleteTrail) || ($.eventName = StartLogging) || ($.eventName = StopLogging) }" 160 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 161 | metric_transformation { 162 | name = "CloudTrailCfgChanges" 163 | namespace = var.alarm_namespace 164 | value = "1" 165 | } 166 | } 167 | 168 | resource "aws_cloudwatch_metric_alarm" "cloudtrail_cfg_changes" { 169 | count = var.alerting_enabled ? 1 : 0 170 | alarm_name = "CIS-3.5-CloudTrailChanges" 171 | comparison_operator = "GreaterThanOrEqualToThreshold" 172 | evaluation_periods = "1" 173 | metric_name = aws_cloudwatch_log_metric_filter.cloudtrail_cfg_changes[0].id 174 | namespace = var.alarm_namespace 175 | period = "300" 176 | statistic = "Sum" 177 | threshold = "1" 178 | alarm_description = "Monitoring changes to CloudTrail's configuration will help ensure sustained visibility to activities performed in the AWS account." 179 | alarm_actions = [var.sns_arn] 180 | treat_missing_data = "notBreaching" 181 | insufficient_data_actions = [] 182 | tags = var.tags 183 | } 184 | 185 | # 3.6 – Ensure a log metric filter and alarm exist for AWS Management Console authentication failures 186 | resource "aws_cloudwatch_log_metric_filter" "console_signin_failures" { 187 | count = var.alerting_enabled ? 1 : 0 188 | name = "ConsoleSigninFailures" 189 | pattern = "{ ($.eventName = ConsoleLogin) && ($.errorMessage = \"Failed authentication\") }" 190 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 191 | metric_transformation { 192 | name = "ConsoleSigninFailures" 193 | namespace = var.alarm_namespace 194 | value = "1" 195 | } 196 | } 197 | 198 | resource "aws_cloudwatch_metric_alarm" "console_signin_failures" { 199 | count = var.alerting_enabled ? 1 : 0 200 | alarm_name = "CIS-3.6-ConsoleAuthenticationFailure" 201 | comparison_operator = "GreaterThanOrEqualToThreshold" 202 | evaluation_periods = "1" 203 | metric_name = aws_cloudwatch_log_metric_filter.console_signin_failures[0].id 204 | namespace = var.alarm_namespace 205 | period = "300" 206 | statistic = "Sum" 207 | threshold = "1" 208 | alarm_description = "Monitoring failed console logins may decrease lead time to detect an attempt to brute force a credential, which may provide an indicator, such as source IP, that can be used in other event correlation." 209 | alarm_actions = [var.sns_arn] 210 | treat_missing_data = "notBreaching" 211 | insufficient_data_actions = [] 212 | tags = var.tags 213 | } 214 | 215 | # 3.7 – Ensure a log metric filter and alarm exist for disabling or scheduled deletion of customer created CMKs 216 | resource "aws_cloudwatch_log_metric_filter" "disable_or_delete_cmk" { 217 | count = var.alerting_enabled ? 1 : 0 218 | name = "DisableOrDeleteCMK" 219 | pattern = "{ ($.eventSource = kms.amazonaws.com) && (($.eventName = DisableKey) || ($.eventName = ScheduleKeyDeletion)) }" 220 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 221 | metric_transformation { 222 | name = "DisableOrDeleteCMK" 223 | namespace = var.alarm_namespace 224 | value = "1" 225 | } 226 | } 227 | 228 | resource "aws_cloudwatch_metric_alarm" "disable_or_delete_cmk" { 229 | count = var.alerting_enabled ? 1 : 0 230 | alarm_name = "CIS-3.7-DisableOrDeleteCMK" 231 | comparison_operator = "GreaterThanOrEqualToThreshold" 232 | evaluation_periods = "1" 233 | metric_name = aws_cloudwatch_log_metric_filter.disable_or_delete_cmk[0].id 234 | namespace = var.alarm_namespace 235 | period = "300" 236 | statistic = "Sum" 237 | threshold = "1" 238 | alarm_description = "Monitoring failed console logins may decrease lead time to detect an attempt to brute force a credential, which may provide an indicator, such as source IP, that can be used in other event correlation." 239 | alarm_actions = [var.sns_arn] 240 | treat_missing_data = "notBreaching" 241 | insufficient_data_actions = [] 242 | tags = var.tags 243 | } 244 | 245 | # 3.8 – Ensure a log metric filter and alarm exist for S3 bucket policy changes 246 | resource "aws_cloudwatch_log_metric_filter" "s3_bucket_policy_changes" { 247 | count = var.alerting_enabled ? 1 : 0 248 | name = "S3BucketPolicyChanges" 249 | pattern = "{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = PutBucketCors) || ($.eventName = PutBucketLifecycle) || ($.eventName = PutBucketReplication) || ($.eventName = DeleteBucketPolicy) || ($.eventName = DeleteBucketCors) || ($.eventName = DeleteBucketLifecycle) || ($.eventName = DeleteBucketReplication)) }" 250 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 251 | metric_transformation { 252 | name = "S3BucketPolicyChanges" 253 | namespace = var.alarm_namespace 254 | value = "1" 255 | } 256 | } 257 | 258 | resource "aws_cloudwatch_metric_alarm" "s3_bucket_policy_changes" { 259 | count = var.alerting_enabled ? 1 : 0 260 | alarm_name = "CIS-3.8-S3BucketPolicyChanges" 261 | comparison_operator = "GreaterThanOrEqualToThreshold" 262 | evaluation_periods = "1" 263 | metric_name = aws_cloudwatch_log_metric_filter.s3_bucket_policy_changes[0].id 264 | namespace = var.alarm_namespace 265 | period = "300" 266 | statistic = "Sum" 267 | threshold = "1" 268 | alarm_description = "Monitoring changes to S3 bucket policies may reduce time to detect and correct permissive policies on sensitive S3 buckets." 269 | alarm_actions = [var.sns_arn] 270 | treat_missing_data = "notBreaching" 271 | insufficient_data_actions = [] 272 | tags = var.tags 273 | } 274 | 275 | # 3.9 – Ensure a log metric filter and alarm exist for AWS Config configuration changes 276 | resource "aws_cloudwatch_log_metric_filter" "aws_config_changes" { 277 | count = var.alerting_enabled ? 1 : 0 278 | 279 | name = "AWSConfigChanges" 280 | pattern = "{ ($.eventSource = config.amazonaws.com) && (($.eventName=StopConfigurationRecorder)||($.eventName=DeleteDeliveryChannel)||($.eventName=PutDeliveryChannel)||($.eventName=PutConfigurationRecorder)) }" 281 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 282 | 283 | metric_transformation { 284 | name = "AWSConfigChanges" 285 | namespace = var.alarm_namespace 286 | value = "1" 287 | } 288 | } 289 | 290 | resource "aws_cloudwatch_metric_alarm" "aws_config_changes" { 291 | count = var.alerting_enabled ? 1 : 0 292 | 293 | alarm_name = "CIS-3.9-AWSConfigChanges" 294 | comparison_operator = "GreaterThanOrEqualToThreshold" 295 | evaluation_periods = "1" 296 | metric_name = aws_cloudwatch_log_metric_filter.aws_config_changes[0].id 297 | namespace = var.alarm_namespace 298 | period = "300" 299 | statistic = "Sum" 300 | threshold = "1" 301 | alarm_description = "Monitoring changes to AWS Config configuration will help ensure sustained visibility of configuration items within the AWS account." 302 | alarm_actions = [var.sns_arn] 303 | treat_missing_data = "notBreaching" 304 | insufficient_data_actions = [] 305 | 306 | tags = var.tags 307 | } 308 | 309 | # 3.10 – Ensure a log metric filter and alarm exist for security group changes 310 | resource "aws_cloudwatch_log_metric_filter" "security_group_changes" { 311 | count = var.alerting_enabled ? 1 : 0 312 | name = "SecurityGroupChanges" 313 | pattern = "{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup)}" 314 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 315 | metric_transformation { 316 | name = "SecurityGroupChanges" 317 | namespace = var.alarm_namespace 318 | value = "1" 319 | } 320 | } 321 | 322 | resource "aws_cloudwatch_metric_alarm" "security_group_changes" { 323 | count = var.alerting_enabled ? 1 : 0 324 | alarm_name = "CIS-3.10-SecurityGroupChanges" 325 | comparison_operator = "GreaterThanOrEqualToThreshold" 326 | evaluation_periods = "1" 327 | metric_name = aws_cloudwatch_log_metric_filter.security_group_changes[0].id 328 | namespace = var.alarm_namespace 329 | period = "300" 330 | statistic = "Sum" 331 | threshold = "1" 332 | alarm_description = "Monitoring changes to security group will help ensure that resources and services are not unintentionally exposed." 333 | alarm_actions = [var.sns_arn] 334 | treat_missing_data = "notBreaching" 335 | insufficient_data_actions = [] 336 | tags = var.tags 337 | } 338 | 339 | # 3.11 – Ensure a log metric filter and alarm exist for changes to Network Access Control Lists (NACL) 340 | resource "aws_cloudwatch_log_metric_filter" "nacl_changes" { 341 | count = var.alerting_enabled ? 1 : 0 342 | name = "NACLChanges" 343 | pattern = "{ ($.eventName = CreateNetworkAcl) || ($.eventName = CreateNetworkAclEntry) || ($.eventName = DeleteNetworkAcl) || ($.eventName = DeleteNetworkAclEntry) || ($.eventName = ReplaceNetworkAclEntry) || ($.eventName = ReplaceNetworkAclAssociation) }" 344 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 345 | metric_transformation { 346 | name = "NACLChanges" 347 | namespace = var.alarm_namespace 348 | value = "1" 349 | } 350 | } 351 | 352 | resource "aws_cloudwatch_metric_alarm" "nacl_changes" { 353 | count = var.alerting_enabled ? 1 : 0 354 | alarm_name = "CIS-3.11-NetworkACLChanges" 355 | comparison_operator = "GreaterThanOrEqualToThreshold" 356 | evaluation_periods = "1" 357 | metric_name = aws_cloudwatch_log_metric_filter.nacl_changes[0].id 358 | namespace = var.alarm_namespace 359 | period = "300" 360 | statistic = "Sum" 361 | threshold = "1" 362 | alarm_description = "Monitoring changes to NACLs will help ensure that AWS resources and services are not unintentionally exposed." 363 | alarm_actions = [var.sns_arn] 364 | treat_missing_data = "notBreaching" 365 | insufficient_data_actions = [] 366 | tags = var.tags 367 | } 368 | 369 | resource "aws_cloudwatch_log_metric_filter" "network_gw_changes" { 370 | count = var.alerting_enabled ? 1 : 0 371 | 372 | name = "NetworkGWChanges" 373 | pattern = "{($.eventName=CreateRoute) || ($.eventName=CreateRouteTable) || ($.eventName=ReplaceRoute) || ($.eventName=ReplaceRouteTableAssociation) || ($.eventName=DeleteRouteTable) || ($.eventName=DeleteRoute) || ($.eventName=DisassociateRouteTable)}" 374 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 375 | 376 | metric_transformation { 377 | name = "NetworkGWChanges" 378 | namespace = var.alarm_namespace 379 | value = "1" 380 | } 381 | } 382 | 383 | # 3.12 – Ensure a log metric filter and alarm exist for changes to network gateways 384 | resource "aws_cloudwatch_metric_alarm" "network_gw_changes" { 385 | count = var.alerting_enabled ? 1 : 0 386 | alarm_name = "CIS-3.12-NetworkGatewayChanges" 387 | comparison_operator = "GreaterThanOrEqualToThreshold" 388 | evaluation_periods = "1" 389 | metric_name = aws_cloudwatch_log_metric_filter.network_gw_changes[0].id 390 | namespace = var.alarm_namespace 391 | period = "300" 392 | statistic = "Sum" 393 | threshold = "1" 394 | alarm_description = "Monitoring changes to network gateways will help ensure that all ingress/egress traffic traverses the VPC border via a controlled path." 395 | alarm_actions = [var.sns_arn] 396 | treat_missing_data = "notBreaching" 397 | insufficient_data_actions = [] 398 | tags = var.tags 399 | } 400 | 401 | resource "aws_cloudwatch_log_metric_filter" "route_table_changes" { 402 | count = var.alerting_enabled ? 1 : 0 403 | name = "RouteTableChanges" 404 | pattern = "{ ($.eventName = CreateRoute) || ($.eventName = CreateRouteTable) || ($.eventName = ReplaceRoute) || ($.eventName = ReplaceRouteTableAssociation) || ($.eventName = DeleteRouteTable) || ($.eventName = DeleteRoute) || ($.eventName = DisassociateRouteTable) }" 405 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 406 | metric_transformation { 407 | name = "RouteTableChanges" 408 | namespace = var.alarm_namespace 409 | value = "1" 410 | } 411 | } 412 | 413 | # 3.13 – Ensure a log metric filter and alarm exist for route table changes 414 | resource "aws_cloudwatch_metric_alarm" "route_table_changes" { 415 | count = var.alerting_enabled ? 1 : 0 416 | alarm_name = "CIS-3.13-RouteTableChanges" 417 | comparison_operator = "GreaterThanOrEqualToThreshold" 418 | evaluation_periods = "1" 419 | metric_name = aws_cloudwatch_log_metric_filter.route_table_changes[0].id 420 | namespace = var.alarm_namespace 421 | period = "300" 422 | statistic = "Sum" 423 | threshold = "1" 424 | alarm_description = "Monitoring changes to route tables will help ensure that all VPC traffic flows through an expected path." 425 | alarm_actions = [var.sns_arn] 426 | treat_missing_data = "notBreaching" 427 | insufficient_data_actions = [] 428 | tags = var.tags 429 | } 430 | 431 | # 3.14 – Ensure a log metric filter and alarm exist for VPC changes 432 | resource "aws_cloudwatch_log_metric_filter" "vpc_changes" { 433 | count = var.alerting_enabled ? 1 : 0 434 | name = "VPCChanges" 435 | pattern = "{($.eventName=CreateVpc) || ($.eventName=DeleteVpc) || ($.eventName=ModifyVpcAttribute) || ($.eventName=AcceptVpcPeeringConnection) || ($.eventName=CreateVpcPeeringConnection) || ($.eventName=DeleteVpcPeeringConnection) || ($.eventName=RejectVpcPeeringConnection) || ($.eventName=AttachClassicLinkVpc) || ($.eventName=DetachClassicLinkVpc) || ($.eventName=DisableVpcClassicLink) || ($.eventName=EnableVpcClassicLink)}" 436 | log_group_name = aws_cloudwatch_log_group.cloudtrail_events[0].id 437 | metric_transformation { 438 | name = "VPCChanges" 439 | namespace = var.alarm_namespace 440 | value = "1" 441 | } 442 | } 443 | 444 | resource "aws_cloudwatch_metric_alarm" "vpc_changes" { 445 | count = var.alerting_enabled ? 1 : 0 446 | alarm_name = "CIS-3.14-VPCChanges" 447 | comparison_operator = "GreaterThanOrEqualToThreshold" 448 | evaluation_periods = "1" 449 | metric_name = aws_cloudwatch_log_metric_filter.vpc_changes[0].id 450 | namespace = var.alarm_namespace 451 | period = "300" 452 | statistic = "Sum" 453 | threshold = "1" 454 | alarm_description = "Monitoring changes to VPC will help ensure that all VPC traffic flows through an expected path." 455 | alarm_actions = [var.sns_arn] 456 | treat_missing_data = "notBreaching" 457 | insufficient_data_actions = [] 458 | tags = var.tags 459 | } 460 | -------------------------------------------------------------------------------- /cloudtrail.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_log_group" "cloudtrail_events" { 2 | count = var.cw_log_enabled ? 1 : 0 3 | name = var.cloudtrail_log_group_name 4 | kms_key_id = var.cloudwatch_logs_kms 5 | tags = var.tags 6 | } 7 | 8 | data "aws_iam_policy_document" "cloudtrail_key_policy" { 9 | policy_id = "Key policy created by CloudTrail" 10 | override_json = var.cloudtrail_kms_policy 11 | 12 | statement { 13 | sid = "Enable IAM User Permissions" 14 | 15 | principals { 16 | type = "AWS" 17 | identifiers = [ 18 | "arn:aws:iam::${var.aws_account_id}:root" 19 | ] 20 | } 21 | actions = ["kms:*"] 22 | resources = ["*"] 23 | } 24 | 25 | statement { 26 | sid = "Allow CloudTrail to encrypt logs" 27 | principals { 28 | type = "Service" 29 | identifiers = ["cloudtrail.amazonaws.com"] 30 | } 31 | actions = ["kms:GenerateDataKey*"] 32 | resources = ["*"] 33 | condition { 34 | test = "StringLike" 35 | variable = "kms:EncryptionContext:aws:cloudtrail:arn" 36 | values = ["arn:aws:cloudtrail:*:${var.aws_account_id}:trail/*"] 37 | } 38 | } 39 | 40 | statement { 41 | sid = "Allow CloudTrail to describe key" 42 | principals { 43 | type = "Service" 44 | identifiers = ["cloudtrail.amazonaws.com"] 45 | } 46 | actions = ["kms:DescribeKey"] 47 | resources = ["*"] 48 | } 49 | 50 | statement { 51 | sid = "Allow principals in the account to decrypt log files" 52 | principals { 53 | type = "AWS" 54 | identifiers = ["*"] 55 | } 56 | actions = [ 57 | "kms:Decrypt", 58 | "kms:ReEncryptFrom" 59 | ] 60 | resources = ["*"] 61 | condition { 62 | test = "StringEquals" 63 | variable = "kms:CallerAccount" 64 | values = ["${var.aws_account_id}"] 65 | } 66 | condition { 67 | test = "StringLike" 68 | variable = "kms:EncryptionContext:aws:cloudtrail:arn" 69 | values = ["arn:aws:cloudtrail:*:${var.aws_account_id}:trail/*"] 70 | } 71 | } 72 | 73 | statement { 74 | sid = "Allow alias creation during setup" 75 | principals { 76 | type = "AWS" 77 | identifiers = ["*"] 78 | } 79 | actions = ["kms:CreateAlias"] 80 | resources = ["*"] 81 | condition { 82 | test = "StringEquals" 83 | variable = "kms:ViaService" 84 | values = ["ec2.${var.region}.amazonaws.com"] 85 | } 86 | condition { 87 | test = "StringEquals" 88 | variable = "kms:CallerAccount" 89 | values = ["${var.aws_account_id}"] 90 | } 91 | } 92 | 93 | statement { 94 | sid = "Enable cross account log decryption" 95 | principals { 96 | type = "AWS" 97 | identifiers = ["*"] 98 | } 99 | actions = [ 100 | "kms:Decrypt", 101 | "kms:ReEncryptFrom" 102 | ] 103 | resources = ["*"] 104 | condition { 105 | test = "StringEquals" 106 | variable = "kms:CallerAccount" 107 | values = ["${var.aws_account_id}"] 108 | } 109 | condition { 110 | test = "StringLike" 111 | variable = "kms:EncryptionContext:aws:cloudtrail:arn" 112 | values = ["arn:aws:cloudtrail:*:${var.aws_account_id}:trail/*"] 113 | } 114 | } 115 | } 116 | 117 | resource "aws_kms_key" "cloudtrail" { 118 | description = "Encrypt/Decrypt cloudtrail logs" 119 | deletion_window_in_days = 30 120 | is_enabled = true 121 | enable_key_rotation = true 122 | policy = data.aws_iam_policy_document.cloudtrail_key_policy.json 123 | tags = var.tags 124 | } 125 | 126 | resource "aws_kms_alias" "cloudtrail" { 127 | name = "alias/${var.resource_name_prefix}-cloudtrail" 128 | target_key_id = aws_kms_key.cloudtrail.key_id 129 | } 130 | 131 | data "aws_iam_policy_document" "cloudwatch_delivery_assume_policy" { 132 | statement { 133 | principals { 134 | type = "Service" 135 | identifiers = ["cloudtrail.amazonaws.com"] 136 | } 137 | actions = ["sts:AssumeRole"] 138 | } 139 | } 140 | 141 | resource "aws_iam_role" "cloudwatch_delivery" { 142 | name = "${var.resource_name_prefix}-cloudtrail-cloudwatch-logs" 143 | assume_role_policy = data.aws_iam_policy_document.cloudwatch_delivery_assume_policy.json 144 | 145 | tags = var.tags 146 | } 147 | 148 | resource "aws_iam_role_policy" "cloudwatch_delivery_policy" { 149 | count = var.cw_log_enabled ? 1 : 0 150 | name = "${var.resource_name_prefix}-cloudtrail-cloudwatch-logs" 151 | role = aws_iam_role.cloudwatch_delivery.id 152 | policy = data.aws_iam_policy_document.cloudwatch_delivery_policy[0].json 153 | } 154 | 155 | data "aws_iam_policy_document" "cloudwatch_delivery_policy" { 156 | count = var.cw_log_enabled ? 1 : 0 157 | statement { 158 | sid = "AWSCloudTrailCreateLogStream20141101" 159 | effect = "Allow" 160 | actions = ["logs:CreateLogStream"] 161 | resources = ["arn:aws:logs:${var.region}:${var.aws_account_id}:log-group:${aws_cloudwatch_log_group.cloudtrail_events[0].name}:log-stream:*"] 162 | } 163 | statement { 164 | sid = "AWSCloudTrailPutLogEvents20141101" 165 | actions = ["logs:PutLogEvents"] 166 | resources = ["arn:aws:logs:${var.region}:${var.aws_account_id}:log-group:${aws_cloudwatch_log_group.cloudtrail_events[0].name}:log-stream:*"] 167 | } 168 | } 169 | 170 | # 2.1 – Ensure CloudTrail is enabled in all Regions 171 | # 2.2. – Ensure CloudTrail log file validation is enabled 172 | # 2.4 – Ensure CloudTrail trails are integrated with Amazon CloudWatch Logs 173 | # 2.7 – Ensure CloudTrail logs are encrypted at rest using AWS KMS CMKs 174 | 175 | resource "aws_cloudtrail" "cloudtrail" { 176 | cloud_watch_logs_group_arn = var.cw_log_enabled ? "${aws_cloudwatch_log_group.cloudtrail_events[0].arn}:*" : "" 177 | cloud_watch_logs_role_arn = var.cw_log_enabled ? aws_iam_role.cloudwatch_delivery.arn : "" 178 | name = "${var.resource_name_prefix}-trail" 179 | s3_key_prefix = "cloudtrail" 180 | s3_bucket_name = aws_s3_bucket.audit[0].id 181 | is_multi_region_trail = true 182 | include_global_service_events = true 183 | enable_log_file_validation = true 184 | kms_key_id = aws_kms_key.cloudtrail.arn 185 | 186 | event_selector { 187 | read_write_type = var.clodtrail_event_selector_type 188 | include_management_events = true 189 | 190 | data_resource { 191 | type = "AWS::S3::Object" 192 | values = ["arn:aws:s3"] 193 | } 194 | 195 | data_resource { 196 | type = "AWS::Lambda::Function" 197 | values = ["arn:aws:lambda"] 198 | } 199 | } 200 | depends_on = [ 201 | aws_s3_bucket_policy.audit_log[0], 202 | aws_s3_bucket_public_access_block.audit[0] 203 | ] 204 | tags = var.tags 205 | } 206 | -------------------------------------------------------------------------------- /config.tf: -------------------------------------------------------------------------------- 1 | # 2.5 – Ensure AWS Config is enabled 2 | 3 | data "aws_iam_policy_document" "recorder_assume_role_policy" { 4 | statement { 5 | principals { 6 | type = "Service" 7 | identifiers = ["config.amazonaws.com"] 8 | } 9 | actions = ["sts:AssumeRole"] 10 | } 11 | } 12 | 13 | resource "aws_iam_role" "recorder" { 14 | name = "${var.resource_name_prefix}-config-role" 15 | assume_role_policy = data.aws_iam_policy_document.recorder_assume_role_policy.json 16 | tags = var.tags 17 | } 18 | 19 | #https://docs.aws.amazon.com/config/latest/developerguide/iamrole-permissions.html 20 | data "aws_iam_policy_document" "recorder_publish_policy" { 21 | depends_on = [aws_s3_bucket.audit[0]] 22 | statement { 23 | actions = ["s3:PutObject"] 24 | resources = ["${aws_s3_bucket.audit[0].arn}/config/AWSLogs/${var.aws_account_id}/*"] 25 | 26 | condition { 27 | test = "StringLike" 28 | variable = "s3:x-amz-acl" 29 | values = ["bucket-owner-full-control"] 30 | } 31 | } 32 | 33 | statement { 34 | actions = ["s3:GetBucketAcl"] 35 | resources = [aws_s3_bucket.audit[0].arn] 36 | } 37 | 38 | statement { 39 | actions = ["sns:Publish"] 40 | 41 | resources = [var.sns_arn] 42 | } 43 | } 44 | 45 | resource "aws_iam_role_policy" "recorder_publish_policy" { 46 | name = "${var.resource_name_prefix}-config-policy" 47 | role = aws_iam_role.recorder.id 48 | policy = data.aws_iam_policy_document.recorder_publish_policy.json 49 | } 50 | 51 | resource "aws_iam_role_policy_attachment" "recorder_read_policy" { 52 | role = aws_iam_role.recorder.id 53 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWS_ConfigRole" 54 | } 55 | 56 | resource "aws_config_configuration_recorder" "recorder" { 57 | count = var.config_enabled ? 1 : 0 58 | 59 | name = var.resource_name_prefix 60 | 61 | role_arn = aws_iam_role.recorder.arn 62 | 63 | recording_group { 64 | all_supported = true 65 | include_global_resource_types = var.include_global_resource_types 66 | } 67 | } 68 | 69 | resource "aws_config_delivery_channel" "bucket" { 70 | count = var.config_enabled ? 1 : 0 71 | 72 | name = var.resource_name_prefix 73 | 74 | s3_bucket_name = aws_s3_bucket.audit[0].id 75 | s3_key_prefix = "config" 76 | sns_topic_arn = var.sns_arn 77 | 78 | snapshot_delivery_properties { 79 | delivery_frequency = "One_Hour" 80 | } 81 | 82 | depends_on = [ 83 | aws_config_configuration_recorder.recorder[0], 84 | aws_s3_bucket_policy.audit_log[0], 85 | aws_s3_bucket_public_access_block.audit[0] 86 | ] 87 | } 88 | 89 | resource "aws_config_configuration_recorder_status" "recorder" { 90 | count = var.config_enabled ? 1 : 0 91 | 92 | name = aws_config_configuration_recorder.recorder[0].id 93 | 94 | is_enabled = true 95 | depends_on = [aws_config_delivery_channel.bucket[0]] 96 | } 97 | -------------------------------------------------------------------------------- /output.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in4it/terraform-aws-cis-controls/5b66ac48677d9b4a5be0f4fe3af7f78480a530ab/output.tf -------------------------------------------------------------------------------- /password_policy.tf: -------------------------------------------------------------------------------- 1 | #https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html 2 | 3 | resource "aws_iam_account_password_policy" "cis" { 4 | #1.5 – Ensure IAM password policy requires at least one uppercase letter 5 | require_uppercase_characters = var.iam_require_uppercase_characters 6 | 7 | #1.6 – Ensure IAM password policy requires at least one lowercase letter 8 | require_lowercase_characters = var.iam_require_lowercase_characters 9 | 10 | # 1.7 – Ensure IAM password policy requires at least one symbol 11 | require_symbols = var.iam_require_symbols 12 | 13 | # 1.8 – Ensure IAM password policy requires at least one number 14 | require_numbers = var.iam_require_numbers 15 | 16 | # 1.9 – Ensure IAM password policy requires a minimum length of 14 or greater 1.9 17 | minimum_password_length = var.iam_minimum_password_length 18 | 19 | # 1.10 – Ensure IAM password policy prevents password reuse 20 | password_reuse_prevention = var.iam_password_reuse_prevention 21 | 22 | # 1.11 – Ensure IAM password policy expires passwords within 90 days or less 23 | max_password_age = var.iam_max_password_age 24 | 25 | allow_users_to_change_password = var.iam_allow_users_to_change_password 26 | 27 | hard_expiry = var.iam_hard_expiry 28 | } 29 | -------------------------------------------------------------------------------- /s3.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "audit_log" { 2 | count = var.s3_enabled ? 1 : 0 3 | 4 | override_json = var.audit_log_bucket_custom_policy_json 5 | 6 | statement { 7 | sid = "AWSCloudTrailAclCheckForConfig" 8 | actions = ["s3:GetBucketAcl"] 9 | principals { 10 | type = "Service" 11 | identifiers = ["config.amazonaws.com"] 12 | } 13 | resources = [aws_s3_bucket.audit[0].arn] 14 | } 15 | 16 | statement { 17 | sid = "AWSCloudTrailWriteForConfig" 18 | actions = ["s3:PutObject"] 19 | principals { 20 | type = "Service" 21 | identifiers = ["config.amazonaws.com"] 22 | } 23 | resources = [ 24 | "${aws_s3_bucket.audit[0].arn}/config/AWSLogs/${var.aws_account_id}/Config/*" 25 | ] 26 | condition { 27 | test = "StringEquals" 28 | variable = "s3:x-amz-acl" 29 | values = ["bucket-owner-full-control"] 30 | } 31 | } 32 | 33 | statement { 34 | sid = "AWSCloudTrailAclCheckForCloudTrail" 35 | actions = ["s3:GetBucketAcl"] 36 | principals { 37 | type = "Service" 38 | identifiers = ["cloudtrail.amazonaws.com"] 39 | } 40 | resources = [aws_s3_bucket.audit[0].arn] 41 | } 42 | statement { 43 | sid = "AWSCloudTrailWriteForCloudTrail" 44 | actions = ["s3:PutObject"] 45 | principals { 46 | type = "Service" 47 | identifiers = ["cloudtrail.amazonaws.com"] 48 | } 49 | resources = [ 50 | "${aws_s3_bucket.audit[0].arn}/cloudtrail/AWSLogs/${var.aws_account_id}/*" 51 | ] 52 | condition { 53 | test = "StringEquals" 54 | variable = "s3:x-amz-acl" 55 | values = ["bucket-owner-full-control"] 56 | } 57 | } 58 | statement { 59 | sid = "AllowSSLRequestsOnly" 60 | principals { 61 | type = "*" 62 | identifiers = ["*"] 63 | } 64 | effect = "Deny" 65 | actions = ["s3:*"] 66 | resources = [ 67 | "${aws_s3_bucket.audit[0].arn}/*", 68 | "${aws_s3_bucket.audit[0].arn}" 69 | ] 70 | condition { 71 | test = "Bool" 72 | variable = "aws:SecureTransport" 73 | values = ["false"] 74 | } 75 | } 76 | statement { 77 | sid = "S3DenyDeletePolicy" 78 | principals { 79 | type = "AWS" 80 | identifiers = ["*"] 81 | } 82 | effect = "Deny" 83 | actions = ["s3:DeleteBucket"] 84 | resources = [aws_s3_bucket.audit[0].arn] 85 | } 86 | } 87 | 88 | resource "aws_s3_bucket_policy" "audit_log" { 89 | depends_on = [aws_s3_bucket_public_access_block.audit] 90 | count = var.s3_enabled ? 1 : 0 91 | bucket = aws_s3_bucket.audit[0].id 92 | policy = data.aws_iam_policy_document.audit_log[0].json 93 | } 94 | 95 | resource "aws_s3_bucket" "access_log" { 96 | count = var.s3_enabled ? 1 : 0 97 | bucket = "${var.resource_name_prefix}-access-logs" 98 | force_destroy = true 99 | tags = var.tags 100 | } 101 | resource "aws_s3_bucket_acl" "access_log" { 102 | count = var.s3_enabled ? 1 : 0 103 | bucket = aws_s3_bucket.access_log[0].id 104 | acl = "log-delivery-write" 105 | } 106 | resource "aws_s3_bucket_server_side_encryption_configuration" "access_log" { 107 | count = var.s3_enabled ? 1 : 0 108 | bucket = aws_s3_bucket.access_log[0].bucket 109 | rule { 110 | apply_server_side_encryption_by_default { 111 | sse_algorithm = "AES256" 112 | } 113 | } 114 | } 115 | 116 | # 2.3 – Ensure the S3 bucket CloudTrail logs to is not publicly accessible 117 | # 2.6 – Ensure S3 bucket access logging is enabled on the CloudTrail S3 bucket 118 | resource "aws_s3_bucket_public_access_block" "access_log" { 119 | count = var.s3_enabled ? 1 : 0 120 | 121 | bucket = aws_s3_bucket.access_log[0].id 122 | 123 | block_public_acls = true 124 | block_public_policy = true 125 | ignore_public_acls = true 126 | restrict_public_buckets = true 127 | } 128 | 129 | resource "aws_s3_bucket" "audit" { 130 | count = var.s3_enabled ? 1 : 0 131 | bucket = "${var.resource_name_prefix}-audit-logs" 132 | force_destroy = true 133 | tags = var.tags 134 | } 135 | 136 | resource "aws_s3_bucket_acl" "audit" { 137 | count = var.s3_enabled ? 1 : 0 138 | bucket = aws_s3_bucket.audit[0].id 139 | acl = "private" 140 | } 141 | 142 | resource "aws_s3_bucket_server_side_encryption_configuration" "audit" { 143 | count = var.s3_enabled ? 1 : 0 144 | bucket = aws_s3_bucket.audit[0].bucket 145 | rule { 146 | apply_server_side_encryption_by_default { 147 | sse_algorithm = "AES256" 148 | } 149 | } 150 | } 151 | resource "aws_s3_bucket_logging" "audit" { 152 | bucket = aws_s3_bucket.audit[0].id 153 | 154 | target_bucket = aws_s3_bucket.access_log[0].id 155 | target_prefix = "log/" 156 | } 157 | 158 | resource "aws_s3_bucket_versioning" "audit" { 159 | bucket = aws_s3_bucket.audit[0].id 160 | versioning_configuration { 161 | status = "Enabled" 162 | } 163 | } 164 | 165 | resource "aws_s3_bucket_public_access_block" "audit" { 166 | count = var.s3_enabled ? 1 : 0 167 | 168 | bucket = aws_s3_bucket.audit[0].id 169 | 170 | block_public_acls = true 171 | block_public_policy = true 172 | ignore_public_acls = true 173 | restrict_public_buckets = true 174 | } 175 | 176 | resource "aws_s3_bucket_policy" "access_log" { 177 | count = var.s3_enabled ? 1 : 0 178 | bucket = aws_s3_bucket.access_log[0].id 179 | policy = jsonencode({ 180 | "Version": "2012-10-17", 181 | "Statement": [ 182 | { 183 | "Sid": "AllowSSLRequestsOnly", 184 | "Effect": "Deny", 185 | "Principal": { 186 | "AWS": "*" 187 | }, 188 | "Action": "s3:*", 189 | "Resource": [ 190 | "${aws_s3_bucket.access_log[0].arn}/*", 191 | "${aws_s3_bucket.access_log[0].arn}" 192 | ], 193 | "Condition": { 194 | "Bool": { 195 | "aws:SecureTransport": "false" 196 | } 197 | } 198 | }, 199 | { 200 | "Sid": "S3DenyDeletePolicy", 201 | "Effect": "Deny", 202 | "Principal": { 203 | "AWS": "*" 204 | }, 205 | "Action": "s3:DeleteBucket", 206 | "Resource": [aws_s3_bucket.access_log[0].arn] 207 | } 208 | ] 209 | }) 210 | } -------------------------------------------------------------------------------- /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 | # SNS 7 | variable sns_arn { 8 | description = "SNS for CIS notifications" 9 | } 10 | 11 | # S3 12 | variable s3_enabled { 13 | default = true 14 | } 15 | 16 | variable audit_log_bucket_custom_policy_json { 17 | default = "" 18 | } 19 | 20 | # AWS Config 21 | variable config_enabled { 22 | default = true 23 | } 24 | 25 | variable include_global_resource_types { 26 | default = true 27 | } 28 | 29 | # CloudTrail 30 | 31 | variable cw_log_enabled { 32 | default = true 33 | } 34 | 35 | variable "cloudwatch_logs_kms" { 36 | description = "kms key for CW logs encryption" 37 | default = "" 38 | } 39 | 40 | variable cloudtrail_log_group_name { 41 | description = "CloudTrail LogGroup name" 42 | } 43 | 44 | variable "clodtrail_event_selector_type" { 45 | description = "Log type for event selectors" 46 | default = "All" 47 | } 48 | 49 | variable aws_account_id { 50 | description = "AWS Account ID" 51 | } 52 | 53 | variable region { 54 | description = "AWS region" 55 | } 56 | 57 | variable cloudtrail_kms_policy { 58 | description = "KMS policy for Cloudtrail logs." 59 | default = "" 60 | } 61 | 62 | # Alerting 63 | variable alerting_enabled { 64 | description = "Enable alerting" 65 | default = true 66 | } 67 | 68 | variable alarm_namespace { 69 | description = "Alarm metric namespace" 70 | default = "CISBenchmark" 71 | } 72 | 73 | variable tags { 74 | default = { 75 | "key" = "AWS_CIS_Benchmark" 76 | "value" = "1.2.0" 77 | } 78 | } 79 | 80 | # Password Policy 81 | variable "iam_allow_users_to_change_password" { 82 | description = "Can users change their own password" 83 | default = true 84 | } 85 | 86 | variable "iam_hard_expiry" { 87 | description = "Everyone needs hard reset for expired passwords" 88 | default = true 89 | } 90 | 91 | variable "iam_require_uppercase_characters" { 92 | description = "Require at least one uppercase letter in passwords" 93 | default = true 94 | } 95 | 96 | variable "iam_require_lowercase_characters" { 97 | description = "Require at least one lowercase letter in passwords" 98 | default = true 99 | } 100 | 101 | variable "iam_require_symbols" { 102 | description = "Require at least one symbol in passwords" 103 | default = true 104 | } 105 | 106 | variable "iam_require_numbers" { 107 | description = "Require at least one number in passwords" 108 | default = true 109 | } 110 | 111 | variable "iam_minimum_password_length" { 112 | description = "Require minimum lenght of password" 113 | default = 14 114 | } 115 | 116 | variable "iam_password_reuse_prevention" { 117 | description = "Prevent password reuse N times" 118 | default = 24 119 | } 120 | 121 | variable "iam_max_password_age" { 122 | description = "Passwords expire in N days" 123 | default = 90 124 | } 125 | -------------------------------------------------------------------------------- /version.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.12" 3 | } 4 | --------------------------------------------------------------------------------