Cloudflare Tunnel: expose without opening a port
Cloudflare Tunnel (formerly Argo Tunnel) creates an encrypted outbound connection between your server and Cloudflare. No port opening, no public IP, no VPN. Ideal for homelabs, or to hide a production server's origin IP.
Introduction
Cloudflare Tunnel:
- Outbound connection from server to Cloudflare (no inbound)
- Automatic TLS (Cloudflare cert)
- No public IP required
- Hides origin IP (anti-DDoS, anti-scraping)
- Zero Trust access (auth via Google, GitHub, etc.)
- Free up to 50 users
- Works even behind strict NAT / firewall
Use cases:
- Self-host Plex, NextCloud, Home Assistant
- Expose internal VPS without opening firewall
- Hide production VPS IP
- Secure SSH access without open port 22
Prerequisites
- Free Cloudflare account
- Domain on Cloudflare (DNS must be managed by CF)
- Linux VPS / server
Step 1: Install cloudflared
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
cloudflared --version
Step 2: Login to Cloudflare
cloudflared tunnel login
URL appears. Open it, authenticate on Cloudflare, select your domain.
Certificate downloaded to ~/.cloudflared/cert.pem.
Step 3: Create a tunnel
cloudflared tunnel create mysite
Output:
Tunnel credentials written to /root/.cloudflared/abc123-def456-ghi789.json
Created tunnel mysite with id abc123-def456-ghi789
Step 4: Tunnel configuration
~/.cloudflared/config.yml:
tunnel: abc123-def456-ghi789
credentials-file: /root/.cloudflared/abc123-def456-ghi789.json
ingress:
- hostname: app.your-domain.com
service: http://localhost:8080
- hostname: api.your-domain.com
service: http://localhost:3000
- hostname: ssh.your-domain.com
service: ssh://localhost:22
# Mandatory catch-all
- service: http_status:404
Step 5: Cloudflare DNS
cloudflared tunnel route dns mysite app.your-domain.com
cloudflared tunnel route dns mysite api.your-domain.com
cloudflared tunnel route dns mysite ssh.your-domain.com
Check Cloudflare DNS dashboard: CNAMEs created, proxied (orange cloud).
Step 6: Start the tunnel
cloudflared tunnel run mysite
Tunnel connects to Cloudflare. Visit https://app.your-domain.com: your local app is exposed, HTTPS, no open port.
Step 7: systemd service
sudo cloudflared service install
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared
Step 8: SSH access via tunnel
On client, ~/.ssh/config:
Host ssh.your-domain.com
HostName ssh.your-domain.com
User root
ProxyCommand cloudflared access ssh --hostname %h
Install cloudflared locally too:
ssh ssh.your-domain.com
First time: browser opens for Cloudflare Zero Trust auth.
Step 9: Zero Trust Access
Cloudflare Dashboard > Zero Trust > Access > Applications > Add an application > Self-hosted.
Configure:
- Application name:
Mysite Admin - Session duration: 24h
- Application domain:
app.your-domain.com - Identity providers: Google / GitHub / Email OTP
Add policy:
- Action:
Allow - Rules:
Emails ending in @your-domain.com
Now https://app.your-domain.com requires SSO login. Useful for Grafana, Portainer, etc.
Step 10: Multiple services on one tunnel
config.yml:
tunnel: abc123
credentials-file: /root/.cloudflared/abc123.json
ingress:
- hostname: app.your-domain.com
service: http://localhost:8080
- hostname: blog.your-domain.com
service: http://localhost:8081
originRequest:
noTLSVerify: true
- hostname: ws.your-domain.com
service: ws://localhost:9000
- hostname: ssh.your-domain.com
service: ssh://localhost:22
- hostname: rdp.your-domain.com
service: rdp://localhost:3389
- hostname: db.your-domain.com
service: tcp://localhost:5432
- service: http_status:404
sudo systemctl restart cloudflared
Step 11: Quick tunnel (short-form)
For fast tests, no tunnel creation needed:
cloudflared tunnel --url http://localhost:8000
Generates temporary *.trycloudflare.com URL. Useful for debug / demo.
Step 12: Monitoring and metrics
/etc/cloudflared/config.yml:
metrics: 0.0.0.0:9090
sudo systemctl restart cloudflared
curl http://localhost:9090/metrics
Integrate with Prometheus to track latency, requests, errors.
Troubleshooting
"tunnel not found"
cloudflared tunnel list
Error 1033 / 1034 on domain
Tunnel not connected:
sudo systemctl status cloudflared
sudo journalctl -u cloudflared -f
"Failed to dial origin"
Local service not listening, or wrong port:
sudo ss -tunlp | grep 8080
curl http://localhost:8080
High latency
ingress:
- hostname: app.your-domain.com
service: http://localhost:8080
originRequest:
keepAliveTimeout: 30s
connectTimeout: 10s
WebSocket closes
originRequest:
noHappyEyeballs: true
Useful commands
cloudflared tunnel login
cloudflared tunnel list
cloudflared tunnel create name
cloudflared tunnel delete name
cloudflared tunnel info name
cloudflared tunnel run name
cloudflared tunnel run --config /path/config.yml name
cloudflared tunnel --url http://localhost:8000
cloudflared tunnel route dns tunnel-name sub.domain.com
sudo cloudflared service install
sudo cloudflared service uninstall
sudo systemctl restart cloudflared
sudo journalctl -u cloudflared -f
cloudflared access ssh --hostname ssh.your-domain.com
Conclusion
Cloudflare Tunnel gives you:
- Secure exposure without open ports
- Auto TLS and hidden origin IP
- Zero Trust with SSO
- Free for most use cases
Limits:
- Cloudflare dependency (DNS + tunnel)
- Added latency
- Bandwidth limits on free plan
Going further:
- Combine with Cloudflare Access for SSO auth
- For SSH, check Cloudflare Tunnel + browser-based SSH
- Self-hosted alternatives: Tailscale Funnel, frp, Wireguard + reverse proxy
Resources
- Official docs: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/
- Cloudflare Zero Trust: https://developers.cloudflare.com/cloudflare-one/
- Cloudflared GitHub: https://github.com/cloudflare/cloudflared
- Quick Tunnel: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/


















