This check flags GCP load balancer backend services that have no Cloud Armor security policy attached, leaving them exposed to DDoS, SQL injection, and OWASP Top 10 attacks. Fix it by creating a Cloud Armor policy and binding it to the backend service with gcloud compute backend-services update --security-policy.
External HTTP(S) load balancers are the front door to most public-facing GCP workloads. They terminate traffic from the open internet and forward it to your backends. Without a Cloud Armor policy in place, that front door has no lock on it. Every request, malicious or not, gets passed straight through to your application servers.
The lb_nosecuritypolicy check looks at each backend service behind your load balancers and reports any that have no Cloud Armor security policy attached. It is a simple binary signal, but the gap it points to can be the difference between absorbing a volumetric attack at the edge and watching your application tip over under load.
What this check detects
In GCP, a Cloud Armor security policy is a set of rules that filter traffic before it reaches your backend instances or serverless NEGs. The policy is attached to a backend service, not to the load balancer or the frontend. A single load balancer can route to several backend services, and each one needs its own policy reference.
This check inspects the securityPolicy field on each applicable backend service. If that field is empty, the backend has no Cloud Armor protection and the check fails.
Note: Cloud Armor only applies to backend services used with external Application Load Balancers (and certain regional and global load balancer types). Internal load balancers and pure TCP/UDP proxy setups have different protection models, so the check scopes itself to backends where a Cloud Armor policy is actually supported.
An empty policy reference looks like this when you describe the backend service:
gcloud compute backend-services describe web-backend \
--global \
--format="value(securityPolicy)"
# (returns nothing — no policy attached)
Why it matters
Cloud Armor sits at Google's edge network, the same infrastructure that fronts Google's own services. When a policy is attached, filtering happens before traffic ever reaches your VPC. When there is no policy, that filtering layer is simply absent.
DDoS exposure
Cloud Armor provides always-on protection against volumetric (L3/L4) attacks even without custom rules, but adaptive protection and application-layer (L7) defenses require an attached policy. A backend with no policy gets the baseline network protection Google applies broadly, but none of the targeted, learned defenses that detect anomalous request patterns against your specific application.
Application-layer attacks
Without a policy, there is nothing stopping common L7 attacks at the edge. Cloud Armor ships preconfigured WAF rules based on the OWASP ModSecurity Core Rule Set, covering SQL injection, cross-site scripting, local and remote file inclusion, and more. If no policy is attached, every one of those payloads reaches your application code, and your only line of defense is whatever input validation the app itself happens to have.
No IP-based controls
Cloud Armor is also where you enforce allow and deny lists, geo-based restrictions, and rate limiting. A backend without a policy cannot block a hostile country range, throttle a credential-stuffing bot, or restrict an admin path to a known office IP range. Those controls have to live somewhere, and pushing them into application code is slower, error-prone, and harder to audit.
Warning: A common false sense of security comes from assuming the load balancer "is" the protection. It is not. The load balancer distributes traffic. Cloud Armor is the policy enforcement layer, and it does nothing until you attach a policy to the backend service.
How to fix it
The fix has two parts: create a Cloud Armor security policy with sensible rules, then attach it to the backend service that failed the check.
1. Create a security policy
gcloud compute security-policies create web-armor-policy \
--description="Baseline WAF and DDoS policy for web backends"
2. Enable a preconfigured WAF rule
Add the OWASP SQL injection ruleset as a starting point. Cloud Armor exposes these as expression-based rules with sensitivity tuning:
gcloud compute security-policies rules create 1000 \
--security-policy=web-armor-policy \
--expression="evaluatePreconfiguredWaf('sqli-v33-stable', {'sensitivity': 1})" \
--action=deny-403 \
--description="Block SQL injection (OWASP CRS)"
gcloud compute security-policies rules create 1100 \
--security-policy=web-armor-policy \
--expression="evaluatePreconfiguredWaf('xss-v33-stable', {'sensitivity': 1})" \
--action=deny-403 \
--description="Block cross-site scripting (OWASP CRS)"
Warning: Start preconfigured WAF rules in preview mode (use --preview on the rule) before enforcing. High sensitivity levels can generate false positives against legitimate traffic, and blocking real users is its own kind of outage. Review the logs for a week, tune the sensitivity, then flip to enforcing.
3. Add a rate limiting rule
gcloud compute security-policies rules create 2000 \
--security-policy=web-armor-policy \
--src-ip-ranges="*" \
--action=rate-based-ban \
--rate-limit-threshold-count=100 \
--rate-limit-threshold-interval-sec=60 \
--ban-duration-sec=300 \
--conform-action=allow \
--exceed-action=deny-429 \
--enforce-on-key=IP \
--description="Throttle abusive clients to 100 req/min"
4. Attach the policy to the backend service
This is the step that actually clears the check. Until the policy is bound, the backend is still unprotected.
Danger: Attaching a policy with aggressive deny rules takes effect immediately on live traffic. If your rules are misconfigured, you can block legitimate production users in seconds. Verify your rules in preview mode first, and have a rollback command ready.
gcloud compute backend-services update web-backend \
--global \
--security-policy=web-armor-policy
Confirm the attachment took effect:
gcloud compute backend-services describe web-backend \
--global \
--format="value(securityPolicy)"
# https://www.googleapis.com/.../securityPolicies/web-armor-policy
If you need to roll back, detach the policy by passing an empty value:
gcloud compute backend-services update web-backend \
--global \
--security-policy=""
Fixing it with Terraform
If your infrastructure is managed as code, define the policy and the binding together so the two never drift apart:
resource "google_compute_security_policy" "web_armor" {
name = "web-armor-policy"
description = "Baseline WAF and DDoS policy for web backends"
rule {
action = "deny(403)"
priority = 1000
match {
expr {
expression = "evaluatePreconfiguredWaf('sqli-v33-stable', {'sensitivity': 1})"
}
}
description = "Block SQL injection"
}
rule {
action = "rate_based_ban"
priority = 2000
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
rate_limit_options {
conform_action = "allow"
exceed_action = "deny(429)"
enforce_on_key = "IP"
rate_limit_threshold {
count = 100
interval_sec = 60
}
ban_duration_sec = 300
}
description = "Rate limit abusive clients"
}
# Default rule must always be present
rule {
action = "allow"
priority = 2147483647
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
description = "Default allow"
}
}
resource "google_compute_backend_service" "web" {
name = "web-backend"
protocol = "HTTP"
security_policy = google_compute_security_policy.web_armor.id
# ... backend, health check, and other config
}
Tip: Define one reusable security policy module and reference it from every backend service. That way a single change to your WAF baseline propagates to all backends on the next apply, and no new backend ships without a policy because the module makes the binding mandatory.
How to prevent it from happening again
Manual remediation closes the gap today. The goal is to make a missing policy impossible to ship tomorrow.
Make the binding part of the module contract
The most reliable prevention is structural. If every backend service in your codebase is created through a wrapper module that requires a security_policy input, there is no path to creating an unprotected backend without editing the module itself.
Gate it in CI with policy-as-code
Add an OPA or Conftest rule to your Terraform plan pipeline that fails any backend service missing a security policy:
package terraform.lb
deny[msg] {
resource := input.resource_changes[_]
resource.type == "google_compute_backend_service"
not resource.change.after.security_policy
msg := sprintf(
"Backend service '%s' has no Cloud Armor security policy attached",
[resource.address],
)
}
Wire that into your pipeline so a plan without a policy never reaches apply:
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
conftest test tfplan.json --policy ./policy
Continuous detection with Lensix
Policy-as-code catches the resources that flow through your pipeline. It does not catch backends created by hand in the console, by a one-off script, or by a different team. Continuous scanning closes that loop by re-checking live infrastructure on a schedule, so a backend that someone created out of band still surfaces as a finding instead of sitting unprotected indefinitely.
Best practices
- Attach a policy to every external backend, no exceptions. Even a baseline policy with only the default allow rule gives you a place to enforce rate limits and IP controls later. The hard part is the binding, so do it up front.
- Use preconfigured WAF rules before writing custom ones. The OWASP CRS rulesets cover the most common attack classes and are maintained by Google. Custom rules are for your application-specific logic, not for reinventing SQL injection detection.
- Always pilot rules in preview mode. Run new WAF and rate-limit rules in preview, watch the Cloud Armor logs, tune sensitivity, then enforce. This avoids blocking real users during rollout.
- Enable Adaptive Protection. It uses machine learning to detect and alert on L7 DDoS attacks targeting your specific traffic patterns, and it can suggest rules to mitigate them.
- Send Cloud Armor logs to your SIEM. Request-level logging tells you what is being blocked and what is being allowed, which is essential for tuning rules and investigating incidents.
- Review policies as your application grows. New endpoints, new admin paths, and new partner integrations all change your threat surface. A policy set once and never revisited slowly drifts away from reality.
Note: Cloud Armor pricing has both a per-policy and per-rule component plus a per-request charge on Managed Protection Plus tiers. For most teams the cost is trivial next to the impact of an unmitigated attack, but check the current pricing before rolling policies out fleet-wide so the bill holds no surprises.
A load balancer with no Cloud Armor policy is not a subtle misconfiguration. It is an exposed front door. The fix is a few commands, and the prevention is a single required input in your module and one CI gate. Once both are in place, every public backend you ship is protected by default, which is exactly where you want to be.

