gatecrash

module
v0.9.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 10, 2026 License: MIT

README

Gatecrash

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.

FeaturesInstallationQuick StartHow It WorksConfigurationAuth & PoliciesDocker

[!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

  1. Provision a server with a public IP (any Linux VPS will do)

  2. Point a hostname at your server's IP via DNS (e.g. admin.example.com)

  3. 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 | sh
    

    Or download from GitHub Releases for manual installation.

  4. Open the admin panel at https://admin.example.com, register your passkey, and click Add Tunnel to create your first tunnel

  5. 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:8000
    

    The SSH port is randomly assigned on first run (check ssh_port in gatecrash.toml). The exact connection command — and a ready-made client.toml you 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.toml and just run gatecrash (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 users list — it's a shared bearer credential, so use it only for trusted automation. Requests authenticated by it arrive as role service. 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_host to 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

Directories

Path Synopsis
cmd
gatecrash command
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL