Skip to content

Tunnel Tools (ngrok / cloudflared)

Expose a local port or service to the internet — useful for webhook testing, sharing a dev server, SSH reverse tunnels, and Cloudflare-native access policies.

Optional role (installTunnelTools). Enable at chezmoi init --force or set installTunnelTools = true in ~/.config/chezmoi/chezmoi.toml.

Tools

Tool Purpose Account required? Free tier
ngrok HTTP/TCP/TLS tunnels, traffic inspection UI Yes (free plan available) 1 agent, 1 endpoint, random subdomain
cloudflared Cloudflare Tunnel — production-grade, no port forwarding needed Yes (Cloudflare account, free) Unlimited named tunnels, custom domains via Cloudflare DNS

Installation

Managed by the networking_tools Ansible role under the tunnel_tools tag.

Platform ngrok cloudflared
macOS brew install ngrok/ngrok/ngrok brew install cloudflared
Linux (Debian/Ubuntu) apt repo (official) → tgz fallback .deb from GitHub releases → binary fallback
Linux (CentOS/RHEL) tgz from equinox.io → ~/.local/bin .rpm from GitHub releases

ngrok

Initial setup

# Authenticate (one-time, token stored in ~/.config/ngrok/ngrok.yml)
ngrok config add-authtoken <YOUR_TOKEN>

Get your token from https://dashboard.ngrok.com/get-started/your-authtoken.

Expose a local HTTP server

# Forward localhost:3000 to a random ngrok HTTPS URL
ngrok http 3000

# Fixed subdomain (paid plan)
ngrok http --domain=myapp.ngrok.app 3000

# Specific host header (useful with vhosts)
ngrok http --host-header=rewrite 3000

Expose TCP / raw ports

# SSH (useful for remote access to a machine behind NAT)
ngrok tcp 22

# Any TCP service
ngrok tcp 5432

Traffic inspection

ngrok runs a local web inspector at http://127.0.0.1:4040 while a tunnel is active:

# Open inspector in browser
open http://127.0.0.1:4040

# List active tunnels via API
curl -s http://127.0.0.1:4041/api/tunnels | jq

Replay requests

In the web inspector (or via ngrok replay) you can replay any captured request — great for debugging webhook handlers without re-triggering the upstream service.

Named tunnels in config

~/.config/ngrok/ngrok.yml:

version: "3"
agent:
  authtoken: <YOUR_TOKEN>
tunnels:
  dev-server:
    proto: http
    addr: 3000
  api:
    proto: http
    addr: 8080
    host_header: rewrite
# Start a specific named tunnel
ngrok start dev-server

# Start all tunnels defined in config
ngrok start --all

cloudflared

Quick tunnel (no config needed)

# Instantly expose port 3000 — generates a random trycloudflare.com URL, no account needed
cloudflared tunnel --url http://localhost:3000

This is the fastest way to share a local server with someone.

Named tunnels (permanent, custom domain)

# Authenticate with Cloudflare (opens browser)
cloudflared tunnel login

# Create a named tunnel
cloudflared tunnel create my-tunnel

# Create DNS record pointing to the tunnel
cloudflared tunnel route dns my-tunnel dev.example.com

# Run the tunnel
cloudflared tunnel run my-tunnel

Config file ~/.cloudflared/config.yml:

tunnel: <TUNNEL_ID>
credentials-file: /Users/you/.cloudflared/<TUNNEL_ID>.json

ingress:
  - hostname: dev.example.com
    service: http://localhost:3000
  - hostname: api.example.com
    service: http://localhost:8080
  - service: http_status:404
cloudflared tunnel run

SSH access via Cloudflare Access

Cloudflare Access lets you expose SSH without opening any port — the client connects through Cloudflare's network.

Server side (machine to expose):

# Create tunnel and configure SSH ingress (hostname: ssh.example.com → localhost:22)
cloudflared tunnel run

Client side:

# Add to ~/.ssh/config
# Host ssh.example.com
#   ProxyCommand cloudflared access ssh --hostname %h
ssh user@ssh.example.com

Run as a system service

# Install as launchd (macOS) or systemd (Linux) service
sudo cloudflared service install

# Check status
sudo launchctl list | grep cloudflared   # macOS
sudo systemctl status cloudflared        # Linux

Comparison

Feature ngrok cloudflared
Quick share (no config) ngrok http 3000 cloudflared tunnel --url localhost:3000
Custom domain (free) Paid Yes (via Cloudflare DNS)
Traffic inspection UI Yes (localhost:4040) No built-in
Replay requests Yes No
Production-grade Limited Yes (runs Cloudflare CDN)
TCP tunnels Yes Yes (via Spectrum, paid)
SSH proxy Yes (TCP tunnel) Yes (Cloudflare Access, free)
Needs open port No No
Self-hosted option ngrok Agent SDK Yes (Cloudflare WARP)

Common Dev Patterns

Webhook testing

# Start your local handler
python -m http.server 8080

# Expose it and copy the HTTPS URL into your webhook provider's dashboard
ngrok http 8080
# or
cloudflared tunnel --url http://localhost:8080

Share a local dev server with a teammate

# One-liner with cloudflared (no account, no install on recipient's side)
cloudflared tunnel --url http://localhost:3000

Remote SSH to a machine behind NAT

# On the remote machine (server)
ngrok tcp 22
# Note the forwarding address, e.g. 0.tcp.ngrok.io:12345

# From your laptop
ssh -p 12345 user@0.tcp.ngrok.io

VS Code Remote via tunnel

# On the remote machine
code tunnel          # uses Microsoft's tunnel (no ngrok/cloudflared needed)

# Alternative: expose SSH port and use Remote-SSH extension
ngrok tcp 22
# Add to ~/.ssh/config:  Host ngrok-remote \n  HostName 0.tcp.ngrok.io \n  Port 12345

Environment variables for automation

# Get the ngrok public URL from the local API (useful in scripts)
NGROK_URL=$(curl -s http://127.0.0.1:4041/api/tunnels | jq -r '.tunnels[0].public_url')
echo "Webhook URL: $NGROK_URL"