Transport security: TLS end-to-end (https + wss) before public exposure #133

Closed
opened 2026-07-22 12:04:42 +02:00 by marco · 1 comment
Owner

Problem. Today the client talks to Auth over http:// and to the GameServer over ws://. The JWT (which fully authenticates a player) travels in the clear: anyone on the network path can sniff it and impersonate that player, or MITM/alter traffic. The wire format (JSON) is not the issue — a custom client can forge any protocol; our defense is server-authoritative validation. The real gap is transport confidentiality/integrity.

Decision (from a protocol-security discussion). Do NOT invest in gRPC / a binary protocol / client attestation / mTLS-cert-pinning: none add trust for a client the user runs, and they are the wrong scale for a friends server. The one concrete, worthwhile hardening is TLS end-to-end.

Scope

  • Serve Auth over https:// and the GameServer WebSocket over wss://.
  • Prefer terminating TLS at a reverse proxy (Caddy/nginx) in the deploy, or Kestrel with a cert — cross-platform, no code coupling.
  • Client config.json uses https/wss URLs; keep ws/http allowed for local dev.
  • Document in docs/deploy.md.

Out of scope

  • gRPC / binary protocol migration, client attestation, mTLS client certs, anti-cheat. (Explicitly rejected — see discussion; wrong scale + false sense of security.)

Definition of Done

  • A deployed stack serves Auth on https and the GameServer on wss; the client connects over TLS end-to-end.
  • The JWT is never sent over an unencrypted connection in a production deploy.
  • Local dev still works over ws/http.
  • docs/deploy.md documents the TLS setup (proxy or Kestrel cert).

Priority: before any public/non-LAN exposure (pairs with the open-registration tech debt).


mTLS / client certificate — considered and rejected. A client-side certificate authenticates the TLS peer ("a build of our client"), not the player, and the cert must ship inside a client the user controls → extractable, so it does not prove "our client" against a determined user. A shared embedded cert is security-through-obscurity (the "keep random scanners out" role is better served by the planned invite/allow-list); a per-player cert merely duplicates the JWT with PKI/rotation/revocation overhead. Correct stack: server-side TLS (wss) + the signed JWT over it. Client certs are an enterprise/public-scale tool for a threat model that is not ours (friends-scale).

**Problem.** Today the client talks to Auth over `http://` and to the GameServer over `ws://`. The JWT (which fully authenticates a player) travels in the clear: anyone on the network path can sniff it and impersonate that player, or MITM/alter traffic. The wire *format* (JSON) is not the issue — a custom client can forge any protocol; our defense is server-authoritative validation. The real gap is **transport confidentiality/integrity**. **Decision (from a protocol-security discussion).** Do NOT invest in gRPC / a binary protocol / client attestation / mTLS-cert-pinning: none add trust for a client the user runs, and they are the wrong scale for a friends server. The one concrete, worthwhile hardening is **TLS end-to-end**. ## Scope - Serve Auth over **`https://`** and the GameServer WebSocket over **`wss://`**. - Prefer terminating TLS at a reverse proxy (Caddy/nginx) in the deploy, or Kestrel with a cert — cross-platform, no code coupling. - Client `config.json` uses `https`/`wss` URLs; keep `ws`/`http` allowed for local dev. - Document in `docs/deploy.md`. ## Out of scope - gRPC / binary protocol migration, client attestation, mTLS client certs, anti-cheat. (Explicitly rejected — see discussion; wrong scale + false sense of security.) ## Definition of Done - [ ] A deployed stack serves Auth on https and the GameServer on wss; the client connects over TLS end-to-end. - [ ] The JWT is never sent over an unencrypted connection in a production deploy. - [ ] Local dev still works over ws/http. - [ ] `docs/deploy.md` documents the TLS setup (proxy or Kestrel cert). _Priority: before any public/non-LAN exposure (pairs with the open-registration tech debt)._ --- **mTLS / client certificate — considered and rejected.** A client-side certificate authenticates the TLS *peer* ("a build of our client"), not the player, and the cert must ship inside a client the user controls → extractable, so it does not prove "our client" against a determined user. A *shared* embedded cert is security-through-obscurity (the "keep random scanners out" role is better served by the planned invite/allow-list); a *per-player* cert merely duplicates the JWT with PKI/rotation/revocation overhead. Correct stack: server-side TLS (wss) + the signed JWT over it. Client certs are an enterprise/public-scale tool for a threat model that is not ours (friends-scale).
Author
Owner

Already satisfied in production — closing as done.

The substantive requirement (TLS end-to-end so the JWT/password never travel in cleartext) is already live via the homelab Traefik setup (home-infra-ng roles/isommo):

  • isommo-auth and isommo-gameserver run behind Traefik on the proxy network, entrypoint websecure (:443). Router isommo-auth serves Host(mmo.homelab.devncode.it); router isommo-ws serves PathPrefix(/ws) → gameserver:5100 with the WebSocket Upgrade proxied transparently.
  • Traefik :80 redirects → websecure, :443 uses certResolver: letsencrypt + ACME (auto-renew), same mechanism as Forgejo.
  • The client already ships a production config.json with https://mmo.homelab.devncode.it + wss://mmo.homelab.devncode.it/ws.

Verification (just now):

$ curl -v https://mmo.homelab.devncode.it/health
< HTTP/2 200
TLSv1.3 · issuer: Lets Encrypt (CN=YR1) · ssl_verify=0 (valid)

So Auth is served over https and the GameServer over wss with a valid Lets Encrypt cert, and local dev still uses ws/http (Debug config). DoD met.

Deferred (rejected here, see thread): gRPC / binary protocol / client mTLS / attestation — wrong scale for a friends server.

Follow-up (optional, not blocking): sync docs/deploy.md (currently a generic Caddy example) to the real Traefik setup, and add UseForwardedHeaders in the two servers so they log the real client IP behind the proxy.

**Already satisfied in production — closing as done.** The substantive requirement (TLS end-to-end so the JWT/password never travel in cleartext) is already live via the homelab Traefik setup (`home-infra-ng` `roles/isommo`): - `isommo-auth` and `isommo-gameserver` run behind Traefik on the `proxy` network, entrypoint **`websecure`** (:443). Router `isommo-auth` serves `Host(mmo.homelab.devncode.it)`; router `isommo-ws` serves `PathPrefix(/ws)` → gameserver:5100 with the WebSocket Upgrade proxied transparently. - Traefik `:80` **redirects → websecure**, `:443` uses `certResolver: letsencrypt` + ACME (auto-renew), same mechanism as Forgejo. - The client already ships a production `config.json` with `https://mmo.homelab.devncode.it` + `wss://mmo.homelab.devncode.it/ws`. **Verification (just now):** ``` $ curl -v https://mmo.homelab.devncode.it/health < HTTP/2 200 TLSv1.3 · issuer: Lets Encrypt (CN=YR1) · ssl_verify=0 (valid) ``` So Auth is served over https and the GameServer over wss with a valid Lets Encrypt cert, and local dev still uses ws/http (Debug config). DoD met. Deferred (rejected here, see thread): gRPC / binary protocol / client mTLS / attestation — wrong scale for a friends server. Follow-up (optional, not blocking): sync `docs/deploy.md` (currently a generic Caddy example) to the real Traefik setup, and add `UseForwardedHeaders` in the two servers so they log the real client IP behind the proxy.
marco closed this issue 2026-07-22 13:24:13 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marco/IsoMmo#133
No description provided.