Chapter 9: Building Real GCP Infrastructure
Topic 58

Firewall Rules and Network Tags

NetworkingSecurity

GCP firewall rules are stateful, attach to a VPC, and — unlike security groups — target VMs not by group membership but by network tag or service account. A rule on hatch-net-host that allows port 443 to instances tagged https-server opens that port on every VM carrying the tag, anywhere in the global VPC, and the return traffic is allowed automatically because the rule is stateful.

The mental shift for an AWS engineer is that the binding is the tag, not a group object the VM belongs to. A network tag is a free-form label any VM can carry, so the firewall surface is only as disciplined as your tag hygiene. Treat tags as a deliberate security control, not as casual metadata, and the model is clean; treat them carelessly and you open ports you never meant to.

Rules Live on the VPC, Target by Tag

A google_compute_firewall attaches to the network and selects targets with target_tags or target_service_accounts, matched against source_ranges or source_tags. A VM picks up a rule simply by carrying the matching network tag — the tag is the binding, not a membership in a group object. Add https-server to a VM's tags and it inherits every rule that targets that tag, across the whole global VPC.

Because the rule lives on the network and Hatch uses Shared VPC, every firewall rule belongs in hatch-net-host. A rule written in hatch-app-prod has no effect on the shared network, which is a common and silent source of "my allow rule isn't doing anything."

A tag-targeted ingress allow on the host VPC
resource "google_compute_firewall" "allow_https" {
  project = "hatch-net-host"
  name    = "allow-https"
  network = google_compute_network.hatch.id

  direction     = "INGRESS"
  source_ranges = ["0.0.0.0/0"]
  target_tags   = ["https-server"]   # any VM carrying this tag is exposed

  allow {
    protocol = "tcp"
    ports    = ["443"]
  }
}

The Implied Defaults

Every VPC ships with two implied rules: default-deny ingress and default-allow egress. Inbound is closed until you write an allow rule, which is the safe direction. Outbound is wide open until you write an explicit deny — nothing blocks egress until you do.

This catches AWS engineers who expect egress to be restricted by default. On GCP a fresh VM can reach the entire internet outbound, so if Hatch's security posture requires locking outbound down, you write explicit deny-egress rules. The implied default will not do it for you.

Priority

Every rule carries a priority from 0 to 65535, defaulting to 1000, and lower wins. A higher-priority deny — a smaller number — beats a lower-priority allow, which is how you carve a narrow exception into a broad rule. Set a deny at priority 100 over an allow at 1000 and the deny takes precedence for the traffic it matches.

Get the numbers backwards and the failure is silent. A broad low-priority allow at 1000 will shadow the narrow high-priority deny you intended if you accidentally gave the deny the larger number — the port stays open and nothing errors. Set priority explicitly on deny rules and document the intended ordering.

Stateful Return Traffic

Because rules are stateful, allowing inbound 443 automatically permits the response back out. You do not write a matching egress rule for the reply — the connection's return path is tracked and allowed. Writing a redundant outbound rule for return traffic does nothing and signals a stateless-ACL mental model that does not apply here.

This trips engineers coming from stateless network ACLs, where you pair an inbound rule with a matching outbound rule for the response. On GCP that pairing is wrong: one stateful rule covers the request and its reply. Only write an egress rule when you genuinely want to govern a new outbound connection, not to permit a response.

Hierarchical Firewall Policies

A google_compute_firewall_policy at the folder or org level applies rules above any individual VPC. The platform team can enforce "deny all RDP from the internet" across every project in the apps/ folder, and an app team cannot override it from inside their own project. This is governance that per-VPC rules cannot provide, because per-VPC rules live where the app team can edit them.

Use hierarchical policies for the non-negotiables — no public SSH, no public RDP — at the apps/ folder, and leave the workload-specific allows as per-VPC rules in hatch-net-host. The hierarchy enforces the floor; the VPC rules handle the specifics.

How a firewall rule binds to a VM
Rule on the VPC
google_compute_firewall attaches to the network, with target_tags or target_service_accounts.
Tag on the VM
A VM carrying the matching network tag picks up the rule — the tag is the binding.
Hierarchical policy
Folder/org rules apply above the VPC; app teams cannot override the org-wide denies.
Network-tag firewall rules vs AWS security groups

GCP firewall rules — live on the VPC, are stateful, and select VMs by network tag or service account. A tag opens a port across the whole global network, and any VM can carry the tag.

AWS security groups — per-ENI objects a VM is a member of, also stateful, but membership is the binding and rules can reference other security groups. The GCP tag is a free-form label any VM can carry, which is looser and easier to over-apply, so treat tags as a deliberate firewall surface, not casual metadata.

Common Mistakes
  • Treating a network tag as harmless metadata and slapping https-server on a VM that should not be exposed — the tag is the firewall binding, so the port opens the instant the tag lands.
  • Expecting egress to be denied by default and being surprised a VM reaches the internet freely — GCP egress is allow-by-default, so you must write explicit deny-egress rules to restrict it.
  • Writing a matching outbound rule for return traffic — rules are stateful, so the redundant egress rule does nothing and signals a stateless-ACL mental model.
  • Getting priority backwards so a broad low-priority allow shadows the narrow high-priority deny you intended — lower number wins, and the open port is silent.
  • Writing firewall rules in a service project under Shared VPC — firewalls live in the host project hatch-net-host, so a rule in hatch-app-prod has no effect.
Best Practices
  • Target rules by target_service_accounts rather than network tags where the workload has a stable SA, because an SA cannot be added as casually as a tag.
  • Keep all firewall rules in the host project and review tag usage as a security surface, not as labels.
  • Use hierarchical firewall policies at the apps/ folder to enforce org-wide denies — no public SSH or RDP — that app teams cannot override.
  • Set priority explicitly on deny rules so a high-priority deny reliably beats lower-priority allows, and document the intended ordering.
  • Write explicit deny-egress rules wherever outbound must be restricted, since GCP allows all egress by default.
Comparable tools AWS security groups and NACLs — the contrasting membership-based model Config Connector ComputeFirewall / ComputeFirewallPolicy gcloud compute firewall-rules — the imperative form

Knowledge Check

How does a specific VM come to be governed by a GCP firewall rule?

  • It carries a network tag (or service account) that the rule targets — the tag is the binding
  • It is explicitly added as a member of a named security group object, and every rule attached to that group then applies to the VM
  • It is listed by internal IP in the rule's target field
  • It inherits every rule in the project automatically

A fresh Hatch VM reaches the public internet outbound with no rule allowing it. Why?

  • Every VPC has an implied default-allow egress; outbound is open until you write an explicit deny
  • The VM inherited an allow-egress rule from the host project
  • Cloud NAT detected the outbound flow and automatically inserted an allow-egress firewall rule to open the path for it
  • Egress is always denied by default and this indicates a misconfiguration

You want a narrow deny to override a broad allow. What must be true of the priority values?

  • The deny needs a lower priority number than the allow, because lower wins
  • The deny needs a higher priority number, because higher numbers take precedence
  • Both must share the same priority so GCP merges them
  • Priority is ignored for deny rules; order in the file decides

Why would the platform team use a hierarchical firewall policy at the apps/ folder?

  • To enforce org-wide denies that app teams cannot override from inside their own projects
  • To make per-VPC rules apply faster by caching them at the folder
  • Because per-VPC rules cannot allow any ingress at all
  • To grant each app team its own editable copy of the network rules, which they can then tune freely inside their own project

You got correct