Last active
January 19, 2024 13:20
-
-
Save atheiman/10a4ff5c63243c9642b324882759e041 to your computer and use it in GitHub Desktop.
Terraform to deploy a prefix list representing all CIDRs outside a given list of CIDRs. The use case for this is to create a security group that allows all traffic to/from CIDRs outside a VPC.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from ipaddress import IPv4Network, IPv4Address, summarize_address_range | |
import json | |
import os | |
def lambda_handler(event, context): | |
print(json.dumps(event)) | |
# Basic event validation | |
if "cidrs" not in event or not isinstance(event["cidrs"], list) or len(event["cidrs"]) < 1: | |
error_msg = f"Lambda function input object should include 'cidrs' array of strings" | |
print("Error:", error_msg) | |
raise Exception(error_msg) | |
resp = {"external_cidrs": external_cidrs_calculator(event["cidrs"])} | |
print(json.dumps(resp)) | |
return resp | |
def external_cidrs_calculator(cidr_strs): | |
# Sort cidrs | |
networks = sorted([IPv4Network(c) for c in cidr_strs]) | |
# Ensure cidrs are continuous (last_ip + 1 == next_first_ip) | |
for idx, network in enumerate(networks): | |
if idx == len(networks) - 1: | |
# skip validation on last network | |
break | |
last_ip = network[-1] | |
next_first_ip = networks[idx + 1][0] | |
if last_ip + 1 != next_first_ip: | |
error_msg = f"Network CIDRs must be continuous: {network} (last ip {last_ip}), {networks[idx + 1]} (first ip {next_first_ip})" | |
print("Error:", error_msg) | |
raise Exception(error_msg) | |
# List of strings less than input list of cidrs | |
external_cidrs = [str(c) for c in summarize_address_range(IPv4Address("0.0.0.0"), networks[0][0] - 1)] | |
# List of strings greater than input list of cidrs | |
external_cidrs += [str(c) for c in summarize_address_range(networks[-1][-1] + 1, IPv4Address("255.255.255.255"))] | |
return external_cidrs | |
if __name__ == "__main__": | |
external_cidrs = external_cidrs_calculator(os.environ["CIDR"].split(",")) | |
print(external_cidrs) | |
print("aws ec2 create-security-group --vpc-id VPC-1234 --group-name OpenExternalToVpc --description 'Allows all traffic to/from addresses outside the VPC'") | |
ip_ranges = ",".join([f"{{CidrIp={c}}}" for c in external_cidrs]) | |
print(f"aws ec2 authorize-security-group-ingress --group-id SG-1234 --ip-permissions 'IpProtocol=-1,FromPort=-1,ToPort=-1,IpRanges=[{ip_ranges}]'") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
resource "aws_iam_role" "lambda" { | |
assume_role_policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Action = "sts:AssumeRole" | |
Principal = { | |
Service = "lambda.amazonaws.com" | |
} | |
}, | |
] | |
}) | |
managed_policy_arns = ["arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"] | |
inline_policy {} | |
} | |
data "archive_file" "lambda" { | |
type = "zip" | |
source_file = "${path.module}/external_cidrs_calculator.py" | |
output_path = "${path.module}/lambda.zip" | |
} | |
resource "aws_cloudwatch_log_group" "lambda_external_cidrs_calculator" { | |
name = "/aws/lambda/external-cidrs-calculator" | |
retention_in_days = 30 | |
} | |
resource "aws_lambda_function" "external_cidrs_calculator" { | |
# Ensure log group is created and managed by terraform before lambda creates the log group | |
function_name = trimprefix(aws_cloudwatch_log_group.lambda_external_cidrs_calculator.name, "/aws/lambda/") | |
description = "Given a list of continuous CIDRs, returns a list of CIDRs representing all IPs not included in the input" | |
role = aws_iam_role.lambda.arn | |
runtime = "python3.11" | |
filename = data.archive_file.lambda.output_path | |
source_code_hash = data.archive_file.lambda.output_base64sha256 | |
handler = "external_cidrs_calculator.lambda_handler" | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
terraform { | |
required_providers { | |
aws = { | |
source = "hashicorp/aws" | |
version = "~> 5.0" | |
} | |
} | |
} | |
data "aws_partition" "current" {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
locals { | |
vpc_cidrs = [ | |
"10.219.15.240/28", | |
# first: 10.219.15.240 | |
# last: 10.219.15.255 | |
"10.219.16.0/20", | |
# first: 10.219.16.0 | |
# last: 10.219.31.255 | |
"10.219.32.0/20", | |
# first: 10.219.32.0 | |
# last: 10.219.47.255 | |
"10.219.48.0/21", | |
# first: 10.219.48.0 | |
# last: 10.219.55.255 | |
"10.219.56.0/28", | |
# first: 10.219.56.0 | |
# last: 10.219.56.15 | |
] | |
} | |
resource "aws_vpc" "example" { | |
cidr_block = local.vpc_cidrs[0] | |
tags = { | |
Name = "Example" | |
} | |
} | |
resource "aws_vpc_ipv4_cidr_block_association" "secondary_cidr" { | |
for_each = toset(slice(local.vpc_cidrs, 1, length(local.vpc_cidrs))) | |
vpc_id = aws_vpc.example.id | |
cidr_block = each.key | |
} | |
data "aws_lambda_invocation" "external_cidrs_calculator" { | |
function_name = aws_lambda_function.external_cidrs_calculator.function_name | |
input = jsonencode({ cidrs = local.vpc_cidrs }) | |
} | |
output "external_cidrs_calculator_result" { | |
value = jsondecode(data.aws_lambda_invocation.external_cidrs_calculator.result) | |
} | |
resource "aws_ec2_managed_prefix_list" "external" { | |
name = "All CIDR blocks outside vpc ${try(aws_vpc.example.tags.Name, "")} ${aws_vpc.example.id}" | |
address_family = "IPv4" | |
# Include a buffer size for modifications to the number of cidrs. | |
# If this becomes problematic, just set max_entries to 60 (default security group rules limit). | |
#max_entries = 60 | |
max_entries = length(jsondecode(data.aws_lambda_invocation.external_cidrs_calculator.result)["external_cidrs"]) + 10 | |
dynamic "entry" { | |
# Dynamic blocks can use for_each with an unknown length. They can still be difficult to work | |
# with. If you encounter `Error: Provider produced inconsistent final plan`, comment out the | |
# dynamic block to remove all CIDR entries, then re-add. | |
for_each = toset(jsondecode(data.aws_lambda_invocation.external_cidrs_calculator.result)["external_cidrs"]) | |
content { | |
cidr = entry.value | |
# Warning from Terraform docs: Due to API limitations, updating only the description of an | |
# existing entry requires temporarily removing and re-adding the entry. | |
description = substr("Generated with Terraform aws_lambda_invocation ${aws_lambda_function.external_cidrs_calculator.arn}", 0, 255) | |
} | |
} | |
} | |
resource "aws_security_group" "external" { | |
name = "External" | |
description = "All ingress and egress traffic outside this vpc" | |
vpc_id = aws_vpc.example.id | |
tags = { | |
Name = "External" | |
} | |
} | |
resource "aws_vpc_security_group_ingress_rule" "external" { | |
security_group_id = aws_security_group.external.id | |
prefix_list_id = aws_ec2_managed_prefix_list.external.id | |
ip_protocol = "-1" | |
} | |
resource "aws_vpc_security_group_egress_rule" "external" { | |
security_group_id = aws_security_group.external.id | |
prefix_list_id = aws_ec2_managed_prefix_list.external.id | |
ip_protocol = "-1" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment