Security checking AWS infrastructure code before deployment with Checkov

There are several methods to deploy infrastructure into AWS, some common techniques include:

  • AWS Console
  • AWS Command Line Interface (CLI)
  • Infrastructure as Code (IaC) – Cloudformation and Terraform

I prefer to use ‘Infrastructure as Code’ (via Terraform) over the other methods mentioned above. One advantage of ‘Infrastructure as Code’, is that you can security check (known as ‘linting’) your code before deploying it into your AWS account. Checkov.io provides some great free tools to security check your Infrastructure as Code (IaC) and guide you to make security improvements to your code. The environment I work in has adopted some new DevSecOps practices where our AWS terraform Infrastructure code is security scanned by Checkov (https://checkov.io) before pull requests are implemented into production via the main branch. I should also mention that Checkov has many integrations including CD/CI pipeline providers such as Bitbucket and GitLab.

In this post I will test our default vpc setup that incorporates the following features:

  • VPC that span across 3 availability zones (AZs)
  • VPC flow logs enabled
  • DNS resolution for VPC
  • SSM role for remote access to EC2 instances
  • Tag resources
  • No internet access (unless specifically required by the application)

To achieve the remote access capabilities we will utilise VPC endpoints and wrap a security group around the VPC endpoints. A visual representation of the target infrastructure is shown below:

For your reference, the following VPC code can be found in the Github repository here (located in the ‘base-code’ sub folder).

Note, I have a preference to use Terraform modules where possible and the following code in the Github repository incorporates Terraform modules in places. A summary of the files in Github repository is:

data.tf – used to discover the target AWS account ID programmatically

data "aws_caller_identity" "current" {}

iam.tf – creates a role which is assigned to EC2 instance for SSM remote access

resource "random_id" "random_id" {
  byte_length = 5

}

module "vpc_iam_assumable_role" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-assumable-role"
  version = "4.17.1"

  trusted_role_services = [
    "ec2.amazonaws.com"
  ]

  role_requires_mfa       = false
  create_role             = true
  create_instance_profile = true

  role_name = "${var.app_name}-vpc-role-${random_id.random_id.hex}"

  custom_role_policy_arns = [
    "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  ]

  tags = local.tags_generic
}

locals.tf – define region for deployment and tagging config

locals {
  region = "ap-southeast-2"

  tags_generic = {
    appname     = var.app_name
    environment = var.environment
    costcentre  = "TBC"
    ManagedBy   = var.ManagedByLocation
  }

  tags_ssm_ssm = {
    Name = "myvpc-vpce-interface-ssm-ssm"
  }

  tags_ssm_ssmmessages = {
    Name = "myvpc-vpce-interface-ssm-ssmmessages"
  }

  tags_ssm_ec2messages = {
    Name = "myvpc-vpce-interface-ssm-ec2messages"
  }

}

provider.tf – default Terraform file and region setting

provider "aws" {
  region = var.region

}

file security-groups.tf – security group for vpc endpoints

module "https_443_security_group" {
  source      = "terraform-aws-modules/security-group/aws//modules/https-443"
  version     = "4.16.2"
  name        = "https-443-sg"
  description = "Allow https 443"
  vpc_id      = module.demo_vpc.vpc_id

  # Allow ingress rules to be accessed only within current VPC
  ingress_cidr_blocks = [module.demo_vpc.vpc_cidr_block]

  # Allow all rules for all protocols
  egress_rules = ["https-443-tcp"]

  tags = local.tags_generic
}

ssm.tf – ssm vpc endpoints

module "vpc_ssm_endpoint" {

  source  = "terraform-aws-modules/vpc/aws//modules/vpc-endpoints"
  version = "3.13.0"

  vpc_id             = module.demo_vpc.vpc_id
  security_group_ids = [module.https_443_security_group.security_group_id]

  endpoints = {
    ssm = {
      service             = "ssm"
      private_dns_enabled = true
      subnet_ids          = module.demo_vpc.private_subnets
      tags                = merge(local.tags_generic, local.tags_ssm_ssm)
    },
    ssmmessages = {
      service             = "ssmmessages"
      private_dns_enabled = true,
      subnet_ids          = module.demo_vpc.private_subnets
      tags                = merge(local.tags_generic, local.tags_ssm_ssmmessages)
    },
    ec2messages = {
      service             = "ec2messages",
      private_dns_enabled = true,
      subnet_ids          = module.demo_vpc.private_subnets
      tags                = merge(local.tags_generic, local.tags_ssm_ec2messages)
    }
  }
}

terraform.tfvars – default terraform file with variable values

environment          = "prod"
vpc_cidr_range       = "172.16.0.0/20"
private_subnets_list = ["172.16.0.0/24", "172.16.1.0/24", "172.16.2.0/24"]
app_name             = "checkov-demo"

variable.tf – default terraform file with variable definitions

variable "region" {
  description = "AWS Region"
  default     = "ap-southeast-2"
  type        = string

}

variable "environment" {
  description = "AWS environment name"
  type        = string

}

variable "app_name" {
  description = "Applicaiton Name"
  type        = string

}

#------------------------------------------------------------------------------
# VPC
#------------------------------------------------------------------------------

variable "vpc_cidr_range" {
  type = string

}

variable "private_subnets_list" {
  description = "Subnet for infrastructure"
  type        = list(string)

}

#------------------------------------------------------------------------------
# Other
#------------------------------------------------------------------------------

variable "ManagedByLocation" {
  description = "IaC location"
  default     = "https://github.com/"
}

vpc.tf – vpc configuration

module "demo_vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.18.1"

  name = "${var.app_name}-${var.environment}-vpc"
  cidr = var.vpc_cidr_range

  azs             = ["${var.region}a", "${var.region}b", "${var.region}c"]
  private_subnets = var.private_subnets_list


  enable_flow_log                      = true
  create_flow_log_cloudwatch_log_group = true
  create_flow_log_cloudwatch_iam_role  = true
  flow_log_max_aggregation_interval    = 60

  create_igw         = false
  enable_nat_gateway = false
  enable_ipv6        = false

  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = local.tags_generic

}

Running ‘terraform init’ and ‘terraform plan’ commands indicates that 27 objects will be added in the apply phase. None of the new infrastructure looks suspicious at first glance!

Chekov installation:

On a mac, run the following command in folder with your code.

pip3 install -U checkov   or
pip install -U checkov

Checkov has a few switches that we need we to apply with the command:

-d (directory to use for checkov)

-download-external-modules True (check Terraform modules)

checkov -d . --download-external-modules True

Running Checkov with the above switches on our code in the repository, shows 4 areas of concerns:

The first concern triggered flagged by Checkov is caused when enabling the option for VPC flow logging. The VPC modules creates an IAM role for logging that is not constrained to a Cloudwatch log group and stream.

There is no facility within the Terraform module to restrict the logging to a log group and stream. How do we reduce/mitigate this risk? We use permission boundaries.

The second concert is in relation to the security groups created.

Checkov has found two security groups not attached to an EC2 instance or Elastic IP address. According to the Checkov website, this concert is low severity. Checkov is throwing a false positive alert here as the security group will be attached to the vpc interfaces. We will need to suppress the false alerts by adding ‘#checkov:skip=CKV2_AWS_5: “Ensure that Security Groups are attached to another resource”‘ to the module code.

The third Checkov message is related to the VPC creation process. When you create a new VPC, AWS creates a default security group with unrestricted traffic flow.

This default security group is not attached to any object but is still an unwanted risk. How do we reduce/mitigate the risk? We can’t delete the VPC default security group, but we can remove all ingress and egress rules so no traffic flows through it.

These migrations and improvements have been applied to the folder labelled post-scan-updates in github repository. Results from scanning the post-scan-updates folder code is shown below. Note, as we added some mitigations, more rules were enabled for scanning.

Even though Checkov was not 100% accurate in its findings, it did alert us to several security issues with the associated Terraform modules (i.e default VPC security group was unconstrained and the VPC flow logs role permissions allows logging to all log streams). I suspect Checkov would produce less false alerts if we had used native Terraform code without any Terraform modules. The information provided by Checkov allowed us to apply some mitigations to improve our security posture. Going forward, security linting tools such as Checkov will help us to improve our code security.

Note, the code in the ‘post-scan-updates’ subfolder in the GitHub repo will be reused in future posts.

Leave a comment