Skip to content

How to Secure Self-Hosted AI Services with Cloudflare Tunnels

Problem

When I started self-hosting AI services at home, I faced a security dilemma. I wanted to access my AI assistant, dashboards, and web UIs from outside my home network, but opening ports on my router felt risky.

I kept thinking about all the bots and scanners constantly probing public IP addresses. Opening ports means:

  • My public IP becomes visible to attackers
  • Each service needs its own authentication layer
  • Certificate management becomes my problem
  • DDoS attacks could target my home connection
  • SSH exposure creates a major attack vector

I tried setting up a VPN, but connecting and disconnecting constantly was annoying. I also looked at reverse proxies with Let’s Encrypt, but certificate renewal and security configuration felt like too much overhead.

Environment

  • Ubuntu Server 22.04 LTS
  • UFW firewall (default deny incoming)
  • Multiple self-hosted services: AI web UI (port 3000), Grafana (port 3001), InfluxDB (port 8086)
  • Domain managed by Cloudflare
  • No ports forwarded on router

What happened?

I discovered Cloudflare Tunnels as a solution. The concept is simple: instead of opening ports and letting traffic in, I create an outbound tunnel to Cloudflare’s edge. Traffic flows like this:

Traditional Setup (Risky):
Internet --> Router (Port Forward) --> Firewall --> Service
Cloudflare Tunnel (Secure):
Internet --> Cloudflare Edge --> Access (Email OTP) --> Tunnel --> Service
|
+-- No firewall ports opened
+-- DDoS protection built-in
+-- Automatic HTTPS
+-- Zero-trust authentication

Here’s how I set it up.

How to solve it?

Step 1: Install cloudflared

First, I installed the cloudflared CLI tool:

Install cloudflared on Linux
# Download the latest release
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
# Install the package
sudo dpkg -i cloudflared-linux-amd64.deb
# Verify installation
cloudflared --version

Step 2: Authenticate and create tunnel

Next, I authenticated with Cloudflare and created a tunnel:

Create Cloudflare tunnel
# Login to Cloudflare (opens browser)
cloudflared tunnel login
# Create a tunnel
cloudflared tunnel create ai-homelab
# Output shows tunnel ID
# Tunnel credentials written to ~/.cloudflared/<TUNNEL_ID>.json

I made note of the tunnel ID from the output. This ID is important for the configuration.

Step 3: Configure tunnel routes

I routed the tunnel to my domain:

Route tunnel DNS
# Route tunnel to your domain
cloudflared tunnel route dns ai-homelab ai.yourdomain.com
cloudflared tunnel route dns ai-homelab grafana.yourdomain.com

Step 4: Create tunnel configuration

I created a configuration file for the tunnel:

~/.cloudflared/config.yml
tunnel: <YOUR_TUNNEL_ID>
credentials-file: /home/user/.cloudflared/<YOUR_TUNNEL_ID>.json
ingress:
# Route AI web UI
- hostname: ai.yourdomain.com
service: http://localhost:3000
# Route Grafana dashboard
- hostname: grafana.yourdomain.com
service: http://localhost:3001
# Route Home Assistant (optional)
- hostname: home.yourdomain.com
service: http://localhost:8123
# Catch-all (required)
- service: http_status:404

Step 5: Run tunnel as service

I set up cloudflared to run as a systemd service:

Install cloudflared as systemd service
# Install as system service
sudo cloudflared service install
# Enable and start
sudo systemctl enable cloudflared
sudo systemctl start cloudflared
# Verify status
sudo systemctl status cloudflared

Step 6: Configure Cloudflare Access

The tunnel provides connectivity, but I needed authentication too. I configured Cloudflare Access with email OTP:

  1. Open Cloudflare Zero Trust dashboard
  2. Go to Access > Applications
  3. Create a new application for each hostname
  4. Add a policy with action “Allow” for emails ending with my domain
  5. Enable “Email OTP” as the login method

Now when I visit https://ai.yourdomain.com, I get an email OTP prompt before reaching my service.

Step 7: Lock down firewall

Finally, I configured UFW to block all incoming traffic except SSH from my LAN:

UFW firewall configuration
# Reset UFW
sudo ufw --force reset
# Default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
# SSH only from LAN
sudo ufw allow from 192.168.1.0/24 to any port 22 comment 'SSH LAN only'
# Enable firewall
sudo ufw enable
# Check status
sudo ufw status verbose

Here’s the key part: no external ports are opened. The tunnel uses outbound connections only.

Verification

I ran a quick check to confirm everything was working:

Verify security configuration
# Check firewall
sudo ufw status numbered
# Check for externally bound ports
sudo ss -tlnp | grep -E "0.0.0.0|::" || echo "No externally bound ports - GOOD!"
# Check tunnel status
sudo systemctl is-active cloudflared && echo "Tunnel: ACTIVE"
# Test external access
curl -s -o /dev/null -w "%{http_code}" https://ai.yourdomain.com
# Should return 401/403 without auth (Access is working)

You can see that I succeeded in creating a secure setup with zero exposed ports.

The reason

I think the key reason this works is that Cloudflare Tunnels reverse the traditional network flow. Instead of my server accepting incoming connections, it maintains an outbound connection to Cloudflare’s edge.

┌─────────────────────┐
│ Cloudflare Edge │
│ (DDoS Protection) │
└──────────┬──────────┘
┌──────────▼──────────┐
│ Cloudflare Access │
│ (Email OTP Auth) │
└──────────┬──────────┘
┌──────────▼──────────┐
│ cloudflared Tunnel │
│ (Outbound Only) │
└──────────┬──────────┘
┌──────────▼──────────┐
│ UFW Firewall │
│ SSH: LAN only │
│ All ports: CLOSED │
└─────────────────────┘

Benefits I get:

  • Zero attack surface: No ports opened on firewall
  • DDoS protection: Cloudflare’s edge absorbs attack traffic
  • Automatic HTTPS: No certificate management
  • Email OTP: User-friendly authentication
  • Audit logging: All access attempts are logged

Common mistakes I avoided

1. Binding services to 0.0.0.0

Services should only listen on localhost:

Service binding configuration
# WRONG - accessible from network
service:
host: 0.0.0.0
port: 3000
# CORRECT - localhost only
service:
host: 127.0.0.1
port: 3000

2. Hardcoding secrets in config files

Never put secrets directly in configuration:

Secrets management
# Create secure secrets directory
mkdir -p ~/.openclaw/secrets
chmod 700 ~/.openclaw/secrets
# Store API key securely
echo "your-api-key" > ~/.openclaw/secrets/api_key
chmod 600 ~/.openclaw/secrets/api_key

3. Exposing SSH to the internet

SSH should never be internet-accessible when using tunnels:

SSH firewall rules
# WRONG - SSH exposed to internet
sudo ufw allow 22
# CORRECT - SSH only from LAN
sudo ufw allow from 192.168.1.0/24 to any port 22

4. Forgetting Access policies

The tunnel provides connectivity, not security. I always configure at least one Access policy with email OTP enabled.

Summary

In this post, I showed how to secure self-hosted AI services using Cloudflare Tunnels with zero exposed ports. The key point is using outbound-only tunnels combined with Cloudflare Access email OTP for authentication. This eliminates port forwarding risks while adding enterprise-grade security features like DDoS protection and automatic HTTPS.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments