Skip to content

Instantly share code, notes, and snippets.

@atheiman
Last active January 19, 2024 13:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save atheiman/10a4ff5c63243c9642b324882759e041 to your computer and use it in GitHub Desktop.
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.
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}]'")
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"
}
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
data "aws_partition" "current" {}
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