Firewall rules are one of the very first things you should take care of when configuring a network, so I decided to show how to do it based on my experience with GCP and Terraform.
Generally speaking, the fewer IPs, and ports allowed, the better for the sake of security.
First things first. In order to even start to configure rules, you have to create a main.tf
file and add their google_compute_firewall resource.
resource "google_compute_firewall" "rules" {
for_each = var.firewall_rules
project = var.project_id
name = "${var.network_name}-${each.key}"
network = var.network_name
description = each.value.description
priority = each.value.priority
target_tags = lookup(each.value, "tags", null)
allow {
protocol = each.value.protocol
ports = lookup(each.value, "ports", null)
}
source_ranges = try(each.value.source_ip_ranges, [])
}
It’s an official Google resource for Terraform.
To even start to use a provider from Terraform you have to specified it in Terraform code as well like described here -> https://registry.terraform.io/providers/hashicorp/google/latest/docs
I will not focus on explaining how this resource is built, because all information can be found in Terraform documentation here -> https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_firewall
The only thing I want to point out is why I use for_each construction:
for_each = var.firewall_rules
The
for_each
meta-argument accepts a map or a set of strings, and creates an instance for each item in that map or set. Each instance has a distinct infrastructure object associated with it, and each is separately created, updated, or destroyed when the configuration is applied.
Link to full description of for_each in Terraform documentation -> https://developer.hashicorp.com/terraform/language/meta-arguments/for_each
So, now you find out that firewall_rules is a map of rules. Let’s take a look at the maps moving to variables.tf
file and discuss their details.
Allow ICMP
The fundamental rule is to allow ICMP protocol to be able to send ping.
In this example, we allow to ping our network from all Internet. We can restrict it to specific IP addresses if we want to.
"allow-icmp" = {
"protocol" = "icmp"
"priority" = "65534"
"description" = "Allow ICMP."
"source_ip_ranges" = ["0.0.0.0/0"]
}
Now, what is this priority number? Based on GCP documentation 65535 is the lowest possible priority reserved for implied rules. Every VPC network has two implied IPv4 firewall rules. If IPv6 is enabled in a VPC network, the network also has two implied IPv6 firewall rules. The implied rules cannot be removed, but you can override them as long as your rules have a priority higher than 65535. That’s why we set ICMP with priority 65534 in order to overwrite rules with priority 65535.
Check details about implied rules and priorities in GCP documentation -> https://cloud.google.com/firewall/docs/firewalls
Allow SSH
SSH protocol will allow us to connect with instances via SSH.
Btw. remember to configure SSH access in a secure way. Disable connection via password, direct login to root account and enable connection via SSH keygen.
Okay, but let’s get back to Terraform. Here is an example of a firewall rule. What it is doing is basically allowing connection on port 22 only from IP addresses specified as source_ip_ranges.
"allow-ssh" = {
"protocol" = "tcp"
"ports" = ["22"]
"priority" = "1000"
"description" = "Allow SSH communication via VPN."
"source_ip_ranges" = [
"160.16.0.0/24",
"160.16.1.0/24",
"160.16.3.0/24"
]
}
Allow HTTPS
Generally, HTTP protocol shouldn’t be allowed, because the connection can be sniffed as it’s not encrypted. HTTPS connection is more secure, however, it also should be allowed as narrowly as possible.
Let’s talk a little bit about building secure infrastructure architecture.
All instances with an external IP address should be hidden if possible.
For example, if we have a website then instance have an external IP address to make it visible on the Internet for everybody. If you can afford it, you should hide this website behind CDN solutions like eg. Akamai, Cloudflare, or Imperva which are also acting like a WAF. They provide us with a solution like Anti-DDOS protection and other fancy stuff to protect our infrastructure and monitor who is doing what on our side.
If you don’t have money to spend on fancy tools, you can always build your own protection using Nginx or Apache, eg. build a reverse proxy, but it’s for sure a topic for a whole another article and it will not be as good as dedicated tools like Imperva or Akamai.
In our case, an instance is hidden behind Imperva CDN.
We don’t want to give access to our instance from all the Internet, but only to people who access our website via Imperva. Why? Because Imperva is our protector, we can eg. block some IP addresses there or block whole country based on geolocation, so it’s important for us to only accept traffic from encrypted traffic which is HTTPS, and from Imperva IP address ranges.
This is an example of such a firewall rule:
"allow-https-imperva" = {
"protocol" = "tcp"
"ports" = ["443"]
"priority" = "1000"
"tags" = ["https-imperva"]
"description" = "Allow http & https communication only via Imperva."
"source_ip_ranges" = [
"199.83.128.0/21",
"149.126.72.0/21",
"103.28.248.0/22",
"45.64.64.0/22",
"185.11.124.0/22",
"192.230.64.0/18",
"107.154.0.0/16",
"45.60.0.0/16",
"45.223.0.0/16"
]
}
Also, HTTPS connection should be allowed for employees via VPN as it’s needed for development tests and so on. That’s why the second rule allowing VPN IPs should be added:
"allow-https-vpn" = {
"protocol" = "tcp"
"ports" = ["443"]
"priority" = "1000"
"tags" = ["https-vpn"]
"description" = "Allow https communication via VPN."
"source_ip_ranges" = [
"172.16.0.0/24",
"172.16.1.0/24",
"172.16.3.0/24"
]
}
Hey, but what are tags?
"tags" = ["https-imperva"]
Let’s say you have a rule that should be attached to specific instances, eg. you have a couple of fronted instances behind Imperva, so you want to use this firewall rule allow-https-imperva for all of them. This is a perfect case for network tags, because now, based on tag, you can connect this firewall rule to other instances which have this tag as well.
If you want to dig deeper in network tags, check GCP documentation -> https://cloud.google.com/vpc/docs/add-remove-network-tags
Allow communication between projects and VPCs
Typical infrastructure architecture in the cloud is a GCP Organisation within which is usually a couple of projects. A single project might contain multiple separate apps, storages, and so on.
Inside projects are VPC networks divided into subnetworks which are basically a virtual version of a physical network, implemented inside of Google’s production network. To provide communication between 2 projects with specific VPC networks and subnetworks you have to add proper firewall rules.
This is how you can configure it:
"allow-example" = {
"protocol" = "all"
"priority" = "1000"
"description" = "Allow communication from Example project - hello VPC."
"source_ip_ranges" = ["10.200.0.0/16"]
}
"allow-example-default" = {
"protocol" = "all"
"priority" = "1000"
"description" = "Allow communication from Example project - default VPC."
"source_ip_ranges" = ["10.160.0.0/10"]
}
These 2 rules allow all communication from the project Example
, where 2 VPCs are located:
- VPC hello with the IP range of subnetwork
10.200.0.0/16
- VPC default with the IP range of subnetwork
10.160.0.0/10
Of course, it’s better to add separate rules only for specific protocols, but it depends on your needs, so I just wanted to show the overall basic version of rules, that you can customize.
Allow communication between other cloud providers
Sometimes you deal with infrastructure that is outside one cloud provider like GCP. Let’s say you want to allow connectivity between GCP and UpCloud networks. To make it possible, you should add IP ranges of the UpCloud private network.
"allow-upcloud" = {
"protocol" = "all"
"priority" = "1000"
"description" = "Allow communication from UpCloud private network."
"source_ip_ranges" = [
"10.2.0.0/16",
]
}
So, we have main.tf
with resource, variables.tf
with map of rules and in order to make it work together we also need outputs.tf
variable "network_name" {
type = string
description = "The name of the network."
}
variable "firewall_rules" {
type = any
description = "The map of firewall rules."
}
variable "project_id" {
type = string
description = <<EOF
The ID of the project in which the resource belongs. If it is not provided, the provider project is used.
EOF
default = null
}
That’s it! This is the basic firewall rules configuration on the GCP project. Usually, in big companies, it’s more complicated, but in my case, this is pretty much all I needed.