Back to blog
Best PracticesCloud SecurityGCPNetworkingOperations & Compliance

Load Balancer Uses Plain HTTP: Encrypting GCP Backend Traffic

Learn why GCP load balancer backends using plain HTTP are a security risk and how to switch to HTTPS or HTTP/2 with CLI, Terraform, and CI policy gates.

TL;DR

This check flags GCP load balancer backend services that serve or proxy traffic over plain HTTP instead of HTTPS, leaving data in transit unencrypted and exposed to interception. Switch the backend protocol to HTTPS (or HTTP/2) and attach a managed SSL certificate to the frontend to fix it.

Encryption in transit is one of those controls that everyone agrees on in principle and then quietly skips when they are wiring up a load balancer at 2am. Plain HTTP works, the demo passes, and the http in the config never gets revisited. Lensix raises lb_nohttps when it finds a GCP load balancer backend service configured to use plain HTTP, and this post walks through what that means, why it is worth fixing, and how to close the gap for good.


What this check detects

The lb_nohttps check inspects your GCP load balancer backend services and flags any that use the HTTP protocol rather than HTTPS or HTTP/2. In GCP, a backend service has a protocol field that controls how the load balancer communicates with its backends. When that value is HTTP, traffic between the load balancer and your instances, network endpoint groups, or other backends travels unencrypted.

It is worth being precise about which leg of the connection this covers. A typical external Application Load Balancer has two segments:

  • Client to load balancer (frontend): governed by the frontend forwarding rule and target proxy. This is where you terminate TLS with an SSL certificate.
  • Load balancer to backend: governed by the backend service protocol field. This is what lb_nohttps looks at.

A load balancer can terminate TLS at the frontend and then talk to backends over plain HTTP. That setup encrypts the public-facing hop but leaves the internal hop in cleartext, which is exactly the kind of partial coverage this check is designed to surface.

Note: GCP backend services support HTTP, HTTPS, HTTP/2, TCP, SSL, and GRPC. For HTTP-based application traffic, HTTPS and HTTP/2 both encrypt the connection to the backend. HTTP does not.


Why it matters

The standard objection is "the traffic stays inside Google's network, so who is going to read it?" That assumption has not aged well, and it is not one most compliance frameworks accept anymore.

Internal networks are not trusted boundaries

Once an attacker gains a foothold, the unencrypted hop between your load balancer and backends becomes a soft target. Anyone who can observe traffic on the path, through a compromised host, a misconfigured packet mirroring policy, or a malicious sidecar, can read session tokens, API keys, personal data, and credentials in plaintext. Encryption in transit removes that entire class of passive interception, even inside a VPC.

Compliance frameworks expect end-to-end encryption

PCI DSS requires strong cryptography for cardholder data across open and, increasingly, internal networks. HIPAA, SOC 2, and ISO 27001 all push toward encryption of data in transit without carving out "but only the parts that face the internet." An auditor who finds TLS terminating at the edge and cleartext flowing to the backend will write it up, and you will be remediating under a deadline instead of on your own schedule.

It undermines the encryption you already paid for

If you went to the trouble of provisioning a managed certificate and terminating TLS at the frontend, leaving the backend hop on HTTP means an attacker only needs to be one hop further in. The weakest link sets your actual security posture, not the strongest one.

Warning: Health checks are configured separately from the backend service protocol. Switching a backend service to HTTPS does not automatically update the associated health check. If your health check still probes an HTTP port that no longer responds, your backends will be marked unhealthy and dropped from rotation. Update both together.


How to fix it

Remediation has two parts: make sure clients reach the load balancer over HTTPS, and make sure the load balancer reaches its backends over HTTPS or HTTP/2. The lb_nohttps check targets the second part, but you usually want both.

Step 1: Confirm the current backend protocol

gcloud compute backend-services describe my-backend-service \
  --global \
  --format="value(protocol)"

If this returns HTTP, the check is firing for a reason.

Step 2: Prepare the backends to serve HTTPS or HTTP/2

Your backend instances or services must actually accept TLS connections before you flip the protocol. That means a certificate on the backend (a self-signed or internal CA certificate is acceptable here, since the load balancer to backend trust model is more permissive than the public-facing one) and a listener on the HTTPS port. Confirm the backend responds on its TLS port:

curl -vk https://BACKEND_INSTANCE_IP:443/healthz

Step 3: Update the backend service protocol

Danger: Changing the backend protocol on a production load balancer affects live traffic. If your backends are not yet serving TLS on the target port, every request will fail the moment you switch. Roll this out during a maintenance window or stage it behind a canary backend service.

gcloud compute backend-services update my-backend-service \
  --global \
  --protocol=HTTPS

For modern HTTP traffic, HTTP/2 is often the better choice. It is encrypted by definition over GCP load balancers and gives you multiplexing and header compression:

gcloud compute backend-services update my-backend-service \
  --global \
  --protocol=HTTP2

Step 4: Update the health check to match

gcloud compute health-checks update https my-health-check \
  --port=443

If you previously used an HTTP health check, create an HTTPS one and re-point the backend service at it.

Step 5: Terminate TLS at the frontend with a managed certificate

If your frontend still listens on HTTP, provision a Google-managed certificate and create an HTTPS target proxy and forwarding rule:

# Create a managed certificate
gcloud compute ssl-certificates create my-cert \
  --domains=app.example.com \
  --global

# Create an HTTPS proxy bound to your existing URL map
gcloud compute target-https-proxies create my-https-proxy \
  --url-map=my-url-map \
  --ssl-certificates=my-cert

# Forward port 443 to the HTTPS proxy
gcloud compute forwarding-rules create my-https-fr \
  --global \
  --target-https-proxy=my-https-proxy \
  --ports=443

Step 6: Verify

gcloud compute backend-services describe my-backend-service \
  --global \
  --format="value(protocol)"
# Expect: HTTPS or HTTP2

Tip: Add an HTTP-to-HTTPS redirect at the frontend so clients hitting port 80 are bounced to 443 instead of failing. You can do this with a dedicated URL map that uses a redirectAction of httpsRedirect: true, then point an HTTP target proxy at it.


Fixing it with Terraform

If you manage load balancers as code, set the protocol explicitly so it cannot drift back to HTTP:

resource "google_compute_backend_service" "app" {
  name          = "my-backend-service"
  protocol      = "HTTPS"
  port_name     = "https"
  health_checks = [google_compute_health_check.app_https.id]

  backend {
    group = google_compute_instance_group_manager.app.instance_group
  }
}

resource "google_compute_health_check" "app_https" {
  name = "my-health-check"

  https_health_check {
    port = 443
  }
}

resource "google_compute_managed_ssl_certificate" "app" {
  name = "my-cert"

  managed {
    domains = ["app.example.com"]
  }
}

resource "google_compute_target_https_proxy" "app" {
  name             = "my-https-proxy"
  url_map          = google_compute_url_map.app.id
  ssl_certificates = [google_compute_managed_ssl_certificate.app.id]
}

How to prevent it from happening again

One-off fixes get undone. The reliable way to keep HTTP out of your load balancers is to block it before it merges.

Policy-as-code with OPA or Conftest

If you run Terraform plans through CI, a Rego policy can fail any plan that introduces an HTTP backend service:

package gcp.lb

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "google_compute_backend_service"
  proto := resource.change.after.protocol
  proto == "HTTP"
  msg := sprintf("backend service '%s' uses plain HTTP; use HTTPS or HTTP2", [resource.address])
}

Wire it into the pipeline with conftest:

terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
conftest test --policy ./policy tfplan.json

Organization policy constraints

GCP does not yet offer a built-in org policy that forbids the HTTP backend protocol directly, so the practical control is a combination of pre-merge policy checks and continuous scanning. Lensix runs lb_nohttps on a schedule so a load balancer created out-of-band, through the console or a manual gcloud call, still gets caught even when it never touched your IaC pipeline.

Tip: Pair the pre-merge gate with continuous scanning. The CI check stops new HTTP backends from being merged, and the scheduled scan catches anything created manually or imported from an older project. Neither one covers the gap alone.


Best practices

  • Encrypt every hop, not just the public one. Treat the load balancer to backend leg as untrusted, because in a breach scenario it is.
  • Prefer HTTP/2 for application backends. It is encrypted on GCP load balancers and brings performance gains over HTTPS for many workloads.
  • Change protocol and health checks as a unit. A protocol switch without a matching health check update is the most common cause of "the fix took down the service."
  • Use Google-managed certificates for frontends. They renew automatically and remove the manual rotation toil that leads to expired-cert outages.
  • Redirect HTTP to HTTPS at the edge. Do not just disable port 80 if real clients still hit it; redirect them so you do not trade a security finding for a support ticket.
  • Codify the protocol. An explicit protocol = "HTTPS" in Terraform is self-documenting and prevents accidental drift to the default.

Plain HTTP on a load balancer backend is a small line in a config file with an outsized blast radius. The fix is mechanical once your backends are ready to serve TLS, and the guardrails to keep it fixed are cheap to add. Close the finding, gate the pipeline, and let the scheduled scan watch your back.