Skip to content

ADR-0003. Mandatory ECDH + AES-256-GCM payload encryption on /stream

FieldValue
StatusAccepted
Date2026-03-28
Deciderssignal-team
Supersedes

Context

  • TLS terminates at Traefik; beyond it, traffic crosses the internal network and a shared Redis bus.
  • WebSocket payloads carry order/payment data that should not be readable at the bus or by other co-tenant connections.
  • We want per-connection key isolation and forward secrecy without a key-management service.

Decision

We will require end-to-end payload encryption for every /stream connection (requireEncryption: true):

  • During the handshake the client sends clientPublicKey; the server generates an ephemeral ECDH P-256 key pair, derives an AES-256 key via HKDF-SHA-256, and stores it in an in-memory _clientAesKeys map keyed by client id.
  • The server returns { serverPublicKey, salt } inside the connected event so the client derives the same key.
  • All subsequent messages are AES-256-GCM { iv, ct }. Only connected and error are sent in plaintext.
  • Keys are deleted on disconnect and lost on restart (clients reconnect to re-derive).

Consequences

ProsCons
Payloads opaque on the wire, bus, and to other connectionsNo persistence — restart forces all clients to reconnect
Forward secrecy (ephemeral key per connection)clientPublicKey is mandatory — clients without ECDH are rejected
No external KMS neededIn-memory key map limits a single process; relies on sticky socket + Redis for scale

Alternatives Considered

OptionProsConsWhy rejected
TLS only (no payload encryption)SimplestPlaintext past Traefik + on Redis busInsufficient for payment data
Shared static AES keyNo handshakeNo forward secrecy; one leak compromises allUnacceptable blast radius
mTLS per clientStrong identityHeavy cert lifecycle on POS/Tauri clientsOperationally impractical for client fleet

References

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.