Skip to content

Latest commit

 

History

History
370 lines (275 loc) · 34.8 KB

File metadata and controls

370 lines (275 loc) · 34.8 KB

Fixit Cloud ☁️ Module: AWS VPC

Terraform module for defining secure-by-default VPC resources.

Table of Contents

Usage Examples

Subnet Types

This module categorizes subnets into the three distinct types: PUBLIC, PRIVATE, and INTRA-ONLY.
These "Subnet-Type" categories are based on the target of a subnet's default route (0.0.0.0/0).

Subnet type Default Route Target Public Ingress Public Egress Use Case Examples
PUBLIC Internet Gateway ✔️ ✔️ • Public-facing workloads
• Load balancers
• NAT gateways
PRIVATE NAT Gateway ✔️ • Non-public-facing workloads
• Service layer components
• Databases
INTRA-ONLY None • Workloads that don't require internet access
• Privacy-critical workloads
• Peered ops-management subnets

Every subnet must be provided with a type value in the var.subnets input variable. This module uses subnet type values to automatically generate network ACLs and route tables with common rules and routes which reflect the subnet types you provide. These resources are referred to as Subnet-Type resources, all of which can be flexibly customized, disabled, or replaced.

Subnet-Type Resources

All Subnet-Type resources are shown below. As documented in the relevant input variables, network ACLs and route tables don't have names in AWS, but this module requires user-provided names for both in order to facilitate a non-index-based organization of resource configs which implements sensible defaults without negatively impacting flexibility. Subnet-Type resources are not created for any types which are not included in the user's var.subnets inputs.

Subnet type Subnet-Type
Network ACL
Subnet-Type
Route Table
Notes
PUBLIC Public_Subnets_NACL Public_Subnets_RouteTable


PRIVATE Private_Subnets_NACL PUBLIC subnet CIDR PRIVATE subnet route table names = the CIDR of the PUBLIC subnet which contains the NAT gateway used as the default route target.
INTRA-ONLY IntraOnly_Subnets_NACL IntraOnly_Subnets_RouteTable
Custom rules/routes can not be added to INTRA-ONLY resources. You can, however, add tags.

How to Customize or Disable Subnet-Type Resources

TO CUSTOMIZE any module-provided Subnet-Type resource, simply use its name as a key in the relevant input variable (var.network_acls or var.route_tables), and the rules/routes you provide will be merged into its configuration. User-provided inputs are given precedence, so you can add your own configs or override existing ones.

Do note, however, the resource configs listed below which can not be overridden:

  • Route table default routes (0.0.0.0/0) are always defined by the type of subnets associated with it and therefore can not be customized.
  • INTRA-ONLY resources can not be customized, as subnets of this type are intended to be entirely closed systems which permit neither ingress nor egress traffic. If you find yourself wanting to add rules/routes to INTRA-ONLY resources, instead either switch the subnet type to PRIVATE and customize the PRIVATE resources, or create your own entirely custom NACLs/route tables.

TO DISABLE any module-provided Subnet-Type resource, simply create your own custom NACLs/route tables in var.network_acls/var.route_tables, and have every subnet of the relevant type configured to use your custom resource using the network_acl/route_table properties in var.subnets config objects.

Subnet-Type Network ACLs: Default Rules

Public_Subnets_NACL

  • Ingress:

    Rule Num Protocol CIDR Block From Port To Port Description
    100 tcp 0.0.0.0/0 80 80 HTTP from anywhere
    200 tcp 0.0.0.0/0 443 443 HTTPS from anywhere
    500 tcp 0.0.0.0/0 1024 65535 Ephemeral ports
  • Egress:

    Rule Num Protocol CIDR Block From Port To Port Description
    100 tcp 0.0.0.0/0 80 80 HTTP to anywhere
    200 tcp 0.0.0.0/0 443 443 HTTPS to anywhere
    500 tcp 0.0.0.0/0 1024 65535 Ephemeral ports

Private_Subnets_NACL

  • Ingress:

    Rule Num Protocol CIDR Block From Port To Port Description
    500 tcp 0.0.0.0/0 1024 65535 Ephemeral ports
  • Egress:

    Rule Num Protocol CIDR Block From Port To Port Description
    100 tcp 0.0.0.0/0 80 80 HTTP to anywhere
    200 tcp 0.0.0.0/0 443 443 HTTPS to anywhere
    500 tcp 0.0.0.0/0 1024 65535 Ephemeral ports

IntraOnly_Subnets_NACL

  • Ingress:

    Rule Num Protocol CIDR Block From Port To Port Description
    - - - - - Ingress not permitted
  • Egress:

    Rule Num Protocol CIDR Block From Port To Port Description
    - - - - - Egress not permitted

Subnet Availability Zones

For each subnet, a valid availability_zone must be specified.

PRIVATE Subnets: Same-AZ NAT Gateway Required

In each availability zone used by your PRIVATE subnets, there must also be at least one PUBLIC subnet configured with contains_nat_gateway = true. This module does not support the creation of PRIVATE subnets with default routes pointing to NAT gateways in different availability zones, for two reasons:

  1. Cross-AZ NAT incurs a slight latency hit
  2. Such a design undermines high-availability

PRIVATE Subnet Route Table Associations

As described in the NAT Gateways section, all NAT gateways have a 1:1 relationship with a corresponding route table - we'll refer to these here as "NAT route tables".

To explicitly associate a PRIVATE subnet with any particular NAT route table, simply set the PRIVATE subnet's route_table property in var.subnets to the CIDR of the PUBLIC subnet which contains the NAT gateway of your choosing.

Any PRIVATE subnets which are not explicitly associated with a NAT are evenly distributed among the NAT route tables within their availability zone.

NAT Gateways

This module creates one NAT gateway for each PUBLIC subnet configured with contains_nat_gateway = true. This module identifies NAT gateways and their associated resources (below) by the CIDR of the PUBLIC subnet in which they're placed. For each NAT gateway, the following resources are also created:

  • 1x elastic IP address
  • 1x route table, for which the NAT gateway is the default route

As mentioned in the section on availability zones, there must be at least one PUBLIC subnet configured with contains_nat_gateway = true in each AZ used by your PRIVATE subnets.

VPC Peering

There are two input variables which can be used to configure VPC peering connections:

  1. peering_request_vpc_ids A map of peer VPC IDs to peering connection config objects; use this variable for peering connections in which your VPC is the requester VPC.
  2. peering_accept_connection_ids A map of VPC peering connection IDs to peering connection config objects; use this property for peering connections in which your VPC is the accepter VPC.

For both the requester and accepter VPCs, remote VPC DNS resolution (whereby public IPs map to private DNS hostnames) is enabled by default. Set allow_remote_vpc_dns_resolution to false in either VPC to disable this feature.

VPC Peering: Connection Auto-Accept

If the requester and accepter VPCs are both in the same region and owned by the same account, the peering connection will be configured to auto-accept, thereby streamlining the connection setup process:

  • the requester VPC does not have to provide the peer VPC's region nor account ID
  • the accepter VPC does not have to manually accept the connection request, unless it's desirable to disable remote VPC DNS resolution

VPC Peering: Required Subnet-Level Configs

Once a VPC peering connection has been established, the following subnet-level resource configs will need to be in place before peering connection traffic can proceed:

  • To permit INGRESS traffic, a subnet within the VPC which is the target of a request must have a network ACL with at least one rule allowing the request using the sending VPC's CIDR (or a subset thereof). The NACL rule must also specify the correct protocol and port-range, which will of course depend on the nature of the request.
  • To permit EGRESS traffic, a subnet within the VPC from which the request originates must have a route table with at least one route configured with the peering connection ID using the receiving VPC's CIDR (or a subset thereof).

    AWS does not permit transitive routing through peering connections
    For more info, see Unsupported VPC Peering Configurations.

To see how this module is used to configure VPC peering, check out this usage example.

Non-Configurable VPC-Default Resources

In accordance with network security best practices, this module brings each VPC's default resources (listed below) under management, and locks them down by implementing configurations that deny all traffic which may otherwise be implicitly allowed. This behavior cannot be overridden, as it ensures that the only network traffic throughout the VPC is that which has been explicitly allowed.

🔒 Locked-Down Resources:

  • Default Route Table
  • Default Network ACL
  • Default Security Group

AWS Service CIDR Blocks

For both Security Group and Network ACL rule config objects, if the CIDR target is one of the supported AWS Services listed in the table below, you can provide the service's enum value to the rule's relevant property: aws_service for Security Group rules, and cidr_block for Network ACL rules. When provided, this module will perform the CIDR lookup for you using the region of the calling AWS Provider. CloudFront also offers "global" CIDRs, which can be obtained via the cloudfront_global enum value.

Most of the services supported by the underlying data source return multiple CIDR block values, which is fine for Security Group rules which accept multiple CIDRs without issue, but this is problematic for NACL rules since we can only provide a single CIDR for any given NACL rule's CIDR parameter. Therefore, the list of supported services for Network ACL rules is restricted at this time to only those which return a single value.

Supported Services:

Service Service Enum Supported for Security Group Rules Supported for NACL Rules
Amazon (amazon.com) amazon ✔️
Amazon Connect amazon_connect ✔️
API Gateway api_gateway ✔️
Cloud9 cloud9 ✔️
CloudFront cloudfront ✔️
CloudFront (global) cloudfront_global ✔️
CodeBuild codebuild ✔️
EC2 ec2 ✔️
EC2 Instance Connect ec2_instance_connect ✔️ ✔️
DynamoDB dynamodb ✔️
GlobalAccelerator globalaccelerator ✔️ ✔️
Route53 route53 ✔️
Route53 HealthChecks route53_healthchecks ✔️
S3 s3 ✔️
Workspaces Gateways workspaces_gateways ✔️

Note: Network ACL Unsupported Services

To address the NACL CIDR param issues, the following approaches are under consideration:

  • When multiple CIDRs are returned,
    • The module could create an allow rule for each CIDR.
             Easiest to implement, but implicit rule decision-making = less caller control.
    • Or rules could be created for just a subset of the CIDRs.
             How should caller determine the subset?
    • Or a rule could be created for just one CIDR.
             How should caller determine which one?

  • When zero CIDRs are returned,
    • We could allow the data block to cause the plan/apply operation to error out.
             More errors, never ideal.
    • Or the NACL rule could be skipped.
             Fewer errors, but again, implicit rule decision-making = less caller control.

⚙️ Module Usage

Usage Examples

Requirements

Name Version
terraform 1.3.2
aws ~> 4.34.0

Providers

Name Version
aws ~> 4.34.0

Modules

No modules.

Resources

Name Type
aws_default_network_acl.this resource
aws_default_route_table.this resource
aws_default_security_group.this resource
aws_eip.nat_gw_elastic_ips resource
aws_internet_gateway.list resource
aws_nat_gateway.map resource
aws_network_acl.map resource
aws_route_table.map resource
aws_route_table_association.map resource
aws_security_group.map resource
aws_security_group_rule.map resource
aws_subnet.map resource
aws_vpc.this resource
aws_vpc_endpoint.map resource
aws_vpc_peering_connection.map resource
aws_vpc_peering_connection_accepter.map resource
aws_vpc_peering_connection_options.map resource
aws_ip_ranges.map data source
aws_region.current data source

Inputs

Name Description Type Default Required
default_network_acl_tags Tags for the VPC's default network ACL. map(string) null no
default_route_table_tags Tags for the VPC's default route table. map(string) null no
default_security_group_tags Tags for the VPC's default security group. map(string) null no
internet_gateway_tags Tags for the internet gateway. map(string) null no
nat_gateway_elastic_ip_tags Map of NAT-containing public subnet CIDRs to tags for each respective
subnet's NAT-associated elastic IP address.
map(map(string)) null no
nat_gateway_tags Map of NAT-containing public subnet CIDRs to tags for each respective
subnet's NAT gateway.
map(map(string)) null no
network_acls Map of network ACL names to config objects. Network ACLs do not have names in AWS,
but the name values you provide will be used by this module to properly identify
network ACLs for the purposes of rule customization and subnet association.

"access.ingress" and "access.egress" map quoted rule numbers (e.g., "100") to objects
configuring each respective rule. For each rule, if "from_port" and "to_port" are the
same, you can simply provide just "port" which will be mapped to both. "protocol"
defaults to "tcp" if not provided.
map(
# map keys: internal NACL "names"
object({
access = optional(object({
ingress = optional(map(
# map keys: quoted rule numbers (e.g., "100")
object({
cidr_block = string
protocol = optional(string, "tcp")
port = optional(number)
from_port = optional(number)
to_port = optional(number)
})
), {})
egress = optional(map(
# map keys: quoted rule numbers (e.g., "100")
object({
cidr_block = string
protocol = optional(string, "tcp")
port = optional(number)
from_port = optional(number)
to_port = optional(number)
})
), {})
}), { access = {}, ingress = {} })
tags = optional(map(string))
})
)
{} no
peering_accept_connection_ids (Optional) VPC Peering connection accepts config; use this variable for
peering connections in which your VPC is the ACCEPTER VPC. Map peering
connection IDs to config objects for each respective peering connection
to accept. If the peering connection was configured for auto-acceptance,
manual acceptance is not required to establish the connection.
"allow_remote_vpc_dns_resolution" defaults to "true". For more info, see
the VPC Peering section of the README.
map(
# map keys: peering connection IDs
object({
allow_remote_vpc_dns_resolution = optional(bool)
tags = optional(map(string))
})
)
{} no
peering_request_vpc_ids (Optional) VPC Peering connection requests config; use this variable for
peering connections in which your VPC is the REQUESTER VPC. Map accepter
VPC IDs to config objects for each respective peering connection request.
"allow_remote_vpc_dns_resolution" defaults to "true". For more info, see
the VPC Peering section of the README.
map(
# map keys: peer VPC IDs
object({
peer_vpc_owner_account_id = optional(string)
peer_vpc_region = optional(string)
allow_remote_vpc_dns_resolution = optional(bool)
tags = optional(map(string))
})
)
{} no
route_tables Map of route table names to route table config objects. Route tables do not have names in
AWS, but the name values you provide will be used by this module to properly identify route
tables for the purposes of route customization and subnet association.

"routes" maps CIDRs of route destinations to objects configuring each respective route.
Custom route tables for PRIVATE subnets can set their default route ("0.0.0.0/0") using

"nat_gateway_subnet_cidr", which must be set to the CIDR of a NAT-containing PUBLIC subnet.
Peering connection routes can be configured in one of two ways: if VPC is the peering
REQUESTER, use "peering_request_vpc_id", otherwise if VPC is the peering ACCEPTER, use
"peering_connection_id".
map(
# map keys: internal route table "names"
object({
routes = optional(map(
# map keys: CIDRs of route destinations
object({
nat_gateway_subnet_cidr = optional(string)
peering_request_vpc_id = optional(string)
peering_accept_connection_id = optional(string)
})
))
tags = optional(map(string))
})
)
{} no
security_groups Map of Security Group names to config objects. For each ingress/egress rule,
"protocol" defaults to "tcp" if not provided. If "from_port" and "to_port" are
the same, you can provide just "port" which will be mapped to both. If a rule
will use the CIDR block of an AWS service, you can provide an enum string to
"aws_service" to have the lookup performed by the module; see the AWS Service
CIDR Blocks section of the README
for a list of
supported services and their enum values.
map(
# map keys: security group names
object({
description = string
access = object({
ingress = optional(list(
object({
description = string
protocol = optional(string, "tcp")
port = optional(number)
from_port = optional(number)
to_port = optional(number)
peer_security_group_id = optional(string)
peer_security_group = optional(string)
aws_service = optional(string)
cidr_blocks = optional(list(string))
self = optional(bool)
})
))
egress = optional(list(
object({
description = string
protocol = optional(string, "tcp")
port = optional(number)
from_port = optional(number)
to_port = optional(number)
peer_security_group_id = optional(string)
peer_security_group = optional(string)
aws_service = optional(string)
cidr_blocks = optional(list(string))
self = optional(bool)
})
))
})
tags = optional(map(string))
})
)
{} no
subnets Map of subnet CIDRs to subnet config objects. For each subnet, "type" must be either
"PUBLIC", "PRIVATE", or "INTRA-ONLY". The properties "map_public_ip_on_launch" and
"contains_nat_gateway" both default to false, and have no effect on non-public subnets.
Public and private subnets can be configured to use specific route tables and/or NACLs
via the "route_table" and "network_acl" properties respectively; these have no effect
on INTRA-ONLY subnets.
map(
# map keys: subnet CIDRs
object({
availability_zone = string
type = string
map_public_ip_on_launch = optional(bool)
contains_nat_gateway = optional(bool)
route_table = optional(string)
network_acl = optional(string)
tags = optional(map(string))
})
)
n/a yes
vpc Config object for the VPC. The optional bools "enable_dns_support"
and "enable_dns_hostnames" both default to "true".
object({
cidr_block = string
enable_dns_support = optional(bool)
enable_dns_hostnames = optional(bool)
tags = optional(map(string))
})
n/a yes
vpc_endpoints Map of VPC Endpoint services to endpoint config objects. Service-keys are
all normalized to lower-case and are therefore case-insensitive. A list of
valid services is available at the link below.
https://docs.aws.amazon.com/vpc/latest/privatelink/aws-services-privatelink-support.html

"type" can be "Interface" (default), "Gateway", or "GatewayLoadBalancer".
Gateway endpoints can only be created for the S3 and DynamoDB services.
All "Gateway" and some "Interface" endpoints can provide "policy", which if
provided must be a valid IAM policy formatted as a JSON string.
If the endpoint and service are owned by the same account, "auto-accept" can
be used to either enable or disable automatic acceptance of the connection.
"enable_private_dns" is only applicable to "Interface" endpoints and defaults
to true. "timeouts" are all optional and default to "10m" if not provided.

Endpoint Resource Associations
"Interface" and "GatewayLoadBalancer" endpoints must provide "subnet_cidrs", a
list of subnet CIDRs in which to place the interface/GWLB. "Interface" endpoints
must additionally provide "security_groups", a list of names of security groups
which should be associated with the endpoint's interface. "Gateway" endpoints
must specify "route_tables"; AWS will automatically add/remove routes to these
route tables which connect the service's AWS-managed prefix-list to the gateway
endpoint.
map(object({
# map keys: names of VPC endpoint services
type = optional(string) # Interface (default), Gateway, or GatewayLoadBalancer
policy = optional(string)
auto_accept = optional(bool)
enable_private_dns = optional(bool) # Only for types: Interface
subnet_cidrs = optional(list(string)) # Only for types: Interface, GWLB
security_groups = optional(list(string)) # Only for types: Interface
route_tables = optional(list(string)) # Only for types: Gateway
timeouts = optional(object({
create = optional(string)
update = optional(string)
delete = optional(string)
}))
tags = optional(map(string))
}))
{} no

Outputs

Name Description
Default_NetworkACL The VPC's default network ACL resource object.
Default_RouteTable The VPC's default route table resource object.
Default_SecurityGroup The VPC's default security group resource object.
Internet_Gateway The internet gateway resource object.
NAT_Gateway_Elastic_IPs Map of NAT gateway elastic IP address resource objects.
NAT_Gateways Map of NAT gateway resource objects.
Network_ACLs Map of network ACL resource objects.
RouteTables Map of route table resource objects.
Security_Groups Map of security group resource objects.
Subnets Map of subnet resource objects merged with their respective input params.
VPC The VPC resource object.
VPC_Endpoints Map of VPC Endpoint resource objects.
VPC_Peering_Connection_Accepts Map of VPC Peering Connection Accepter resource objects.
VPC_Peering_Connection_Options Map of VPC Peering Connection Options resource objects.
VPC_Peering_Connection_Requests Map of VPC Peering Connection resource objects.

📝 License

All scripts and source code contained herein are for commercial use only by Nerdware, LLC.

See LICENSE for more information.

💬 Contact

Trevor Anderson - @TeeRevTweets - Trevor@Nerdware.cloud

     

Dare Mighty Things.