Have you ever tried to set up CloudFront WAF rules and Security Groups to allow access only from specific IP addresses? Having the list of these specific IP addresses coded only once. We’ll see how to use Terraform to solve and to automate this task.
The Application
We have a traditional application on AWS, where
a CloudFront distribution handles
the incoming traffic.
Behind it, we have static pages on
S3 and API endpoints
behind Application Load Balancer (ALB).
We use Terraform to manage
production and staging environments, v0.11.11
in our case.
The project is new and not yet public. We allow access to the project only from specific IP addresses of developers and offices. We set up IP filtering at both Cloud Front (WAF rules) and Security Groups levels, depending on the AWS entities.
IP Whitelist Module
Terraform Module
is the standard way to avoid code duplicates in the infrastructure code.
I have the module called ip-whitelist
(in the ip-whitelist
folder) to
hold and export the list of whitelisted IPv4 addresses. It is used everywhere
in the code instead to avoid hard-coded IP addresses (which are subject to change).
Let’s create a module that exports all IP addresses for the white list.
The following .tf
file in ip-whitelist
folder makes it:
output "cidr" {
value = [
"1.2.3.4/32",
"5.6.7.8/32",
//...
]
}
Security Groups
There are many entities, that we create in Terraform. There are several places in an infrastructure, where one uses security groups. Let’s follow an easy strategy:
- create a unique security group per usage
- do not duplicate code
Both statements of the strategy comes from the programming background. The fewer dependencies between modules one has, the easier it will be to update or refactor the scripts in the future. We tend to extract common parts of our programs to avoid duplicates and improve maintainability of the code.
What is the common part of all of those AWS service? Yes, Security Groups. There are Security Groups in a VPC and without a VPC. In both we’d like to reuse the same IP addresses filter list. Security Groups are easy to create with the module above, for example with the following code The module is easy to call from other places of the project:
module "ip-whitelist" {
source = "<relative path to module>ip-whitelist"
}
resource "aws_security_group" "name" {
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["${module.ip-whitelist.cidr}"]
}
//...
}
Let’s switch to the Cloud Front, where WAF rules are used to implement IP whitelists
Cloud Front WAF Rules
CloudFront distribution uses Web Application Firewall (WAF) to limit the access. The main part of WAF configuration in Terraform uses the aws_waf_ipset resource:
resource "aws_waf_ipset" "ipset" {
name = "tfIPSet"
ip_set_descriptors {
type = "IPV4"
value = "192.0.7.0/24"
}
ip_set_descriptors {
type = "IPV4"
value = "10.16.16.0/16"
}
}
The following few more resources configures Web Application Firewall (WAF) to allow connections only from our whitelisted IP addresses:
resource "aws_waf_rule" "wafrule" {
depends_on = ["aws_waf_ipset.ipset"]
name = "${local.cf_waf_rule}"
metric_name = "${local.cf_waf_rule}"
predicates {
data_id = "${aws_waf_ipset.ipset.id}"
negated = false
type = "IPMatch"
}
}
resource "aws_waf_web_acl" "waf_acl" {
depends_on = ["aws_waf_ipset.ipset", "aws_waf_rule.wafrule"]
name = "${local.cf_waf_acl}"
metric_name = "${local.cf_waf_acl}"
default_action {
type = "BLOCK"
}
rules {
action {
type = "ALLOW"
}
priority = 1
rule_id = "${aws_waf_rule.wafrule.id}"
type = "REGULAR"
}
}
As we see, ip_set_descriptors
parameter has type list
,
each element of which is a map with two keys: type
and value
.
The format is different from one we use in the ip-whitelist
module,
Let’s see how we may avoid duplication
List to List of Maps
First idea - let’s convert the existing list of IP addresses into
WAF rules in Terraform by turning every entry
of cidr
list into a map.
Please do not try that way, it does not work, I suppose that the
problem in Terraform 0.11.11
does not make it work. As far as I see, Terraform loses the fact a list item was a map.
An attempt to implement that may fail with an error like that:
Error: module.staging.aws_waf_ipset.name: "ip_set_descriptors.0.type": required field is not set
Error: module.staging.aws_waf_ipset.name: "ip_set_descriptors.0.value": required field is not set
Map of Lists to List
The second approach it to update the
format in my ip-whitelist
module. IP addresses are now written in the
aws_waf_ipset
format, aka as a list of maps. The only missing part - we need the opposite conversion
to implement cidr
output value: We need to convert that list of maps back to
a plain list of CIDR blocks (for Security Groups).
It works! I use the following code:
locals {
wafs = [
{ type = "IPV4", value = "1.2.3.4/32"},
{ type = "IPV4", value = "5.6.7.8/32" },
// ...
]
}
resource "null_resource" "ipv4" {
count = "${length(local.wafs)}"
triggers {
cidr = "${
lookup(local.wafs[count.index], "type") == "IPV4"
? lookup(local.wafs[count.index], "value")
: ""
}"
}
}
output "cidr" {
value = ["${compact(null_resource.ipv4.*.triggers.cidr)}"]
}
output "waf" {
value = ["${local.wafs}"]
}
The module exports waf
variable with WAF ipset rules, and the cidr
variable with IPv4 security groups. IPv6 list can be added similarly.
The conversion from list
of map
to list
I do via null_resource
and count
attribute.
The cidr
block is only IPv4 elements, we need to filter waf
elements.
Let’s take a look at the expression:
lookup(local.wafs[count.index], "type") == "IPV4"
? lookup(local.wafs[count.index], "value")
: ""
We replace incorrect elements with empty strings. Terraform has the
compact
function to remove empty strings from a list.
There is no direct loop function in Terraform 0.11.11.
The null_resource
resource with count
attribute works as the loop.
The last expression null_resource.ipv4.*.triggers.cidr
selects the addresses
as a list.
Conclusion
All sources from the post are available on the GitHub repository. You’ll find a live example and templates to use it in your projects easily.
We’ve seen how to create and share the list of IP addresses between different security groups and WAF rules. It helps to avoid duplicates in the deployment code. Should something change in the company infrastructure, we could easily change only one file in the deployments code to replicate it.
Do you use WAF? Check out the previous post
to see how to configure a Security Group to allow access only from
CloudFront IP addresses.
Sometimes, one needs a if
statement in Terraform. We discuss
the workaround in an older post too.
I code Terraform scripts in IntelliJ IDEA with the fantastic plugin done by a friend of mine: Terraform Support plugin.
comments powered by Disqus