This check flags GCP firewall rules that allow inbound FTP (ports 20 and 21) from 0.0.0.0/0. FTP sends credentials and data in cleartext, so exposing it publicly hands attackers an easy path to sniff logins or brute-force your server. Restrict the rule to known source IPs or drop FTP entirely in favor of SFTP.
FTP refuses to die. It shows up in legacy data pipelines, vendor file drops, and that one server someone spun up in 2016 and forgot about. The trouble is that FTP was designed in an era when nobody worried about traffic interception, and opening it to the public internet on a GCP VPC is one of the more dangerous firewall misconfigurations you can ship.
The vpc_openftp check looks at your GCP firewall rules and raises a finding when a rule permits ingress on TCP ports 20 or 21 from a public source range such as 0.0.0.0/0.
What this check detects
Lensix evaluates every ALLOW firewall rule in your VPC networks and looks for two conditions occurring together:
- The rule targets TCP port 21 (FTP control), port 20 (FTP active-mode data), or a range that includes them.
- The source range includes a public CIDR, most commonly
0.0.0.0/0(IPv4) or::/0(IPv6).
When both are true, anyone on the internet can reach the FTP service behind that rule, assuming a VM is listening on those ports and serving the relevant network tags or service accounts.
Note: GCP firewall rules attach to networks and apply to instances by target tags or target service accounts. A public FTP rule is only exploitable if it actually targets a running VM with an FTP daemon listening. But you should treat the rule itself as the risk, since target instances change constantly and a benign rule today becomes an exposure tomorrow.
Why it matters
FTP has no transport encryption. The control channel that carries your username and password travels in plaintext, and so does every file you transfer. That single fact drives most of the risk here.
Credential interception
Anyone positioned on the network path, a compromised router, a malicious Wi-Fi hotspot, or a peer in a shared cloud environment, can read FTP credentials straight off the wire. Once they have a valid login, they can read, modify, or replace whatever files that account can touch.
Brute-force and credential stuffing
A publicly reachable FTP port is a magnet for automated scanners. Bots find port 21 within hours of exposure and start hammering common usernames and leaked password lists. If anonymous FTP is enabled, they skip the guessing entirely.
Anonymous access and data leakage
Plenty of FTP servers ship with anonymous access turned on. Combine that with a public firewall rule and you have an open file share that search engines and scanning services index. This is a classic way that source code, backups, and customer data end up leaking.
Pivot point into your VPC
An exposed, often unpatched FTP daemon is a foothold. Older versions of vsftpd, ProFTPD, and similar servers have had remote code execution bugs. A compromised FTP host inside your VPC gives an attacker a base to scan internal subnets and move laterally.
Danger: If your FTP server allows writes, a public rule means anyone can upload files. Attackers use this to drop web shells, host malware, or stage data for exfiltration. Treat a writable, publicly exposed FTP server as an active incident, not just a misconfiguration.
How to fix it
The goal is to remove public access. You have three realistic options, in order of preference: drop FTP for SFTP, restrict the source range to known IPs, or delete the rule outright if nothing depends on it.
Step 1: Identify the offending rule
List firewall rules that allow FTP ports and check their source ranges:
gcloud compute firewall-rules list \
--filter="allowed.ports:(20 OR 21) AND direction=INGRESS" \
--format="table(name, sourceRanges.list(), allowed[].map().firewall_rule().list(), targetTags.list())"
Pick out any rule whose sourceRanges includes 0.0.0.0/0 or another broad public block.
Step 2: Decide what the rule is actually for
Before changing anything, find out which instances the rule targets and whether they still serve FTP:
gcloud compute instances list \
--filter="tags.items=YOUR_TARGET_TAG" \
--format="table(name, zone, status)"
Warning: Vendors and batch jobs sometimes depend on a public FTP endpoint. Tightening or deleting the rule without checking can break a file feed and cause silent data loss. Confirm the dependency before you remove access in production.
Step 3a (preferred): Restrict the source range
If FTP must stay for now, limit it to the specific IPs that need it. Update the rule to replace the public range with known CIDRs:
gcloud compute firewall-rules update allow-ftp \
--source-ranges=203.0.113.10/32,198.51.100.0/24
Step 3b: Delete the rule if FTP is no longer needed
Danger: Deleting a firewall rule is immediate and affects all targeted instances. Make sure no active transfers or integrations rely on this port before running the command below.
gcloud compute firewall-rules delete allow-ftp
Step 3c (best long term): Move to SFTP
SFTP runs over SSH on port 22 and encrypts both authentication and data. If you control the server, stand up SFTP and retire the FTP daemon. Then allow SSH only from trusted ranges:
gcloud compute firewall-rules create allow-sftp \
--direction=INGRESS \
--action=ALLOW \
--rules=tcp:22 \
--source-ranges=203.0.113.10/32 \
--target-tags=sftp-server \
--network=default
Tip: For one-off or vendor file exchanges, skip running a server entirely. A signed URL to a Cloud Storage bucket gives you encrypted transit, fine-grained access control, and audit logging with none of the FTP maintenance burden.
Fixing it in Terraform
If your firewall rules live in Terraform, fix the source instead of clicking around the console. A public FTP rule typically looks like this:
resource "google_compute_firewall" "allow_ftp" {
name = "allow-ftp"
network = "default"
allow {
protocol = "tcp"
ports = ["20", "21"]
}
source_ranges = ["0.0.0.0/0"] # the problem
target_tags = ["ftp-server"]
}
Scope the source range to the IPs that actually need it:
resource "google_compute_firewall" "allow_ftp" {
name = "allow-ftp"
network = "default"
allow {
protocol = "tcp"
ports = ["20", "21"]
}
source_ranges = ["203.0.113.10/32", "198.51.100.0/24"]
target_tags = ["ftp-server"]
}
How to prevent it from happening again
Manual fixes do not stick. Someone will recreate the rule next quarter. Catch it earlier with automation.
Policy-as-code in CI/CD
Run a scanner like tfsec, Checkov, or Terraform's native validation against your plans before they merge. A Checkov custom policy can fail any build that opens FTP to the world:
metadata:
id: "CKV_GCP_CUSTOM_FTP"
name: "Firewall must not allow public FTP"
category: "networking"
definition:
cond_type: "attribute"
resource_types:
- "google_compute_firewall"
attribute: "source_ranges"
operator: "not_contains"
value: "0.0.0.0/0"
Organization policy constraints
Use a GCP Org Policy to block instances with external IPs where they are not needed, which shrinks the attack surface even if a bad firewall rule slips through. Pair that with VPC Service Controls for sensitive projects.
Tip: Wire Lensix into your workflow so the vpc_openftp check runs continuously, not just at deploy time. Drift happens through the console and emergency changes, and continuous scanning catches the rule someone added at 2am to unblock a release.
Alert on firewall changes
Create a log-based alert on firewall rule modifications so a new public port opening generates a notification:
gcloud logging metrics create firewall-public-changes \
--description="Public firewall rule changes" \
--log-filter='resource.type="gce_firewall_rule" AND protoPayload.methodName:("compute.firewalls.insert" OR "compute.firewalls.patch")'
Best practices
- Default deny. Build firewall rules around least privilege. Allow only the specific ports and source ranges each service needs.
- Retire FTP. If a workload still uses plain FTP, plan its migration to SFTP, FTPS, or object storage. Cleartext protocols on the public internet have no place in a modern stack.
- Use network tags deliberately. Target rules at specific tags or service accounts rather than applying them broadly across a network.
- Avoid
0.0.0.0/0on any admin or transfer port. Public access should be a conscious, documented decision, not a default. - Prefer a bastion or IAP. For remote access, route through Identity-Aware Proxy or a bastion host instead of opening ports to the world.
- Review rules regularly. Firewall rules accumulate. Audit them on a schedule and delete anything without a clear owner or purpose.
FTP exposure is one of those findings that looks small in a scan report and turns into a breach in an incident review. Close the rule, move to an encrypted protocol, and put a guardrail in CI so it never comes back.

