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
- 2. Provision Project-Level IAM Policies & OS Login
- 3. Configure a Custom VPC and Subnets
- 4. Implement Strict Ingress Firewall Rules
- 5. Zero Trust Egress via FQDN Filtering
- 6. Enable Telemetry & Provision Hardened VMs
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:
- Navigate to the Google Workspace Admin Console or Cloud Identity.
- Go to Directory > Groups.
- 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
Compliance Control Mapping (RBAC & Least Privilege)
- PCI-DSS v4.0: Requirements 7.1.1, 7.2.1, 7.2.2 (Restricting access based on job responsibility).
- SOC 2 (SSAE 18): CC6.1, CC6.3 (Logical access security and role-based authorizations).
- ISO/IEC 27001:2022: A.5.15, A.8.2 (Access control policies and full identity lifecycle management).
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:
- In the GCP Console, go to IAM & Admin > IAM.
- Click Grant Access and assign the exact roles required for each group within this specific project.
- Go to Compute Engine > Metadata and add the key
enable-osloginwith the valueTRUE.
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
Compliance Control Mapping (Identity Verification & MFA)
- PCI-DSS v4.0: Requirements 8.2.1, 8.2.2, 8.4.2 (Unique ID assignment and mandatory MFA for secure administration).
- SOC 2 (SSAE 18): CC6.2, CC6.7 (Enforcing strong authentication mechanisms and restricting transmission).
- ISO/IEC 27001:2022: A.8.3, A.8.5 (Access rights provisioning and secure authentication methods).
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:
- Go to VPC Network > VPC networks > Create VPC network.
- Select Custom subnet creation mode.
- Create an
app-subnetand adb-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
Compliance Control Mapping (Network Segmentation & Isolation)
- PCI-DSS v4.0: Requirements 1.2.1, 1.3.1, 1.3.2 (Isolating cardholder data environments from untrusted networks).
- SOC 2 (SSAE 18): CC6.6 (Logical boundary defense mechanisms and subnet isolation).
- ISO/IEC 27001:2022: A.8.20, A.8.22 (Network security management and segregation in 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:
- Go to VPC Network > Firewalls.
- Create a rule named
deny-all-ingress, set priority to65000, action to Deny, and enable Logging. - Create an
allow-iap-sshrule targeting Google's IAP range (35.235.240.0/20) on TCP22. - Create an
allow-webrule allowing0.0.0.0/0strictly on TCP80and443.
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
Compliance Control Mapping (Default-Deny & Ingress Filtering)
- PCI-DSS v4.0: Requirements 1.2.2, 1.4.1, 1.4.2 (Denying all traffic by default and restricting inbound connections).
- SOC 2 (SSAE 18): CC6.6 (Stateful inspection firewalls restricting inbound traffic to authorized protocols).
- ISO/IEC 27001:2022: A.8.20 (Implementation of packet filtering and default-deny network policies).
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:
- Go to VPC Network > Firewalls > Policies.
- Create a default-deny Egress rule at
65000for all IPv4 (0.0.0.0/0). - Add a rule at
1000for Egress. In the Match criteria, select URLs/FQDNs and input domains likearchive.ubuntu.com,github.com, etc. - 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
Companion Article
For an in-depth guide on analyzing and correlating denied firewall connection logs, check out our companion article: Correlating GCP Firewall Denies with DNS Logs using Log Analytics.
Compliance Control Mapping (Egress Control & Exfiltration Defense)
- PCI-DSS v4.0: Requirements 1.2.2, 1.4.3 (Restricting outbound traffic to verified, authorized destinations).
- SOC 2 (SSAE 18): CC6.6 (Outbound filtering mechanisms mitigating unauthorized data exfiltration).
- ISO/IEC 27001:2022: A.8.20 (Restricting outbound network connections to verified external domain names).
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:
- Go to Cloud DNS > DNS Server Policies and toggle Logging on.
- Go to Compute Engine > VM Instances and create your instances, ensuring you attach tags like
app-tierordb-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
Recommended Hardening Guide
For a comprehensive, production-ready checklist on securing the operating system of your VMs, see our complete guide: Enterprise Linux Hardening for Financial Applications on GCP.
Compliance Control Mapping (Telemetry, Monitoring & Audit Logging)
- PCI-DSS v4.0: Requirements 10.2.1, 10.2.2, 10.4.1 (Enabling audit logs for all access attempts and reviewing anomalies).
- SOC 2 (SSAE 18): CC7.1, CC7.2 (Detection and monitoring procedures for identifying vulnerabilities and anomalies).
- ISO/IEC 27001:2022: A.8.15, A.8.16 (Producing, protecting, and analyzing audit logs and monitoring anomalous behavior).
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.