GCP Cloud Armor DDoS Protection: Adaptive Protection and Real Attack Mitigation
Implementing Cloud Armor for DDoS protection with adaptive protection, custom WAF rules, and real-world attack mitigation data from production incidents.

In the past 12 months, our platform absorbed 47 distinct DDoS attacks ranging from simple volumetric floods to sophisticated application-layer attacks. Cloud Armor handled most of them automatically — but the ones it didn't taught us more about defense architecture than any documentation could. Here's what production DDoS defense actually looks like on GCP.
The Problem: Growing Attack Surface
Our SaaS platform serves 200K+ daily active users through a global HTTP(S) load balancer. As we grew, we became a target. The first attack came 6 weeks after reaching $1M ARR — a 15Gbps volumetric flood that took us offline for 23 minutes because we had no protection configured.
After that incident, we implemented a layered Cloud Armor strategy that has maintained 99.99% availability through 47 subsequent attacks.
Cloud Armor Architecture
Cloud Armor operates at the edge of Google's network, filtering traffic before it reaches your backend:
Three protection layers:
- Network DDoS protection (always on): Google's network absorbs volumetric attacks automatically. No configuration needed.
- Cloud Armor security policies: Custom rules for application-layer filtering (WAF).
- Adaptive Protection: ML-based anomaly detection that creates rules automatically during attacks.
Adaptive Protection Configuration
Adaptive Protection is Cloud Armor's ML-based system that learns your normal traffic patterns and alerts on anomalies:
# Enable Adaptive Protection on your security policy
gcloud compute security-policies update web-policy \
--enable-layer7-ddos-defense \
--layer7-ddos-defense-rule-visibility=STANDARD \
--global
# Configure alert thresholds
gcloud compute security-policies update web-policy \
--layer7-ddos-defense-threshold-config='{
"autoDeployLoadThreshold": 0.8,
"autoDeployConfidenceThreshold": 0.7,
"autoDeployImpactedBaselineThreshold": 0.01,
"autoDeployExpirationSec": 7200
}' \
--global
When Adaptive Protection detects an attack, it:
- Identifies the attack signature (source IPs, request patterns, headers)
- Generates a proposed rule that would block the attack traffic
- Optionally auto-deploys the rule (if configured) or alerts for manual review
Security Policy Rules
Our production security policy has evolved through 47 attacks into this configuration:
# Create the security policy
gcloud compute security-policies create web-policy \
--description="Production web application protection" \
--global
# Rule 1: Block known bad actors (highest priority)
gcloud compute security-policies rules create 1000 \
--security-policy=web-policy \
--src-ip-ranges="198.51.100.0/24,203.0.113.0/24" \
--action=deny-403 \
--description="Known malicious IP ranges" \
--global
# Rule 2: Rate limiting per IP
gcloud compute security-policies rules create 2000 \
--security-policy=web-policy \
--expression="true" \
--action=throttle \
--rate-limit-threshold-count=100 \
--rate-limit-threshold-interval-sec=60 \
--conform-action=allow \
--exceed-action=deny-429 \
--enforce-on-key=IP \
--description="Global rate limit: 100 req/min per IP" \
--global
# Rule 3: Block requests without valid User-Agent
gcloud compute security-policies rules create 3000 \
--security-policy=web-policy \
--expression="!has(request.headers['user-agent']) || request.headers['user-agent'] == ''" \
--action=deny-403 \
--description="Block empty User-Agent" \
--global
# Rule 4: Geographic restrictions (block high-attack-origin countries)
gcloud compute security-policies rules create 4000 \
--security-policy=web-policy \
--expression="origin.region_code == 'XX' || origin.region_code == 'YY'" \
--action=deny-403 \
--description="Geo-block non-served regions" \
--global
# Rule 5: WAF rules - SQL injection and XSS
gcloud compute security-policies rules create 5000 \
--security-policy=web-policy \
--expression="evaluatePreconfiguredExpr('sqli-v33-stable') || evaluatePreconfiguredExpr('xss-v33-stable')" \
--action=deny-403 \
--description="OWASP SQLi and XSS protection" \
--global
# Default rule: Allow all other traffic
gcloud compute security-policies rules update 2147483647 \
--security-policy=web-policy \
--action=allow \
--global
Real Attack Data
Here's a summary of the 5 most significant attacks we absorbed:
| Attack # | Type | Peak Volume | Duration | Mitigation Time | Impact |
|---|---|---|---|---|---|
| 1 | Volumetric (UDP flood) | 15 Gbps | 4 hours | 23 min (no protection) | Full outage |
| 12 | HTTP flood | 2.8M RPS | 2 hours | 0 sec (auto) | Zero |
| 23 | Slowloris | 50K connections | 6 hours | 45 sec (adaptive) | 2s latency spike |
| 31 | API abuse | 180K RPS | 12 hours | 3 min (manual rule) | Zero after rule |
| 47 | L7 + credential stuffing | 850K RPS | 8 hours | 12 sec (adaptive auto-deploy) | Zero |
The progression from Attack #1 (full outage) to Attack #47 (zero impact, auto-mitigated) represents our defense maturation.
Custom Rule Language (CEL Expressions)
Cloud Armor uses Common Expression Language (CEL) for rule conditions. Complex patterns we use in production:
# Block requests to sensitive endpoints from non-authenticated sources
# (defense in depth — application auth should catch this too)
sensitive_paths = """
request.path.matches('/api/admin/.*') &&
!has(request.headers['x-internal-token'])
"""
# Rate limit by authenticated user (not just IP)
# Prevents credential sharing for DDoS amplification
per_user_rate_limit = """
has(request.headers['authorization']) &&
request.headers['authorization'].startsWith('Bearer ')
"""
# Block known bot signatures targeting login endpoints
bot_signature_block = """
request.path == '/api/auth/login' && (
request.headers['user-agent'].contains('python-requests') ||
request.headers['user-agent'].contains('Go-http-client') ||
request.headers['user-agent'].contains('curl/')
)
"""
Monitoring and Alerting
# Query Cloud Armor logs for blocked requests
gcloud logging read '
resource.type="http_load_balancer" AND
jsonPayload.enforcedSecurityPolicy.outcome="DENY"
' --limit 1000 --format json | \
jq 'group_by(.jsonPayload.enforcedSecurityPolicy.name) |
map({rule: .[0].jsonPayload.enforcedSecurityPolicy.name, count: length})'
We built alerting around three signals:
# Alert: Traffic spike detected (potential attack)
- alert: CloudArmorTrafficSpike
expr: |
rate(loadbalancing_googleapis_com:https_request_count[5m]) >
3 * avg_over_time(loadbalancing_googleapis_com:https_request_count[24h])
for: 2m
labels:
severity: warning
annotations:
summary: "Traffic 3x above 24h average"
# Alert: High deny rate (active attack being blocked)
- alert: CloudArmorHighDenyRate
expr: |
sum(rate(networksecurity_googleapis_com:denied_requests_count[5m])) /
sum(rate(loadbalancing_googleapis_com:https_request_count[5m])) > 0.3
for: 5m
labels:
severity: critical
annotations:
summary: "30%+ of requests being denied - active attack"
# Alert: Adaptive Protection triggered
- alert: CloudArmorAdaptiveTriggered
expr: |
increase(networksecurity_googleapis_com:adaptive_protection_alert_count[5m]) > 0
labels:
severity: critical
annotations:
summary: "Adaptive Protection detected anomaly"
Cost of Protection
| Component | Monthly Cost |
|---|---|
| Security policy (per policy) | $5 |
| Rules (per rule) | $1 |
| Requests evaluated (per 1M) | $0.75 |
| Adaptive Protection | $25/policy |
| WAF preconfigured rules | $5/rule |
| Total (our configuration) | ~$380/month |
At $380/month for protection against attacks that previously caused $12,000+ in downtime costs, the ROI is obvious.
Lessons Learned
Lesson 1: Network-layer DDoS protection is free and always on. You don't need Cloud Armor for volumetric floods — Google's network handles those automatically. Cloud Armor's value is application-layer (L7) protection.
Lesson 2: Adaptive Protection needs 2-3 weeks of baseline data. Don't enable it and expect immediate protection. It needs to learn your traffic patterns before it can detect anomalies.
Lesson 3: Rate limiting by IP is necessary but insufficient. Sophisticated attackers use thousands of IPs. Combine IP-based rate limiting with behavioral analysis (request patterns, header fingerprinting).
Lesson 4: Auto-deploy Adaptive Protection rules in production. We initially required manual approval. Attack #23 (Slowloris) caused a 2-second latency spike during the 45 seconds it took us to approve the rule. Auto-deploy eliminated this for Attack #47.
Lesson 5: Test your rules before production. Cloud Armor's preview mode lets you see what traffic a rule would block without actually blocking it.
Key Takeaways
- Layer your defense: Network DDoS (free) + WAF rules (explicit) + Adaptive Protection (ML-based) + rate limiting (per-IP and per-user).
- Enable Adaptive Protection auto-deploy once you've validated it doesn't produce false positives in your traffic patterns.
- $380/month is cheap insurance. One prevented outage pays for years of Cloud Armor.
- Log everything. Attack forensics are impossible without complete request logs. Enable verbose logging during active attacks.
- Practice incident response. Our first attack caused a 23-minute outage. Our most recent attack caused zero impact. The difference is preparation, not technology.
The attacks will come. The question is whether you've built the defense before or after the first outage.
Recommended reading

Per-Team Cost Allocation in Shared Kubernetes Clusters: From Chaos to Clarity
Implementing accurate per-namespace cost allocation in multi-tenant Kubernetes clusters, covering request vs. usage attribution, shared resource amortization, and building showback dashboards that drive accountability.

Measuring and Eliminating Toil: From 40% to 12% of Engineering Time
A systematic approach to identifying, measuring, and automating toil—the repetitive operational work that scales linearly with service growth and prevents engineers from doing creative work.

Serverless Postgres in Production: Branching, Scale-to-Zero, and the End of Database Provisioning
Running Neon serverless Postgres in production for 8 months — covering database branching workflows, scale-to-zero economics, connection pooling, and migration from RDS.

Comments
No comments yet. Be the first to share your thoughts.