How is the connection between server and client secured?
Two independent layers:
- Encryption (TLS). The client dials the server's HTTPS port over TLS — the same
:443browsers use, with the tunnel distinguished by theseikan-tunnelALPN protocol — 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 private is a peer tunnel? Does anything ever touch the public internet?
No — that's the entire point of seikan serve --peer / seikan forward. A peer frontend has no domain and binds no public port; the server never accepts a public connection for it. Both sides only ever dial out to the server's :443, exactly like every other client, and the server relays bytes between their two sessions internally. Nothing about the backend service is reachable — or even discoverable — from the public internet at any point.
The two capabilities are split into independent one-time secrets: a provider token (register the backend) and a consumer token (relay a connection in). They're validated separately and are not interchangeable — holding the consumer token never lets you register a fake backend and intercept or inject traffic meant for real consumers. This matters most for the case that motivates the feature: sharing access to something like a database without also handing out the ability to impersonate it.
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 either an administrator session (username + password) or a client key as Authorization: Bearer <sk_…>. Passwords are stored as bcrypt hashes, never recoverable. Failed attempts of either kind are rate-limited/locked out per IP.
What is the setup key, and how long does it live?
A brand-new server has no account to authenticate against, so something has to prove that whoever is filling in the setup form is the person who runs the machine. That's the setup key: the server prints it at first start and keeps it in <state-dir>/setup-key (mode 0600) — or uses SEIKAN_SETUP_KEY if you supply one, which is the usual container path. seikan-server setup-key prints it again.
It authorizes exactly one thing — creating the first administrator — and stops working the moment one exists: POST /api/setup answers 409 from then on, and the generated file is deleted. A key that leaks after setup is worthless. If you lose the admin password, the recovery path is seikan-server user passwd <username> on the host, not a new setup key.
Is seikan init without a key safe? Anyone can call that endpoint.
They can, and it gets them a row in a table. POST /api/register creates a request, not a credential: nothing is issued until an administrator approves it in the admin UI, and even then the client key is minted only when the requesting client comes back to collect it — so an approval nobody picks up leaves nothing behind, and no raw secret is ever written to disk.
Two secrets keep an approval attached to the right machine. The poll token, handed only to the requesting client, is what collects the key; knowing a request's id gets you nothing without it. The confirmation code is the other direction: it is printed on the requesting machine and shown in the admin UI, so the administrator can see that the row they are approving is the machine in front of them and not another request that arrived at the same moment. That check is the one thing the flow asks a human to actually do.
Requests expire after 15 minutes and are capped — 3 live per source address, 32 in total — so the endpoint can't be used to grow the database. Denying one tells the client and closes it out. If you'd rather not have the endpoint at all, issue client keys yourself and pass them with --api-key; the approval path is opt-in per client, not a mode the server is in.
Can I give other people scoped access (multi-tenancy)?
Yes, with client keys. An administrator issues them in the admin UI (Client keys page) or via POST /api/apikeys; each is a scoped tenant credential.
- A client key registers frontends; each frontend is owned by the key that created it.
- A client key lists and deletes only its own frontends; others' are invisible to it (returns 404, so existence isn't leaked).
- A client key cannot issue or list keys (403). That's deliberate: a key sitting on a laptop or in CI can't mint more keys or reach another tenant, so losing one costs you that tenant and nothing else.
- An administrator lists and deletes all frontends, and is the only one who can manage client keys.
- Domains stay globally unique — while a domain exists, only its owner can re-claim it.
- Revoking a client key cascade-deletes its frontends and closes their open tunnels.
A tenant sets their client up either by requesting approval (seikan init --server admin.example.com, which mints them their own key when you approve it) or with a key you issue and hand over (--api-key sk_…); their add/list/remove 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).
- Peer → same active/standby, on the provider side only. A second
serve --peerwith the same provider token is rejected the same way TCP is — one backend instance. The consumer side is unaffected by this: any number of boxes canseikan forwardwith the same consumer token concurrently, each relaying its own local connections in.
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 client keys and connect tokens like passwords; rotate by issuing a new key and revoking the old. Give each client its own key so revoking one doesn't disturb the others.