README
¶
Gatecrash
Self-hosted tunnel server. Expose local services through a public server with automatic TLS.
A self-hosted alternative to Cloudflare Tunnels, ngrok, and similar services.
Features • Installation • Quick Start • How It Works • Configuration • Auth & Policies • Docker
[!NOTE] This project is vibe coded with Claude Code. The entire codebase — server, client, admin panel, CI/CD, and this README — was built collaboratively with AI. Use at your own risk, but it actually works pretty well.
Features
- Automatic HTTPS — per-hostname Let's Encrypt certificates issued on demand, with a self-signed fallback while DNS settles.
- HTTP, TCP & TLS passthrough — route web traffic by hostname, forward raw TCP, or pass TLS straight through for the backend to terminate (mTLS, your own certs).
- One tunnel, many backends — route multiple hostnames to different services through a single client, with WebSocket/streaming support.
- Passkey admin panel — phishing-resistant WebAuthn sign-in (user verification required), multi-user with admin/user roles, invite-link onboarding, and server-side session revocation (logout kills every device).
- Reusable access policies — gate any tunnel by signed-in user, by source-IP allowlist (with self-service enrollment links), or both — independently, and shared across tunnels.
- Service secrets — machine-generated credentials for CI, scripts, and webhooks via HTTP Basic.
- Live dashboard — real-time traffic, bandwidth, per-tunnel speed test, cert status, and an append-only audit log.
- Easy ops — single static binary, systemd installer, Docker images, config hot-reload,
client.toml, and self-update.
Installation
Gatecrash ships as two separate binaries:
| Binary | Purpose | Size |
|---|---|---|
gatecrash-server |
Tunnel server with admin panel, TLS, passkey auth | ~15 MB |
gatecrash |
Lightweight tunnel client | ~7 MB |
Linux Server (Quick Install)
Installs gatecrash-server, creates a systemd service, and starts it:
curl -fsSL https://raw.githubusercontent.com/jclement/gatecrash/main/deploy/install.sh | sh
Homebrew (macOS/Linux)
# Server
brew install jclement/tap/gatecrash-server
# Client
brew install jclement/tap/gatecrash
Go
# Server
go install github.com/jclement/gatecrash/cmd/gatecrash-server@latest
# Client
go install github.com/jclement/gatecrash/cmd/gatecrash@latest
Docker
# Server
docker pull ghcr.io/jclement/gatecrash-server:latest
# Client (lightweight)
docker pull ghcr.io/jclement/gatecrash:latest
Binary Releases
Download pre-built binaries for Linux, macOS, and Windows from GitHub Releases.
Quick Start
-
Provision a server with a public IP (any Linux VPS will do)
-
Point a hostname at your server's IP via DNS (e.g.
admin.example.com) -
Run the installer — it downloads the binary, generates a config, creates a systemd service, and starts Gatecrash. It will ask for your admin hostname during setup.
curl -fsSL https://raw.githubusercontent.com/jclement/gatecrash/main/deploy/install.sh | shOr download from GitHub Releases for manual installation.
-
Open the admin panel at
https://admin.example.com, register your passkey, and click Add Tunnel to create your first tunnel -
Connect a client using the command shown in the admin panel (the SSH port and host key are displayed when you create a tunnel):
gatecrash \ --server tunnel.example.com:51234 \ --host-key "SHA256:..." \ --token "web-app:YOUR_SECRET" \ --target 127.0.0.1:8000The SSH port is randomly assigned on first run (check
ssh_portingatecrash.toml). The exact connection command — and a ready-madeclient.tomlyou can download — are shown in the admin panel when you create a tunnel or regenerate a secret. To run the client as a service, drop that file at/etc/gatecrash/client.tomland just rungatecrash(see Client config file).
That's it. Requests to your configured hostname now reach your local service on port 8000.
How It Works
Gatecrash uses SSH as the transport layer for tunnel connections. The server accepts SSH connections from clients, then reverse-opens channels back to the client for each incoming HTTP request or TCP connection.
sequenceDiagram
participant User as Browser
participant Server as Gatecrash Server
participant Client as Gatecrash Client
participant App as Local App
Client->>Server: SSH connect (authenticate with token)
Server-->>Client: Connection established
User->>Server: HTTPS request to app.example.com
Server->>Server: SNI routing -> find tunnel for hostname
Server->>Client: Open HTTP channel (reverse)
Client->>App: Forward request to 127.0.0.1:8000
App-->>Client: Response
Client-->>Server: Response via SSH channel
Server-->>User: HTTPS response
HTTP Tunnels
HTTP tunnels route traffic based on the Host header. Multiple hostnames can map to a single tunnel. TLS certificates are automatically provisioned via Let's Encrypt.
TCP Tunnels
TCP tunnels forward raw TCP connections on a dedicated port. Useful for databases, game servers, or any non-HTTP protocol.
Config Live Reload
The server watches gatecrash.toml for changes. Valid changes are applied immediately without dropping existing connections. Invalid changes are rejected and the error is shown in the admin panel.
HTTP Redirects
Redirect hostnames without needing a tunnel:
[[redirect]]
from = "www.example.com"
to = "example.com"
preserve_path = true
Configuration
Server Configuration (gatecrash.toml)
The config file is auto-generated on first run with sensible defaults and inline documentation.
Server Settings
| Field | Default | Description |
|---|---|---|
server.secret |
auto-generated | Session signing secret (do not share) |
server.ssh_port |
random high port | SSH listen port for tunnel connections |
server.https_port |
443 |
HTTPS listen port |
server.http_port |
80 |
HTTP->HTTPS redirect port (0 to disable) |
server.bind_addr |
0.0.0.0 |
Bind address |
server.admin_host |
(disabled) | Hostname for admin panel (required to enable it) |
TLS Settings
| Field | Default | Description |
|---|---|---|
tls.acme_email |
Email for Let's Encrypt expiration notices | |
tls.cert_dir |
./certs |
Certificate storage directory |
tls.staging |
false |
Use Let's Encrypt staging CA |
Update Settings
| Field | Default | Description |
|---|---|---|
update.enabled |
true |
Check for updates on startup |
update.check_interval |
6h |
How often to check for updates |
update.github_repo |
jclement/gatecrash |
GitHub repo for update checks |
Tunnel Options
| Option | Default | Description |
|---|---|---|
preserve_host |
false |
Pass the original Host header to the backend |
tls_passthrough |
false |
Forward raw TLS without terminating (backend handles TLS) |
ip_policy |
ID of an IP policy to restrict by source IP (HTTP and TCP, including passthrough) | |
auth_policy |
ID of an auth policy requiring a signed-in user (HTTP only; not with tls_passthrough) |
Example:
[[tunnel]]
id = "backend-api"
type = "http"
hostnames = ["api.example.com"]
secret_hash = "$2a$12$..."
preserve_host = true
ip_policy = "internal" # see [[ip_policy]]
auth_policy = "staff" # see [[auth_policy]]
Forwarding Headers
For HTTP tunnels (when tls_passthrough = false), the following headers are injected:
| Header | Value |
|---|---|
X-Forwarded-For |
Original client IP |
X-Forwarded-Proto |
https or http |
X-Forwarded-Host |
Original Host header |
X-Real-IP |
Original client IP |
X-Request-Id |
Unique request ID |
When an auth_policy authenticates a request, the backend also receives the user's name on x-Gatecrash-User (header name configurable), their stable ID on X-Gatecrash-Id, and their role on X-Gatecrash-Role (or service for service-secret clients). The whole X-Gatecrash-* namespace is stripped from inbound requests so it can't be spoofed.
Authentication & Access Policies
Users
Gatecrash authenticates with passkeys — phishing-resistant credentials bound to a device. Each user has a stable, opaque ID (a GUID that passkeys, sessions, and policies bind to — it never changes), a renameable name label, a role (admin or user), and one or more passkeys:
- admin — manages tunnels, access policies, users, and the audit log.
- user — signs in and can be granted access to protected tunnels; lands on their own Passkeys page.
First boot. There is no open setup page to race. On startup, while no admin exists, Gatecrash prints a one-time setup link to the logs and writes it to <config_dir>/bootstrap-invite.txt (0600); opening it registers the first admin's passkey, after which the link and file are retired. (The link needs admin_host configured.)
On the Users page an admin adds a user (name + role), which produces a one-time invite link (https://<admin_host>/invite/<token>); opening it registers the user's passkey. Rename changes the label freely; Reset clears a user's passkeys and issues a fresh invite. Everyone manages their own passkeys (and names them) from the Passkeys page. Users live in <config_dir>/users.json.
Sessions are revocable server-side: logging out, resetting a user's passkeys, changing their role, or deleting them immediately invalidates all of that user's existing sessions on every device (not just the current browser) — a captured session cookie stops working at once. Passkey login also requires user verification (biometric/PIN), so an unlocked-but-unattended authenticator isn't enough.
Access policies
Policies are reusable and assigned to tunnels by ID. The two are independent gates — a tunnel may use either, both (AND), or neither.
Auth policy — a set of allowed users (signed in with a passkey), plus an optional service secret for non-interactive clients. HTTP-only; not available with tls_passthrough. Visitors are sent to the admin host to sign in and bounced back to the tunnel. On success the backend receives the user's name on x-Gatecrash-User (header name configurable), the stable id on X-Gatecrash-Id, and the role on X-Gatecrash-Role. The whole X-Gatecrash-* namespace is stripped from inbound requests, so these headers can't be spoofed.
[[auth_policy]]
id = "staff"
users = ["u_1a2b3c4d5e6f7080", "u_90ab..."] # allowed user IDs (opaque; pick by name in the UI)
# header = "x-Gatecrash-User" # optional: override the identity (name) header
# secret_hash = "$2a$12$..." # bcrypt of a machine-generated service secret (generated in the UI)
Service secret (for machines/CI/webhooks). Instead of a human password, generate a strong random service secret from the policy editor — it's shown once and stored only as a bcrypt hash. Non-interactive clients send it as the HTTP Basic password (username service; the username isn't checked):
curl -u service:<SERVICE_SECRET> https://app.example.com/
A service secret grants access independent of the
userslist — it's a shared bearer credential, so use it only for trusted automation. Requests authenticated by it arrive as roleservice. Remove or regenerate it from the policy editor to revoke.
IP policy — a source-IP allowlist (with optional comments), reusable across tunnels. Works on HTTP and TCP (including passthrough). Because allowed clients pass through with no credential, it suits services that can't authenticate (MCP servers, API/tool endpoints).
[[ip_policy]]
id = "internal"
[[ip_policy.range]]
cidr = "10.0.0.0/8"
comment = "office LAN"
An IP gets allowed three ways: a permanent range; self-service (a blocked HTTP visitor signs in and grants their IP for 7 days); or a shareable enrollment link (/enroll/<token>) that lets anyone authorize their own IP for 7 days without signing in — the only self-service option for TCP tunnels. Grants are listed and revocable from the policy's IPs & Link panel, and are keyed by source IP (everyone behind one public IP shares access; prefer a permanent CIDR for rotating IPv6 prefixes). Treat enrollment links like passwords; Rotate invalidates old ones.
User login and enrollment links require
server.admin_hostto be configured.
Run Gatecrash as the public edge — do not put it behind a reverse proxy, load balancer, or CDN. IP policies (and per-client source-IP logic) trust the connection's real source address and deliberately ignore
X-Forwarded-For, so a spoofed header can never bypass an allowlist. If another proxy fronts Gatecrash, every visitor appears to come from that proxy's IP — collapsing IP policies to "allow everyone or no one." Terminate the public connection on Gatecrash itself.
Audit Log
All admin panel changes are recorded in an audit log, viewable from the Audit Log tab.
Logged events include:
- Tunnel create, edit, delete, and secret regeneration
- Redirect create, edit, delete
- User and access-policy changes (create, edit/reset, delete)
- Sign-ins and IP authorizations
Each entry records the timestamp, the acting user's ID, the action, and a detail. The log is stored at <config_dir>/audit.json (NDJSON, rotated) and the admin UI shows the most recent 1,000 entries, filterable by user and action.
CLI Reference
Server (gatecrash-server)
gatecrash-server Start the tunnel server (default)
gatecrash-server make-config Generate a config file
gatecrash-server update Self-update to latest release
gatecrash-server version Print version
gatecrash-server help Show help
| Flag | Env Var | Default | Description |
|---|---|---|---|
--config |
GATECRASH_CONFIG |
/etc/gatecrash/gatecrash.toml |
Config file path |
--debug |
false |
Enable debug logging |
Client (gatecrash)
The client runs directly without subcommands:
gatecrash [flags] Connect a tunnel to the server
gatecrash update Self-update to latest release
gatecrash version Print version
gatecrash help Show help
| Flag | Env Var | Required | Default | Description |
|---|---|---|---|---|
-c, --config |
No | /etc/gatecrash/client.toml |
Load settings from a TOML file (see below) | |
--server |
GATECRASH_SERVER |
Yes | Server SSH address (host:port) |
|
--token |
GATECRASH_TOKEN |
Yes | Tunnel token (tunnel_id:secret) |
|
--target |
GATECRASH_TARGET |
Yes | Target address (repeatable, see below) | |
--host-key |
GATECRASH_HOST_KEY |
Yes | Server SSH fingerprint (SHA256:...) |
|
--count |
GATECRASH_COUNT |
No | 1 |
Parallel tunnel connections (1-10) |
--debug |
GATECRASH_DEBUG |
No | false |
Enable debug logging |
"Required" means it must be supplied somehow — flag, env var, or config file.
Config File
Instead of a long flag string, the client reads a TOML file — ideal for running as a service. It loads /etc/gatecrash/client.toml automatically if present, or pass -c /path/to/client.toml. Generate one from the admin panel: open a tunnel's secret dialog and click Download client.toml (it's pre-filled with the server, host key, and token — just set your target).
# /etc/gatecrash/client.toml
server = "tunnel.example.com:51234"
host_key = "SHA256:abc..."
token = "web-app:YOUR_SECRET"
targets = [
"127.0.0.1:8000", # default target (TCP, or unmatched HTTP)
"git.example.com=127.0.0.1:3000", # route an HTTP hostname to a backend
]
# count = 1 # parallel connections for redundancy (1-10)
# debug = false
Precedence is flag > environment variable > config file > built-in default, so you can keep a base file and override a single value on the command line. Then just run gatecrash (or gatecrash -c ...).
Multi-Target Routing
An HTTP tunnel with multiple hostnames can route each to a different backend using --target:
gatecrash --server host:port --token homelab:secret \
--target git.example.com=forgejo:3000 \
--target gist.example.com=opengist:8080
A bare --target host:port (without =) sets the default target for unmatched hostnames and TCP tunnels.
Via environment variable: GATECRASH_TARGET=git.example.com=forgejo:3000,gist.example.com=opengist:8080
Target Schemes
Route targets support scheme prefixes:
| Target | Behavior |
|---|---|
localhost:8080 |
Plain HTTP / TCP (default) |
https://localhost:8443 |
TLS with certificate verification |
https+insecure://localhost:8443 |
TLS without certificate verification |
Docker
Server
# Replace 51234 with the ssh_port from your gatecrash.toml
docker run -d \
-p 443:443 -p 80:80 -p 51234:51234 \
-v gatecrash-config:/etc/gatecrash \
ghcr.io/jclement/gatecrash-server:latest
The SSH port is randomly generated on first run. Check ssh_port in your gatecrash.toml to find the assigned port.
Client
docker run -d \
-e GATECRASH_SERVER=tunnel.example.com:51234 \
-e GATECRASH_HOST_KEY=SHA256:your_host_key_fingerprint \
-e GATECRASH_TOKEN=web-app:YOUR_SECRET \
-e GATECRASH_TARGET=app:8000 \
--network=app-network \
ghcr.io/jclement/gatecrash:latest
The client image is minimal -- just the tunnel client binary with ca-certificates.
Docker Compose -- Client with Service
A typical deployment pairs the gatecrash client with your application:
services:
# Your application
whoami:
image: traefik/whoami
expose:
- "80"
# Gatecrash tunnel client
tunnel:
image: ghcr.io/jclement/gatecrash:latest
environment:
GATECRASH_SERVER: tunnel.example.com:51234
GATECRASH_HOST_KEY: "SHA256:your_host_key_fingerprint"
GATECRASH_TOKEN: "web-app:YOUR_SECRET"
GATECRASH_TARGET: whoami:80
depends_on:
- whoami
restart: unless-stopped
Docker Compose -- Multi-Service Routing
Route multiple hostnames to different containers through a single tunnel:
services:
forgejo:
image: codeberg.org/forgejo/forgejo:latest
expose:
- "3000"
- "22"
opengist:
image: ghcr.io/thomiceli/opengist:latest
expose:
- "8080"
tunnel:
image: ghcr.io/jclement/gatecrash:latest
environment:
GATECRASH_SERVER: tunnel.example.com:51234
GATECRASH_HOST_KEY: "SHA256:your_host_key_fingerprint"
GATECRASH_TOKEN: "homelab:YOUR_SECRET"
GATECRASH_TARGET: "git.example.com=forgejo:3000,gist.example.com=opengist:8080"
depends_on:
- forgejo
- opengist
restart: unless-stopped
Docker Compose -- Server
services:
server:
image: ghcr.io/jclement/gatecrash-server:latest
ports:
- "80:80"
- "443:443"
- "51234:51234" # Match ssh_port in your gatecrash.toml
volumes:
- config:/etc/gatecrash
restart: unless-stopped
volumes:
config:
Settings are configured in gatecrash.toml inside the config volume, or via the admin panel.
Development
# Install tools
mise install
# Download frontend assets + Go deps
mise run setup
# Build the CSS (Tailwind scans templates/*.html and bakes in the classes it finds)
mise run tailwind # one-shot
mise run tailwind:watch # rebuild on template changes — run alongside `dev`
# Run server with hot-reload (Go only; run tailwind:watch in another terminal for CSS)
mise run dev
# Run tests
mise run test
# Build both binaries (depends on `tailwind`, so CSS is always fresh)
mise run build
# Test release
mise run release-snapshot
Architecture
gatecrash/
├── cmd/
│ ├── gatecrash/ # Client binary
│ └── gatecrash-server/ # Server binary
├── internal/
│ ├── config/ # TOML config + file watcher
│ ├── server/ # SSH server, HTTP proxy, TCP forward, vhost routing, TLS
│ ├── client/ # SSH client, HTTP/TCP handlers, reconnect logic
│ ├── admin/ # Web admin panel (users, passkeys, sessions, audit log)
│ ├── protocol/ # SSH channel types and control messages (shared)
│ ├── token/ # bcrypt-based tunnel authentication
│ └── update/ # Self-update via GitHub releases (shared)
├── web/ # HTML templates + static assets (server only)
├── deploy/ # Install script, docker-compose examples
└── .github/ # CI/CD, dependabot
License
MIT
Vibe coded with Claude Code