Skip to content

Commit d8187b6

Browse files
authored
Merge pull request #31 from appvia/feat/nacls
feat: added the ability to support adding nacls
2 parents 605d1f2 + 1831269 commit d8187b6

File tree

16 files changed

+647
-1
lines changed

16 files changed

+647
-1
lines changed

.github/workflows/terraform.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,9 @@ jobs:
2424
name: Shared Module Validation
2525
with:
2626
working-directory: modules/shared
27+
28+
nacls-validation:
29+
uses: appvia/appvia-cicd-workflows/.github/workflows/terraform-module-validation.yml@main
30+
name: NACLS Validation
31+
with:
32+
working-directory: modules/nacls

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,52 @@ module "share_dev" {
266266
}
267267
```
268268

269+
## Network Access Control Lists (NACLS)
270+
271+
Network Access Control Lists (NACLs) are an optional layer of security for your VPC that acts as a firewall for controlling traffic in and out of one or more subnets. Unlike security groups, NACLs are stateless, meaning that responses to allowed inbound traffic are subject to the rules for outbound traffic. NACLs allow you to explicitly allow or deny traffic based on IP address, port, and protocol. Here's an example of how to configure NACLs in this module:
272+
273+
```hcl
274+
module "vpc" {
275+
source = "../.."
276+
277+
name = "production"
278+
vpc_cidr = "10.0.0.0/16"
279+
availability_zones = 3
280+
tags = local.tags
281+
282+
subnets = {
283+
private = {
284+
netmask = 24
285+
}
286+
}
287+
288+
nacl_rules = {
289+
private = {
290+
inbound_rules = [
291+
{
292+
cidr_block = "10.0.0.0/24"
293+
from_port = 22
294+
to_port = 22
295+
protocol = 6 # TCP
296+
rule_action = "allow"
297+
rule_number = 100
298+
}
299+
],
300+
outbound_rules = [
301+
{
302+
cidr_block = "0.0.0.0/0"
303+
from_port = 0
304+
to_port = 65535
305+
protocol = -1 # All traffic
306+
rule_action = "allow"
307+
rule_number = 100
308+
}
309+
]
310+
}
311+
}
312+
}
313+
```
314+
269315
## Update Documentation
270316

271317
The `terraform-docs` utility is used to generate this README. Follow the below steps to update:
@@ -299,6 +345,7 @@ The `terraform-docs` utility is used to generate this README. Follow the below s
299345
| <a name="input_enable_transit_gateway_subnet_natgw"></a> [enable\_transit\_gateway\_subnet\_natgw](#input\_enable\_transit\_gateway\_subnet\_natgw) | Indicates if the transit gateway subnets should be connected to a nat gateway | `bool` | `false` | no |
300346
| <a name="input_exclude_route53_resolver_rules"></a> [exclude\_route53\_resolver\_rules](#input\_exclude\_route53\_resolver\_rules) | List of resolver rules to exclude from association | `list(string)` | `[]` | no |
301347
| <a name="input_ipam_pool_id"></a> [ipam\_pool\_id](#input\_ipam\_pool\_id) | An optional pool id to use for IPAM pool to use | `string` | `null` | no |
348+
| <a name="input_nacl_rules"></a> [nacl\_rules](#input\_nacl\_rules) | Map of NACL rules to apply to different subnet types. Each rule requires from\_port, to\_port, protocol, rule\_action, cidr\_block, and rule\_number | <pre>map(object({<br/> inbound_rules = list(object({<br/> cidr_block = string<br/> from_port = number<br/> icmp_code = optional(number, 0)<br/> icmp_type = optional(number, 0)<br/> ipv6_cidr_block = optional(string, null)<br/> protocol = optional(number, -1)<br/> rule_action = string<br/> rule_number = number<br/> to_port = number<br/> }))<br/> outbound_rules = list(object({<br/> cidr_block = string<br/> from_port = number<br/> icmp_code = optional(number, 0)<br/> icmp_type = optional(number, 0)<br/> ipv6_cidr_block = optional(string, null)<br/> protocol = optional(number, -1)<br/> rule_action = string<br/> rule_number = number<br/> to_port = number<br/> }))<br/> }))</pre> | `{}` | no |
302349
| <a name="input_nat_gateway_mode"></a> [nat\_gateway\_mode](#input\_nat\_gateway\_mode) | The configuration mode of the NAT gateways | `string` | `"none"` | no |
303350
| <a name="input_private_subnet_netmask"></a> [private\_subnet\_netmask](#input\_private\_subnet\_netmask) | The netmask for the private subnets | `number` | `0` | no |
304351
| <a name="input_private_subnet_tags"></a> [private\_subnet\_tags](#input\_private\_subnet\_tags) | Additional tags for the private subnets | `map(string)` | `{}` | no |

examples/nacls/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<!-- BEGIN_TF_DOCS -->
2+
## Providers
3+
4+
No providers.
5+
6+
## Inputs
7+
8+
No inputs.
9+
10+
## Outputs
11+
12+
| Name | Description |
13+
|------|-------------|
14+
| <a name="output_nat_public_ips"></a> [nat\_public\_ips](#output\_nat\_public\_ips) | The public IPs of the NAT Gateways i.e [public\_ip, public\_ip] |
15+
| <a name="output_natgw_id_per_az"></a> [natgw\_id\_per\_az](#output\_natgw\_id\_per\_az) | The IDs of the NAT Gateways (see aws-ia/vpc/aws for details) |
16+
| <a name="output_private_route_table_ids"></a> [private\_route\_table\_ids](#output\_private\_route\_table\_ids) | The IDs of the private route tables ie. [route\_table\_id, route\_table\_id] |
17+
| <a name="output_private_subnet_attributes_by_az"></a> [private\_subnet\_attributes\_by\_az](#output\_private\_subnet\_attributes\_by\_az) | The attributes of the private subnets (see aws-ia/vpc/aws for details) |
18+
| <a name="output_private_subnet_cidr_by_id"></a> [private\_subnet\_cidr\_by\_id](#output\_private\_subnet\_cidr\_by\_id) | A map of the private subnet ID to CIDR block i.e. us-west-2a => subnet\_cidr |
19+
| <a name="output_private_subnet_cidrs"></a> [private\_subnet\_cidrs](#output\_private\_subnet\_cidrs) | A list of the CIDRs for the private subnets |
20+
| <a name="output_private_subnet_id_by_az"></a> [private\_subnet\_id\_by\_az](#output\_private\_subnet\_id\_by\_az) | A map of availability zone to subnet id of the private subnets i.e. eu-west-2a => subnet\_id |
21+
| <a name="output_private_subnet_ids"></a> [private\_subnet\_ids](#output\_private\_subnet\_ids) | The IDs of the private subnets i.e. [subnet\_id, subnet\_id] |
22+
| <a name="output_public_route_table_ids"></a> [public\_route\_table\_ids](#output\_public\_route\_table\_ids) | The IDs of the public route tables ie. [route\_table\_id, route\_table\_id] |
23+
| <a name="output_public_subnet_attributes_by_az"></a> [public\_subnet\_attributes\_by\_az](#output\_public\_subnet\_attributes\_by\_az) | The attributes of the public subnets (see aws-ia/vpc/aws for details) |
24+
| <a name="output_public_subnet_cidr_by_id"></a> [public\_subnet\_cidr\_by\_id](#output\_public\_subnet\_cidr\_by\_id) | A map of the public subnet ID to CIDR block i.e. us-west-2a => subnet\_cidr |
25+
| <a name="output_public_subnet_cidrs"></a> [public\_subnet\_cidrs](#output\_public\_subnet\_cidrs) | A list of the CIDRs for the public subnets i.e. [subnet\_cidr, subnet\_cidr] |
26+
| <a name="output_public_subnet_id_by_az"></a> [public\_subnet\_id\_by\_az](#output\_public\_subnet\_id\_by\_az) | A map of availability zone to subnet id of the public subnets i.e. eu-west-2a => subnet\_id |
27+
| <a name="output_public_subnet_ids"></a> [public\_subnet\_ids](#output\_public\_subnet\_ids) | The IDs of the public subnets i.e. [subnet\_id, subnet\_id] |
28+
| <a name="output_rt_attributes_by_type_by_az"></a> [rt\_attributes\_by\_type\_by\_az](#output\_rt\_attributes\_by\_type\_by\_az) | The attributes of the route tables (see aws-ia/vpc/aws for details) |
29+
| <a name="output_transit_gateway_attachment_id"></a> [transit\_gateway\_attachment\_id](#output\_transit\_gateway\_attachment\_id) | The ID of the transit gateway attachment if enabled |
30+
| <a name="output_transit_route_table_by_az"></a> [transit\_route\_table\_by\_az](#output\_transit\_route\_table\_by\_az) | A map of availability zone to transit gateway route table ID i.e eu-west-2a => route\_table\_id |
31+
| <a name="output_transit_route_table_ids"></a> [transit\_route\_table\_ids](#output\_transit\_route\_table\_ids) | The IDs of the transit gateway route tables ie. [route\_table\_id, route\_table\_id] |
32+
| <a name="output_transit_subnet_attributes_by_az"></a> [transit\_subnet\_attributes\_by\_az](#output\_transit\_subnet\_attributes\_by\_az) | The attributes of the transit gateway subnets (see aws-ia/vpc/aws for details) |
33+
| <a name="output_transit_subnet_ids"></a> [transit\_subnet\_ids](#output\_transit\_subnet\_ids) | The IDs of the transit gateway subnets ie. [subnet\_id, subnet\_id] |
34+
| <a name="output_vpc_attributes"></a> [vpc\_attributes](#output\_vpc\_attributes) | The attributes of the VPC (see aws-ia/vpc/aws for details) |
35+
| <a name="output_vpc_cidr"></a> [vpc\_cidr](#output\_vpc\_cidr) | The CIDR block of the VPC |
36+
| <a name="output_vpc_id"></a> [vpc\_id](#output\_vpc\_id) | The ID of the VPC |
37+
<!-- END_TF_DOCS -->

examples/nacls/main.tf

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
2+
locals {
3+
tags = {
4+
"Environment" = "test"
5+
"GitRepo" = "https://github.com/appvia/terraform-aws-network"
6+
"Terraform" = "true"
7+
}
8+
}
9+
10+
## Provision a VPC with public and private subnets
11+
module "vpc" {
12+
source = "../.."
13+
14+
availability_zones = 2
15+
enable_ssm = false
16+
enable_route53_resolver_rules = false
17+
name = "operations"
18+
tags = local.tags
19+
vpc_cidr = "10.100.0.0/21"
20+
21+
subnets = {
22+
private = {
23+
netmask = 24
24+
}
25+
}
26+
27+
nacl_rules = {
28+
private = {
29+
inbound_rules = [
30+
{
31+
cidr_block = "10.100.0.0/24"
32+
from_port = 22
33+
to_port = 22
34+
protocol = -1
35+
rule_action = "allow"
36+
rule_number = 50
37+
}
38+
]
39+
outbound_rules = [
40+
{
41+
cidr_block = "10.100.0.0/24"
42+
from_port = 22
43+
to_port = 22
44+
protocol = -1
45+
rule_action = "allow"
46+
rule_number = 50
47+
}
48+
]
49+
}
50+
}
51+
}

examples/nacls/outputs.tf

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
2+
output "vpc_id" {
3+
description = "The ID of the VPC"
4+
value = module.vpc.vpc_id
5+
}
6+
7+
output "vpc_cidr" {
8+
description = "The CIDR block of the VPC"
9+
value = module.vpc.vpc_attributes.cidr_block
10+
}
11+
12+
output "vpc_attributes" {
13+
description = "The attributes of the VPC (see aws-ia/vpc/aws for details)"
14+
value = module.vpc.vpc_attributes
15+
}
16+
17+
output "private_subnet_cidrs" {
18+
description = "A list of the CIDRs for the private subnets"
19+
value = module.vpc.private_subnet_cidrs
20+
}
21+
22+
output "public_subnet_cidrs" {
23+
description = "A list of the CIDRs for the public subnets i.e. [subnet_cidr, subnet_cidr]"
24+
value = module.vpc.public_subnet_cidrs
25+
}
26+
27+
output "private_subnet_cidr_by_id" {
28+
description = "A map of the private subnet ID to CIDR block i.e. us-west-2a => subnet_cidr"
29+
value = module.vpc.private_subnet_cidr_by_id
30+
}
31+
32+
output "public_subnet_cidr_by_id" {
33+
description = "A map of the public subnet ID to CIDR block i.e. us-west-2a => subnet_cidr"
34+
value = module.vpc.public_subnet_cidr_by_id
35+
}
36+
37+
output "private_subnet_id_by_az" {
38+
description = "A map of availability zone to subnet id of the private subnets i.e. eu-west-2a => subnet_id"
39+
value = module.vpc.private_subnet_id_by_az
40+
}
41+
42+
output "public_subnet_id_by_az" {
43+
description = "A map of availability zone to subnet id of the public subnets i.e. eu-west-2a => subnet_id"
44+
value = module.vpc.public_subnet_id_by_az
45+
}
46+
47+
output "private_subnet_attributes_by_az" {
48+
description = "The attributes of the private subnets (see aws-ia/vpc/aws for details)"
49+
value = module.vpc.private_subnet_attributes_by_az
50+
}
51+
52+
output "public_subnet_attributes_by_az" {
53+
description = "The attributes of the public subnets (see aws-ia/vpc/aws for details)"
54+
value = module.vpc.public_subnet_attributes_by_az
55+
}
56+
57+
output "transit_subnet_attributes_by_az" {
58+
description = "The attributes of the transit gateway subnets (see aws-ia/vpc/aws for details)"
59+
value = module.vpc.transit_subnet_attributes_by_az
60+
}
61+
62+
output "natgw_id_per_az" {
63+
description = "The IDs of the NAT Gateways (see aws-ia/vpc/aws for details)"
64+
value = module.vpc.natgw_id_per_az
65+
}
66+
67+
output "rt_attributes_by_type_by_az" {
68+
description = "The attributes of the route tables (see aws-ia/vpc/aws for details)"
69+
value = module.vpc.rt_attributes_by_type_by_az
70+
}
71+
72+
output "private_subnet_ids" {
73+
description = "The IDs of the private subnets i.e. [subnet_id, subnet_id]"
74+
value = module.vpc.private_subnet_ids
75+
}
76+
77+
output "public_subnet_ids" {
78+
description = "The IDs of the public subnets i.e. [subnet_id, subnet_id]"
79+
value = module.vpc.public_subnet_ids
80+
}
81+
82+
output "transit_subnet_ids" {
83+
description = "The IDs of the transit gateway subnets ie. [subnet_id, subnet_id]"
84+
value = module.vpc.transit_subnet_ids
85+
}
86+
87+
output "private_route_table_ids" {
88+
description = "The IDs of the private route tables ie. [route_table_id, route_table_id]"
89+
value = module.vpc.private_route_table_ids
90+
}
91+
92+
output "public_route_table_ids" {
93+
description = "The IDs of the public route tables ie. [route_table_id, route_table_id]"
94+
value = module.vpc.public_route_table_ids
95+
}
96+
97+
output "transit_route_table_ids" {
98+
description = "The IDs of the transit gateway route tables ie. [route_table_id, route_table_id]"
99+
value = module.vpc.transit_route_table_ids
100+
}
101+
102+
output "transit_route_table_by_az" {
103+
description = "A map of availability zone to transit gateway route table ID i.e eu-west-2a => route_table_id"
104+
value = module.vpc.transit_route_table_by_az
105+
}
106+
107+
output "transit_gateway_attachment_id" {
108+
description = "The ID of the transit gateway attachment if enabled"
109+
value = module.vpc.transit_gateway_attachment_id
110+
}
111+
112+
output "nat_public_ips" {
113+
description = "The public IPs of the NAT Gateways i.e [public_ip, public_ip]"
114+
value = module.vpc.nat_public_ips
115+
}

examples/nacls/terraform.tf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
terraform {
3+
required_version = ">= 1.0.0"
4+
5+
required_providers {
6+
aws = {
7+
source = "hashicorp/aws"
8+
version = ">= 5.0.0"
9+
}
10+
}
11+
}

examples/nacls/variables.tf

Whitespace-only changes.

main.tf

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ module "vpc" {
1717
vpc_ipv4_netmask_length = var.vpc_netmask
1818
}
1919

20+
## Provision the NACLs for each of the subnets
21+
module "nacls" {
22+
for_each = var.nacl_rules
23+
source = "./modules/nacls"
24+
25+
inbound_rules = var.nacl_rules[each.key].inbound_rules
26+
name = each.key
27+
outbound_rules = var.nacl_rules[each.key].outbound_rules
28+
subnet_count = var.availability_zones
29+
subnet_ids = local.all_subnets_by_name[each.key].ids
30+
tags = var.tags
31+
vpc_id = module.vpc.vpc_attributes.id
32+
33+
depends_on = [module.vpc]
34+
}
35+
2036
## Enable DNS request logging if required
2137
resource "aws_cloudwatch_log_group" "dns_query_logs" {
2238
count = var.enable_dns_request_logging ? 1 : 0
@@ -44,7 +60,7 @@ resource "aws_route53_resolver_query_log_config_association" "dns_query_log_asso
4460

4561
## Associate any resolver rules with the vpc if required
4662
resource "aws_route53_resolver_rule_association" "vpc_associations" {
47-
for_each = var.enable_route53_resolver_rules ? toset(local.resolver_rules) : null
63+
for_each = var.enable_route53_resolver_rules ? toset(local.resolver_rules) : []
4864

4965
resolver_rule_id = each.value
5066
vpc_id = module.vpc.vpc_attributes.id

modules/nacls/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!-- BEGIN_TF_DOCS -->
2+
## Providers
3+
4+
| Name | Version |
5+
|------|---------|
6+
| <a name="provider_aws"></a> [aws](#provider\_aws) | ~> 5.0 |
7+
8+
## Inputs
9+
10+
| Name | Description | Type | Default | Required |
11+
|------|-------------|------|---------|:--------:|
12+
| <a name="input_name"></a> [name](#input\_name) | The name of the subnets to create the NACL for | `string` | n/a | yes |
13+
| <a name="input_subnet_count"></a> [subnet\_count](#input\_subnet\_count) | The number of subnets to create the NACL for | `number` | n/a | yes |
14+
| <a name="input_subnet_ids"></a> [subnet\_ids](#input\_subnet\_ids) | The subnet IDs to apply the NACL to | `list(string)` | n/a | yes |
15+
| <a name="input_tags"></a> [tags](#input\_tags) | A map of tags to apply to the NACL | `map(string)` | n/a | yes |
16+
| <a name="input_vpc_id"></a> [vpc\_id](#input\_vpc\_id) | The VPC ID to create the NACL in | `string` | n/a | yes |
17+
| <a name="input_inbound_rules"></a> [inbound\_rules](#input\_inbound\_rules) | The inbound rules to apply to the NACL | <pre>list(object({<br/> cidr_block = string<br/> from_port = number<br/> icmp_code = optional(number, 0)<br/> icmp_type = optional(number, 0)<br/> ipv6_cidr_block = optional(string, null)<br/> protocol = optional(number, -1)<br/> rule_action = string<br/> rule_number = number<br/> to_port = number<br/> }))</pre> | `[]` | no |
18+
| <a name="input_outbound_rules"></a> [outbound\_rules](#input\_outbound\_rules) | The outbound rules to apply to the NACL | <pre>list(object({<br/> cidr_block = string<br/> from_port = number<br/> icmp_code = optional(number, 0)<br/> icmp_type = optional(number, 0)<br/> ipv6_cidr_block = optional(string, null)<br/> protocol = optional(number, -1)<br/> rule_action = string<br/> rule_number = number<br/> to_port = number<br/> }))</pre> | `[]` | no |
19+
20+
## Outputs
21+
22+
No outputs.
23+
<!-- END_TF_DOCS -->

modules/nacls/locals.tf

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
locals {
2+
inbound = merge([
3+
for idx in range(var.subnet_count) : {
4+
for rule_idx, rule in try(var.inbound_rules, []) :
5+
"${idx}-${rule_idx}" => {
6+
id = var.subnet_ids[idx]
7+
rule = rule
8+
}
9+
}
10+
]...)
11+
12+
outbound = merge([
13+
for idx in range(var.subnet_count) : {
14+
for rule_idx, rule in try(var.outbound_rules, []) :
15+
"${idx}-${rule_idx}" => {
16+
id = var.subnet_ids[idx]
17+
rule = rule
18+
}
19+
}
20+
]...)
21+
}

0 commit comments

Comments
 (0)