How is the connection between server and client secured?
Two independent layers:
- Encryption (TLS). The client dials the server's tunnel port over TLS, and all tunnelled traffic rides inside that single TLS connection (multiplexed with yamux). So everything between server and client is encrypted in transit.
- Server identity. The client verifies the server's TLS certificate against the public CA chain (the same Let's Encrypt cert the server serves publicly). An attacker can't impersonate the server without a valid cert for its hostname. Exception: the
--insecureflag disables this check — for local/staging use only. - Client authentication. After the TLS handshake the client presents a per-frontend connect token (a 256-bit random secret). The server accepts the tunnel only if the token is valid, so only an authorized client can attach to a frontend. The token never appears on the wire in cleartext (it's sent inside the TLS tunnel).
In short: TLS encrypts and authenticates the server; the connect token authenticates the client. (This is server-auth TLS + token auth, not mutual-TLS.)
Is an HTTP frontend end-to-end encrypted from the visitor to my local app?
No — like ngrok, for HTTP frontends TLS terminates at the server (it must, to answer ACME challenges and route by Host). The path is: visitor →(public TLS)→ server →(tunnel TLS)→ client →(plaintext, on your machine)→ local app. The server can see request content; the only unencrypted hop is client-to-localhost on your own machine. Run the server on infrastructure you trust. (TCP frontends are different — see below.)
I tunnel a TCP service with its own TLS (e.g. Postgres SSL). Is that end-to-end encrypted?
Yes. A TCP frontend is a raw passthrough — the server never terminates or inspects the connection, it just forwards bytes. So the application's own TLS handshake (Postgres SSL, an MQTT/AMQP TLS, etc.) happens directly between your client and your actual service; seikan only ever sees opaque ciphertext (double-encrypted on the server↔client hop, since the tunnel itself is TLS):
psql ──TLS (Postgres SSL, end-to-end)───────────────────────────► postgres
carried as opaque bytes: public TCP port → server →(tunnel TLS)→ client → localhost:5432
For real end-to-end safety, connect with sslmode=verify-full so the client validates Postgres's own certificate (the cert presented is Postgres's, passed through untouched), and make sure the cert's SAN matches how you connect.
How are secrets stored?
API keys (sk_…) and connect tokens (sk_t_…) are 256-bit random and stored only as SHA-256 hashes in the server's bbolt database; raw values are shown once at creation. Validation is constant-time. Repeated bad credentials trigger a temporary per-IP lockout (brute-force protection).
How is the admin API protected?
It's served only on the reserved admin host over HTTPS and requires Authorization: Bearer <sk_…>. Failed attempts are rate-limited/locked out per IP.
Can I give other people scoped access (multi-tenancy)?
Yes, with sub-tokens. The admin key has two roles. The root key — printed on first boot — can issue sub-tokens with seikan apikey create; each is a scoped tenant credential.
- Any key (root or sub) can register frontends; each frontend is owned by the key that created it.
- A sub key lists and deletes only its own frontends; others' are invisible to it (returns 404, so existence isn't leaked).
- The root key lists and deletes all frontends and is the only key that can manage sub-tokens (sub keys get 403).
- Domains stay globally unique — while a domain exists, only its owner can re-claim it.
- Revoking a sub-token (
seikan apikey rm <id>) cascade-deletes its frontends and closes their open tunnels.
A tenant sets up their client with the sub-token exactly like the root user (seikan init --server admin.example.com --api-key sk_…); their serve/list/rm are automatically scoped to their own frontends. Per-frontend connect tokens (sk_t_…) and the data plane are unchanged — ownership is purely an admin-API concern.
Can one tunnel serve several domains, or a wildcard?
Yes. An HTTP frontend can carry multiple domains and wildcards: seikan serve app.example.com,www.example.com 3000, or seikan serve '*.example.com' 3000. A wildcard matches a single label (a.example.com, not a.b.example.com). Certificates for wildcard frontends are issued per concrete subdomain on demand (TLS-ALPN-01/HTTP-01) the first time each subdomain is hit — so every subdomain you actually use must have DNS pointing at the server. There's no single *.example.com certificate (that would need DNS-01).
Is the metrics / pprof endpoint exposed?
No — it binds to 127.0.0.1:9090 by default (not public). Scrape it locally or over an SSH tunnel, or set SEIKAN_METRICS_ADDR="" to disable it.
What happens if two clients connect with the same token / domain?
It's a feature, not a conflict — behavior depends on the frontend type:
- HTTP → load balanced. Every client connected with the token joins a pool and public requests are spread across them (round-robin), with sticky sessions via an
sk_affcookie so a browser keeps hitting the same backend. If a client drops, traffic fails over to the rest (no 502s). Use it to scale a service across machines or do zero-downtime rolling restarts. All clients must serve the same app, and stickiness is cookie-level (not a replacement for shared server-side sessions). - TCP → active/standby. Only one client is active; a second connecting with the same token is rejected (and keeps retrying). When the active disconnects, a standby takes over automatically (in-flight connections drop; there's a brief takeover delay).
What should I avoid in production?
Don't use --insecure / SEIKAN_INSECURE (it disables server-cert verification). Keep ACME staging off once issuance works. Treat the admin API key and connect tokens like passwords; rotate by creating a new key and deleting the old.