Why Zero-Trust: Moving Beyond the Perimeter
Traditional network security operates on a simple assumption: traffic inside the firewall is trusted, traffic outside is not. This model fails in cloud environments for three reasons. First, there is no meaningful perimeter — workloads run across regions, projects, and managed services that share physical infrastructure with other tenants. Second, lateral movement after a single compromised credential can reach every resource on the internal network. Third, VPN-based access grants network-level trust to entire subnets rather than scoping access to individual applications.
Zero-trust inverts this model. Every request — whether it originates from an employee's laptop, a CI/CD pipeline, or a Kubernetes pod — must present a verified identity, satisfy context-aware access policies, and be authorized for the specific resource it is trying to reach. The network itself grants no implicit trust. In practical terms, this means:
- ✓Identity is the perimeter. Access decisions are based on who (or what) is making the request, not where the request comes from.
- ✓Least-privilege by default. Every principal gets the minimum permissions required, scoped to specific resources.
- ✓Continuous verification. Session context (device posture, location, time) is re-evaluated, not just checked at login.
- ✓Assume breach. Design controls so that a compromised component cannot escalate to full environment access.
On Google Cloud Platform, this translates to a concrete set of services and configurations. The rest of this post walks through each one.
GCP's Zero-Trust Building Blocks
GCP provides several services that, when combined, implement a defense-in-depth zero-trust architecture:
| Layer | Service | Function |
|---|---|---|
| API-level data exfiltration prevention | VPC Service Controls | Service perimeters that restrict which projects and networks can access sensitive APIs |
| Application-level access | Identity-Aware Proxy (IAP) | Context-aware authentication/authorization for web apps and SSH without VPN |
| Workload identity | Workload Identity Federation | Federated tokens from external IdPs, eliminating long-lived service account keys |
| Network-level isolation | Private Google Access, VPC firewalls | No public IPs on VMs; API access over internal routes |
| Supply chain integrity | Binary Authorization | Admission control ensuring only signed, verified container images run in GKE |
| Edge protection | Cloud Armor | WAF rules, DDoS mitigation, and rate limiting at the global load balancer |
| Governance | Organization Policy constraints | Hard guardrails on resource locations, service usage, and sharing |
| Observability | Cloud Audit Logs, VPC Flow Logs, SCC | Complete audit trail of admin and data access events |
None of these services alone constitutes zero-trust. The architecture emerges from layering them together.
VPC Service Controls: Preventing Data Exfiltration at the API Layer
VPC Service Controls (VPC-SC) create a security boundary around Google Cloud services. Even if an attacker obtains valid IAM credentials, they cannot exfiltrate data to a project outside the perimeter. This is the single most important control for sensitive data on GCP, and it operates at a layer that IAM alone cannot cover.
A service perimeter defines which projects are "inside" and which Google API services are protected. Any API call that crosses the perimeter boundary — for example, copying a BigQuery table to an external project — is denied.
Creating a Perimeter with gcloud
# Create an access policy (one per organization)
gcloud access-context-manager policies create \
--organization=123456789012 \
--title="org-zero-trust-policy"
# Define an access level based on corporate IP ranges and device policy
gcloud access-context-manager levels create corp-trusted-access \
--policy=POLICY_ID \
--title="Corporate Trusted Access" \
--basic-level-spec=access-level-spec.yaml
# Create a service perimeter protecting BigQuery and Cloud Storage
gcloud access-context-manager perimeters create healthcare-data-perimeter \
--policy=POLICY_ID \
--title="Healthcare Data Perimeter" \
--resources="projects/12345,projects/67890" \
--restricted-services="bigquery.googleapis.com,storage.googleapis.com" \
--access-levels="accessPolicies/POLICY_ID/accessLevels/corp-trusted-access"
The access-level-spec.yaml defines which conditions allow perimeter traversal:
# access-level-spec.yaml
- ipSubnetworks:
- "203.0.113.0/24" # Corporate office IP range
- "198.51.100.0/24" # VPN egress range
devicePolicy:
requireScreenlock: true
osConstraints:
- osType: DESKTOP_CHROME_OS
minimumVersion: "13816.0.0"
- osType: DESKTOP_MAC
- osType: DESKTOP_WINDOWS
allowedEncryptionStatuses:
- ENCRYPTED
Terraform Configuration for VPC Service Controls
resource "google_access_context_manager_service_perimeter" "healthcare_perimeter" {
parent = "accessPolicies/${google_access_context_manager_access_policy.org_policy.name}"
name = "accessPolicies/${google_access_context_manager_access_policy.org_policy.name}/servicePerimeters/healthcare_data"
title = "Healthcare Data Perimeter"
status {
resources = [
"projects/${data.google_project.data_project.number}",
"projects/${data.google_project.analytics_project.number}",
]
restricted_services = [
"bigquery.googleapis.com",
"storage.googleapis.com",
"healthcare.googleapis.com",
]
access_levels = [
google_access_context_manager_access_level.corp_trusted.name,
]
# Allow CI/CD pipeline to write to storage from outside perimeter
ingress_policies {
ingress_from {
identity_type = "ANY_IDENTITY"
sources {
access_level = google_access_context_manager_access_level.cicd_access.name
}
}
ingress_to {
resources = ["projects/${data.google_project.data_project.number}"]
operations {
service_name = "storage.googleapis.com"
method_selectors {
method = "google.storage.objects.create"
}
}
}
}
# Allow BigQuery export only to internal project
egress_policies {
egress_from {
identity_type = "ANY_IDENTITY"
}
egress_to {
resources = ["projects/${data.google_project.reporting_project.number}"]
operations {
service_name = "bigquery.googleapis.com"
method_selectors {
method = "google.cloud.bigquery.v2.JobService.InsertJob"
}
}
}
}
}
}
Key point: Ingress and egress policies are method-level. You do not need to allow all operations — scope them to the exact API methods your workloads require.
Identity-Aware Proxy: Context-Aware Access Without VPN
IAP places an authentication and authorization layer in front of your applications. Users authenticate via Google Identity (or a configured IdP), and IAP evaluates access policies that can include device posture, IP address, and user group membership. The application itself never needs to implement authentication — it receives verified identity headers from IAP.
Enabling IAP for a GKE Service
For a service running behind a GKE Ingress with a BackendConfig:
# backend-config.yaml
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
name: iap-backend-config
namespace: production
spec:
iap:
enabled: true
oauthclientCredentials:
secretName: iap-oauth-secret
Create the OAuth credentials and secret:
# Create OAuth consent screen and credentials in Cloud Console first,
# then store them as a Kubernetes secret
kubectl create secret generic iap-oauth-secret \
--namespace=production \
--from-literal=client_id=CLIENT_ID.apps.googleusercontent.com \
--from-literal=client_secret=CLIENT_SECRET
# Set IAM policy to allow specific group access through IAP
gcloud iap web add-iam-policy-binding \
--resource-type=backend-services \
--service=admin-dashboard-backend \
--member="group:[email protected]" \
--role="roles/iap.httpsResourceAccessAllowed"
IAP for SSH Access (Replacing VPN for VM Access)
IAP TCP forwarding allows SSH access to VMs that have no external IP addresses:
# SSH through IAP tunnel — no VPN, no public IP needed
gcloud compute ssh my-instance \
--zone=us-central1-a \
--tunnel-through-iap
# Forward a port through IAP (e.g., for database access)
gcloud compute start-iap-tunnel my-database-vm 5432 \
--local-host-port=localhost:5432 \
--zone=us-central1-a
The corresponding firewall rule allows IAP's IP range only:
resource "google_compute_firewall" "allow_iap_ssh" {
name = "allow-iap-ssh"
network = google_compute_network.main.id
allow {
protocol = "tcp"
ports = ["22"]
}
# IAP's IP range — the only source that can reach SSH
source_ranges = ["35.235.240.0/20"]
target_tags = ["iap-ssh"]
}
Block all other SSH ingress. The VM has no public IP and the only path in is through IAP, which enforces authentication and context-aware access policies before forwarding any traffic.
Workload Identity Federation: Eliminating Service Account Keys
Service account key files are the most common credential leak vector on GCP. Workload Identity Federation replaces them entirely by allowing external identity providers (GitHub Actions OIDC, AWS IAM, Azure AD, on-prem OIDC) to exchange tokens for short-lived GCP access tokens.
The flow:
- ✓External workload obtains a token from its native IdP (e.g., GitHub Actions provides an OIDC token to every workflow run).
- ✓The token is exchanged via GCP's Security Token Service (STS) for a federated token.
- ✓The federated token impersonates a GCP service account.
- ✓The resulting access token is short-lived (1 hour by default) and cannot be exported.
Terraform Configuration for GitHub Actions Federation
resource "google_iam_workload_identity_pool" "github_pool" {
workload_identity_pool_id = "github-actions-pool"
display_name = "GitHub Actions Pool"
description = "Identity pool for GitHub Actions OIDC"
}
resource "google_iam_workload_identity_pool_provider" "github_provider" {
workload_identity_pool_id = google_iam_workload_identity_pool.github_pool.workload_identity_pool_id
workload_identity_pool_provider_id = "github-oidc-provider"
display_name = "GitHub OIDC"
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.actor" = "assertion.actor"
"attribute.repository" = "assertion.repository"
"attribute.ref" = "assertion.ref"
}
# Restrict to your organization's repositories
attribute_condition = "assertion.repository_owner == 'your-github-org'"
oidc {
issuer_uri = "https://token.actions.githubusercontent.com"
}
}
resource "google_service_account_iam_binding" "github_deploy_binding" {
service_account_id = google_service_account.deploy_sa.name
role = "roles/iam.workloadIdentityUser"
members = [
"principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github_pool.name}/attribute.repository/your-github-org/your-repo",
]
}
In the GitHub Actions workflow:
# .github/workflows/deploy.yaml
jobs:
deploy:
permissions:
contents: read
id-token: write # Required for OIDC token
steps:
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: "projects/PROJECT_NUM/locations/global/workloadIdentityPools/github-actions-pool/providers/github-oidc-provider"
service_account: "[email protected]"
- uses: google-github-actions/setup-gcloud@v2
- run: gcloud run deploy my-service --image=...
After this configuration, delete every service account key in the project. Run this audit regularly:
# Find all user-managed service account keys
gcloud iam service-accounts list --format="value(email)" \
--project=my-project | while read sa; do
gcloud iam service-accounts keys list \
--iam-account="$sa" \
--managed-by=user \
--format="table(name,validAfterTime,validBeforeTime)"
done
Private Google Access: No Public IPs, No Exceptions
Private Google Access allows VM instances without external IP addresses to reach Google APIs and services. Combined with VPC Service Controls, this ensures that data never traverses the public internet.
Configure DNS to resolve Google API endpoints to the private or restricted VIP ranges:
# DNS zone for restricted.googleapis.com
resource "google_dns_managed_zone" "restricted_apis" {
name = "restricted-googleapis"
dns_name = "googleapis.com."
visibility = "private"
private_visibility_config {
networks {
network_url = google_compute_network.main.id
}
}
}
resource "google_dns_record_set" "restricted_api_cname" {
name = "*.googleapis.com."
managed_zone = google_dns_managed_zone.restricted_apis.name
type = "CNAME"
ttl = 300
rrdatas = ["restricted.googleapis.com."]
}
resource "google_dns_record_set" "restricted_api_a" {
name = "restricted.googleapis.com."
managed_zone = google_dns_managed_zone.restricted_apis.name
type = "A"
ttl = 300
rrdatas = [
"199.36.153.4",
"199.36.153.5",
"199.36.153.6",
"199.36.153.7",
]
}
Two VIP ranges are available:
- ✓
private.googleapis.com(199.36.153.8/30) — reaches all Google APIs, does not enforce VPC Service Controls. - ✓
restricted.googleapis.com(199.36.153.4/30) — reaches only APIs that support VPC Service Controls, enforces perimeter policies.
For a zero-trust deployment, always use restricted.googleapis.com. It ensures that even if a workload attempts to call a Google API that is not within the service perimeter, the request is blocked at the network layer.
Enable Private Google Access on the subnet:
gcloud compute networks subnets update my-subnet \
--region=us-central1 \
--enable-private-ip-google-access
Binary Authorization: Supply Chain Integrity for Containers
Binary Authorization is an admission controller for GKE that verifies container images are signed by trusted authorities before allowing them to run. This prevents deployment of tampered images, images from untrusted registries, or images that did not pass through your CI pipeline.
Setting Up an Attestor with Cloud KMS
# Create a Cloud KMS key for signing attestations
gcloud kms keyrings create build-attestors --location=global
gcloud kms keys create ci-pipeline-signer \
--keyring=build-attestors \
--location=global \
--purpose=asymmetric-signing \
--default-algorithm=ec-sign-p256-sha256
# Create a Container Analysis note (the attestor links to this)
cat > note.json << 'EOF'
{
"attestation": {
"hint": {
"humanReadableName": "CI Pipeline Attestor"
}
}
}
EOF
curl -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
"https://containeranalysis.googleapis.com/v1/projects/PROJECT_ID/notes/ci-pipeline-attestor" \
-d @note.json
# Create the attestor
gcloud container binauthz attestors create ci-pipeline-attestor \
--attestation-authority-note=ci-pipeline-attestor \
--attestation-authority-note-project=PROJECT_ID
# Add the KMS key to the attestor
gcloud container binauthz attestors public-keys add \
--attestor=ci-pipeline-attestor \
--keyversion-project=PROJECT_ID \
--keyversion-location=global \
--keyversion-keyring=build-attestors \
--keyversion-key=ci-pipeline-signer \
--keyversion=1
Binary Authorization Policy
# binauthz-policy.yaml
defaultAdmissionRule:
evaluationMode: REQUIRE_ATTESTATION
enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
requireAttestationsBy:
- projects/PROJECT_ID/attestors/ci-pipeline-attestor
globalPolicyEvaluationMode: ENABLE
clusterAdmissionRules:
us-central1-a.production-cluster:
evaluationMode: REQUIRE_ATTESTATION
enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
requireAttestationsBy:
- projects/PROJECT_ID/attestors/ci-pipeline-attestor
Terraform for Binary Authorization
resource "google_binary_authorization_policy" "policy" {
project = var.project_id
global_policy_evaluation_mode = "ENABLE"
default_admission_rule {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = [
google_binary_authorization_attestor.ci_pipeline.id,
]
}
cluster_admission_rules {
cluster = "us-central1-a.production-cluster"
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = [
google_binary_authorization_attestor.ci_pipeline.id,
]
}
}
resource "google_binary_authorization_attestor" "ci_pipeline" {
name = "ci-pipeline-attestor"
project = var.project_id
attestation_authority_note {
note_reference = google_container_analysis_note.ci_attestor_note.name
public_keys {
id = data.google_kms_crypto_key_version.signer_version.id
pkix_public_key {
public_key_pem = data.google_kms_crypto_key_version.signer_version.public_key[0].pem
signature_algorithm = "ECDSA_P256_SHA256"
}
}
}
}
Sign images in your CI pipeline after successful tests and security scans:
# In CI pipeline — sign the image after all checks pass
IMAGE_DIGEST=$(gcloud artifacts docker images describe \
us-central1-docker.pkg.dev/PROJECT_ID/repo/my-app:${GIT_SHA} \
--format="value(image_summary.digest)")
gcloud container binauthz attestations sign-and-create \
--artifact-url="us-central1-docker.pkg.dev/PROJECT_ID/repo/my-app@${IMAGE_DIGEST}" \
--attestor="ci-pipeline-attestor" \
--attestor-project=PROJECT_ID \
--keyversion-project=PROJECT_ID \
--keyversion-location=global \
--keyversion-keyring=build-attestors \
--keyversion-key=ci-pipeline-signer \
--keyversion=1
Cloud Armor: WAF and DDoS Protection at the Edge
Cloud Armor policies attach to backend services on the global HTTP(S) load balancer. They evaluate before traffic reaches your application.
Terraform Configuration
resource "google_compute_security_policy" "app_waf" {
name = "app-waf-policy"
# Default rule: allow
rule {
action = "allow"
priority = 2147483647
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
description = "Default allow rule"
}
# Block OWASP Top 10: SQL injection
rule {
action = "deny(403)"
priority = 1000
match {
expr {
expression = "evaluatePreconfiguredExpr('sqli-v33-stable')"
}
}
description = "Block SQL injection"
}
# Block OWASP Top 10: XSS
rule {
action = "deny(403)"
priority = 1001
match {
expr {
expression = "evaluatePreconfiguredExpr('xss-v33-stable')"
}
}
description = "Block cross-site scripting"
}
# Block OWASP Top 10: Local file inclusion
rule {
action = "deny(403)"
priority = 1002
match {
expr {
expression = "evaluatePreconfiguredExpr('lfi-v33-stable')"
}
}
description = "Block local file inclusion"
}
# Block OWASP Top 10: Remote code execution
rule {
action = "deny(403)"
priority = 1003
match {
expr {
expression = "evaluatePreconfiguredExpr('rce-v33-stable')"
}
}
description = "Block remote code execution"
}
# Rate limiting: max 100 requests per minute per IP
rule {
action = "throttle"
priority = 2000
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
rate_limit_options {
rate_limit_threshold {
count = 100
interval_sec = 60
}
conform_action = "allow"
exceed_action = "deny(429)"
enforce_on_key = "IP"
}
description = "Rate limit per IP"
}
# Geo-blocking: deny traffic from embargoed regions
rule {
action = "deny(403)"
priority = 500
match {
expr {
expression = "origin.region_code == 'KP' || origin.region_code == 'IR'"
}
}
description = "Block embargoed regions"
}
}
Attach the policy to a backend service:
gcloud compute backend-services update my-backend-service \
--security-policy=app-waf-policy \
--global
Organization Policy Constraints
Organization policies enforce guardrails that IAM cannot. They restrict what resources can be created, regardless of a principal's IAM permissions.
# Restrict resource creation to approved regions only
gcloud resource-manager org-policies set-policy \
--organization=123456789012 policy.yaml
# policy.yaml — restrict to US and EU regions
constraint: constraints/gcp.resourceLocations
listPolicy:
allowedValues:
- in:us-locations
- in:eu-locations
deniedValues:
- in:asia-locations
Other critical constraints:
# Disable service account key creation entirely
gcloud resource-manager org-policies enable-enforce \
constraints/iam.disableServiceAccountKeyCreation \
--organization=123456789012
# Require OS Login for all compute instances
gcloud resource-manager org-policies enable-enforce \
constraints/compute.requireOsLogin \
--organization=123456789012
# Disable serial port access
gcloud resource-manager org-policies enable-enforce \
constraints/compute.disableSerialPortAccess \
--organization=123456789012
# Restrict VM external IPs (deny all)
gcloud resource-manager org-policies set-policy \
--organization=123456789012 deny-external-ip.yaml
# deny-external-ip.yaml
constraint: constraints/compute.vmExternalIpAccess
listPolicy:
allValues: DENY
These constraints form the non-negotiable foundation. Even an Organization Admin cannot create resources that violate them without first modifying the policy — and that modification itself generates an audit log entry.
Audit Logging: Visibility Into Everything
Zero-trust requires comprehensive logging. On GCP, this means enabling Data Access audit logs (which are off by default for most services) and aggregating them for analysis.
Enable data access logs for all services:
# Enable data access audit logs for BigQuery and Cloud Storage
gcloud projects set-iam-policy my-project <(
gcloud projects get-iam-policy my-project --format=json | \
python3 -c "
import json, sys
policy = json.load(sys.stdin)
policy.setdefault('auditConfigs', [])
for svc in ['bigquery.googleapis.com', 'storage.googleapis.com', 'allServices']:
policy['auditConfigs'].append({
'service': svc,
'auditLogConfigs': [
{'logType': 'ADMIN_READ'},
{'logType': 'DATA_READ'},
{'logType': 'DATA_WRITE'},
]
})
json.dump(policy, sys.stdout)
")
Query audit logs for suspicious activity:
# Find all BigQuery data access events in the last 24 hours
gcloud logging read '
logName="projects/my-project/logs/cloudaudit.googleapis.com%2Fdata_access"
AND resource.type="bigquery_resource"
AND timestamp>="2025-10-07T00:00:00Z"
' --format="table(timestamp,protoPayload.authenticationInfo.principalEmail,protoPayload.methodName,protoPayload.resourceName)" \
--limit=50
# Find VPC Service Controls violations
gcloud logging read '
logName="projects/my-project/logs/cloudaudit.googleapis.com%2Fpolicy"
AND protoPayload.metadata.@type="type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata"
' --format=json --limit=20
# Find IAM policy changes
gcloud logging read '
logName="projects/my-project/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.methodName="SetIamPolicy"
' --format="table(timestamp,protoPayload.authenticationInfo.principalEmail,protoPayload.resourceName)" \
--limit=25
Enable VPC Flow Logs on every subnet to capture network-level traffic metadata:
resource "google_compute_subnetwork" "main" {
name = "main-subnet"
ip_cidr_range = "10.0.0.0/20"
region = "us-central1"
network = google_compute_network.main.id
log_config {
aggregation_interval = "INTERVAL_5_SEC"
flow_sampling = 1.0
metadata = "INCLUDE_ALL_METADATA"
filter_expr = "true"
}
}
Terraform Module Structure for Reproducible Zero-Trust
Rather than configuring each service ad hoc, structure your Terraform as composable modules:
modules/
├── zero-trust-foundation/
│ ├── main.tf # Org policies, audit log sinks
│ ├── variables.tf
│ └── outputs.tf
├── vpc-service-controls/
│ ├── main.tf # Perimeters, access levels, ingress/egress
│ ├── variables.tf
│ └── outputs.tf
├── iap/
│ ├── main.tf # IAP configuration, firewall rules, OAuth
│ ├── variables.tf
│ └── outputs.tf
├── workload-identity/
│ ├── main.tf # Identity pools, providers, SA bindings
│ ├── variables.tf
│ └── outputs.tf
├── binary-authorization/
│ ├── main.tf # Attestors, policies, KMS keys
│ ├── variables.tf
│ └── outputs.tf
├── cloud-armor/
│ ├── main.tf # Security policies, WAF rules
│ ├── variables.tf
│ └── outputs.tf
└── private-networking/
├── main.tf # VPC, subnets, DNS zones, NAT
├── variables.tf
└── outputs.tf
The root module composes them:
module "foundation" {
source = "./modules/zero-trust-foundation"
organization_id = var.organization_id
allowed_regions = ["us-locations", "eu-locations"]
}
module "networking" {
source = "./modules/private-networking"
project_id = var.project_id
region = var.region
}
module "vpc_sc" {
source = "./modules/vpc-service-controls"
access_policy_id = module.foundation.access_policy_id
protected_projects = [var.data_project_number]
restricted_services = [
"bigquery.googleapis.com",
"storage.googleapis.com",
"healthcare.googleapis.com",
]
}
module "iap" {
source = "./modules/iap"
project_id = var.project_id
network_id = module.networking.network_id
oauth_brand = var.oauth_brand
}
module "binary_auth" {
source = "./modules/binary-authorization"
project_id = var.project_id
attestor_kms_key = var.attestor_kms_key
gke_cluster_zones = var.gke_cluster_zones
}
module "cloud_armor" {
source = "./modules/cloud-armor"
rate_limit_per_ip = 100
blocked_regions = ["KP", "IR"]
enable_owasp_rules = true
}
Every module should output the resource IDs and names that downstream modules need. Version-pin your module sources in production.
Case Study: Healthcare SaaS Platform — Passing a HITRUST Audit
The Problem
A healthcare SaaS platform running on GCP needed to achieve HITRUST CSF certification. The platform processed Protected Health Information (PHI) in BigQuery and Cloud Storage, served an admin dashboard to internal staff, and ran containerized microservices on GKE. The initial audit readiness assessment flagged multiple control gaps:
- ✓Access Control (01.c, 01.v): Overly broad IAM roles. Several engineers had
roles/bigquery.adminat the project level. No context-aware access controls. - ✓Audit Logging (09.aa, 09.ad): Data access audit logs were not enabled. No mechanism to detect data exfiltration attempts.
- ✓Network Security (09.m): Admin dashboard was accessible via a public IP with IP-based allowlisting. VPN was the only access control for SSH.
- ✓System Integrity (10.h): No controls on what container images could run in production. Any image from any registry could be deployed.
The Implementation
Stripe Systems implemented a layered zero-trust architecture over a 10-week engagement. The following sections detail the exact configurations deployed.
VPC Service Controls Around PHI Data
The most critical control: a service perimeter around BigQuery datasets and Cloud Storage buckets containing PHI.
resource "google_access_context_manager_service_perimeter" "phi_perimeter" {
parent = "accessPolicies/${var.access_policy_id}"
name = "accessPolicies/${var.access_policy_id}/servicePerimeters/phi_data_perimeter"
title = "PHI Data Perimeter"
status {
resources = [
"projects/${var.phi_data_project_number}",
]
restricted_services = [
"bigquery.googleapis.com",
"storage.googleapis.com",
]
access_levels = [
"accessPolicies/${var.access_policy_id}/accessLevels/admin_corp_access",
]
vpc_accessible_services {
enable_restriction = true
allowed_services = [
"bigquery.googleapis.com",
"storage.googleapis.com",
"logging.googleapis.com",
]
}
ingress_policies {
ingress_from {
sources {
resource = "projects/${var.gke_project_number}"
}
identity_type = "ANY_IDENTITY"
}
ingress_to {
resources = ["*"]
operations {
service_name = "bigquery.googleapis.com"
method_selectors {
method = "google.cloud.bigquery.v2.JobService.InsertJob"
}
method_selectors {
method = "google.cloud.bigquery.v2.JobService.GetQueryResults"
}
}
}
}
}
}
This configuration means: even if an engineer with roles/bigquery.admin attempts to export a table to a personal project, the request is denied at the perimeter. The vpc_accessible_services block ensures that only explicitly listed services are reachable from within the perimeter's VPC network.
IAP for Admin Dashboard Access
The admin dashboard previously sat behind a public IP with an IP allowlist. Stripe Systems replaced this with IAP, eliminating the need for both the public IP and the corporate VPN.
# Enable IAP on the admin backend service
gcloud iap web enable \
--resource-type=backend-services \
--service=admin-dashboard-backend
# Grant access to the admin group only
gcloud iap web add-iam-policy-binding \
--resource-type=backend-services \
--service=admin-dashboard-backend \
--member="group:[email protected]" \
--role="roles/iap.httpsResourceAccessAllowed"
# Set up IAP access level requiring managed device
gcloud access-context-manager levels create admin-device-policy \
--policy=POLICY_ID \
--title="Admin Device Policy" \
--basic-level-spec=admin-access-level.yaml
# admin-access-level.yaml
- devicePolicy:
requireScreenlock: true
requireAdminApproval: true
allowedEncryptionStatuses:
- ENCRYPTED
allowedDeviceManagementLevels:
- COMPLETE
Terraform for the IAP-protected backend:
resource "google_iap_web_backend_service_iam_member" "admin_access" {
project = var.project_id
web_backend_service = google_compute_backend_service.admin_dashboard.name
role = "roles/iap.httpsResourceAccessAllowed"
member = "group:[email protected]"
condition {
title = "require-managed-device"
expression = "'accessPolicies/${var.access_policy_id}/accessLevels/admin-device-policy' in request.auth.access_levels"
}
}
Binary Authorization in GKE
Only images built by the CI pipeline, from the approved Artifact Registry, and signed after passing security scans, are allowed to run:
# binary-authorization-policy.yaml
defaultAdmissionRule:
evaluationMode: REQUIRE_ATTESTATION
enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
requireAttestationsBy:
- projects/healthcare-saas-prod/attestors/ci-pipeline-attestor
- projects/healthcare-saas-prod/attestors/security-scan-attestor
globalPolicyEvaluationMode: ENABLE
clusterAdmissionRules:
us-central1-a.phi-processing-cluster:
evaluationMode: REQUIRE_ATTESTATION
enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
requireAttestationsBy:
- projects/healthcare-saas-prod/attestors/ci-pipeline-attestor
- projects/healthcare-saas-prod/attestors/security-scan-attestor
Two attestors are required: one from the CI build step (proving the image was built from the approved repository) and one from the security scanning step (proving the image passed vulnerability scanning). An image must have both attestations to be admitted.
resource "google_binary_authorization_policy" "phi_cluster_policy" {
project = var.project_id
global_policy_evaluation_mode = "ENABLE"
default_admission_rule {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = [
google_binary_authorization_attestor.ci_pipeline.id,
google_binary_authorization_attestor.security_scan.id,
]
}
cluster_admission_rules {
cluster = "${var.region}-a.phi-processing-cluster"
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = [
google_binary_authorization_attestor.ci_pipeline.id,
google_binary_authorization_attestor.security_scan.id,
]
}
}
Network Architecture (Zero-Trust Boundaries)
The resulting architecture has three distinct trust boundaries:
┌─────────────────────────────────────────────────────────────────┐
│ GCP Organization │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Org Policies: No SA keys, No external IPs, US/EU only │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ VPC Service Controls Perimeter (PHI Data) │ │ │
│ │ │ │ │ │
│ │ │ ┌──────────────┐ ┌────────────────────────────┐ │ │ │
│ │ │ │ BigQuery │ │ Cloud Storage (PHI) │ │ │ │
│ │ │ │ (PHI data) │ │ CMEK-encrypted buckets │ │ │ │
│ │ │ └──────────────┘ └────────────────────────────┘ │ │ │
│ │ │ ▲ ingress only from GKE project │ │ │
│ │ └───────┼─────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────┼──────────────────────────────────────────┐ │ │
│ │ │ GKE Project (Private Cluster) │ │ │
│ │ │ ┌────┴──────┐ ┌──────────────┐ │ │ │
│ │ │ │ App Pods │ │ Admin Dash │◄── IAP ◄── Users│ │ │
│ │ │ │ (Binary │ │ (IAP- │ (Identity + │ │ │
│ │ │ │ Auth'd) │ │ protected) │ Device check)│ │ │
│ │ │ └───────────┘ └──────────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ restricted.googleapis.com (private) │ │ │
│ │ │ Cloud Armor WAF ◄── Internet traffic │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
- ✓Outer boundary: Organization policies enforce hard constraints (no service account keys, no external IPs, restricted regions).
- ✓Middle boundary: VPC Service Controls prevent data from leaving the PHI project, regardless of IAM permissions.
- ✓Inner boundary: IAP authenticates every user request to the admin dashboard. Binary Authorization validates every container before admission. Cloud Armor filters malicious traffic at the edge.
Audit Results: Before and After
| HITRUST Control | Before | After |
|---|---|---|
| 01.c — Privilege Management | Broad project-level IAM roles; no context-aware access | Least-privilege roles; IAP with device posture and group-based access |
| 01.v — Information Access Restriction | IP allowlists only; no API-level exfiltration controls | VPC Service Controls perimeter blocks cross-project data movement |
| 09.aa — Audit Logging | Admin activity logs only (default); no data access logs | Full data access logging on all services; log sinks to locked CMEK-encrypted bucket |
| 09.ab — Monitoring System Use | Manual log review | Automated alerts on VPC-SC violations, IAM changes, and Binary Auth denials |
| 09.ad — Administrator and Operator Logs | No separation of admin audit trail | Cloud Audit Logs with immutable admin activity records; 400-day retention |
| 09.m — Network Controls | Flat VPC; public IPs on admin servers; VPN for SSH | Private cluster; no public IPs; IAP TCP forwarding for SSH; VPC Flow Logs |
| 10.h — Control of Operational Software | Any Docker image could be deployed | Binary Authorization with dual attestor (CI build + security scan) |
The HITRUST audit was completed with zero critical findings related to GCP infrastructure controls. Three informational findings were logged (documentation completeness items) and resolved within the remediation period.
Key Takeaways
Zero-trust on GCP is not a product you purchase — it is an architecture you build by layering identity-based access, API-level perimeters, supply chain verification, and comprehensive logging. The concrete steps:
- ✓Start with VPC Service Controls around your most sensitive data. This is the highest-impact control and the hardest to retrofit later.
- ✓Enable IAP for all internal tools. Remove VPN dependencies for application access.
- ✓Eliminate service account keys with Workload Identity Federation. Set the
iam.disableServiceAccountKeyCreationorg policy. - ✓Enforce Binary Authorization in GKE before deploying to production.
- ✓Enable data access audit logs on all services. The cost is marginal compared to the visibility gained.
- ✓Deploy Cloud Armor with OWASP rules on every external-facing load balancer.
- ✓Set organization policies as hard guardrails that cannot be overridden by individual project owners.
Each layer compensates for failures in other layers. That is the point of defense in depth: no single control needs to be perfect because the attacker must bypass all of them.
Ready to discuss your project?
Get in Touch →