Skip to content
Indus LeveL
GCP Zero Trust Firewall Security Migration IAM

Zero Trust on GCP: Migrating from a Vulnerable VPS to a Hardened Architecture

A comprehensive step-by-step guide on how we migrated a standalone VPS to a secure Google Cloud architecture using Cloud Identity, project-level IAM, and strict default-deny firewalls.

2 min read
Zero Trust on GCP: Migrating from a Vulnerable VPS to a Hardened Architecture

Migrating from a legacy, standalone Virtual Private Server (VPS) to Google Cloud Platform (GCP) requires more than just a lift-and-shift. It is an opportunity to completely overhaul your security posture.

In my recent migration project, we implemented a strict Zero Trust network architecture. We disabled all default routes, denied all ingress and egress traffic by default, explicitly whitelisted only the required outbound FQDNs, and centralized access using Cloud Identity with project-scoped IAM bindings.

Here is the exact step-by-step breakdown of what we built and how you can replicate it in your environment:


1. Create Cloud Identity Groups

Before creating infrastructure, we secured the human element. Relying on local Linux SSH keys or individual user permissions is a management nightmare. Instead, we started by creating dedicated Cloud Identity groups for Role-Based Access Control (RBAC).

Based on our separation of duties, we created several specific groups:

UI Steps:

  1. Navigate to the Google Workspace Admin Console or Cloud Identity.
  2. Go to Directory > Groups.
  3. Create distinct groups (e.g., gcp-network-admins, gcp-sys-admins-app-sudo, gcp-infra-builders, etc.).

Using gcloud:

# Create core administrative and infrastructure groups
gcloud identity groups create "gcp-network-admins@<DOMAIN_NAME>" \
  --organization="<ORG_ID>" \
  --description="Network Administrators for GCP"

gcloud identity groups create "gcp-infra-builders@<DOMAIN_NAME>" \
  --organization="<ORG_ID>" \
  --description="Infrastructure Provisioning Team"

# Create segmented system administration groups
gcloud identity groups create "gcp-sys-admins-app-sudo@<DOMAIN_NAME>" \
  --organization="<ORG_ID>" \
  --description="App Administrators (Root Access)"

gcloud identity groups create "gcp-sys-admins-db-nosudo@<DOMAIN_NAME>" \
  --organization="<ORG_ID>" \
  --description="Database Administrators (Standard Access)"

Documentation Link: Creating and managing Google Groups


2. Provision Project-Level IAM Policies & OS Login

Once the groups exist, we map them directly to Google Cloud IAM roles. To maintain strict boundary control, we bound all IAM policies at the project level rather than the organization level. We also enforced project-wide OS Login so administrators can authenticate via Identity-Aware Proxy (IAP) using their Google credentials, entirely eliminating static SSH keys.

UI Steps:

  1. In the GCP Console, go to IAM & Admin > IAM.
  2. Click Grant Access and assign the exact roles required for each group within this specific project.
  3. Go to Compute Engine > Metadata and add the key enable-oslogin with the value TRUE.

Using gcloud:

# Enable Project-Wide OS Login
gcloud compute project-info add-metadata \
  --metadata enable-oslogin=TRUE \
  --project="<PROJECT_ID>"

# Bind IAM Roles for Network Admins
gcloud projects add-iam-policy-binding "<PROJECT_ID>" \
  --member="group:gcp-network-admins@<DOMAIN_NAME>" \
  --role="roles/compute.networkAdmin"
gcloud projects add-iam-policy-binding "<PROJECT_ID>" \
  --member="group:gcp-network-admins@<DOMAIN_NAME>" \
  --role="roles/compute.securityAdmin"

# Bind IAM Roles for App Sys Admins (Sudo)
gcloud projects add-iam-policy-binding "<PROJECT_ID>" \
  --member="group:gcp-sys-admins-app-sudo@<DOMAIN_NAME>" \
  --role="roles/compute.admin"
gcloud projects add-iam-policy-binding "<PROJECT_ID>" \
  --member="group:gcp-sys-admins-app-sudo@<DOMAIN_NAME>" \
  --role="roles/compute.osAdminLogin"
gcloud projects add-iam-policy-binding "<PROJECT_ID>" \
  --member="group:gcp-sys-admins-app-sudo@<DOMAIN_NAME>" \
  --role="roles/iap.tunnelResourceAccessor"

# Bind IAM Roles for Infra Builders
gcloud projects add-iam-policy-binding "<PROJECT_ID>" \
  --member="group:gcp-infra-builders@<DOMAIN_NAME>" \
  --role="roles/compute.instanceAdmin.v1"

Documentation Link: Setting up OS Login


3. Configure a Custom VPC and Subnets

We abandoned the default network in favor of a custom VPC. This allowed us to isolate the database in a private subnet while keeping the application tier in a public subnet.

UI Steps:

  1. Go to VPC Network > VPC networks > Create VPC network.
  2. Select Custom subnet creation mode.
  3. Create an app-subnet and a db-subnet (ensure Private Google Access is enabled for the DB subnet).

Using gcloud:

# Create Custom VPC
gcloud compute networks create "<VPC_NAME>" \
  --subnet-mode=custom \
  --project="<PROJECT_ID>"

# Create Private DB Subnet
gcloud compute networks subnets create "db-subnet" \
  --network="<VPC_NAME>" \
  --range="10.0.1.0/24" \
  --region="europe-central2" \
  --enable-private-ip-google-access \
  --project="<PROJECT_ID>"

# Create Public App Subnet
gcloud compute networks subnets create "app-subnet" \
  --network="<VPC_NAME>" \
  --range="10.0.2.0/24" \
  --region="europe-central2" \
  --project="<PROJECT_ID>"

Documentation Link: Creating and managing VPC networks


4. Implement Strict Ingress Firewall Rules

Our ingress strategy was simple: Deny everything by default, log the drops, and explicitly allow required traffic using network tags. For public traffic, we exclusively opened standard HTTP and HTTPS web ports (80 and 443). Everything else, including database access and SSH, is routed securely through IAP or internal network rules.

UI Steps:

  1. Go to VPC Network > Firewalls.
  2. Create a rule named deny-all-ingress, set priority to 65000, action to Deny, and enable Logging.
  3. Create an allow-iap-ssh rule targeting Google's IAP range (35.235.240.0/20) on TCP 22.
  4. Create an allow-web rule allowing 0.0.0.0/0 strictly on TCP 80 and 443.

Using gcloud:

# 1. Catch-all Ingress Deny (with Logging)
gcloud compute firewall-rules create "deny-all-ingress" \
  --network="<VPC_NAME>" \
  --direction=INGRESS \
  --priority=65000 \
  --action=DENY \
  --source-ranges="0.0.0.0/0" \
  --rules=all \
  --enable-logging \
  --project="<PROJECT_ID>"

# 2. Allow IAP Tunnels for SSH and DB (from Google's IAP IPs)
gcloud compute firewall-rules create "allow-iap-ssh" \
  --network="<VPC_NAME>" \
  --direction=INGRESS \
  --priority=1000 \
  --action=ALLOW \
  --source-ranges="35.235.240.0/20" \
  --rules=tcp:22 \
  --target-tags="allow-iap-ssh" \
  --project="<PROJECT_ID>"

gcloud compute firewall-rules create "allow-iap-db-tunnels" \
  --network="<VPC_NAME>" \
  --direction=INGRESS \
  --priority=1000 \
  --action=ALLOW \
  --source-ranges="35.235.240.0/20" \
  --rules=tcp:27017 \
  --target-tags="allow-iap-db" \
  --project="<PROJECT_ID>"

# 3. Allow Public Web Traffic (Strictly 80 and 443)
gcloud compute firewall-rules create "allow-web" \
  --network="<VPC_NAME>" \
  --direction=INGRESS \
  --priority=1000 \
  --action=ALLOW \
  --source-ranges="0.0.0.0/0" \
  --rules=tcp:80,tcp:443 \
  --target-tags="app-tier" \
  --project="<PROJECT_ID>"

Documentation Link: VPC Firewall Rules Overview


5. Zero Trust Egress via FQDN Filtering

Standard firewall rules only filter by IP, which is insufficient for modern cloud environments where API IPs change constantly. We implemented Global Network Firewall Policies to whitelist exact domains (FQDNs) for our outbound traffic.

We set priority 65000 to deny all outgoing IPv4 traffic, and priority 1000 to allow connections specifically to package repositories (Ubuntu, Docker, NPM), necessary APIs (Alchemy, Let's Encrypt), and GitHub.

UI Steps:

  1. Go to VPC Network > Firewalls > Policies.
  2. Create a default-deny Egress rule at 65000 for all IPv4 (0.0.0.0/0).
  3. Add a rule at 1000 for Egress. In the Match criteria, select URLs/FQDNs and input domains like archive.ubuntu.com, github.com, etc.
  4. Set Action to Allow on TCP 80, 443.

Using gcloud:

# 1. Catch-all Egress Deny
gcloud compute firewall-policies rules create 65000 \
  --firewall-policy="<FIREWALL_POLICY_NAME>" \
  --direction=EGRESS \
  --action=deny \
  --match="srcIpRanges=['0.0.0.0/0']" \
  --description="Deny all outbound traffic by default" \
  --project="<PROJECT_ID>"

# 2. Allow specific FQDNs for Updates and APIs (TCP 80, 443)
gcloud compute firewall-policies rules create 1000 \
  --firewall-policy="<FIREWALL_POLICY_NAME>" \
  --direction=EGRESS \
  --action=allow \
  --match="destFqdns=['archive.ubuntu.com', 'download.docker.com', 'github.com', 'acme-v02.api.letsencrypt.org']" \
  --layer4-configs="tcp:80,tcp:443" \
  --description="Allow whitelisted HTTPS FQDNs" \
  --project="<PROJECT_ID>"

# 3. Allow Outbound SSH explicitly to GitHub
gcloud compute firewall-policies rules create 1002 \
  --firewall-policy="<FIREWALL_POLICY_NAME>" \
  --direction=EGRESS \
  --action=allow \
  --match="destFqdns=['github.com']" \
  --layer4-configs="tcp:22" \
  --description="Allow outbound SSH strictly to GitHub" \
  --project="<PROJECT_ID>"

Documentation Link: Cloud NGFW FQDN filtering


6. Enable Telemetry & Provision Hardened VMs

Finally, a secure network is useless if you cannot monitor it. We enabled DNS logging and VPC flow logs to catch any unauthorized connection attempts dropping on our 65000 priority rules.

We then provisioned the VMs, ensuring they utilized the correct network tags to inherit these strict protections.

UI Steps:

  1. Go to Cloud DNS > DNS Server Policies and toggle Logging on.
  2. Go to Compute Engine > VM Instances and create your instances, ensuring you attach tags like app-tier or db-tier.

Using gcloud:

# Enable DNS Logging
gcloud dns policies create "secure-dns-policy" \
  --networks="<VPC_NAME>" \
  --enable-logging \
  --description="Enable DNS logging to correlate with egress denies" \
  --project="<PROJECT_ID>"

# Provision Application VM inheriting our tags and Service Account
gcloud compute instances create "app-vm-prod" \
  --zone="europe-central2-a" \
  --machine-type="e2-medium" \
  --network="<VPC_NAME>" \
  --subnet="app-subnet" \
  --tags="app-tier,allow-iap-ssh" \
  --metadata="enable-oslogin=TRUE" \
  --service-account="app-service-account@<PROJECT_ID>.iam.gserviceaccount.com" \
  --project="<PROJECT_ID>"

Documentation Link: Logging and monitoring DNS

By wrapping the environment in Identity-Aware Proxy for administration and strict FQDN egress rules for outbound requests, we ensured that even if an application vulnerability was exploited, the attacker would have zero ability to exfiltrate data or download external payloads.

Back to Blog
Share:

Follow along

Stay in the loop — new articles, thoughts, and updates.