Skip to content

Commit a9b9f55

Browse files
committed
feat: Add Cloud Armor security policy to block abusive IP ranges from API at load balancer
1 parent 7e0ba56 commit a9b9f55

10 files changed

Lines changed: 168 additions & 0 deletions

File tree

infra/gcp-domains/.auto.tfvars.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,9 @@ domain_mappings = {
3636
# mycity-gov = ["suggest.mycity.gov", "suggest-staging.mycity.gov"]
3737
# bikeshare-com = ["suggest.bikeshare.com"]
3838
# }
39+
40+
# Optional: List of IP CIDR ranges to block at the load balancer level via Cloud Armor
41+
# blocked_ip_ranges = [
42+
# "192.0.2.0/24",
43+
# ]
44+

infra/gcp-domains/backend.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ resource "google_compute_backend_service" "default" {
2424
name = "${each.key}-backend"
2525
protocol = "HTTP"
2626
load_balancing_scheme = "EXTERNAL_MANAGED"
27+
security_policy = length(google_compute_security_policy.ip_blocklist) > 0 ? google_compute_security_policy.ip_blocklist[0].id : null
2728

2829
backend {
2930
group = google_compute_region_network_endpoint_group.serverless_neg[each.key].id

infra/gcp-domains/security.tf

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# ------------------------------------------------------------------------------
2+
# Cloud Armor Security Policy
3+
# Blocks traffic from specified IP CIDR ranges at the Load Balancer level
4+
# ------------------------------------------------------------------------------
5+
6+
resource "google_compute_security_policy" "ip_blocklist" {
7+
count = length(var.blocked_ip_ranges) > 0 ? 1 : 0
8+
name = "${var.load_balancer_name}-ip-blocklist"
9+
description = "Cloud Armor security policy to block abusive IP ranges"
10+
11+
dynamic "rule" {
12+
for_each = length(var.blocked_ip_ranges) > 0 ? [1] : []
13+
content {
14+
action = "deny(403)"
15+
priority = "1000"
16+
match {
17+
versioned_expr = "SRC_IPS_V1"
18+
config {
19+
src_ip_ranges = var.blocked_ip_ranges
20+
}
21+
}
22+
description = "Deny access to specified IP ranges"
23+
}
24+
}
25+
26+
rule {
27+
action = "allow"
28+
priority = "2147483647"
29+
match {
30+
versioned_expr = "SRC_IPS_V1"
31+
config {
32+
src_ip_ranges = ["*"]
33+
}
34+
}
35+
description = "Default rule, allow all traffic"
36+
}
37+
}

infra/gcp-domains/variables.tf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,10 @@ variable "ssl_certs" {
5353
description = "A map of custom SSL certificate groups. Any domains not in this map will be grouped into a default certificate."
5454
default = {}
5555
}
56+
57+
variable "blocked_ip_ranges" {
58+
type = list(string)
59+
description = "List of IP CIDR ranges to block at the load balancer level via Cloud Armor security policy"
60+
default = []
61+
}
62+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-08-01
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## Context
2+
3+
The service infrastructure in `infra/gcp-domains/` manages a central GCP Global External Application Load Balancer using OpenTofu/Terraform. Backend services are defined as `google_compute_backend_service` connected to Cloud Run instances via Serverless Network Endpoint Groups (`google_compute_region_network_endpoint_group`). See `proposal.md` for motivation.
4+
5+
## Goals / Non-Goals
6+
7+
**Goals:**
8+
- Add a GCP Cloud Armor Security Policy (`google_compute_security_policy`) in `infra/gcp-domains/`.
9+
- Configure rule priorities to deny requests matching blocked IP CIDR ranges with a 403 response.
10+
- Attach the security policy to `google_compute_backend_service.default`.
11+
- Allow specifying blocked CIDR ranges via OpenTofu variables (`blocked_ip_ranges`).
12+
13+
**Non-Goals:**
14+
- Application-level IP filtering in Django middleware.
15+
- Dynamic IP blocking database/admin interfaces.
16+
- Advanced Cloud Armor WAF rules (e.g., OWASP top 10 rulesets, reCAPTCHA enterprise) - focus specifically on IP range blocklists.
17+
18+
## Decisions
19+
20+
### Decision 1: GCP Cloud Armor Security Policy over Django Middleware
21+
- **Rationale**: Cloud Armor drops traffic at GCP edge forwarding rules before requests reach Cloud Run, protecting compute resources, memory, and database connections.
22+
- **Alternatives Considered**: Django middleware was rejected because requests would still trigger Cloud Run container invocations and consume backend resources.
23+
24+
### Decision 2: Managed via OpenTofu variables in `infra/gcp-domains/`
25+
- **Rationale**: `infra/gcp-domains/` centralizes domain routing and load balancing backend services. Adding the security policy here keeps infrastructure definitions cohesive and version-controlled.
26+
- **Implementation**:
27+
- Add `blocked_ip_ranges` variable (`list(string)`) in `infra/gcp-domains/variables.tf` (default `[]`).
28+
- Declare `google_compute_security_policy` in `infra/gcp-domains/security.tf` (or `main.tf`).
29+
- Attach `security_policy = google_compute_security_policy.ip_blocklist[0].id` to `google_compute_backend_service.default`.
30+
31+
## Risks / Trade-offs
32+
33+
- **[Risk] Accidental blocking of legitimate users in shared IP ranges****Mitigation**: Specify tight CIDR masks where possible, and document how to update or remove CIDR blocks in `.auto.tfvars`.
34+
- **[Risk] Cloud Armor rule limit (standard tier limits rules per policy)****Mitigation**: Use CIDR ranges (`src_ip_ranges`) to group multiple IPs into concise rule blocks.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
## Why
2+
3+
Abusive HTTP traffic originating from specific IP subnet ranges (e.g., `X.X.X.0/25`) is hitting the service, taking up resources. We need to block abusive IP ranges at the GCP Load Balancer level using Cloud Armor security policies so that unwanted requests are rejected immediately at the edge with a 403 Forbidden status code, protecting Cloud Run compute and database resources.
4+
5+
## What Changes
6+
7+
- Add a GCP Cloud Armor security policy resource in OpenTofu/Terraform (`infra/gcp-domains/`) to block designated IP ranges with a 403 HTTP response.
8+
- Attach the Cloud Armor security policy to the GCP Load Balancer's backend services (`google_compute_backend_service.default`).
9+
- Expose configurable variables for the blocked IP CIDR ranges in `infra/gcp-domains/variables.tf` and `.auto.tfvars`.
10+
11+
## Capabilities
12+
13+
### New Capabilities
14+
15+
- `ip-range-blocking`: Support configuring and enforcing IP range blocklists at the load balancer level via Terraform.
16+
17+
### Modified Capabilities
18+
19+
(None)
20+
21+
## Impact
22+
23+
- **Infrastructure**: Updates OpenTofu/Terraform state and configuration in `infra/gcp-domains/`.
24+
- **GCP Resources**: Provisions a `google_compute_security_policy` in GCP and links it to `google_compute_backend_service`.
25+
- **Traffic**: Requests originating from IPs within blocked CIDR ranges will receive HTTP 403 Forbidden responses at the GCP edge load balancer prior to reaching Cloud Run.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
## Purpose
2+
3+
Configures IP range blocklists at the GCP load balancer level to reject abusive network traffic before it reaches backend services.
4+
5+
## ADDED Requirements
6+
7+
### Requirement: Block Traffic from Specified IP Ranges
8+
The load balancer security policy SHALL deny HTTP requests originating from client IP addresses matching configured CIDR ranges with a 403 Forbidden status code.
9+
10+
#### Scenario: Request from a blocked IP CIDR range
11+
- **WHEN** an HTTP request is received from an IP address within a configured blocked CIDR range
12+
- **THEN** the load balancer immediately responds with HTTP status 403 Forbidden without forwarding the request to Cloud Run
13+
14+
#### Scenario: Request from an unblocked IP address
15+
- **WHEN** an HTTP request is received from an IP address outside the configured blocked CIDR ranges
16+
- **THEN** the load balancer evaluates default rules and forwards legitimate traffic to Cloud Run
17+
18+
### Requirement: Configurable IP Blocklist Ranges via Infrastructure Code
19+
The infrastructure module SHALL accept a configurable list of IP CIDR strings for the load balancer security policy.
20+
21+
#### Scenario: Adding a new IP range to the blocklist
22+
- **WHEN** a new IP CIDR range is added to the Terraform variable `blocked_ip_ranges` and applied
23+
- **THEN** Cloud Armor updates the security policy rule set to block requests from that CIDR range
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## 1. Terraform Configuration for Security Policy
2+
3+
- [x] 1.1 Declare the `blocked_ip_ranges` input variable in `infra/gcp-domains/variables.tf`.
4+
- [x] 1.2 Create `infra/gcp-domains/security.tf` defining `google_compute_security_policy` with rules to deny blocked IP CIDR ranges with HTTP 403.
5+
- [x] 1.3 Update `google_compute_backend_service.default` in `infra/gcp-domains/backend.tf` to reference the Cloud Armor security policy.
6+
7+
## 2. Validation & Deployment Setup
8+
9+
- [x] 2.1 Add example blocked IP configuration to `infra/gcp-domains/.auto.tfvars.example` and `.auto.tfvars`.
10+
- [x] 2.2 Validate Terraform / OpenTofu syntax and formatting using `tofu fmt` / `tofu validate` or `terraform validate`.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
## Purpose
2+
3+
Configures IP range blocklists at the GCP load balancer level to reject abusive network traffic before it reaches backend services.
4+
5+
## Requirements
6+
7+
### Requirement: Block Traffic from Specified IP Ranges
8+
The load balancer security policy SHALL deny HTTP requests originating from client IP addresses matching configured CIDR ranges with a 403 Forbidden status code.
9+
10+
#### Scenario: Request from a blocked IP CIDR range
11+
- **WHEN** an HTTP request is received from an IP address within a configured blocked CIDR range
12+
- **THEN** the load balancer immediately responds with HTTP status 403 Forbidden without forwarding the request to Cloud Run
13+
14+
#### Scenario: Request from an unblocked IP address
15+
- **WHEN** an HTTP request is received from an IP address outside the configured blocked CIDR ranges
16+
- **THEN** the load balancer evaluates default rules and forwards legitimate traffic to Cloud Run
17+
18+
### Requirement: Configurable IP Blocklist Ranges via Infrastructure Code
19+
The infrastructure module SHALL accept a configurable list of IP CIDR strings for the load balancer security policy.
20+
21+
#### Scenario: Adding a new IP range to the blocklist
22+
- **WHEN** a new IP CIDR range is added to the Terraform variable `blocked_ip_ranges` and applied
23+
- **THEN** Cloud Armor updates the security policy rule set to block requests from that CIDR range

0 commit comments

Comments
 (0)