Seikan is a self-hosted tunneling reverse proxy — an ngrok replacement — with automatic Let's Encrypt.
A public server exposes services running behind NAT/firewalls through a long-lived outbound tunnel from a client. The client dials the server over one TLS connection multiplexed with yamux; the server terminates public HTTPS (per-domain Let's Encrypt certs) and raw TCP, and proxies each public connection over the tunnel to the client, which forwards it to a local address.
:443 HTTPS (autocert) same :443, ALPN "seikan-tunnel"
browser ───────────────────────► server ◄──────────────────────── client ──► localhost:3000
Host: app.example.com │ route Host→frontend→stream (your machine)
tcp ───────────────────────► │ per-frontend public TCP port
:10000-20000 ▼ admin API on the admin host (Bearer key)
Two binaries, no external services. Client keys and connect tokens are stored only as SHA-256 hashes in an embedded bbolt database; administrator passwords as bcrypt hashes.
Clients dial the same :443 as browsers do. A client announces the seikan-tunnel ALPN protocol during the TLS handshake and the server hands that connection to the tunnel instead of the HTTP server, so exposing a service costs no extra inbound port.
Prerequisites
Server
- A public Linux host (
amd64orarm64) with a stable public IP, and root on it — the installer sets up a hardened systemd service that runs the server itself as an unprivileged user withCAP_NET_BIND_SERVICE(running the container image instead needs Docker rather than root). There is no server build for macOS or Windows. - A public domain for the endpoint itself — the reserved admin host (
SEIKAN_ADMIN_HOST, e.g.admin.example.com) with an A/AAAA record pointing at that host. It serves the admin API on:443and is what clients dial (seikan init --server admin.example.com). The server refuses to start if that name doesn't resolve to one of its own interfaces, so a typo fails loudly instead of silently breaking certificate issuance; behind Docker/NAT/a reverse proxy setSEIKAN_SKIP_ADMIN_HOST_CHECK=true. - A public domain for each service you expose over HTTPS — either a dedicated record per service (
app.example.com,api.example.com) or a wildcard (*.example.com), all pointing at the same host. Certificates for a wildcard frontend are still issued per concrete subdomain on demand, so every subdomain you actually use needs its own DNS record. Raw TCP and private peer frontends need no domain at all. - Inbound ports reachable:
:80and:443(both required for ACME;:443also carries the client tunnels), plus the public TCP frontend range (10000-20000by default) if you expose TCP services.
Nothing else: no database, no reverse proxy in front, no external services. State (frontends, key hashes, autocert cache) lives on disk in /var/lib/seikan.
Client
- macOS or Linux (
amd64orarm64). No root: the installer places the binary onPATH, andseikan startregisters and runs it as a per-usersystemctl --userunit or launchd LaunchAgent. There is no Windows build. - Outbound TCP to the server on
:443— and nothing else. That's the same port a browser uses, so egress filtering that permits normal HTTPS permits the tunnel. No public IP, no inbound ports, no port forwarding, no DNS of its own: sitting behind NAT or a restrictive firewall is the case this is built for. - An administrator to let it in. Run
seikan init --server <admin-host>: the client prints a confirmation code and waits while an administrator approves it in the admin UI, then receives its own client key (sk_…). Alternatively an administrator issues a key up front (admin UI → Client keys) and you pass it as--api-key. Either way that key is what lets the client create and manage its own frontends; it carries no admin rights, so it's safe on a laptop or in CI. A machine that only needs to run an already-created tunnel can skip all of this and use the low-levelseikan connectwith that frontend's connect token; the consumer side of a peer tunnel likewise needs only its one-time token andseikan forward. - The service you're exposing reachable from this machine — any
host:portthe client can dial. Usuallylocalhost:3000, but it doesn't have to be local to the client.