Skip to main content

Tunnels as a Service: We Built a Managed ngrok Alternative on SSH

· 7 min read
Filess Team
Database Experts

You're building a Stripe webhook handler. Your local dev server is running on port 3000. Stripe needs a public URL to send events to. You do what every developer has done before: open a terminal and type ngrok http 3000.

It works. For a while.

Then you hit the rate limit. Or the random subdomain changes every restart. Or you need to whitelist a specific IP and ngrok's plan doesn't cover it. Or your company's security policy blocks third-party tunneling software.

At Filess, we built our own managed tunnel infrastructure, and we're making it available as part of the platform. Here's the full technical breakdown of how it works.

The Architecture: SSH Reverse Tunnels

The core mechanism is the classic SSH reverse tunnel (-R flag). When you connect, your machine dials outward through SSH to our tunnel server, and the server opens a port on its side that forwards all incoming traffic back to you through the open SSH connection.

Internet → Filess Tunnel Server → SSH connection → Your localhost:3000

No firewall rules to open. No router config. No public IP required. The only prerequisite is that your machine can make an outbound TCP connection on port 22222.

Here's the raw SSH command (we also have a CLI that wraps this):

ssh -R filess-tunnel-hostname:80:localhost:3000 \
[email protected] \
-p 22222 \
-i ~/.ssh/id_rsa \
-N

Your local port 3000 is now reachable at https://filess-tunnel-hostname.tunnel.eu.filess.io.


What Makes It Different from a Simple SSH -R

Running your own reverse tunnel on a VPS is straightforward, but productionizing it is a different story. Here's what we built on top:

TLS Certificates — Automatic, Zero Config

Every tunnel gets a unique hostname and a TLS certificate provisioned automatically via Let's Encrypt. No self-signed certs. No "your connection is not secure" warnings.

The certificate lifecycle is tracked in our database:

TunnelCertificate {
status: pending | obtaining | valid | expired | failed
domain: "abc123.tunnel.eu.filess.io"
issuer: "Let's Encrypt"
expiresAt: 2026-07-03T00:00:00Z
obtainedAt: 2026-04-03T12:00:00Z
}

When a certificate nears expiration, our job scheduler picks it up and renews it. You never think about this.

Request Observability

Every HTTP request that flows through your tunnel is logged:

GET /webhooks/stripe 200 47ms 1.2kb from 54.187.174.169
POST /api/callback 201 12ms 0.4kb from 185.93.54.20
GET /health 200 3ms 0.1kb from 10.0.0.1

The dashboard shows:

  • Total requests in the last 24h
  • Average response time
  • Status code distribution (2xx, 4xx, 5xx)
  • Bandwidth transferred
  • Top client IPs

This is valuable during development. Instead of sprinkling console.log statements and squinting at terminal output, you see exactly what's hitting your tunnel in a clean UI.

Connection Events

Every time a client connects or disconnects from the tunnel via SSH, the event is recorded with timestamp, client IP, and auth method used. If your tunnel shows unexpected connection attempts from unknown IPs, you'll know.

Webhook Delivery

You can configure webhooks on your tunnel to get notified about connection events. Useful if you're building automation: start your dev server automatically when someone dials into the tunnel, for example.


Security Controls

The default configuration is secure. But you can tighten it further:

IP Allowlists

Two independent allowlists:

  1. sshIPWhitelist: Controls who can connect to your tunnel via SSH. Lock it down to your office IP range.
  2. ingressIPWhitelist: Controls who can send HTTP traffic through your tunnel. Lock it to Stripe's IP ranges if that's all you need.
{
"sshIPWhitelist": ["203.0.113.42/32"],
"ingressIPWhitelist": [
"3.18.12.63/32",
"3.130.192.231/32",
"13.235.14.237/32"
]
}

Auth Methods

Three modes:

  • key: SSH key only. No passwords. Recommended.
  • token: Temporary password (4-hour TTL, bcrypt-hashed in storage). Useful for CI runners where you can't manage SSH keys.
  • both: Accept either. Flexible for team setups.

HTTPS-Only Mode

Set httpsOnly: true and the tunnel server will reject plain HTTP connections, redirecting to HTTPS with a 301. Your webhook receiver always gets encrypted traffic.


Live Header Modifications

This one is for power users. You can inject or modify HTTP headers on every request/response flowing through your tunnel, without touching your backend code.

{
"liveHeaderModifications": [
{
"name": "X-Filess-Tunnel-ID",
"value": "my-dev-tunnel",
"type": "request"
},
{
"name": "Access-Control-Allow-Origin",
"value": "*",
"type": "response"
}
]
}

Use cases:

  • Inject an Authorization header so your backend can verify requests came through the tunnel.
  • Add CORS headers without modifying your app code.
  • Stamp a X-Environment: development header to prevent your app from sending real emails when receiving tunneled requests.

CORS Preflight Passthrough

If your frontend calls your tunneled backend directly, enable allowCorsPreflight: true. The tunnel server will handle OPTIONS requests automatically, which is useful when your local server doesn't respond to preflight properly.


The forwardToOtherHosts Flag

By default, the tunnel only forwards to localhost. Enable forwardToOtherHosts: true and you can forward to any host reachable from your machine:

# Forward to a database in your local Docker network
ssh -R tunnel-hostname:5432:db.internal:5432 ...

This is how you can use a tunnel to give a colleague temporary read access to your local database without setting up a VPN.


Real Use Cases

1. Receiving Webhooks During Local Development

The canonical use case. Instead of deploying a staging environment every time you need to test a Stripe, GitHub, or Twilio webhook, you point the webhook URL at your tunnel and develop against real events locally.

# Start tunnel
filess tunnel start --port 3000 --name dev-webhooks

# Your webhook URL is now:
# https://abc123.tunnel.eu.filess.io/webhooks/stripe

Configure the URL once in Stripe's dashboard. Since the hostname is persistent (not random like ngrok's free tier), you don't need to update it every time you restart.

2. Sharing a Work-in-Progress UI with a Client

You're building a feature. Your client wants to see it before it's deployed. Instead of pushing to staging, run:

filess tunnel start --port 5173 --name client-preview
# https://abc123.tunnel.eu.filess.io — share this with your client

The tunnel stays up as long as your dev server runs. The client sees live changes as you code.

3. Testing Mobile Apps Against a Local API

Your React Native app needs to hit your API. You can't use localhost from a physical device. Point the app at your tunnel URL, and all traffic flows through to your local machine.

4. CI/CD Smoke Tests Against a Local Database

Some integration tests need a real database. Instead of spinning up a cloud database for every test run, you keep a local database running and expose it through a tunnel for your CI pipeline to hit.


How Tunnels Fit Into Filess Organizations

Tunnels are first-class resources in the Filess data model. They live inside a Namespace (within an Organization), which means:

  • Multiple team members can have tunnel access via RBAC.
  • Audit logs capture who created, modified, or deleted a tunnel.
  • Billing is per-tunnel, tracked via Stripe subscriptions.
  • Tunnels can be created, listed, and deleted via the REST API — automatable from scripts or CI.
# Create a tunnel via API
curl -X POST https://api.filess.io/v1/organizations/my-org/namespaces/dev/tunnels \
-H "Authorization: Bearer $FILESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "webhook-receiver",
"regionId": 1,
"config": {
"httpsOnly": true,
"ingressIPWhitelist": ["3.18.12.63/32"]
}
}'

The Infrastructure Behind It

Each region runs one or more Tunnel Endpoints — servers that handle SSH connections and proxy HTTP/HTTPS traffic. The endpoint schema:

TunnelEndpoint {
domain: "tunnel.eu.filess.io"
sshHost: "ssh.eu.filess.io"
sshPort: 22222
httpPort: 28080
httpsPort: 28443
}

SSH runs on a non-standard port (22222) to avoid collisions with system SSH. The tunnel server manages the mapping from {hostname}.{domain} to the corresponding SSH connection.

When you create a tunnel via the API, the system:

  1. Generates a random 16-character hex hostname.
  2. Finds an active endpoint in the requested region.
  3. Records the tunnel in the database.
  4. Provisions a TLS certificate.
  5. Returns the full tunnel URL.

The hostname is permanent for the lifetime of the tunnel. Delete the tunnel, the hostname is released.


Try It

Tunnels are available to all Filess users. Head to the dashboard, create a namespace, and spin up your first tunnel in under a minute.

If you're building something where persistent, observable, secure tunnels would help — local development, webhook testing, client previews, or team demos — give it a shot.

Get started with Filess Tunnels →