Back to Library
CybersecurityDec 2025

Zero Open Ports: Secure Your VPS in 15 Minutes

Your VPS has ports exposed to the internet right now. SSH on 22, maybe a database on 5432, internal tools on random ports. Millions of bots are scanning 24/7 looking for exactly this. Here's how to close every single port and still access everything using Cloudflare Tunnel.

Why Close All Ports?

Most developers expose ports directly to the internet: SSH on 22, databases on 5432, analytics dashboards on 8080. Even with strong passwords, you're giving bots a door to knock on. DNS doesn't help - anyone can dig your domain and find your real IP. Even Cloudflare's proxy only covers HTTP ports.

Zero Attack Surface

No open ports means nothing for bots to scan - your server becomes invisible

Identity-Based Access

Every connection authenticated - SSH, databases, internal tools, everything

No VPN Required

Works from anywhere, any network - just authenticate and you're in

Before You Start

This guide uses Cloudflare DNS + Cloudflare One (their Zero Trust platform, free tier available).

If your domain isn't on Cloudflare yet, you can add it for free - takes about 5 minutes. Or just read along to see if this approach fits your setup.

Step 1: Create the Tunnel (Takes 3 Minutes)

In Cloudflare One Dashboard:

  1. Go to Networks Connectors Cloudflare Tunnels
  2. Click Create a tunnel
  3. Choose Cloudflared as the connector type → Next
  4. Name it something like vps-tunnel Save tunnel
  5. Select your OS environment, then copy the install command - it looks like this:
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
sudo cloudflared service install eyJhIjoiYWJjZGVm...your-token-here

Run it on your VPS. Once the command finishes, your connector will appear in Cloudflare One.

What just happenedWhy it matters
Outbound-only connectionNo inbound ports needed
Runs as a systemd serviceSurvives reboots automatically
Encrypted by defaultTraffic is secured end-to-end

Step 2: Add SSH as a Private Application

Still in the Cloudflare Dashboard:

  1. Go to your tunnel → Public Hostnames Add a public hostname
  2. Configure it:
FieldValue
Subdomainssh (or whatever you want)
Domainyourdomain.com
TypeSSH
URLlocalhost:22
  1. Hit Save. Cloudflare creates the DNS record automatically.

Now lock it down:

  1. Go to Access Applications Add an application
  2. Select Self-hosted
  3. Set application domain: ssh.yourdomain.com
  4. Create a policy - example:
  • Policy name: SSH Access
  • Action: Allow
  • Include: Emails ending in @yourdomain.com

Or use one-time PIN, GitHub SSO, Google Workspace - whatever fits your setup.

Step 3: Connect From Your Machine

Install cloudflared locally:

# macOS
brew install cloudflared

# Linux
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb

# Windows
winget install Cloudflare.cloudflared

Add this to your ~/.ssh/config:

Host vps
    HostName ssh.yourdomain.com
    User your-username
    ProxyCommand /usr/local/bin/cloudflared access ssh --hostname %h
    # Run 'which cloudflared' to find your path
    # macOS Homebrew: /opt/homebrew/bin/cloudflared

Now connect:

ssh vps 

First time: browser opens for auth. After that, tokens are cached. You're in.

Step 4: Close All Ports

Don't lock yourself out!

Open a new terminal and test ssh vps first. Keep your current SSH session open as a backup until the tunnel is confirmed working.

Once you've confirmed all services work through the tunnel:

# Close ALL ports with UFW (recommended)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable

# Or bind services to localhost only
# SSH: Edit /etc/ssh/sshd_config → ListenAddress 127.0.0.1
# Postgres: Edit postgresql.conf → listen_addresses = 'localhost'
# Any app: Bind to 127.0.0.1 or 0.0.0.0 → localhost only

Restart services if you changed their configs:

sudo systemctl restart sshd\nsudo systemctl restart postgresql  # if applicable

Done. All ports closed. Everything still works through the tunnel.

What You Get

  • Zero open ports - your server becomes invisible to port scanners and bot networks
  • All services protected - SSH, databases, internal dashboards, analytics - everything through one tunnel
  • Identity-based access - every connection authenticated, not just SSH keys
  • Audit logs - every connection logged in Cloudflare dashboard with user identity
  • No VPN required - works from anywhere, any network, just authenticate and connect
  • Free tier - Cloudflare Tunnel is free for unlimited bandwidth and connections

Emergency access: If the tunnel goes down, you'll need console/VNC access from your VPS provider (Hetzner, DigitalOcean, Linode, etc. all offer this). Keep those credentials safe.

The Security Evolution

Most developers go through these stages. Each step seems reasonable, but only the last one actually protects you.

Scenario 1: Direct IP MappingWORST CASE

┌─────────────────┐                    ┌─────────────────────────┐
│ Developer       │───SSH:22──────────>│  VPS (24.102.42.42)     │
│ Laptop          │                    │                         │
└─────────────────┘                    │  0.0.0.0:22   SSH       │
                                       │  0.0.0.0:8080 Analytics │
┌─────────────────┐                    │  localhost:5432 DB      │
│ Cloud (Vercel)  │───:8080───────────>│                         │
│                 │                    └─────────────────────────┘
└─────────────────┘                               ↑ ↑ ↑ ↑ ↑
                                         Millions of bots scanning
                                         24/7 for vulnerabilities

Services bound to 0.0.0.0 = visible to the entire internet. Your IP is in env vars, configs, and DNS records. Bots find you in seconds.

Scenario 2: DNS MappingSTILL EXPOSED

┌─────────────────┐     ┌──────────┐     ┌─────────────────────────┐
│ Developer       │────>│   DNS    │────>│  VPS (24.102.42.42)     │
│ Laptop          │     │ Provider │     │                         │
└─────────────────┘     └──────────┘     │  0.0.0.0:22   SSH       │
                             │           │  0.0.0.0:8080 Analytics │
┌─────────────────┐          │           │  localhost:5432 DB      │
│ Cloud (Vercel)  │──────────┘           └─────────────────────────┘
│ env: analytics  │                               ↑ ↑ ↑ ↑ ↑
│ .domain.com     │                      dig analytics.domain.com
└─────────────────┘                      → reveals 24.102.42.42

"I'm using a domain now!" - Cool, but dig analytics.domain.com still shows your real IP. DNS is not security. Same exposure, extra steps.

⚠️ Scenario 3: Cloudflare Proxy (Orange Cloud)PARTIAL FIX

┌─────────────────┐                          ┌─────────────────────────┐
│ Developer       │─────SSH cannot be────────│  VPS (24.102.42.42)     │
│ Laptop          │     proxied! ───────────>│  :22 STILL EXPOSED      │
└─────────────────┘                          │                         │
                       ┌───────────────┐     │  0.0.0.0:22   SSH       │
┌─────────────────┐    │  Cloudflare   │     │  0.0.0.0:8080 Analytics │
│ Cloud (Vercel)  │───>│  DNS + Proxy  │────>│  localhost:5432 DB      │
│ (IP is hidden)  │    │  (HTTP only)  │     └─────────────────────────┘
└─────────────────┘    └───────────────┘              ↑ ↑ ↑
                                             Port 22 still open
                                             Bots still scanning

Cloudflare proxy hides your IP for HTTP traffic - nice! But SSH (port 22) can't be proxied. Port still open, bots still knocking. You need something else for SSH.

Scenario 4: Cloudflare TunnelZERO OPEN PORTS

┌─────────────────┐       ┌───────────────┐       ┌─────────────────────────┐
│ Developer       │ ───>  │               │  <═══ │  VPS (24.102.42.42)     │
│ (cloudflared)   │       │  Cloudflare   │       │  cloudflared tunnel     │
└─────────────────┘       │     Edge      │       │  (OUTBOUND connection)  │
                          │               │       │                         │
┌─────────────────┐       │               │  <═══ │  localhost:22   SSH     │
│ Cloud (Vercel)  │ ───>  │               │       │  localhost:8080 Analytics│
│                 │       │               │       │  localhost:5432 DB      │
└─────────────────┘       └───────────────┘       └─────────────────────────┘

              ═══> = Outbound tunnel (VPS connects TO Cloudflare, not the other way)
                   No inbound ports needed. Bots find NOTHING.

All services bound to localhost. Tunnel makes outbound connection to Cloudflare. No inbound ports. Bots scan your IP, find nothing, move on. You win.

The Key Insight

Inbound vs Outbound: Traditional setups require your server to accept connections (inbound). Cloudflare Tunnel flips this - your server initiates the connection (outbound) to Cloudflare's edge. No listening ports, no attack surface, no bots.

Add More Services (Same Tunnel)

One tunnel handles everything. Add more hostnames for each service:

# /etc/cloudflared/config.yml (if using config-based setup)
ingress:
  - hostname: ssh.yourdomain.com
    service: ssh://localhost:22
  - hostname: analytics.yourdomain.com
    service: http://localhost:8080
  - hostname: grafana.yourdomain.com
    service: http://localhost:3000
  # Database access via tunnel (no public hostname needed)
  # Connect via: cloudflared access tcp --hostname db.yourdomain.com --url localhost:5432
  - service: http_status:404

Each service gets its own Access policy. Your analytics dashboard can require GitHub SSO, your SSH can require email verification, your database can be internal-only. Granular control over who gets access to what.

Don't forget: Update your environment variables wherever your app is deployed (Vercel, Netlify, Railway, etc.) to use the new tunnel hostnames instead of direct IPs.

Learn More

💡 15 minutes of setup. Zero open ports. All your services still accessible. Bots move on to easier targets.

Build on an infinite canvas with Woltex AI

The AI workspace built for production. Access all models, infinite canvas for complex workflows, and tools designed for real-world projects, not just code generation. Join the waitlist to get early adopter perks.