This page covers Google Cloud Platform network hardening across the surfaces that decide whether an attacker reaching the network edge can pivot inward, exfiltrate data, or sustain disruption. Scope is the commercial GCP regions; GCP Sovereign Cloud (formerly Assured Workloads and the Google Cloud Air-Gapped offering) inherits the same controls but exposes a different region table, different service-availability matrices, and tenant topology constraints — re-verify region availability and the relevant cloud.google.com sovereign endpoint documentation before applying any of the IaC below to a sovereign or air-gapped deployment. CIS sub-IDs and NIST / ISO mappings throughout this page reference the CIS Google Cloud Platform Foundation Benchmark v4.0.0 — May 2025 release (accessed 2026-05) unless explicitly annotated as a post-v4.0.0 feature or a best-practice recommendation that the current benchmark has not yet codified. The crosswalk page at compliance frameworks describes how the seven pinned framework columns relate to each other.
The GCP network model is the product of an organization (the root policy boundary, where Org Policy constraints live), folders (intermediate policy boundaries for business units or environments), projects (the unit of billing, IAM, and quota), VPC networks (regional or global, with their own custom subnet plan; Shared VPC lets one host project lend its network to many service projects), subnets (regional CIDR slices that anchor Private Google Access and flow logging), VPC firewall rules (stateful L4 allow/deny at VPC scope, evaluated per packet), Hierarchical Firewall Policies (stateful L4 allow/deny attached at organization or folder scope, evaluated before VPC firewall rules), Cloud Armor security policies (L7 WAF at the external HTTPS Load Balancer edge), Private Google Access and the private.googleapis.com / restricted.googleapis.com DNS endpoints (private consumption of Google APIs from VMs with no external IP), Private Service Connect (explicit per-VPC private endpoints for Google APIs and third-party services), and edge primitives Cloud Load Balancing, Cloud DNS, and Cloud NAT. The cross-cutting principles — segmentation, zero trust, egress control, private connectivity, encryption in transit, and DNS security — are owned by the General Network page; this page maps them to GCP primitives. Severity is assigned from the methodology severity rubric; equivalence callouts at the bottom of each control point at the matching control on the AWS, Azure, and OCI sibling pages.
Three anti-conflation callouts up front, because each pair gets conflated in audit reports and architecture reviews and the distinction matters for control design. First: VPC firewall rules, Hierarchical Firewall Policies, and Cloud Armor are complementary, not alternative. VPC firewall rules are L4 stateful per-VPC ACLs (the legacy ingress/egress allow/deny surface) and exist only inside the VPC they are defined in. Hierarchical Firewall Policies (gcp-net-02) operate at organization or folder scope and evaluate before VPC firewall rules; they enforce tenant-wide invariants that survive VPC creation and cannot be overridden by a project-level admin. Cloud Armor (gcp-net-05) is the L7 WAF at the external HTTPS Load Balancer edge, inspecting HTTP and HTTPS payloads — URI, headers, body, cookies — and is therefore a different plane of inspection from L4 firewalls entirely. Each addresses a different scope; reviewers who insist on "pick one" are wrong.
Second: Private Google Access and Private Service Connect are complementary, not alternative. Private Google Access (gcp-net-03) is a per-subnet setting that lets private-IP-only VMs reach Google APIs via the private.googleapis.com (199.36.153.8/30) or restricted.googleapis.com (199.36.153.4/30) anycast ranges — the workload doesn't need an external IP and the request never traverses the public internet, but the API still has a Google-owned endpoint. Private Service Connect (gcp-net-04) creates an explicit private-IP endpoint inside your VPC that fronts a Google API or a third-party service via a service attachment; the consuming VM connects to a 10.x address you chose, with no public-IP exposure of the underlying service at all. PGA is the consumption mode for existing workloads; PSC is the publication mode for explicit endpoints. PSC supersedes the legacy Private Service Access (VPC peering) model for new designs.
Third: Cloud Armor security policies and Cloud Armor Adaptive Protection are layered on the same resource, not substitutes. Cloud Armor security policies (gcp-net-05) carry preconfigured WAF rules (ModSecurity CRS 3.x — SQLi, XSS, LFI, RFI, RCE, scanner-detection, protocol-anomaly) and custom L7 rules, evaluated at the LB edge. Cloud Armor Adaptive Protection (gcp-net-06) is an ML-driven L7 anomaly-detection layer that runs on the same security policy resource and emits Adaptive Protection alerts and suggested mitigation rules during a sustained attack. Cloud Armor Standard (free, always-on L3/L4 platform protection) is the platform default; Cloud Armor Managed Protection Plus (the Enterprise subscription tier) is the entitlement that unlocks Adaptive Protection plus DDoS Rapid Response engagement.
Order and scope matter. Controls 01–04 are foundational invariants enforced organization-wide via Org Policy (gcloud org-policies set-policy at organization or folder level) and Hierarchical Firewall Policies: have no reliance on default networking, lock admin ports against 0.0.0.0/0 at the organization scope, route private workloads to Google APIs without an external IP, and front third-party and Google-managed services with Private Service Connect endpoints. Controls 05–06 protect the L7 and DDoS edge of public web traffic. Control 07 signs the organisation's public DNS zones. Control 08 closes the egress loop with Cloud NAT and egress firewall rules — the missing complement to PSC, which only covers Google-managed and explicitly-published service traffic. The VPC Service Controls identity-plane perimeter is owned by the GCP IAM page and cross-referenced from this page where relevant; do not re-author it here.
gcp-net-01-no-default-network!MEDIUMPREVENTIVE
Disable creation of the legacy "default" VPC organization-wide via the constraints/compute.skipDefaultNetworkCreation Org Policy constraint, and design every workload network as an explicit custom-mode VPC (typically a Shared VPC host project lent to many service projects in a hub-and-spoke topology). The default VPC ships with auto-mode subnets in every region and a permissive set of pre-baked firewall rules — exactly the surface area an Org-level policy should remove before any project is created (Google Cloud — VPC documentation (accessed 2026-05)). The principle is reinforced in General Network — segmentation: a network the organisation did not consciously design is a network whose blast radius the organisation cannot reason about. Custom-mode (--subnet-mode=custom) is the only acceptable VPC mode for new workloads; auto-mode subnets in every region make egress controls and CIDR planning impossible to enforce consistently.
Remediation — gcloud CLI
# gcloud CLI (latest stable)
# Step 1: enforce the skip-default-network constraint at the organization scope.
cat > default-network-deny.yaml <<'YAML'
name: organizations/ORG_ID/policies/compute.skipDefaultNetworkCreation
spec:
rules:
- enforce: true
YAML
gcloud org-policies set-policy default-network-deny.yaml \
--organization=ORG_ID
# Step 2: inventory existing default VPCs across all projects in the org.
for project in $(gcloud projects list --format='value(projectId)'); do
gcloud compute networks list --project="$project" \
--filter='name=default' \
--format="value(name)" 2>/dev/null \
| sed "s|^|$project: |"
done
# Step 3: create the explicit per-workload VPC in custom mode (no auto subnets).
gcloud compute networks create vpc-app-prod \
--project=svc-app-prod \
--subnet-mode=custom \
--bgp-routing-mode=regional
# Step 4: create a single explicit subnet in the approved region + CIDR.
gcloud compute networks subnets create snet-app-prod-euw1 \
--project=svc-app-prod \
--network=vpc-app-prod \
--region=europe-west1 \
--range=10.40.0.0/22 \
--enable-flow-logs \
--enable-private-ip-google-access
Cloud Audit Logs on compute.googleapis.com for v1.compute.networks.insert where protoPayload.resourceName ends in /networks/default — the default network ships with overly permissive firewall rules and recreating it re-introduces them.
Constraint drift on compute.skipDefaultNetworkCreation via orgpolicy.googleapis.comUpdatePolicy moving from enforce: true to enforce: false.
logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.serviceName="compute.googleapis.com"
AND ((protoPayload.methodName="v1.compute.networks.insert"
AND protoPayload.resourceName=~".*/networks/default$")
OR (protoPayload.methodName="v1.compute.firewalls.insert"
AND protoPayload.request.name=~"default-allow-(icmp|internal|rdp|ssh)"))
This Cloud Logging filter is project-scoped; pair with a Cloud Asset Inventory query enumerating every project for the presence of a default VPC so steady-state population is visible alongside change events.
Alert threshold
Page on any insert of a network named default or any of the four implicit firewall rules; the org-policy constraint should prevent this and a successful insert means the constraint was relaxed.
Page on any update to compute.skipDefaultNetworkCreation moving away from enforced.
Initial response
Delete the default network and its implicit firewall rules; re-assert the Org Policy constraint and back-fill the project with the documented hardened VPC template.
Audit VM creates in the gap window — any instance bound to the default VPC inherited the permissive firewall set and the workload should be re-deployed onto the hardened VPC.
Pin the constraint via Terraform google_org_policy_policy and gate edits through change-management; the constraint is a one-time set-and-forget invariant.
No Hierarchical Firewall Policy and no VPC firewall rule in any project of the organization may permit ingress from 0.0.0.0/0 on administrative ports — SSH 22, RDP 3389, SQL Server 1433, PostgreSQL 5432, MySQL 3306, MongoDB 27017, Redis 6379, and any other database or management port the organization uses. Author the canonical deny at organization scope as a Hierarchical Firewall Policy so the invariant survives VPC creation, then layer a VPC-level deny for defence in depth (Google Cloud — Hierarchical Firewall Policies overview (accessed 2026-05)). Anti-conflation: Hierarchical Firewall Policies attach at organization or folder scope and evaluate before VPC firewall rules — a project-level admin cannot override an Org-scope HFP deny. VPC firewall rules attach at network scope and are the per-VPC fallback. Cloud Armor (gcp-net-05) is the L7 WAF at the external HTTPS LB edge and is a complementary inspection plane, not a substitute for L4 firewall policy. CRITICAL because this is the canonical "open the internet to my database" misconfiguration; Shodan-style scanners locate exposures within minutes, and CIS GCP v4.0.0 §3.6 and §3.7 codify the requirement.
Remediation — gcloud CLI
# gcloud CLI (latest stable)
# Step 1: create the Hierarchical Firewall Policy at organization scope.
gcloud compute firewall-policies create ORG_NO_ADMIN_INGRESS \
--organization=ORG_ID \
--short-name=org-no-admin-ingress \
--description="Org-wide deny: 0.0.0.0/0 -> admin ports"
# Step 2: deny 0.0.0.0/0 -> admin TCP ports at priority 100 (low-numbered = earlier).
gcloud compute firewall-policies rules create 100 \
--firewall-policy=ORG_NO_ADMIN_INGRESS \
--organization=ORG_ID \
--action=deny \
--direction=INGRESS \
--layer4-configs=tcp:22,tcp:3389,tcp:1433,tcp:3306,tcp:5432,tcp:27017,tcp:6379 \
--src-ip-ranges=0.0.0.0/0 \
--enable-logging \
--description="Deny Internet -> admin / DB ports (org-wide invariant)"
# Step 3: attach the policy to the organization (or to specific folders).
gcloud compute firewall-policies associations create \
--firewall-policy=ORG_NO_ADMIN_INGRESS \
--organization=ORG_ID \
--name=org-root-association
# Audit: list every VPC firewall rule across the org that still allows 0.0.0.0/0 on admin ports.
for project in $(gcloud projects list --format='value(projectId)'); do
gcloud compute firewall-rules list --project="$project" \
--filter='direction=INGRESS AND disabled=false AND sourceRanges:0.0.0.0/0' \
--format="value(name,allowed,sourceRanges)" 2>/dev/null \
| sed "s|^|$project: |"
done
import * as gcp from "@pulumi/gcp";
// Explicit-deny SSH/RDP from the public internet — paired with allow-list rules for
// IAP TCP forwarding (35.235.240.0/20) when bastion access is required.
const denyAdminInternet = new gcp.compute.Firewall("deny-ssh-rdp-internet", {
name: "deny-ssh-rdp-internet",
network: prodVpc.id,
direction: "INGRESS",
priority: 65534,
denies: [{ protocol: "tcp", ports: ["22", "3389"] }],
sourceRanges: ["0.0.0.0/0"],
});
Compliance mapping
CIS AWS Foundations v3.0.0
CIS Microsoft Azure Foundations v3.0.0
CIS GCP Foundation v4.0.0
CIS OCI Foundation v2.0.0
NIST SP 800-53 rev5
ISO/IEC 27001:2022
ISO/IEC 27017:2015
5.2; 5.3
6.1; 6.2
3.6; 3.7
2.1; 2.2
SC-7(5); SC-7
A.8.20; A.8.22
CLD.9.5.1
Log signals
Cloud Audit Logs on compute.googleapis.com for v1.compute.firewalls.insert or v1.compute.firewalls.patch where sourceRanges contains 0.0.0.0/0 and allowed.ports includes 22 or 3389.
Hierarchical firewall policy edits at organisation / folder scope where new rules with the same admin-port + world-open pattern are inserted via FirewallPolicies.patch.
Tag-based firewall rules where the source-tag is removed and replaced with 0.0.0.0/0 — silent broadening of an existing rule.
Query
logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.serviceName="compute.googleapis.com"
AND protoPayload.methodName=~"v1.compute.(firewalls|firewallPolicies).*"
AND protoPayload.request.sourceRanges="0.0.0.0/0"
AND (protoPayload.request.allowed.ports="22"
OR protoPayload.request.allowed.ports="3389")
Stream this Cloud Logging filter through a Cloud Monitoring log-based metric grouped by firewall name; pair with VPC Flow Logs queries scoped to the rule's target tags so post-rule traffic on the admin port is visible alongside the rule creation.
Alert threshold
Page on any rule insert / patch admitting world-open SSH or RDP; the steady-state count of such rules is zero.
Page on a hierarchical firewall policy patch adding the same pattern at folder or organisation scope; impact radius is broader than a single project rule.
Initial response
Delete the offending firewall rule via gcloud compute firewall-rules delete or revert the policy patch from the captured baseline policy JSON.
Inspect VPC Flow Logs for accepted-connection records on the admin port during the exposed window; treat any non-corporate source IP as a candidate compromise of the targeted VM.
Force-rotate SSH host keys / Windows administrator credentials on every VM that was reachable; pivot the workload to IAP TCP-forwarding (control 03) so future SSH/RDP access no longer requires a perimeter rule.
Enable Private Google Access on every workload subnet and route Google API consumption through the private.googleapis.com (199.36.153.8/30) or restricted.googleapis.com (199.36.153.4/30, used in conjunction with VPC Service Controls perimeters) anycast endpoints, so private-IP-only VMs reach Google APIs without traversing the public internet (Google Cloud — Private Google Access documentation (accessed 2026-05)). Pair PGA with Org Policy constraints/compute.vmExternalIpAccess to deny external IP assignment on workload VMs by default; PGA is what makes that denial operationally viable. The principle is reinforced in General Network — private connectivity: traffic to managed services should never cross the public internet when a private path exists. Anti-conflation: PGA is per-subnet, free, and applies to consumption of Google-managed APIs from existing VMs; Private Service Connect (gcp-net-04) creates explicit private-IP endpoints inside the VPC for Google or third-party services and is the publication-mode complement. restricted.googleapis.com is specifically the entry endpoint for VPC SC perimeters; cross-link: VPC Service Controls (Phase 5; owned by the IAM page and not re-authored here). HIGH PREVENTIVE because PGA + the external-IP-deny Org Policy together remove the public-IP attack surface from workload VMs entirely while preserving their ability to reach Cloud Storage, BigQuery, and the other Google APIs they need.
Remediation — gcloud CLI
# gcloud CLI (latest stable)
# Step 1: enable Private Google Access on the workload subnet.
gcloud compute networks subnets update snet-app-prod-euw1 \
--project=svc-app-prod \
--region=europe-west1 \
--enable-private-ip-google-access
# Step 2: enforce the no-external-IP Org Policy at organization scope.
cat > deny-external-ip.yaml <<'YAML'
name: organizations/ORG_ID/policies/compute.vmExternalIpAccess
spec:
rules:
- values:
deniedValues: ["all"]
YAML
gcloud org-policies set-policy deny-external-ip.yaml \
--organization=ORG_ID
# Step 3: create the Cloud DNS private zone that resolves googleapis.com to the
# private.googleapis.com range, so workloads don't need explicit code changes.
gcloud dns managed-zones create googleapis-com-private \
--project=svc-app-prod \
--description="Private resolution of googleapis.com -> 199.36.153.8/30" \
--dns-name=googleapis.com. \
--networks=vpc-app-prod \
--visibility=private
gcloud dns record-sets create '*.googleapis.com.' \
--project=svc-app-prod \
--zone=googleapis-com-private \
--type=A \
--ttl=300 \
--rrdatas=199.36.153.8,199.36.153.9,199.36.153.10,199.36.153.11
Remediation — Terraform
# Terraform Google provider ~> 5.0
# Source: Google Cloud docs (accessed 2026-05)
# Subnet with PGA enabled (boolean flips PGA on for the whole subnet).
resource "google_compute_subnetwork" "app_pga" {
project = var.host_project_id
name = "snet-app-prod-euw1"
network = google_compute_network.vpc_app_prod.id
region = "europe-west1"
ip_cidr_range = "10.40.0.0/22"
private_ip_google_access = true
}
# Org Policy: deny external IPs on workload VMs.
resource "google_org_policy_policy" "deny_external_ip" {
name = "organizations/${var.org_id}/policies/compute.vmExternalIpAccess"
parent = "organizations/${var.org_id}"
spec {
rules {
values {
denied_values = ["all"]
}
}
}
}
# Cloud DNS private zone: resolve googleapis.com to private.googleapis.com range.
resource "google_dns_managed_zone" "googleapis_private" {
project = var.host_project_id
name = "googleapis-com-private"
dns_name = "googleapis.com."
description = "Private resolution of googleapis.com -> 199.36.153.8/30"
visibility = "private"
private_visibility_config {
networks {
network_url = google_compute_network.vpc_app_prod.id
}
}
}
resource "google_dns_record_set" "googleapis_wildcard" {
project = var.host_project_id
managed_zone = google_dns_managed_zone.googleapis_private.name
name = "*.googleapis.com."
type = "A"
ttl = 300
rrdatas = ["199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"]
}
Cloud Audit Logs on compute.googleapis.com for v1.compute.serviceAttachments.insert exposing internal services via Private Service Connect — any new attachment widens the producer surface to consumer VPCs.
Service-attachment IAM mutations adding roles/compute.networkUser for consumer projects outside the documented allow-list.
PSC endpoint creation events from consumer side: forwardingRules.insert targeting a service attachment whose project is foreign to the consumer's organisation.
Query
logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.serviceName="compute.googleapis.com"
AND (protoPayload.methodName=~"v1.compute.serviceAttachments.(insert|patch)"
OR (protoPayload.methodName="v1.compute.forwardingRules.insert"
AND protoPayload.request.target=~".*serviceAttachments.*"))
Use this Cloud Logging filter at organisation scope; combine with Cloud Asset Inventory's compute.googleapis.com/ServiceAttachment resource feed so steady-state attachment population and IAM bindings stay visible alongside changes.
Alert threshold
Page on any new service-attachment insert from a producer project outside the documented PSC-publisher allow-list.
Page on any IAM binding addition granting compute.networkUser on a service-attachment to an external organisation principal.
Initial response
Disable the service attachment via gcloud compute service-attachments update --connection-preference=ACCEPT_MANUAL and revoke any auto-accepted consumer projects.
Inspect Cloud Logging for data-plane connection events to the attached service during the exposed window; treat any consumer-project session as a candidate cross-org pivot.
Pin service-attachment IAM and connection-preference in Terraform; require consumer-project list to be source-of-truth and gate edits through change-management.
Front Google-managed and third-party services with Private Service Connect endpoints — explicit private-IP forwarding rules inside the consuming VPC that target a service attachment for the upstream service. PSC supersedes the legacy Private Service Access (VPC peering) model for new designs and is the only access pattern that fully removes the upstream service's public-IP exposure from the consuming VPC's perspective (Google Cloud — Private Service Connect documentation (accessed 2026-05)). The principle is reinforced in General Network — zero trust: never traverse a network you do not control. Anti-pattern to flag: Private Service Access (VPC peering to a Google-managed producer VPC) was the original private connectivity pattern for managed services like Cloud SQL and Memorystore; it works but the consuming VPC's route table absorbs the producer's CIDR, transitive peering is constrained, and the model does not scale to per-service granular access. PSC creates one forwarding rule per consumer-to-service binding, eliminates transitive peering questions, and supports IAM at the service-attachment level. Anti-conflation with PGA: PGA is the consumption mode for any VM with a private IP and a route to the anycast googleapis.com range; PSC is the publication mode for explicit per-service endpoints in your VPC. They can coexist: PGA for the broad set of Google APIs, PSC for specific high-value services where IAM at the endpoint is required.
Remediation — gcloud CLI
# gcloud CLI (latest stable)
# Variant A: global PSC endpoint for Google APIs (all-apis bundle).
gcloud compute addresses create psc-google-apis \
--project=svc-app-prod \
--global \
--purpose=PRIVATE_SERVICE_CONNECT \
--addresses=10.40.255.2 \
--network=vpc-app-prod
gcloud compute forwarding-rules create psc-google-apis \
--project=svc-app-prod \
--global \
--network=vpc-app-prod \
--address=psc-google-apis \
--target-google-apis-bundle=all-apis
# Variant B: per-service PSC endpoint for a published service attachment
# (e.g. a partner SaaS or another team's Cloud SQL instance).
gcloud compute forwarding-rules create psc-vendor-api \
--project=svc-app-prod \
--region=europe-west1 \
--network=vpc-app-prod \
--subnet=snet-psc-euw1 \
--target-service-attachment=projects/vendor-prod/regions/europe-west1/serviceAttachments/vendor-api
Cloud Audit Logs on compute.googleapis.com for v1.compute.subnetworks.patch where privateIpGoogleAccess transitions from true to false.
Cloud DNS private-zone mutations removing the private.googleapis.com / restricted.googleapis.com resolution path: dns.googleapis.comManagedZones.update on the override zone.
NAT-gateway adds on subnets that were previously private-Google-Access-only — egress fallback to the public Googleapis endpoint pattern.
Query
logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.serviceName="compute.googleapis.com"
AND protoPayload.methodName="v1.compute.subnetworks.patch"
AND protoPayload.request.privateIpGoogleAccess=false
Pair this Cloud Logging filter with a Cloud DNS query against the override zone resource feed so private/restricted googleapis resolution changes surface alongside subnet flag changes.
Alert threshold
Page on any subnet transitioning out of Private Google Access on a production VPC.
Page on Cloud DNS private-zone mutations affecting private.googleapis.com resolution; that path is a hard tenancy-perimeter invariant.
Initial response
Re-enable Private Google Access on the subnet via gcloud compute networks subnets update --enable-private-ip-google-access and restore the Cloud DNS override zone from the captured baseline.
Audit VM-→-Googleapis traffic during the gap window via VPC Flow Logs joined against the published Google service-IP ranges; any egress that resolved to the public endpoint is a candidate VPC-SC-bypass attempt.
Pin subnet flag and DNS zone records in Terraform and gate edits through change-management.
Front internet-facing web traffic with an external HTTPS Load Balancer and attach a Cloud Armor security policy carrying the preconfigured WAF rule sets — ModSecurity CRS 3.x for SQL injection (sqli-v33-stable), cross-site scripting (xss-v33-stable), local file inclusion (lfi-v33-stable), remote file inclusion (rfi-v33-stable), remote code execution (rce-v33-stable), scanner-detection, and protocol-anomaly — and integrate reCAPTCHA Enterprise for bot mitigation on credential-bearing endpoints (Google Cloud — Cloud Armor security policy overview (accessed 2026-05)). Cloud Armor evaluates at the LB edge across Google's global PoP fleet, inspecting HTTP and HTTPS payloads — URI, headers, body, cookies, query strings — and is therefore an L7 control. Anti-conflation: Cloud Armor is the L7 WAF at the LB edge; Hierarchical Firewall Policies and VPC firewall rules (gcp-net-02) operate at L4 in the network plane. WAF cannot help with a volumetric SYN flood any more than L4 firewalls can stop an SQLi attempt in a well-formed HTTPS request. They are layered, not alternative. Adaptive Protection (gcp-net-06) is a different feature configured on the same security policy resource. HIGH PREVENTIVE because managed rules block known-exploit-pattern traffic at the edge before it ever reaches the application's parsing logic.
Cloud Audit Logs on compute.googleapis.com for v1.compute.securityPolicies.patch removing rules from a Cloud Armor security policy attached to a production backend service.
Security-policy backend-service unbind via backendServices.patch clearing the securityPolicy field — silent unprotection.
WAF rule-toggle events: preconfigured-WAF rule moving from action: deny(403) to action: preview on production targets.
Query
logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.serviceName="compute.googleapis.com"
AND ((protoPayload.methodName="v1.compute.securityPolicies.patch"
AND protoPayload.request.rules.action="preview")
OR (protoPayload.methodName="v1.compute.backendServices.patch"
AND protoPayload.request.securityPolicy=""))
Stream this Cloud Logging filter into a Cloud Monitoring alert policy grouped by policy name; pair with the Cloud Armor request-log feed (resource.type="http_load_balancer") so WAF denial-rate dips show alongside the patch events.
Alert threshold
Page on any backend-service patch that unbinds a Cloud Armor policy from a production target.
Page on a denial-rate drop greater than 60% versus rolling-baseline on any backend service still bound to a security policy — captures silent rule-→-preview moves.
Initial response
Restore the policy via gcloud compute security-policies import from the captured baseline YAML and re-attach the backend-service binding.
Sample HTTP load-balancer logs from the unprotected window for attack patterns the policy would otherwise have blocked (SQLi, XSS, RCE indicators); treat any match as candidate-exploitation against the backend.
Pin Cloud Armor policy spec + backend-service binding in Terraform; gate edits via change-management to prevent unilateral console patches.
Enable Cloud Armor Adaptive Protection on the security policy fronting internet-facing external HTTPS LBs, and consider the Cloud Armor Managed Protection Plus (Enterprise tier subscription) entitlement for DDoS Rapid Response engagement. Cloud Armor Standard is automatic and free at the Google platform level — it provides always-on L3/L4 volumetric mitigation across every Google Cloud LB. Adaptive Protection layers ML-driven L7 anomaly detection on top: it baselines per-backend-service traffic patterns, surfaces alerts when sustained anomalies appear, and emits suggested mitigation rules the operator can promote into the active security policy with one command (Google Cloud — Cloud Armor Adaptive Protection overview (accessed 2026-05)). Anti-conflation: Adaptive Protection and the preconfigured WAF rules in gcp-net-05 are distinct features on the same security policy resource — WAF deals with known-pattern requests (one request can match), Adaptive Protection deals with anomalous distributions across many requests (only a sustained attack matches). MEDIUM RESPONSIVE because Adaptive Protection's value-add over Standard is in the mitigation feedback loop during a sustained L7 attack, not in preventing volumetric traffic from ever arriving — Standard absorbs the bulk of L3/L4 volumetric attacks by default.
Remediation — gcloud CLI
# gcloud CLI (latest stable)
# Enable Adaptive Protection (L7 DDoS defence) on the existing security policy.
gcloud compute security-policies update armor-prod \
--project=svc-edge-prod \
--enable-layer7-ddos-defense \
--layer7-ddos-defense-rule-visibility=STANDARD
# Verify the configuration.
gcloud compute security-policies describe armor-prod \
--project=svc-edge-prod \
--format='value(adaptiveProtectionConfig)'
# When Adaptive Protection emits a suggested rule via Cloud Monitoring, promote it
# into the active policy at a chosen priority. Example: deny a flagged source set.
gcloud compute security-policies rules create 500 \
--project=svc-edge-prod \
--security-policy=armor-prod \
--src-ip-ranges="203.0.113.0/24,198.51.100.0/24" \
--action=deny-403 \
--description="Adaptive Protection suggested rule promoted 2026-05-23"
Cloud Audit Logs on compute.googleapis.comsecurityPolicies.patch where adaptiveProtectionConfig.layer7DdosDefenseConfig.enable transitions to false.
Cloud Armor Adaptive Protection signal disable: edgeSecurityPolicy binding cleared from a global external Application Load Balancer.
L3/L4 DDoS event-frequency anomaly: resource.type="http_load_balancer" entries with jsonPayload.adaptiveProtection.alertId populated drop to zero unexpectedly.
Query
logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.serviceName="compute.googleapis.com"
AND protoPayload.methodName="v1.compute.securityPolicies.patch"
AND protoPayload.request.adaptiveProtectionConfig.layer7DdosDefenseConfig.enable=false
Pair this Cloud Logging filter with a saved query on resource.type="http_load_balancer" Adaptive Protection alerts so disable events and the resulting alert-rate collapse appear in one Cloud Monitoring pane.
Alert threshold
Page on any security-policy patch disabling Adaptive Protection on policies bound to production load balancers.
Surface (do not page) Adaptive Protection alert-rate drops below 10% of baseline for more than 60 minutes — confirm with the LB owner whether the underlying traffic mix actually changed.
Initial response
Re-enable Adaptive Protection via gcloud compute security-policies update --enable-layer7-ddos-defense on the affected policy.
Pull HTTP load-balancer logs for the gap window and run a rate-anomaly scan; if traffic-volume anomalies were present they were not auto-mitigated and the backend may have absorbed the burst.
Pin Adaptive Protection enablement + edge-security-policy binding in Terraform; require explicit override for any production-backend disable.
Enable DNSSEC signing on every public Cloud DNS managed zone the organisation owns. DNSSEC binds each DNS record to a cryptographic signature that validating resolvers (and intermediate caches) check, defeating cache-poisoning and on-path response-rewrite attacks (Google Cloud — Cloud DNS DNSSEC documentation (accessed 2026-05)). The principle is reinforced in General Network — DNS security. Cloud DNS auto-rotates the Zone Signing Key (ZSK); the Key Signing Key (KSK) is read with gcloud dns dns-keys list and the resulting DS record must be lodged at the parent registrar to complete the delegation chain. Two GCP-specific constraints to flag in advance: (1) DNSSEC applies to public Cloud DNS zones only — Cloud DNS private zones do not support DNSSEC and never will, because the validation chain assumes a public root and a key signing key tied to ICANN's root trust anchor; (2) algorithm choice — RSASHA256 (algo 8) is the safe default; ECDSAP256SHA256 (algo 13) yields smaller signatures and faster validation but historical resolver-side support is mixed in legacy enterprise resolvers. CIS GCP v4.0.0 §3.3, §3.4, and §3.5 codify the DNSSEC + algorithm requirements.
Remediation — gcloud CLI
# gcloud CLI (latest stable)
# Step 1: enable DNSSEC on a public managed zone (creates KSK + ZSK; signs records).
gcloud dns managed-zones update example-com \
--project=svc-dns-prod \
--dnssec-state=on \
--ksk-algorithm=rsasha256 \
--zsk-algorithm=rsasha256 \
--denial-of-existence=nsec3
# Step 2: read the DS record values to lodge at the parent registrar.
gcloud dns dns-keys list \
--project=svc-dns-prod \
--zone=example-com \
--format='table(keyTag,type,algorithm,digests[0].digest)'
# Audit: list every public managed zone in the org that does NOT have DNSSEC enabled.
for project in $(gcloud projects list --format='value(projectId)'); do
gcloud dns managed-zones list --project="$project" \
--filter='visibility=public AND dnssecConfig.state!=on' \
--format="value(name)" 2>/dev/null \
| sed "s|^|$project: |"
done
Remediation — Terraform
# Terraform Google provider ~> 5.0
# Source: Google Cloud docs (accessed 2026-05)
resource "google_dns_managed_zone" "example_com" {
project = var.dns_project_id
name = "example-com"
dns_name = "example.com."
description = "Public zone for example.com (DNSSEC-signed)"
visibility = "public"
dnssec_config {
state = "on"
non_existence = "nsec3"
default_key_specs {
algorithm = "rsasha256"
key_length = 2048
key_type = "keySigning"
kind = "dns#dnsKeySpec"
}
default_key_specs {
algorithm = "rsasha256"
key_length = 1024
key_type = "zoneSigning"
kind = "dns#dnsKeySpec"
}
}
}
# Cloud Monitoring alert on a sustained DNSSEC validation-failure rate
# (signal that the parent DS record is stale or rotation went wrong).
resource "google_monitoring_alert_policy" "dnssec_validation_failures" {
project = var.dns_project_id
display_name = "Cloud DNS DNSSEC validation failures"
combiner = "OR"
conditions {
display_name = "Validation failure rate"
condition_threshold {
filter = "resource.type=\"dns_query\" AND metric.type=\"dns.googleapis.com/query/response_count\" AND metric.label.\"response_code\"=\"SERVFAIL\""
duration = "300s"
comparison = "COMPARISON_GT"
threshold_value = 100
}
}
notification_channels = var.security_oncall_channels
}
Cloud Audit Logs on dns.googleapis.com for ManagedZones.update where dnssecConfig.state transitions from on to off or transfer.
Key-spec changes on the managed zone where the algorithm drops below RSASHA256 or the keyLength shrinks.
Registrar-side DS-record mismatch: parent-zone DS record diverges from the active KSK fingerprint surfaced by gcloud dns dns-keys list.
Query
logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.serviceName="dns.googleapis.com"
AND protoPayload.methodName=~".*ManagedZones.(update|patch)"
AND (protoPayload.request.dnssecConfig.state="off"
OR protoPayload.request.dnssecConfig.state="transfer")
This Cloud Logging filter catches the zone-side disable; pair with an external DNS-validation probe (Cloud Functions executing dig +dnssec against the zone every 5 minutes from a Cloud Scheduler trigger) so registrar-side DS-record drift surfaces alongside zone-side state.
Alert threshold
Page on any managed zone transitioning out of on; DNSSEC enablement is a one-way invariant once the parent DS record is published.
Page on the external probe failing DNSSEC validation for 3 consecutive checks; indicates registrar-side DS-record mismatch even if the zone-side state is unchanged.
Initial response
Re-enable DNSSEC via gcloud dns managed-zones update --dnssec-state=on; if the registrar DS record was changed externally, contact the registrar to restore the previous DS record per the captured baseline.
Audit Cloud Logging for resolver activity during the validation gap — any client that resolved against the zone in the gap may have received a spoofed response (the cache TTL extends the blast radius beyond the gap itself).
Pin DNSSEC state + KSK rotation policy in Terraform and document the registrar-side DS-record publication procedure in the runbook for next rotation.
Provide internet egress to private-IP-only workload VMs via Cloud NAT attached to a Cloud Router in each region, deny external IPs at the Org Policy layer (constraints/compute.vmExternalIpAccess, also referenced in gcp-net-03), and enforce egress filtering through Hierarchical Firewall Policy egress rules plus — where URL-/FQDN-level egress filtering is required — Secure Web Proxy or a third-party managed NGFW (Palo Alto VM-Series, Fortinet FortiGate) in front of Cloud NAT (Google Cloud — Cloud NAT documentation (accessed 2026-05)). The general principle — workloads talk only to destinations they need — is documented at General Network — egress control. Anti-conflation: Cloud NAT does NAT-for-egress only — it does not inspect, filter by URL, or terminate TLS. Hierarchical Firewall Policies and VPC firewall rules (gcp-net-02) supply L4 destination policy. Secure Web Proxy (currently in preview-to-GA progression at the time of writing — re-verify cloud.google.com) and managed NGFW deployments add L7 URL / FQDN filtering and IDPS. Cloud NAT + HFP egress + Secure Web Proxy together are the GCP analog of the Azure Firewall Premium hub pattern. HIGH PREVENTIVE because egress filtering is the single largest exfiltration-path mitigation GCP exposes once east-west is constrained — it is the missing complement to Private Service Connect, which only covers Google-managed and explicitly-published service traffic.
Cloud Audit Logs on compute.googleapis.com for v1.compute.routers.patch removing the nats entry or for routers.delete on a router carrying NAT config — VMs fall back to attempting public-IP egress.
NAT-rule changes broadening sourceSubnetworkIpRangesToNat to ALL_SUBNETWORKS_ALL_IP_RANGES on a router previously restricted to a specific range.
VM instance creates where networkInterfaces.accessConfigs assigns an ephemeral public IP against the documented policy — silent perforation of the NAT-only egress invariant.
Query
logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.serviceName="compute.googleapis.com"
AND ((protoPayload.methodName="v1.compute.routers.patch"
AND NOT protoPayload.request.nats.name=~".*")
OR (protoPayload.methodName="v1.compute.instances.insert"
AND protoPayload.request.networkInterfaces.accessConfigs.natIP=~".*"))
Run this Cloud Logging filter alongside a Cloud Asset Inventory query enumerating VMs whose networkInterfaces.accessConfigs is non-empty; the inventory view catches steady-state drift while the audit-log view catches change events.
Alert threshold
Page on any router-patch removing NAT entries from production routers, or any VM create assigning an ephemeral external IP on subnets tagged as egress-via-NAT only.
Page on the NAT-rule broadening pattern; subnet allow-list erosion is a slow-burn lateral-egress vector.
Initial response
Restore the router-NAT config via gcloud compute routers update with the captured baseline; remove any unauthorised accessConfigs via gcloud compute instances delete-access-config.
Audit Cloud Logging + VPC Flow Logs for egress traffic during the NAT-bypass window; treat any direct-public-IP egress from a workload VM as candidate-exfil and capture the destination set.
Pin Org Policy constraint compute.vmExternalIpAccess at denyAll and pin router NAT config in Terraform to lock the egress topology against future drift.