This issue has been assessed as high severity. Review affected configurations immediately.
Google Cloud Armor provides DDoS protection and WAF capabilities for workloads behind Cloud Load Balancing. It is well-integrated into the GCP ecosystem and genuinely effective when configured correctly. The problem is that “correctly” requires deliberate configuration choices — Cloud Armor’s defaults protect against volumetric DDoS but leave application-layer threats largely unaddressed until security policies are explicitly authored and attached.
The result is a consistent pattern: teams deploy Cloud Armor, see it listed as active in the Console, and assume WAF protection is in place. The actual security posture depends on which policies exist, what mode they run in, and whether they are attached to the right backends.
Misconfiguration 1: Preview Mode Left in Production
Cloud Armor security policies support two rule evaluation modes: PREVIEW and DENY. When a rule is in preview mode, matching requests are logged but not blocked. Preview mode is designed for rule tuning — you evaluate whether a rule would generate unacceptable false positives before enforcing it.
The misconfiguration is deploying preconfigured WAF rules in preview mode and leaving them there indefinitely. Logs show SQL injection attempts being “detected,” which creates a false sense of protection. The requests are processed and reach the application unchanged.
Audit command:
gcloud compute security-policies rules list POLICY_NAME \
--format="table(priority, action, preview, description)"
Any rule where PREVIEW: True and ACTION: allow in the output is logging only, not blocking. The fix is explicit:
gcloud compute security-policies rules update PRIORITY \
--security-policy POLICY_NAME \
--action deny-404 \
--no-preview-mode
Replace deny-404 with deny-403 or deny-502 depending on your preferred response code for blocked requests.
Misconfiguration 2: Security Policies Not Attached to Backends
Creating a security policy in Cloud Armor does nothing by itself. The policy must be attached to a backend service. In GCP projects with multiple backend services — load balancers for different application tiers, internal vs external-facing backends — it is straightforward to create and attach a policy for one backend and leave others unprotected.
Audit command — list all backend services and their attached security policies:
gcloud compute backend-services list \
--global \
--format="table(name, securityPolicy.basename())"
Backend services with an empty SECURITY_POLICY column have no Cloud Armor policy attached. If those backends are externally reachable, they receive no WAF or DDoS protection.
Attach a security policy to an unprotected backend:
gcloud compute backend-services update BACKEND_NAME \
--global \
--security-policy POLICY_NAME
For regional backends (Internal Application Load Balancers), use --region REGION instead of --global.
Misconfiguration 3: The 8KB Request Body Inspection Limit
Cloud Armor evaluates request bodies up to 8,192 bytes (8KB). Requests with bodies larger than this limit are inspected only up to the 8KB boundary — content beyond that point is not evaluated by WAF rules. An attacker can exploit this by padding a payload beyond the 8KB threshold, placing the malicious content after the inspection boundary.
This is an architectural constraint, not a misconfiguration in the traditional sense, but its implications require explicit mitigation. The documented bypass places SQL injection or XSS payloads after 8KB of padding content:
POST /api/query HTTP/1.1
Content-Length: 10000
Content-Type: application/json
{"padding": "AAAAAAAAAA..."[continues to ~8200 bytes]..., "query": "' OR 1=1--"}
Cloud Armor inspects the first 8KB and finds only padding. The malicious payload at byte 8200 reaches the application uninspected.
Mitigation — Content-Length blocking rule:
Add a Cloud Armor rule that blocks requests with Content-Length headers exceeding the limit before body inspection:
gcloud compute security-policies rules create 900 \
--security-policy POLICY_NAME \
--expression "int(request.headers['content-length']) > 8192" \
--action deny-413 \
--description "Block request bodies exceeding WAF inspection limit"
This denies requests where the client declares a body larger than the inspection limit. Note: clients can omit Content-Length for chunked transfers — combine this rule with application-layer body size limits in your web framework or API gateway.
Misconfiguration 4: No Preconfigured WAF Rules Enabled
Cloud Armor ships with preconfigured WAF rule sets based on OWASP ModSecurity Core Rule Set: rules for SQL injection, XSS, local file inclusion, remote file inclusion, RCE, session fixation, and scanner detection. These are not enabled by default — they must be added to your security policy explicitly.
Check which preconfigured rules are active:
gcloud compute security-policies describe POLICY_NAME \
--format="yaml(rules)"
A policy with only default rules (typically a single priority: 2147483647 allow-all rule) has no application-layer protection.
Add the OWASP SQL injection and XSS preconfigured rules:
# SQL injection protection (sensitivity level 2 for balanced false-positive rate)
gcloud compute security-policies rules create 1000 \
--security-policy POLICY_NAME \
--expression "evaluatePreconfiguredExpr('sqli-v33-stable', {'sensitivity': 2})" \
--action deny-403 \
--description "OWASP SQLi protection"
# XSS protection
gcloud compute security-policies rules create 1010 \
--security-policy POLICY_NAME \
--expression "evaluatePreconfiguredExpr('xss-v33-stable', {'sensitivity': 2})" \
--action deny-403 \
--description "OWASP XSS protection"
# LFI protection
gcloud compute security-policies rules create 1020 \
--security-policy POLICY_NAME \
--expression "evaluatePreconfiguredExpr('lfi-v33-stable')" \
--action deny-403 \
--description "Local file inclusion protection"
Start with sensitivity level 2 for SQLi and XSS rules. Sensitivity 1 is most permissive (lower false positive rate, lower true positive rate); sensitivity 4 catches more attacks but generates more false positives. Run new rules in preview mode for 48-72 hours before enforcing to validate false positive rates against your production traffic.
Misconfiguration 5: No Rate Limiting
Cloud Armor supports rate limiting rules (rate_based_ban and throttle actions). Without rate limiting, your application backend must handle the full volume of any request flood — Cloud Armor provides DDoS protection at the network layer but will not limit application-layer request rates without an explicit policy rule.
Add a rate limiting rule for API endpoints:
gcloud compute security-policies rules create 2000 \
--security-policy POLICY_NAME \
--expression "request.path.matches('/api/.*')" \
--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 "API rate limit: 100 req/min per IP"
For unauthenticated endpoints (login, password reset), set lower thresholds — 5-10 requests per minute per IP is appropriate for credential submission endpoints.
Ongoing Monitoring
Cloud Armor logs to Cloud Logging by default when a security policy is attached. The key queries for Cloud Monitoring:
# Count blocked requests by rule in the last 24 hours
gcloud logging read \
'resource.type="http_load_balancer" AND jsonPayload.enforcedSecurityPolicy.outcome="DENY"' \
--format="table(jsonPayload.enforcedSecurityPolicy.name, jsonPayload.enforcedSecurityPolicy.priority)" \
--limit 1000
In BigQuery (if log sink configured):
SELECT
jsonPayload.enforcedSecurityPolicy.name AS policy,
jsonPayload.enforcedSecurityPolicy.priority AS rule_priority,
COUNT(*) AS denied_requests,
APPROX_TOP_COUNT(jsonPayload.remoteIp, 5) AS top_source_ips
FROM `PROJECT.DATASET.cloudarmor_*`
WHERE timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 24 HOUR)
AND jsonPayload.enforcedSecurityPolicy.outcome = 'DENY'
GROUP BY 1, 2
ORDER BY denied_requests DESC
Terraform-Based Policy Enforcement
Managing Cloud Armor policies in Terraform prevents configuration drift and enables policy-as-code review:
resource "google_compute_security_policy" "waf_policy" {
name = "waf-policy"
rule {
priority = 1000
action = "deny(403)"
description = "SQLi protection"
match {
expr {
expression = "evaluatePreconfiguredExpr('sqli-v33-stable', {'sensitivity': 2})"
}
}
}
rule {
priority = 2000
action = "throttle"
description = "API rate limit"
match {
expr {
expression = "request.path.matches('/api/.*')"
}
}
rate_limit_options {
rate_limit_threshold {
count = 100
interval_sec = 60
}
conform_action = "allow"
exceed_action = "deny(429)"
enforce_on_key = "IP"
}
}
rule {
priority = 2147483647
action = "allow"
description = "Default allow"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
}
}
resource "google_compute_backend_service" "app_backend" {
# ... other backend config ...
security_policy = google_compute_security_policy.waf_policy.id
}
The depends_on between the backend and the policy is implicit via the security_policy attribute reference — Terraform will create the policy before attaching it.