Woltex

Zero Open Ports: Secure Your VPS in 15 Minutes

Your VPS has ports exposed to the internet right now. Here's how to close every single port and still access everything using Cloudflare Tunnel.

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.

BenefitDescription
Zero Attack SurfaceNo open ports means nothing for bots to scan - your server becomes invisible
Identity-Based AccessEvery connection authenticated - SSH, databases, internal tools, everything
No VPN RequiredWorks 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.

Setup Guide

Close all your VPS ports and secure everything behind Cloudflare Tunnel:

Create the tunnel

In Cloudflare One Dashboard:

  1. Go to NetworksConnectorsCloudflare Tunnels
  2. Click Create a tunnel
  3. Choose Cloudflared as the connector type → Next
  4. Name it something like vps-tunnelSave tunnel
  5. Select your OS environment, then copy the install command

The install command looks like this:

Install cloudflared on your VPS
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

Add SSH as a private application

Still in the Cloudflare Dashboard:

  1. Go to your tunnel → Public HostnamesAdd 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 AccessApplicationsAdd 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.

Connect from your machine

Install cloudflared on your local machine:

Install via Homebrew
brew install cloudflared
Install via package
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
Install via winget
winget install Cloudflare.cloudflared

Add this to your ~/.ssh/config:

~/.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.

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
# 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:

Restart services
sudo systemctl restart sshd
sudo systemctl restart postgresql  # if applicable

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


What You Get

Once configured, your infrastructure is completely protected:

Security Benefits

  • Zero open ports — 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

Operational Benefits

  • No VPN required — works from anywhere, any network, just authenticate and connect
  • Free tier — Cloudflare Tunnel is free for unlimited bandwidth and connections
  • Easy to manage — all access policies in one dashboard

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.


Add More Services

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

/etc/cloudflared/config.yml
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

Granular Access Control

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. Complete 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.


Additional Resources

Learn more about Cloudflare Tunnel and Zero Trust security:


On this page