Cloud Armor WAF & DDoS Protection

Cloud & Infrastructure intermediate Building with AI ~22 min read

Overview

Google Cloud Armor is a network security service that provides DDoS protection and Web Application Firewall (WAF) capabilities at the edge of Google’s global network. It integrates with HTTP(S) Load Balancers to inspect, filter, and rate-limit incoming traffic before it reaches backend services. For applications exposed to the internet, Cloud Armor is the first line of defense against volumetric attacks, application-layer exploits like SQL injection and cross-site scripting, and automated abuse like credential stuffing and scraping.

Cloud Armor operates in two tiers. The standard tier provides always-on DDoS protection for L3/L4 attacks (SYN floods, UDP amplification) at no additional cost for all HTTP(S) Load Balancer traffic. The managed protection plus tier (Cloud Armor Enterprise, formerly Managed Protection Plus) adds adaptive protection: a machine learning system that profiles normal traffic patterns and automatically detects and mitigates L7 DDoS attacks without requiring manual rule configuration. This is particularly valuable for APIs with unpredictable traffic patterns where static rate limits would either be too permissive during attacks or too restrictive during legitimate traffic spikes.

As a practical example, an infrastructure codebase can provision Cloud Armor security policies for the search API and SaaS management endpoints. These policies satisfy controls around web application security, DDoS resilience, and API abuse prevention. The policies include OWASP Top 10 WAF rules, geo-based access restrictions, rate limiting per client IP, and adaptive protection for production workloads.

Core Concepts

Security Policy

A Cloud Armor security policy is an ordered list of rules that evaluate incoming requests against match conditions and take actions (allow, deny, rate-limit, redirect, or throttle). Rules are evaluated in priority order (lowest number first), and the first matching rule determines the action. Every policy has a default rule at priority 2147483647 that handles unmatched traffic. Policies are attached to backend services of HTTP(S) Load Balancers, meaning different backend services can have different security policies.

Preconfigured WAF Rules

Cloud Armor provides preconfigured rule sets based on the OWASP ModSecurity Core Rule Set (CRS). These detect common attack patterns including SQL injection (sqli-v33-stable), cross-site scripting (xss-v33-stable), local file inclusion (lfi-v33-stable), remote code execution (rce-v33-stable), and protocol attacks. Each rule set can be tuned with sensitivity levels (1–4) to balance detection accuracy against false positive rates. A production setup uses sensitivity level 2 for production APIs, which catches obvious attacks without blocking legitimate search queries that happen to contain SQL-like syntax.

Adaptive Protection

Adaptive Protection is an ML-based system that continuously profiles baseline traffic patterns (request rates, geographic distribution, HTTP headers, URL paths) and alerts when traffic significantly deviates from normal. When it detects an attack, it generates a suggested rule with a confidence score that an operator can approve or that can be auto-deployed above a confidence threshold. This is Cloud Armor’s most powerful feature for L7 DDoS attacks that look like legitimate traffic at the individual request level but are anomalous in aggregate.

Rate Limiting

Cloud Armor supports rate-based rules that throttle or deny clients exceeding a request threshold within a time window. Rate limiting can be applied per source IP, per HTTP header value (e.g., API key), or per a combination of attributes. The rate_limit_threshold defines the count and interval (e.g., 100 requests per 60 seconds), and the conform_action and exceed_action define what happens to requests within and over the limit. This is essential for API abuse prevention without affecting legitimate high-volume clients.

Geo-Based Access Control

Cloud Armor can match requests by source country using the origin.region_code attribute derived from GeoIP data. This allows blocking or allowing traffic from specific countries, which is useful for compliance (data residency requirements), risk reduction (blocking traffic from regions where the service has no users), or attack mitigation (blocking the source country of an ongoing attack). A production setup uses geo-blocking as a supplementary control, not a primary defense, since attackers can route through proxies in allowed regions.

How It Works

The infrastructure project provisions Cloud Armor security policies as Pulumi resources and attaches them to the HTTP(S) Load Balancer backend services that front the search API and SaaS management endpoints.

Security Policy with WAF Rules

The core security policy defines a default deny rule, WAF rules for OWASP Top 10 protection, and specific allow rules for expected traffic patterns.

package infra

import (
    "github.com/pulumi/pulumi-gcp/sdk/v7/go/gcp/compute"
    "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func NewCloudArmorPolicy(ctx *pulumi.Context, env string) (*compute.SecurityPolicy, error) {
    policy, err := compute.NewSecurityPolicy(ctx, env+"-waf-policy", &compute.SecurityPolicyArgs{
        Name:        pulumi.String(env + "-waf-policy"),
        Description: pulumi.String("WAF and DDoS protection for " + env),

        // Enable adaptive protection for L7 DDoS detection
        AdaptiveProtectionConfig: &compute.SecurityPolicyAdaptiveProtectionConfigArgs{
            Layer7DdosDefenseConfig: &compute.SecurityPolicyAdaptiveProtectionConfigLayer7DdosDefenseConfigArgs{
                Enable:    pulumi.Bool(true),
                RuleVisibility: pulumi.String("STANDARD"),
            },
        },

        // Default rule: allow (specific denies are added as higher-priority rules)
        Rules: compute.SecurityPolicyRuleArray{
            // SQL injection protection
            &compute.SecurityPolicyRuleArgs{
                Action:      pulumi.String("deny(403)"),
                Priority:    pulumi.Int(1000),
                Description: pulumi.String("Block SQL injection attempts"),
                Match: &compute.SecurityPolicyRuleMatchArgs{
                    Expr: &compute.SecurityPolicyRuleMatchExprArgs{
                        Expression: pulumi.String(
                            "evaluatePreconfiguredWaf('sqli-v33-stable', {'sensitivity': 2})",
                        ),
                    },
                },
            },
            // XSS protection
            &compute.SecurityPolicyRuleArgs{
                Action:      pulumi.String("deny(403)"),
                Priority:    pulumi.Int(1001),
                Description: pulumi.String("Block cross-site scripting attempts"),
                Match: &compute.SecurityPolicyRuleMatchArgs{
                    Expr: &compute.SecurityPolicyRuleMatchExprArgs{
                        Expression: pulumi.String(
                            "evaluatePreconfiguredWaf('xss-v33-stable', {'sensitivity': 2})",
                        ),
                    },
                },
            },
            // Remote code execution protection
            &compute.SecurityPolicyRuleArgs{
                Action:      pulumi.String("deny(403)"),
                Priority:    pulumi.Int(1002),
                Description: pulumi.String("Block remote code execution attempts"),
                Match: &compute.SecurityPolicyRuleMatchArgs{
                    Expr: &compute.SecurityPolicyRuleMatchExprArgs{
                        Expression: pulumi.String(
                            "evaluatePreconfiguredWaf('rce-v33-stable', {'sensitivity': 2})",
                        ),
                    },
                },
            },
            // Local file inclusion protection
            &compute.SecurityPolicyRuleArgs{
                Action:      pulumi.String("deny(403)"),
                Priority:    pulumi.Int(1003),
                Description: pulumi.String("Block local file inclusion attempts"),
                Match: &compute.SecurityPolicyRuleMatchArgs{
                    Expr: &compute.SecurityPolicyRuleMatchExprArgs{
                        Expression: pulumi.String(
                            "evaluatePreconfiguredWaf('lfi-v33-stable', {'sensitivity': 2})",
                        ),
                    },
                },
            },
            // Default allow rule
            &compute.SecurityPolicyRuleArgs{
                Action:      pulumi.String("allow"),
                Priority:    pulumi.Int(2147483647),
                Description: pulumi.String("Default allow rule"),
                Match: &compute.SecurityPolicyRuleMatchArgs{
                    VersionedExpr: pulumi.String("SRC_IPS_V1"),
                    Config: &compute.SecurityPolicyRuleMatchConfigArgs{
                        SrcIpRanges: pulumi.StringArray{pulumi.String("*")},
                    },
                },
            },
        },
    })
    if err != nil {
        return nil, err
    }

    return policy, nil
}

Rate Limiting Rules

Rate limiting is configured per endpoint pattern. The search API gets a higher limit than auth endpoints, which are frequent targets for credential stuffing.

func NewRateLimitRule(ctx *pulumi.Context, policyName pulumi.StringOutput) error {
    // Rate limit auth endpoints: 20 requests per minute per IP
    _, err := compute.NewSecurityPolicyRule(ctx, "rate-limit-auth", &compute.SecurityPolicyRuleArgs{
        SecurityPolicy: policyName,
        Action:         pulumi.String("rate_based_ban"),
        Priority:       pulumi.Int(500),
        Description:    pulumi.String("Rate limit authentication endpoints"),
        Match: &compute.SecurityPolicyRuleMatchArgs{
            Expr: &compute.SecurityPolicyRuleMatchExprArgs{
                Expression: pulumi.String(
                    "request.path.matches('/api/v1/auth/.*')",
                ),
            },
        },
        RateLimitOptions: &compute.SecurityPolicyRuleRateLimitOptionsArgs{
            ConformAction: pulumi.String("allow"),
            ExceedAction:  pulumi.String("deny(429)"),
            RateLimitThreshold: &compute.SecurityPolicyRuleRateLimitOptionsRateLimitThresholdArgs{
                Count:       pulumi.Int(20),
                IntervalSec: pulumi.Int(60),
            },
            BanThreshold: &compute.SecurityPolicyRuleRateLimitOptionsBanThresholdArgs{
                Count:       pulumi.Int(100),
                IntervalSec: pulumi.Int(120),
            },
            BanDurationSec: pulumi.Int(600),
            EnforceOnKey:   pulumi.String("IP"),
        },
    })
    if err != nil {
        return err
    }

    // Rate limit search API: 300 requests per minute per IP
    _, err = compute.NewSecurityPolicyRule(ctx, "rate-limit-search", &compute.SecurityPolicyRuleArgs{
        SecurityPolicy: policyName,
        Action:         pulumi.String("throttle"),
        Priority:       pulumi.Int(501),
        Description:    pulumi.String("Rate limit search API requests"),
        Match: &compute.SecurityPolicyRuleMatchArgs{
            Expr: &compute.SecurityPolicyRuleMatchExprArgs{
                Expression: pulumi.String(
                    "request.path.matches('/api/v1/search/.*')",
                ),
            },
        },
        RateLimitOptions: &compute.SecurityPolicyRuleRateLimitOptionsArgs{
            ConformAction: pulumi.String("allow"),
            ExceedAction:  pulumi.String("deny(429)"),
            RateLimitThreshold: &compute.SecurityPolicyRuleRateLimitOptionsRateLimitThresholdArgs{
                Count:       pulumi.Int(300),
                IntervalSec: pulumi.Int(60),
            },
            EnforceOnKey: pulumi.String("IP"),
        },
    })
    return err
}

Geo-Based Blocking

For services that only operate in specific regions, geo-blocking reduces the attack surface by rejecting traffic from countries where the service has no users.

func NewGeoBlockRule(ctx *pulumi.Context, policyName pulumi.StringOutput, blockedRegions []string) error {
    // Build the CEL expression for geo-blocking
    // Example: origin.region_code == 'CN' || origin.region_code == 'RU'
    expr := ""
    for i, region := range blockedRegions {
        if i > 0 {
            expr += " || "
        }
        expr += "origin.region_code == '" + region + "'"
    }

    _, err := compute.NewSecurityPolicyRule(ctx, "geo-block", &compute.SecurityPolicyRuleArgs{
        SecurityPolicy: policyName,
        Action:         pulumi.String("deny(403)"),
        Priority:       pulumi.Int(100),
        Description:    pulumi.String("Block traffic from restricted regions"),
        Match: &compute.SecurityPolicyRuleMatchArgs{
            Expr: &compute.SecurityPolicyRuleMatchExprArgs{
                Expression: pulumi.String(expr),
            },
        },
    })
    return err
}

Implementation Patterns

Staged Rollout with Preview Mode

A production setup uses a staged approach when deploying new WAF rules. New rules are first deployed in preview mode (preview: true), which logs matches without blocking traffic. The team monitors Cloud Logging for false positives over a 48-hour window, tunes sensitivity levels or adds exception rules, and then switches to enforcement mode. This prevents legitimate traffic from being blocked when new rules are deployed.

Custom Header Injection for Debugging

Cloud Armor can insert custom headers into requests that match specific rules. A production setup adds an X-Cloud-Armor-Rule header to throttled requests, which backend services log for debugging. When a user reports being rate-limited, the ops team can search for the header value to identify which rule triggered and adjust the threshold if the traffic was legitimate.

Per-Environment Policy Differentiation

Development environments use a permissive policy (WAF rules in preview mode, no rate limiting, no geo-blocking) to avoid slowing down testing. Staging mirrors production rules exactly to catch false positives before they hit real users. Production runs with full enforcement, adaptive protection, and rate limiting. This tiering is driven by the Pulumi stack configuration.

Practical Applications

Cloud Armor can secure the externally facing services in a cloud-native platform:

Common Pitfalls

Further Reading