Securely Exposing a Local Nginx Server with Cloudflare Tunnel

By leveraging Cloudflare Tunnel and Access, I was able to securely expose my local Nginx server to the internet -without opening a single port -while enforcing authentication through Google Workspace, all within minutes.

Introduction

Setting up a web server on a local network and making it publicly accessible is typically a security nightmare. Traditionally, you’d have to:

  • Open ports on your router (risky)
  • Expose your server’s public IP
  • Deal with firewall & DDoS protection
  • Manually set up SSL certificates

Cloudflare Tunnel solves this problem by flipping the connection:


Your server initiates the connection to Cloudflare
No need for open firewall ports
Free HTTPS via Cloudflare’s network
DDoS protection and access control

In this article, we’ll walk through how to:

  • Set up Nginx locally
  • Use Cloudflare Tunnel’s easy install script
  • Explore manual config.yml configuration
  • Understand how Cloudflare Tunnel works

1. Setting Up a Local Nginx Server

Before exposing anything, we need a web server. If you don’t already have Nginx installed, install it:

sudo apt update && sudo apt install nginx -y

Start and enable the service:

sudo systemctl start nginx
sudo systemctl enable nginx

By default, Nginx serves a test page on port 80. You can verify by running:

curl -I http://localhost

Or, open your browser and visit:

http://localhost

Now let’s securely expose this Nginx instance using Cloudflare Tunnel.


2. Installing Cloudflare Tunnel (Easy Script Method)

Cloudflare offers a one-command install that sets up the tunnel without requiring manual configuration:

Step 1: Create a Tunnel in Cloudflare Dashboard

  1. Go to Cloudflare Zero Trust
  2. Navigate to Zero Trust > Networks > Tunnels
  1. Click Create a Tunnel and name it (e.g., my-nginx-tunnel)
  1. Name your tunnel
  2. Select Debian/Ubuntu as the environment
  3. Copy the provided installation command, which 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 <your-tunnel-token>
  1. Run this command on your server.

Step 2: Add a Public Hostname

  1. In the Cloudflare Tunnel dashboard, under Public Hostnames, click Add a hostname.
  2. Enter your subdomain (e.g., nginx.mydomain.com).
  3. For Service Type, choose HTTP.
  4. Set the URL as http://localhost:80 (this tells Cloudflare to route traffic to Nginx).
  5. Save and restart the tunnel.

Your Nginx server is now accessible at https://nginx.mydomain.com.

Optional: Choose Service Type HTTPS

  1. If choosing HTTPS and receiving 301 redirects disable TLS verification.
  2. Will allow any certificate from the origin to be accepted.

3. Using config.yml for More Control

The easy script installation does not create a config file. If you want manual control over settings, create one:

Step 1: Find Your Tunnel ID

cloudflared tunnel list

This will output something like:

Tunnel ID                            Name
a73b68489d2ff389d2ec7c6eab1aa2fe     my-nginx-tunnel

Step 2: Create a Config File

  1. Create the config directory:
sudo mkdir -p /etc/cloudflared
  1. Open a new config file:
sudo nano /etc/cloudflared/config.yml
  1. Add the following:
tunnel: a73b68489d2ff389d2ec7c6eab1aa2fe
credentials-file: /root/.cloudflared/a73b68489d2ff389d2ec7c6eab1aa2fe.json

ingress:
  - hostname: nginx.mydomain.com
    service: http://localhost:80
  - service: http_status:404
  1. Restart the tunnel:
sudo systemctl restart cloudflared

This method allows you to easily add more services under different subdomains.


4. How Cloudflare Tunnel Works

Instead of exposing your local web server to the internet, Cloudflare Tunnel inverts the connection:

🔄 Request Flow

1️⃣ User visits https://nginx.mydomain.com
2️⃣ Cloudflare resolves DNS to *.cfargotunnel.com
3️⃣ Cloudflare forwards the request through its network
4️⃣ Your cloudflared client pulls the request via an outbound HTTPS connection
5️⃣ The request is delivered securely to Nginx

💡 Key advantage: Your server is never directly exposed to the internet.

🔍 How It Works

1️⃣ Client (Browser, API, User)

  • Requests access to https://nginx.mydomain.com

2️⃣ Cloudflare Edge (DNS + WAF + Proxy)

  • Resolves the domain
  • Routes traffic through Cloudflare’s network

3️⃣ Cloudflare Access (Zero Trust Authentication)

  • Checks if the user is authenticated (Google, GitHub, etc.)
  • If not authenticated, redirects to login
  • If authenticated, grants access and forwards traffic

4️⃣ Cloudflare Tunnel (cloudflared)

  • Runs inside your private network
  • Maintains a secure outbound connection to Cloudflare
  • Pulls requests from Cloudflare and sends them to your server

5️⃣ Your Internal Server (Nginx, API, App)

  • Receives requests only from Cloudflare
  • Stays hidden from the public internet (no open ports!)

5. Security Benefits & Risks

Why This is Secure

No exposed IPs or open ports
Cloudflare WAF protects against attacks
Authentication ensures only authorized users access the service
Logs & monitoring available via Cloudflare Dashboard

Security Benefits

  • No open ports → No need for port forwarding
  • DDoS protection → Cloudflare shields your server
  • Automatic HTTPS → No need to manually install certificates
  • Access Control → You can restrict access via Cloudflare Access

⚠️ Potential Risks

  • Cloudflare dependency → If Cloudflare has downtime, your tunnel won’t work
  • Man-in-the-middle risk → If noTLSVerify is enabled, an attacker could hijack traffic inside the tunnel
  • Rate limits → Free Cloudflare accounts have API rate limits

🛡️ How to Mitigate Risks

Use Cloudflare Access → Require login before accessing services
Avoid noTLSVerify when possible
Monitor tunnel status → Run:

cloudflared tunnel list

Conclusion

Cloudflare Tunnel makes it extremely easy to expose a local Nginx server securely, without opening firewall ports or exposing an IP.

🔹 The easy script installation works out of the box
🔹 Using a config file gives more control
🔹 Cloudflare handles DDoS protection, HTTPS, and security


1️⃣ How Cloudflare Access Works

Instead of just letting anyone hit your tunnel-protected server, Cloudflare Access acts like a secure login gateway:

🔄 Request Flow with Cloudflare Access

1️⃣ User visits https://nginx.mydomain.com
2️⃣ Cloudflare intercepts the request and checks if the user is authenticated
3️⃣ If NOT authenticated, they are redirected to a login page
4️⃣ User logs in via Google, GitHub, Microsoft, or custom SSO
5️⃣ Cloudflare verifies identity & applies rules (e.g., allow only yourcompany.com emails)
6️⃣ If allowed, Cloudflare forwards the request through the tunnel
7️⃣ User reaches the internal web service 🎉


2️⃣ Setting Up Cloudflare Access

Step 1: Enable Cloudflare Access for Your Tunnel

  1. Go to Cloudflare Zero Trust Dashboard
  2. Navigate to Access > Applications
  3. Click Create an Application
  4. Choose Self-hosted
  5. Application Name → (e.g., My Internal Nginx)
  6. Application Domain → Enter your hostname (nginx.mydomain.com)

Step 2: Configure Authentication Rules

Now, define who is allowed to access your service.

  1. Under “Policies”, click “Create a policy”
  2. Policy Name: (e.g., Allow Employees)
  3. Action: Allow
  4. Include: Choose authentication methods:
    • ✅ Google Workspace (@yourcompany.com emails only)
    • ✅ GitHub (Restrict to specific orgs)
    • ✅ One-time PIN (for flexible access)
    • ✅ IP Ranges (Allow office VPN only)
  5. Click Save Policy

Step 3: Apply Access Protection to Your Cloudflare Tunnel

  1. Go to Zero Trust > Access > Tunnels
  2. Click your tunnel (my-nginx-tunnel)
  3. Click Public Hostname (e.g., nginx.mydomain.com)
  4. Enable Cloudflare Access
  5. Select the Access Policy you created earlier
  6. Save changes ✅

3️⃣ Testing Cloudflare Access

Now, try visiting your secured service:

https://nginx.mydomain.com

1️⃣ If you’re not logged in, you’ll see Cloudflare’s login page
2️⃣ Enter credentials (Google, GitHub, or other configured SSO)
3️⃣ If allowed, you’ll be redirected to the internal service
4️⃣ If denied, Cloudflare blocks the request


4️⃣ Bonus: Enforcing Authentication for API Requests

By default, API requests will also be blocked if they don’t pass Cloudflare Access authentication.

For API access, you have two options:

🔹 Option 1: Use API Tokens

  • Generate an API Service Token in Cloudflare Zero Trust > Access > Service Auth
  • Use the token in API requests:
curl -H "CF-Access-Client-Id: <your-client-id>" \
     -H "CF-Access-Client-Secret: <your-client-secret>" \
     https://nginx.mydomain.com/api/data

🔹 Option 2: Whitelist API Clients

  • In Cloudflare Access, allow certain IPs (e.g., VPN users, corporate network)
  • Or bypass authentication for specific API routes

Final Thoughts

With Cloudflare Access, you now have:

✔ A Zero Trust authentication layer for your internal apps
No open firewall ports but still secure remote access
Fine-grained control over who can access what
Logs & visibility into all access requests

Stay In Touch.

Let's Get Creative.