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:
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 destinationsPriority 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 ruleKey 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 possibleIngress 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=appStateful 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 priorityCommon 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:10250Pattern 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:22Pattern 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: DENYTroubleshooting 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) firstConclusion
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.