Networking & Firewalls: AWS vs Azure vs GCP
Coming from AWS/Azure. Focus: the firewall / security-group model, plus the structural VPC differences that change how everything else feels.
1. Structural model (learn this first)
| Concept | AWS | Azure | GCP |
|---|---|---|---|
| VPC scope | Regional | Regional (VNet) | Global — one VPC spans every region |
| Subnet scope | Zonal (1 AZ) | Regional | Regional (spans zones in the region) |
| Routing | Route table per subnet | Route table (UDR) per subnet | Routes are VPC-global, next-hop by instance tag |
Key shift: a GCP VPC is global. A single VPC reaches europe-west1 and
us-central1 over internal IPs with no peering (in AWS you'd need Transit
Gateway / VPC peering). Subnets stay regional (like Azure).
Because subnets auto-create in every region by default, real setups use
auto_create_subnetworks = falseand define subnets explicitly.
2. Firewall model — the core difference
AWS splits firewalling into two layers; GCP collapses it into one.
| Cloud | Layered constructs |
|---|---|
| AWS | Security Groups (stateful, instance-level, allow-only) + NACLs (stateless, subnet-level, allow+deny, ordered) |
| Azure | NSGs (stateful, allow+deny, priority) + ASGs (logical NIC groups) |
| OCI | Security Lists (subnet-level) + NSGs (VNIC-level) |
| GCP | VPC Firewall Rules — one construct doing the job of both AWS layers |
The defining GCP trait
Firewall rules are NOT attached to instances. They live on the VPC network, and you pick which instances they hit via targets:
target_tags— network tags on instancestarget_service_accounts— the instance's identity- or all instances in the network
Source (for ingress) can equally be IP ranges, tags, or service accounts.
Property comparison
| Property | GCP firewall rule | Closest AWS | Closest Azure |
|---|---|---|---|
| Stateful? | Yes (return traffic auto-allowed) | SG yes / NACL no | NSG yes |
| Allow and deny? | Yes | NACL only (SG allow-only) | NSG yes |
| Priority? | Yes, 0–65535, lower wins, default 1000 | NACL rule #s; SG none | NSG 100–4096, lower wins |
| Applied to | Instances via tag / service account | ENI (SG) / subnet (NACL) | NIC and/or subnet |
| Direction | Ingress / egress | Both | Both |
So: as stateful as an AWS SG, but with deny + priority like a NACL/NSG, all in one object, targeted by tag or identity instead of by attachment.
Implied / default rules (all clouds: deny-inbound by default)
Every GCP VPC has two implied rules at priority 65535:
- Implied deny ingress
- Implied allow egress
Same posture as AWS SG and Azure NSG defaults. The GCP default network also
ships default-allow-ssh/-rdp/-icmp/-internal — a reason to build a custom
VPC so nothing is pre-opened.
3. Two GCP superpowers with no clean AWS/Azure twin
1) Identity-based firewalling via service accounts. A rule's source/target can be a service account, not an IP or tag:
"Allow 5432 to instances running as
db-sa@…, only from instances running asapp-sa@…."
Microsegmentation by workload identity. AWS approximates with SG-references-SG; Azure with ASGs; neither ties to a real IAM identity like GCP does.
⚠️ Network tags are not a security boundary — anyone with
compute.instances.setTagscan add a tag and inherit its access. Service-account rules are the hardened choice (changing an instance's SA needs a stop + stronger IAM).
2) Hierarchical Firewall Policies. Rules attached at org / folder level, evaluated before VPC rules — org-wide guardrails. Analogous to AWS Firewall Manager / Org SCP or Azure Firewall Manager / Policy, but native to the firewall. Newer Network Firewall Policies add address groups, FQDN, geo/threat-intel (≈ Azure Firewall L7).
4. Cheat sheet
| You want to… | AWS | Azure | GCP |
|---|---|---|---|
| Stateful instance firewall | Security Group | NSG (on NIC) | Firewall rule w/ target tag/SA |
| Stateless subnet ACL w/ deny | NACL | NSG (on subnet) | Firewall rule w/ priority + deny |
| Group workloads as a rule source | SG-references-SG | ASG | Target/source service account |
| Org-wide network guardrails | Firewall Manager | Firewall Manager / Policy | Hierarchical firewall policy |
| Private admin access (no bastion) | SSM Session Manager | Azure Bastion | IAP TCP forwarding |
| Managed L7 firewall | Network Firewall | Azure Firewall | Network Firewall Policy / Cloud NGFW |
5. Example rule (Terraform)
resource "google_compute_firewall" "allow_pg" {
network = google_compute_network.vpc.id # rule lives on the NETWORK
source_ranges = ["34.x.x.x/29"] # ingress source = IP CIDRs
target_tags = ["pg"] # applies to tagged instances
allow { protocol = "tcp"; ports = ["5432"] } # stateful; default priority 1000
}
AWS eyes: behaves like a Security Group allowing 5432, but instead of
attaching to the instance it targets the pg tag — and it could have been
a deny, which an AWS SG can't express.
SSH without a public bastion: open only Google's IAP range
35.235.240.0/20 (≈ AWS SSM Session Manager / Azure Bastion).
6. "Unlearn from AWS" takeaways
- Stop thinking "attach SG to instance" → write a network rule, target it by tag or identity.
- No separate NACL layer needed — deny + priority already live in the same firewall-rules system.
- Firewall by service-account identity, not IP, wherever possible — spoofing-resistant, unlike tags.
7. Q&A
Q1. What do "stateful" and "stateless" mean? (with example)
Stateful = the firewall keeps a connection-tracking table. Once it allows one direction of a connection, the return traffic is allowed automatically — you never write a rule for the reply.
Stateless = every packet is judged on its own, no memory of the connection. You must explicitly allow both directions (request and reply).
Example — a client hitting a web server on 443
A client at 10.0.0.5:54321 (a random ephemeral source port) connects to a
web server at 10.0.1.9:443.
| Stateful (GCP firewall rule, AWS SG, Azure NSG) | Stateless (AWS NACL) | |
|---|---|---|
| Rule you write | ALLOW ingress tcp/443 | ALLOW ingress tcp/443 and ALLOW egress tcp 1024–65535 |
The reply (443 → 54321) | Auto-allowed (connection is tracked) | Dropped unless you also add the egress rule for the ephemeral range |
| If you forget the reply rule | Still works | Broken — request arrives, response silently dropped |
The classic stateless footgun: you open inbound 443, it "half works," and you're left debugging why responses vanish — because you never opened the outbound ephemeral port range for the replies.
Takeaway: GCP firewall rules are stateful, so you almost never think about return traffic. The only stateless model among the big clouds is the AWS NACL — which is exactly why NACLs feel fiddly.
Q2. How is a firewall "attached" to a service account? (scenario + AWS equivalent)
In GCP, an instance runs as a service account (an IAM identity bound to the
VM). A firewall rule can then match on that identity via
source_service_accounts / target_service_accounts instead of IPs or tags. The
rule applies to whichever instances run as that SA — no IP bookkeeping.
Scenario — app tier may reach the DB, nothing else may
- App VMs run as
app-sa@proj.iam.gserviceaccount.com - DB VMs run as
db-sa@proj.iam.gserviceaccount.com - Goal: only app-tier VMs can reach Postgres (5432) on the DB tier.
# The instances declare their identity:
resource "google_compute_instance" "app" {
# ...
service_account { email = google_service_account.app.email }
}
resource "google_compute_instance" "db" {
# ...
service_account { email = google_service_account.db.email }
}
# The rule matches on identity, not IP or tag:
resource "google_compute_firewall" "db_from_app" {
name = "allow-app-to-db"
network = google_compute_network.vpc.id
direction = "INGRESS"
allow {
protocol = "tcp"
ports = ["5432"]
}
target_service_accounts = [google_service_account.db.email] # applies to DB VMs
source_service_accounts = [google_service_account.app.email] # allowed callers
}
Why it's nice: an autoscaler can spin up 50 new app VMs with brand-new IPs —
they're allowed the instant they boot, because they run as app-sa. No IP lists,
no tag to spoof. Changing a VM's SA requires **stopping it + iam.serviceAccountUser
- compute permissions**, so access is gated by IAM, not by a mutable label.
⚠️ Restriction: you can't mix service accounts and tags in the same rule (no
source_service_accountstogether withsource_tags/target_tags). Pick one model per rule.
AWS equivalent — Security Group referencing another Security Group
AWS has no IAM-identity firewalling, but the same pattern is expressed with one SG referencing another as its source. Membership in the app SG grants access, much like membership-by-service-account in GCP.
resource "aws_security_group" "app" { name = "app-sg" vpc_id = aws_vpc.main.id }
resource "aws_security_group" "db" { name = "db-sg" vpc_id = aws_vpc.main.id }
# DB SG allows 5432 only from instances in the app SG:
resource "aws_security_group_rule" "db_from_app" {
type = "ingress"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_group_id = aws_security_group.db.id # attached to DB instances
source_security_group_id = aws_security_group.app.id # membership = access
}
| GCP | AWS | Azure | |
|---|---|---|---|
| Access is granted by… | IAM service account the VM runs as | SG membership on the ENI | ASG membership on the NIC |
| Rule construct | source/target_service_accounts | source_security_group_id | NSG rule with source = ASG |
| Gated by | IAM permissions (change SA) | EC2 permissions (change SG attach) | Network permissions (change ASG) |
Difference that matters: GCP binds to a real IAM identity (spoofing- resistant, auditable in IAM); AWS/Azure bind to network-group membership on the interface. Same "membership grants access" idea, different trust anchor.