Skip to content

appvia/terraform-aws-firewall

Github Actions

Terraform AWS Inspection VPC

Note: the following is purely for illustrative purposes

This repository manages the inspection vpc and rulesets within the AWS estate. The inspection VPC is seated at the heart of the estate, and leverages AWS Network Firewall as a managed solution. It's remit is

  • To filter all traffic between networks and enviroments (i.e. production, development, ci).
  • To filter all egress traffic from the spokes to the outbound internet.
  • To provide the spokes with a central place to egress all traffic to the internet i.e. sharing NAT gateways.

Rule Group Variables

A firewall policy in AWS Network firewall comprises of one of more references stateful / stateless rule groups. The difference between these two groups is similar to NACL vs security groups in AWS; where SG's have knowlegde of direction and permit established connections to return traffic without the need of an additional rule. Note this module follows AWS recommendations, and has opted to ignore stateless rules completely, deferring purely to stateful Suricata rules.

Rule groups also have the ability to source in variables containing ipsets (a collection of CIDR block) or portsets (a collection of ports). These be referenced within the Suricata rules themselves, providing a reusable snippet i.e.

(in the tfvars)
policy_variables = {
  devops_net = ["10.128.0.0/24"]
  remote_net = ["10.230.0.0/24"]
}

# This will produce variables DEVOPS_NET and REMOTE_NET, and make
# them available in the ruleset

pass  tcp $REMOTE_NET any -> $HOME_NET
or
pass  tcp [!$REMOTE_NET, $DEVOPS_NET] any -> $HOME_NET

The module use the contents of the var.firewall_rules to source in the files and merge them together to produce the final ruleset.

Egress Support

The inspection VPC can be configured to support egress traffic. This is useful when the inspection VPC is used as a central point for all egress traffic from the spokes. The egress support is enabled by setting the var.enable_egress to true. When enabled, the inspection VPC will have a route table that routes all traffic to the internet gateway. The route table is associated with the private subnets, and the internet gateway is attached to the VPC.

To enable egress support,

  • var.enable_egress must be set to true.
  • var.publie_subnet_netmask must be set to a non-zero value.
## Provision a inspection firewall, but with an existing vpc
module "inspection" {
  source = "../.."

  availability_zones     = var.availability_zones
  firewall_rules         = local.firewall_rules
  name                   = var.name
  private_subnet_netmask = var.private_subnet_netmask
  public_subnet_netmask  = var.public_subnet_netmask
  ram_principals         = var.ram_principals
  tags                   = var.tags
  transit_gateway_id     = var.transit_gateway_id
}

Event Logging

Currently the inspection VPC is setup to segregate the flow and alert logs into two CloudWatch log groups:

  • Alerts: are directed to ${var.name}-alert-log.
  • Flows: are directed to ${var.name}-flow-log.

This module also supports the ability to encrypt the logs using a KMS key. If the var.create_kms_key is set to true, a KMS key will be created and used to encrypt the logs. The key will be created in the same region as the logs.

CloudWatch Dashboard

The module also supports the ability to deploy a CloudWatch dashboard to visualise the logs. The dashboard is created using a CloudFormation template, and is deployed into the same region as the logs. The dashboard is created using the aws_cloudformation_stack resource, and is created using the assets/cloudfomation/nfw-cloudwatch-dashboard template.

Reusing an Existing VPC

The module supports the ability to reuse an existing VPC. This is useful when the inspection VPC is being deployed into an existing environment. The options defined depend on whether egress is enabled or not.

To reuse an existing VPC, with egress support

  • var.vpc_id must be set to the ID of the VPC.
  • var.private_subnet_id_by_az must be set to a map of availability zone to subnet id i.e { "eu-west-1a" = "subnet-12345678" }.
  • var.public_route_table_ids must be set to a list of public route table ids associated with the public subnets.
  • var.transit_route_table_by_az must be set to a map of availability zone to transit route table id i.e { "eu-west-1a" = "rtb-12345678" }.
  • var.transit_route_table_ids must be set to a list of transit route table ids associated with the transit subnets.
## Provision a inspection firewall, but with an existing vpc
module "inspection" {
  source = "../.."

  availability_zones        = var.availability_zones
  firewall_rules            = local.firewall_rules
  name                      = var.name
  private_subnet_id_by_az   = var.private_subnet_id_by_az
  public_route_table_ids    = var.public_route_table_ids
  ram_principals            = var.ram_principals
  tags                      = var.tags
  transit_gateway_id        = var.transit_gateway_id
  transit_route_table_by_az = var.transit_route_table_by_az
  transit_route_table_ids   = var.transit_route_table_ids
  vpc_id                    = var.vpc_id
}

To reuse an existing VPC, without egress support

  • var.vpc_id must be set to the ID of the VPC.
  • var.private_subnet_id_by_az must be set to a map of availability zone to subnet id i.e { "eu-west-1a" = "subnet-12345678" }.
  • var.transit_route_table_by_az must be set to a map of availability zone to transit route table id i.e { "eu-west-1a" = "rtb-12345678" }.
module "inspection" {
  source = "../.."

  availability_zones        = var.availability_zones
  create_kms_key            = false
  enable_dashboard          = var.enable_dashboard
  firewall_rules            = local.firewall_rules
  name                      = var.name
  private_subnet_id_by_az   = var.vpc.private_subnet_id_by_az
  ram_principals            = var.ram_principals
  tags                      = var.tags
  transit_gateway_id        = var.transit_gateway_id
  transit_route_table_by_az = var.vpc.transit_route_table_by_az
  vpc_id                    = var.vpc.vpc_id
}

Pipeline Permissions

The following pipeline permissions are required to deploy the inspection VPC

# tfsec:ignore:aws-iam-no-policy-wildcards
module "network_inspection_vpc_admin" {
  count   = var.repositories.firewall != null ? 1 : 0
  source  = "appvia/oidc/aws//modules/role"
  version = "1.2.0"

  name                = var.repositories.firewall.role_name
  common_provider     = var.scm_name
  description         = "Deployment role used to deploy the inspection vpc"
  permission_boundary = var.default_permissions_boundary_name
  repository          = var.repositories.firewall.url
  tags                = var.tags

  read_only_policy_arns = [
    "arn:aws:iam::aws:policy/AWSResourceAccessManagerReadOnlyAccess",
    "arn:aws:iam::aws:policy/ReadOnlyAccess",
  ]
  read_write_policy_arns = [
    "arn:aws:iam::aws:policy/AWSResourceAccessManagerFullAccess",
    "arn:aws:iam::aws:policy/AmazonEC2FullAccess",
    "arn:aws:iam::aws:policy/CloudFormationFullAccess", # Assuming you are deploying the dashboard
    "arn:aws:iam::aws:policy/LambdaFullAccess",
    "arn:aws:iam::aws:policy/ReadOnlyAccess",
    "arn:aws:iam::aws:policy/job-function/NetworkAdministrator",
  ]

  read_write_inline_policies = {
    "additional" = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Action = [
            "network-firewall:Associate*",
            "network-firewall:Create*",
            "network-firewall:Delete*",
            "network-firewall:Describe*",
            "network-firewall:Disassociate*",
            "network-firewall:List*",
            "network-firewall:Put*",
            "network-firewall:Tag*",
            "network-firewall:Untag*",
            "network-firewall:Update*",
          ]
          Effect   = "Allow"
          Resource = "*"
        },
        {
          Action   = ["iam:CreateServiceLinkedRole"],
          Effect   = "Allow",
          Resource = ["arn:aws:iam::*:role/aws-service-role/network-firewall.amazonaws.com/AWSServiceRoleForNetworkFirewall"]
        },
        {
          Action   = ["logs:*"],
          Effect   = "Allow",
          Resource = ["*"]
        }
      ]


      Version = "2012-10-17"
      Statement = [
        {
          Action = [
            "network-firewall:Describe*",
            "network-firewall:List*"
          ]
          Effect   = "Allow"
          Resource = "*"
        },
        {
          Action = [
            "logs:Get*",
            "logs:List*",
            "logs:Describe*",
          ],
          Effect   = "Allow",
          Resource = ["*"]
        }
      ]
    })
  }

  read_only_inline_policies = {
    "additional" = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Action = [
            "network-firewall:Describe*",
            "network-firewall:List*"
          ]
          Effect   = "Allow"
          Resource = "*"
        },
        {
          Action = [
            "logs:Describe*",
            "logs:Get*",
            "logs:List*",
          ],
          Effect   = "Allow",
          Resource = ["*"]
        }
      ]
    })
  }

Requirements

Name Version
terraform >= 1.0
aws ~> 5.0

Providers

Name Version
aws ~> 5.0

Modules

Name Source Version
network_firewall aws-ia/networkfirewall/aws 1.0.1
vpc appvia/network/aws 0.3.0

Resources

Name Type
aws_cloudformation_stack.dashboard resource
aws_cloudwatch_log_group.alert_log resource
aws_cloudwatch_log_group.flow_log resource
aws_ec2_managed_prefix_list.this resource
aws_kms_key.current resource
aws_networkfirewall_firewall_policy.this resource
aws_networkfirewall_rule_group.stateful resource
aws_ram_principal_association.this resource
aws_ram_resource_association.this resource
aws_ram_resource_share.this resource
aws_s3_bucket.dashboard resource
aws_s3_bucket_acl.dashboard resource
aws_s3_bucket_ownership_controls.dashboard resource
aws_s3_bucket_policy.dashboard resource
aws_s3_bucket_public_access_block.dashboard resource
aws_s3_bucket_server_side_encryption_configuration.dashboard resource
aws_s3_bucket_versioning.dashboard resource
aws_s3_object.dashboard resource
aws_caller_identity.current data source
aws_iam_policy_document.dashboard data source
aws_iam_policy_document.logging data source
aws_kms_key.current data source
aws_region.current data source

Inputs

Name Description Type Default Required
availability_zones Number of availability zones to deploy into number n/a yes
name Name of the environment to deploy into string n/a yes
tags Tags to apply to all resources map(string) n/a yes
transit_gateway_id The ID of the Transit Gateway string n/a yes
cloudwatch_kms Name of the KMS key to use for CloudWatch logs string "" no
cloudwatch_retention_in_days Number of days to retain CloudWatch logs number 30 no
create_kms_key Create a KMS key for CloudWatch logs bool false no
dashboard_bucket The name of the S3 bucket to store the CloudWatch Insights dashboard string "lza-inspection-cw-dashboard" no
dashboard_key The name of the S3 bucket key to store the CloudWatch Insights dashboard string "nfw-cloudwatch-dashboard.yml" no
enable_dashboard Indicates we should deploy the CloudWatch Insights dashboard bool false no
enable_egress Indicates the inspectio vpc should have egress enabled bool false no
enable_policy_change_protection Indicates the firewall policy should be protected from changes bool false no
enable_subnet_change_protection Indicates the firewall subnets should be protected from changes bool false no
external_rule_groups A collection of additional rule groups to add to the policy
list(object({
priority = number
arn = string
}))
null no
firewall_rules A collection of firewall rules to add to the policy
list(object({
name = string
content = string
}))
null no
ip_prefixes A collection of ip sets which can be referenced by the rules
map(object({
name = string
address_family = string
max_entries = number
description = string
entries = list(object({
cidr = string
description = string
}))
}))
{} no
network_cidr_blocks List of CIDR blocks defining the aws environment list(string)
[
"10.0.0.0/8",
"192.168.0.0/24"
]
no
policy_variables A map of policy variables made available to the suricata rules map(list(string)) {} no
private_subnet_id_by_az If reusing an existing VPC, provider a map of az to subnet id map(string) {} no
private_subnet_netmask Netmask for the private subnets number 24 no
public_route_table_ids If reusing an existing VPC, provide the public route table ids list(string) [] no
public_subnet_netmask Netmask for the public subnets number 0 no
ram_principals A list of principals to share the firewall policy with map(string) {} no
stateful_capacity The number of stateful rule groups to create number 5000 no
transit_route_table_by_az If reusing an existing VPC, provider a map of az to subnet id map(string) {} no
transit_route_table_ids If reusing an existing VPC, provide the transit route table ids list(string) [] no
vpc_cidr CIDR block for the VPC string "100.64.0.0/21" no
vpc_id If reusing an existing VPC, provide the VPC ID and private subnets ids string "" no

Outputs

Name Description
firewall_id The ARN of the firewall.
firewall_rule_groups The rule groups to associate with the firewall.
policy_variables The policy variables to associate with the firewall.
private_subnet_id_by_az The private subnet IDs by availability zone.
private_subnet_ids The IDs of the private subnets.
public_subnet_ids The IDs of the public subnets.
ram_principals The principals to share the firewall with.
routing_configuration The routing configuration for the firewall.
transit_attachment_id The ID of the transit gateway attachment.
transit_route_table_by_az The transit route table by availability zone.
transit_subnet_ids The IDs of the transit subnets.
vpc_id The ID of the VPC.