Introduction to Ngrok Alternatives and Security Hardening
Ngrok has long been the go-to tool for exposing local development servers to the internet via secure tunnels. However, as developers increasingly seek self-hosted, cost-effective, or more secure alternatives, tools like Cloudflare Tunnel (cloudflared), Frp (Fast Reverse Proxy), SSH reverse tunnels, and Tailscale Funnel have gained significant traction. While these alternatives offer flexibility and control, they also introduce their own security considerations that must be addressed to prevent unauthorized access, data leakage, and exposure of sensitive endpoints.
This tutorial explores the security landscape of ngrok alternatives, walks through practical hardening techniques, and outlines best practices to keep your tunnels locked down. Whether you are exposing a local webhook for development or running a production-grade reverse proxy, these strategies will help you minimize your attack surface.
Why Security Hardening Matters for Tunneling Tools
Tunneling tools, by design, punch a hole through your NAT or firewall to expose a local service to the public internet. This convenience is also their greatest risk. Without proper hardening, you may inadvertently expose:
- Development databases (MongoDB, PostgreSQL, Redis) without authentication
- Admin panels and dashboards (Django admin, Laravel Telescope, Spring Boot Actuator)
- Internal APIs with debug mode enabled
- Source code via misconfigured static file servers
- Environment variables and secrets through error pages
Attackers actively scan public tunnel endpoints. A misconfigured tunnel can be discovered within minutes of going live. Hardening is not optional — it is a fundamental requirement.
Popular Ngrok Alternatives
Cloudflare Tunnel (cloudflared)
Cloudflare Tunnel creates a secure outbound connection from your machine to Cloudflare's edge network. It requires no inbound firewall rules and integrates with Cloudflare Access for identity-based authorization.
Frp (Fast Reverse Proxy)
Frp is an open-source, self-hosted reverse proxy that supports TCP, UDP, and HTTP/HTTPS tunneling. It gives you full control over the server and client components but requires you to manage your own infrastructure and security.
SSH Reverse Tunnels
A built-in option using OpenSSH, reverse tunnels forward a remote port back to your local machine. They are lightweight and rely on SSH's mature cryptographic model, but lack advanced features like load balancing and custom domains.
Tailscale Funnel
Tailscale Funnel extends your Tailscale mesh network by exposing services publicly through Tailscale's relay servers. It leverages WireGuard and Tailscale's identity system for strong default security.
Hardening Cloudflare Tunnels
Cloudflare Tunnel is one of the most secure options due to its integration with Cloudflare Access. However, you should still apply additional hardening layers.
Install and Authenticate
# Install cloudflared
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared
# Authenticate with your Cloudflare account
cloudflared tunnel login
Create and Configure a Tunnel
# Create a named tunnel
cloudflared tunnel create dev-tunnel
# Generate a configuration file
cat > ~/.cloudflared/config.yml << EOF
tunnel: dev-tunnel
credentials-file: /home/user/.cloudflared/<TUNNEL_ID>.json
ingress:
- hostname: dev.example.com
service: http://localhost:3000
- service: http_status:404
EOF
# Route DNS to the tunnel
cloudflared tunnel route dns dev-tunnel dev.example.com
# Run the tunnel
cloudflared tunnel run dev-tunnel
Enforce Cloudflare Access Policies
The most critical hardening step is requiring authentication at the Cloudflare edge before traffic ever reaches your local service. Configure an Access policy that restricts access to specific email addresses, identity providers, or IP ranges.
# Create an Access application via wrangler or the dashboard
# Example policy using wrangler
cat > access-policy.json << EOF
{
"name": "Dev Tunnel Access",
"domain": "dev.example.com",
"type": "self_hosted",
"session_duration": "8h",
"policies": [
{
"name": "Allow Developers",
"decision": "allow",
"include": {
"emails": ["dev@yourcompany.com"]
}
}
]
}
EOF
With Access enabled, even if someone discovers your tunnel URL, they will be prompted to authenticate before reaching your application. This is a dramatic improvement over exposing a raw endpoint.
Lock Down the Tunnel Token
The tunnel credentials JSON file is effectively a key to your tunnel. Protect it with strict file permissions and never commit it to version control.
# Restrict permissions on credentials
chmod 600 ~/.cloudflared/*.json
# Add to .gitignore
echo ".cloudflared/" >> .gitignore
Hardening Frp (Fast Reverse Proxy)
Because Frp is self-hosted, you bear full responsibility for its security. The default configuration is not production-safe and must be hardened.
Server Configuration (frps.toml)
# frps.toml - Server side configuration
bindPort = 7000
# Enable TLS for all connections - CRITICAL
transport.tls.force = true
# Set a strong authentication token
auth.token = "GENERATE_A_64_CHAR_RANDOM_STRING_HERE"
# Enable the dashboard but restrict access
webServer.addr = "127.0.0.1"
webServer.port = 7500
webServer.user = "admin"
webServer.password = "USE_A_STRONG_PASSWORD"
# Limit ports that clients can expose
allowPorts = [
{ start = 6000, end = 6100 }
]
# Optional: restrict to specific HTTP vhosts
vhostHTTPPort = 8080
vhostHTTPSPort = 8443
Client Configuration (frpc.toml)
# frpc.toml - Client side configuration
serverAddr = "your-server-ip"
serverPort = 7000
# Must match the server token
auth.token = "GENERATE_A_64_CHAR_RANDOM_STRING_HERE"
# Enable TLS
transport.tls.enable = true
# Enable TLS server name verification
transport.tls.serverName = "your-server-domain.com"
[[proxies]]
name = "dev-web"
type = "http"
localPort = 3000
customDomains = ["dev.your-server-domain.com"]
# Add HTTP basic auth for the proxied service
[[proxies]]
name = "secure-api"
type = "tcp"
localPort = 8080
remotePort = 6000
Generate a Strong Auth Token
# Generate a cryptographically secure token
openssl rand -hex 32
Run Frp as a Non-Root User
# Create a dedicated user
sudo useradd -r -s /bin/false frps
# Set ownership
sudo chown frps:frps /etc/frp/frps.toml
sudo chown frps:frps /usr/bin/frps
# systemd service file
cat > /etc/systemd/system/frps.service << EOF
[Unit]
Description=frps service
After=network.target
[Service]
Type=simple
User=frps
ExecStart=/usr/bin/frps -c /etc/frp/frps.toml
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now frps
Firewall the Dashboard and Bind Port
# Only allow your client IP to reach the frps bind port
sudo ufw deny 7000
sudo ufw allow from YOUR_CLIENT_IP to any port 7000
# Keep the dashboard local-only (already bound to 127.0.0.1)
# If you need remote access, use SSH port forwarding instead:
ssh -L 7500:127.0.0.1:7500 user@your-server-ip
Hardening SSH Reverse Tunnels
SSH reverse tunnels are simple but can be dangerous if your SSH server is not properly secured. Apply these hardening measures to your sshd_config.
Secure sshd_config
# /etc/ssh/sshd_config hardening
# Disable root login
PermitRootLogin no
# Disable password authentication
PasswordAuthentication no
# Limit to key-based auth only
PubkeyAuthentication yes
# Restrict which users can connect
AllowUsers tunnel-user
# Change default port (optional but reduces noise)
Port 2222
# Disable X11 forwarding if not needed
X11Forwarding no
# Set idle timeout
ClientAliveInterval 300
ClientAliveCountMax 0
# Use strong ciphers
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
Create a Restricted Tunnel User
# Create a user with no shell access, only tunneling
sudo useradd -m -s /usr/sbin/nologin tunnel-user
# Add forced command in authorized_keys to restrict tunnel scope
mkdir -p /home/tunnel-user/.ssh
cat > /home/tunnel-user/.ssh/authorized_keys << EOF
permitlisten="8080:localhost",permitopen="localhost:3000" ssh-rsa AAAA...your-public-key... user@client
EOF
chmod 700 /home/tunnel-user/.ssh
chmod 600 /home/tunnel-user/.ssh/authorized_keys
chown -R tunnel-user:tunnel-user /home/tunnel-user/.ssh
Establish the Reverse Tunnel
# From the client machine, establish a reverse tunnel
# Remote port 8080 forwards to local port 3000
ssh -R 8080:localhost:3000 -p 2222 tunnel-user@your-server-ip
# For a persistent tunnel, use autossh
autossh -M 0 -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" \
-R 8080:localhost:3000 -p 2222 tunnel-user@your-server-ip -N
Hardening Tailscale Funnel
Tailscale Funnel is relatively secure by default because it builds on WireGuard and Tailscale's identity model, but you should still apply additional controls.
Enable Funnel for a Specific Service
# Ensure Tailscale is installed and you are logged in
tailscale up
# Enable HTTPS for your tailnet
tailscale serve https / http://localhost:3000
# Expose via Funnel (publicly accessible)
tailscale funnel 443 on
Restrict Funnel Access with ACLs
Edit your Tailscale ACLs in the admin console to control who can use Funnel and which nodes can accept public traffic.
// Tailscale ACL (JSON in admin console)
{
"nodeAttrs": [
{
"target": ["tag:dev-server"],
"attr": ["funnel"]
}
],
"tagOwners": {
"tag:dev-server": ["dev@yourcompany.com"]
}
}
By tagging nodes and restricting the funnel attribute to specific tags, you prevent arbitrary devices from exposing services publicly.
General Best Practices Across All Alternatives
1. Never Expose Unauthenticated Services
Regardless of which tool you use, always place an authentication layer in front of your tunneled service. This can be HTTP Basic Auth, an OAuth proxy, or Cloudflare Access.
# Example: Add Basic Auth with an Nginx reverse proxy in front of your app
cat > /etc/nginx/conf.d/tunnel-auth.conf << EOF
server {
listen 8080;
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
}
}
EOF
# Generate the htpasswd file
htpasswd -c /etc/nginx/.htpasswd devuser
2. Disable Debug and Development Features
Before exposing any service, ensure debug modes, stack traces, and developer toolbars are disabled.
# Django: set DEBUG = False
DEBUG = False
ALLOWED_HOSTS = ['dev.example.com']
# Flask: disable debug mode
app.run(debug=False)
# Express: disable stack traces in production
app.use((err, req, res, next) => {
res.status(500).json({ error: 'Internal Server Error' });
});
# Spring Boot: disable actuator endpoints
management.endpoints.web.exposure.include=health
3. Use TLS End-to-End
Ensure traffic is encrypted from the client through the tunnel to your local service. Avoid terminating TLS at the tunnel edge and sending plaintext to localhost if your service handles sensitive data.
# Frp: enable TLS on both sides
# Server: transport.tls.force = true
# Client: transport.tls.enable = true
# Cloudflare: configure origin certificates
cloudflared tunnel --origincert ~/.cloudflared/cert.pem run dev-tunnel
4. Implement Rate Limiting
Public tunnels are targets for brute force and scraping. Add rate limiting at the proxy layer.
# Nginx rate limiting
limit_req_zone $binary_remote_addr zone=tunnel:10m rate=10r/s;
server {
listen 8080;
location / {
limit_req zone=tunnel burst=20 nodelay;
proxy_pass http://localhost:3000;
}
}
5. Monitor and Log Tunnel Traffic
Enable logging on your tunnel server and forward logs to a monitoring system. Watch for unusual traffic patterns, repeated 401/403 responses, and unexpected source IPs.
# Frp: enable detailed logging
log.to = "/var/log/frp/frps.log"
log.level = "info"
log.maxDays = 7
# Cloudflare: use Cloudflare Analytics and Access logs
# Configure logpush to a destination like S3 or Datadog
6. Rotate Credentials Regularly
Treat tunnel tokens and SSH keys like any other secret. Rotate them on a schedule and immediately when team members leave.
# Script to rotate Frp auth token
#!/bin/bash
NEW_TOKEN=$(openssl rand -hex 32)
sed -i "s/auth.token = .*/auth.token = \"$NEW_TOKEN\"/" /etc/frp/frps.toml
sed -i "s/auth.token = .*/auth.token = \"$NEW_TOKEN\"/" ~/.frp/frpc.toml
systemctl restart frps
echo "New token: $NEW_TOKEN"
# Distribute the new token to clients securely
7. Use Time-Limited Tunnels for Development
For temporary development tunnels, set an expiration so they do not remain exposed indefinitely.
# Run cloudflared with a timeout using timeout command
timeout 3600 cloudflared tunnel run dev-tunnel
# Or use a systemd timer to stop the service after a period
systemctl start cloudflared-dev.timer
8. Segment Tunnel Networks
Do not run tunnels on the same machine or network segment as production databases or critical infrastructure. Use containers or VMs to isolate tunneled services.
# Run your dev app in a Docker container with limited network access
docker run -d \
--name dev-app \
--network tunnel-net \
--restart unless-stopped \
-p 127.0.0.1:3000:3000 \
my-dev-app:latest
# The tunnel connects to 127.0.0.1:3000, which is only the container
Conclusion
Ngrok alternatives offer powerful, flexible, and often more secure options for exposing local services, but they shift security responsibility onto you. By enforcing authentication at every layer, encrypting traffic end-to-end with TLS, restricting access with firewalls and identity policies, disabling debug features, rotating credentials, and monitoring traffic, you can achieve a security posture that matches or exceeds managed solutions. The key principle is simple: treat every public tunnel as if it were a production endpoint exposed to the entire internet, because in practice, it is. Apply these hardening techniques consistently, automate credential rotation, and regularly audit your tunnel configurations to maintain a strong security baseline as your development and infrastructure needs evolve.