Skip to content

Firewall Rules Fundamentals — The Gatekeeper of VPC Networking

Executive Summary

GCP firewall rules = stateful packet filters at VPC level (not VM level like AWS Security Groups).

Key characteristics:

  • ✅ Stateful: bidirectional connection tracking
  • ✅ Priority-based: 0-65535 (lower = higher priority)
  • ✅ Asymmetric: ingress = implied deny, egress = implied allow
  • ✅ Tag/SA-based: target VMs by network tags or service accounts
  • ❌ Complex: can affect 10K+ VMs in single misconfiguration

Firewall Rule Components

Each rule specifies:

yaml
Direction: INGRESS | EGRESS
Priority: 0-65535 (lower = matches first)
Name: descriptive (firewall-rule-name)
Network: which VPC
Enabled: true/false

# Matching conditions:
Protocol/Port: tcp:443, udp:53, icmp, all
Source Ranges: [INGRESS] - who sends (10.0.0.0/8, 0.0.0.0/0)
Destination Ranges: [EGRESS] - who receives (10.0.0.0/8, 8.8.8.8/32)

# Target specification:
Target Tags: [tag1, tag2] - VMs with these network tags
Target Service Accounts: [sa@project.iam.gserviceaccount.com] - VMs with this SA
# Note: Cannot mix tags + SAs in same rule

# Action:
Allow | Deny

# Source/Destination criteria:
Source Tags: [INGRESS] - if source is VMs with these tags
Source Service Accounts: [INGRESS] - if source VM has this SA
# (Only for ingress! Sources are always external for egress)

Implied Rules

GCP automatically creates two implied rules for each VPC:

Implied Rule 1 (INGRESS):
  Direction: INGRESS
  Priority: 65534
  Action: DENY
  Effect: Deny all ingress traffic by default
  
  Implication: Must explicitly allow every inbound connection

Implied Rule 2 (EGRESS):
  Direction: EGRESS
  Priority: 65534
  Action: ALLOW
  Effect: Allow all egress traffic by default
  
  Implication: Egress must be explicitly denied (opposite of ingress!)

This creates fundamental asymmetry:

Default VPC with no rules:

VM cannot receive incoming packets (DENY by default)
VM can send outgoing packets (ALLOW by default)

To enable web server:
  Step 1: Add INGRESS allow rule tcp:443 from 0.0.0.0/0

To disable outgoing internet:
  Step 1: Add EGRESS deny rule to 0.0.0.0/0
  Step 2: Add EGRESS allow rules for specific destinations

Priority Mechanism

Rules evaluated by priority (0 = highest, 65535 = lowest):

Rules in VPC:

Rule Name         Priority  Direction  Protocol  Action
allow-ssh         100       INGRESS    tcp:22    ALLOW
deny-ssh-from-x   200       INGRESS    tcp:22    DENY
allow-http        1000      INGRESS    tcp:80    ALLOW
(implied rule)    65534     INGRESS    *         DENY

Packet arrives: tcp:22 from external IP

Step 1: Match against priority 100 (allow-ssh)
  Source: 0.0.0.0/0, Dest: internal
  Protocol: tcp:22
  → MATCHES ✓ Action: ALLOW
  
Rule matching stops here (first match wins)
Priority 200 (deny-ssh) ignored

Result: Packet ALLOWED despite deny-ssh-from-x rule

Key principle: Lower priority number = evaluated first

Tricky Case: Same Priority, Multiple Matches

Rule A: Priority 1000, allow tcp:22 from 10.0.0.0/8
Rule B: Priority 1000, deny tcp:22 from 10.0.1.0/16

Packet: tcp:22 from 10.0.1.5

Both rules match priority 1000
Which rule applies?

Answer: UNDEFINED (GCP uses internal algorithm)
Result: Behavior unpredictable

Best practice: Use unique priorities when possible

Ingress Rules: Explicit Allow Required

Network "default-vpc" with no custom rules:

VM "web-server" (tags: web)
  ├── Receives: tcp:443 from internet
  │   Firewall check: Any allow rule for ingress tcp:443?
  │   → NO (only implied deny at priority 65534)
  │   → DROPPED ✗

Add rule:
gcloud compute firewall-rules create allow-https \
  --network=default-vpc \
  --direction=INGRESS \
  --priority=1000 \
  --action=ALLOW \
  --source-ranges=0.0.0.0/0 \
  --target-tags=web \
  --allow=tcp:443

Now same packet:
  ├── Receives: tcp:443 from internet
  │   Firewall check: Allow rule priority 1000 matches?
  │   → YES (allow-https)
  │   → ALLOWED ✓

Egress Rules: Explicit Deny Required

Network "prod-vpc" with no custom rules:

VM "app-server" (tags: app)
  ├── Sends: tcp:443 to 8.8.8.8 (external)
  │   Firewall check: Any deny rule for egress tcp:443 to 8.8.8.8?
  │   → NO (only implied allow at priority 65534)
  │   → ALLOWED ✓ (exits VPC)

Add rule to disable egress:
gcloud compute firewall-rules create deny-external-egress \
  --network=prod-vpc \
  --direction=EGRESS \
  --priority=900 \
  --action=DENY \
  --destination-ranges=0.0.0.0/0 \
  --target-tags=app

Now same packet:
  ├── Sends: tcp:443 to 8.8.8.8
  │   Firewall check: Deny rule priority 900 matches?
  │   → YES (deny-external-egress)
  │   → DENIED ✗ (blocked)

Must add explicit allow for permitted destinations:
gcloud compute firewall-rules create allow-internal-egress \
  --network=prod-vpc \
  --direction=EGRESS \
  --priority=850 \
  --action=ALLOW \
  --destination-ranges=10.0.0.0/8 \
  --target-tags=app

Stateful Connection Tracking

GCP firewall is stateful: if allow inbound, return traffic is automatically allowed:

VM "web-server" (10.0.1.5) handles inbound request from client (203.0.113.5)

Request packet:
  Source: 203.0.113.5:54321
  Dest: 10.0.1.5:443
  Firewall rule: allow ingress tcp:443 from 0.0.0.0/0
  → ALLOWED ✓
  
  Connection tracked: (203.0.113.5:54321 ↔ 10.0.1.5:443)

Response packet (sent by web-server):
  Source: 10.0.1.5:443
  Dest: 203.0.113.5:54321
  Firewall rule: no explicit allow needed!
  Connection state: ESTABLISHED (tracked from request)
  → ALLOWED ✓ (via state table, not rule matching)

Implication: Return traffic doesn't need explicit egress allow (tracking handles it)

Connection Limit: 130K per vCPU

GCP soft-enforces connection tracking limit:
  130,000 concurrent connections per vCPU

VM with 4 vCPUs:
  Max connections: 4 × 130K = 520K

Exceeding limit:
  → Connection tracking table overflows
  → New connections are DROPPED (silently)
  → Symptoms:
    - "Connection timeout" errors for new clients
    - Old connections still work
    - No firewall denial log (it's a resource exhaustion)

Mitigation:
  - Horizontal scaling (more VMs)
  - Connection pooling (fewer connections per client)
  - Stateless protocol (HTTP/1.1 keep-alive, HTTP/2)

Network Tags: Target Specification

Network tags = labels on VMs for firewall targeting:

VM "api-server" with tags:
  ├── tag:api-server
  ├── tag:backend
  └── tag:production

Firewall rule:
gcloud compute firewall-rules create allow-api \
  --direction=INGRESS \
  --priority=1000 \
  --action=ALLOW \
  --source-ranges=0.0.0.0/0 \
  --target-tags=api-server \
  --allow=tcp:8080

Effect: Only VMs with tag "api-server" receive traffic
  ├── api-server: tag:api-server present → ALLOWED ✓
  ├── other-server: tag:api-server missing → DENIED ✗
  └── another-server: tag:api-server missing → DENIED ✗

Limitation: Tags are case-sensitive, no wildcards:

Tag: api-server
Firewall rule --target-tags: api-server ✓ (match)
Firewall rule --target-tags: API-server ✗ (no match)
Firewall rule --target-tags: api-* ✗ (wildcards not supported)

Service Accounts: RBAC-based Targeting

Service accounts = IAM-integrated targeting:

Service Account: backend-sa@project.iam.gserviceaccount.com

VM "api-1" assigned to: backend-sa ✓
VM "api-2" assigned to: backend-sa ✓
VM "web-1" assigned to: frontend-sa

Firewall rule:
gcloud compute firewall-rules create allow-from-backend \
  --direction=INGRESS \
  --priority=1000 \
  --action=ALLOW \
  --source-service-accounts=backend-sa@project.iam.gserviceaccount.com \
  --target-tags=database \
  --allow=tcp:3306

Effect: Only VMs with backend-sa can connect to database VMs
  ├── api-1 (backend-sa): → ALLOWED ✓
  ├── api-2 (backend-sa): → ALLOWED ✓
  └── web-1 (frontend-sa): → DENIED ✗

Advantage over tags: IAM-enforced (cannot add/remove tags without permission)

Priority Strategy: Organizing Complex Rules

Best practice for large VPCs:

Priority ranges:

0-999: Emergency/security rules
  - rule-001: DDoS protection rules
  - rule-010: Zero-day mitigation

1000-2999: Critical application rules
  - rule-1000: allow-ingress-https
  - rule-1100: allow-loadbalancer-health-check
  - rule-1500: allow-gke-pods

3000-9999: Standard application rules
  - rule-3000: allow-internal-communication
  - rule-5000: allow-app-database

10000-59999: Infrastructure rules
  - rule-10000: allow-cloudflare-cdn
  - rule-20000: allow-monitoring

60000-64999: Deny rules (lower priority)
  - rule-60000: deny-external-database-access
  - rule-61000: deny-egress-to-internet

65534: (Implied deny/allow - system)
65535: (Unused - reserved)

Benefit:
  - New rules can slot in between
  - Clear separation of concerns
  - Easy to override with higher priority

Common Patterns

Pattern 1: Allow Health Checks

GCP health checks use source ranges:

  • IPv4: 35.191.0.0/16, 130.211.0.0/22
  • IPv6: 2600:2d00:1:1::/64
gcloud compute firewall-rules create allow-health-checks \
  --direction=INGRESS \
  --priority=1000 \
  --action=ALLOW \
  --source-ranges=35.191.0.0/16,130.211.0.0/22 \
  --target-tags=gke-node \
  --allow=tcp:10250

Pattern 2: Deny All Except Specific

# Step 1: Create default deny
gcloud compute firewall-rules create deny-all-ingress \
  --direction=INGRESS \
  --priority=65000 \
  --action=DENY \
  --allow=tcp,udp

# Step 2: Add allow rules with higher priority
gcloud compute firewall-rules create allow-https \
  --direction=INGRESS \
  --priority=1000 \
  --action=ALLOW \
  --source-ranges=0.0.0.0/0 \
  --allow=tcp:443

# Step 3: Add more allow rules
gcloud compute firewall-rules create allow-ssh \
  --direction=INGRESS \
  --priority=1100 \
  --action=ALLOW \
  --source-ranges=203.0.113.0/24 \
  --allow=tcp:22

Pattern 3: Multi-tier Architecture

rule-1000: allow ingress https from internet to tier1 (LB)
  source: 0.0.0.0/0
  target-tags: tier1-lb
  allow: tcp:443

rule-1100: allow ingress http from tier1 to tier2 (app)
  source-tags: tier1-lb
  target-tags: tier2-app
  allow: tcp:8080

rule-1200: allow ingress mysql from tier2 to tier3 (db)
  source-tags: tier2-app
  target-tags: tier3-database
  allow: tcp:3306

rule-60000: deny all other
  action: DENY

Troubleshooting Firewall Issues

Symptom: Connection Timeout

Diagnosis:

1. Check rule exists and enabled:
   gcloud compute firewall-rules list --filter="name:rule-name"
   
2. Check priority doesn't conflict:
   gcloud compute firewall-rules list \
     --sort-by=priority
   
   Look for DENY rule with lower priority blocking traffic
   
3. Check source/destination ranges:
   gcloud compute firewall-rules describe allow-https \
     --format="table(sourceRanges, targetTags)"
   
4. Check rule targets correct VMs:
   gcloud compute instances describe vm1 \
     --format="table(tags.items)"
   
   Does VM have correct tag?
   
5. Check rule is INGRESS (not EGRESS):
   gcloud compute firewall-rules describe allow-https \
     --format="value(direction)"

Symptom: Asymmetric Connectivity

Problem: VM A → VM B works, but B → A fails

Causes:
1. Reverse traffic blocked by EGRESS deny rule in B
2. Return traffic mismatch (different protocols)

Solution:
  - Check EGRESS rules on B (should be ALLOW by default)
  - Verify rule uses bidirectional protocol (tcp, not separate in/out)
  - Test with non-stateful protocol (ICMP) first

Conclusion

Firewall rules are powerful but require disciplined design:

Do:

  • Use unique priorities
  • Document rule purpose
  • Test before production
  • Use tags/SAs for targeting
  • Monitor firewall logs

Don't:

  • Rely on implied rules (document explicitly)
  • Mix tags + SAs in same rule
  • Create overlapping deny/allow with same priority
  • Allow all sources (0.0.0.0/0) without filtering

Firewall is your last line of defense. Get it wrong = entire VPC exposed.