Secure, test, and operate

Threat-model the live channel

Review origins, credentials, message validation, authorization, rate limits, and data exposure together.

TLS on wss: protects bytes on the wire. It does not stop a connected client from sending oversized frames, forged commands, or ten thousand connections from a botnet.

A long-lived channel gives attackers more time and volume to probe. Threat-model the whole surface, not just the handshake.

Trust boundaries for the board

Public SSE feed -> only public incident fields
Operator WebSocket -> authenticated, authorized, audited
HTTP commands -> same auth as WebSocket, rate limited

The public feed never includes internal runbook links or customer names. Operator payloads do, so they ride on authenticated channels with logging.

Abuse cases to write down

CaseControl
Connection floodMax connections per IP, global cap, fail closed
Oversized messageReject frames above 32 KB
Forged ack commandAuthorize role per command id
Stale credentialClose socket after revocation
Sensitive field leakSeparate public and private event types

Validate every inbound JSON frame against the envelope schema. Reject unknown types instead of partially applying them.

Rate limits and audit

if (!rateLimiter.allow(clientIp)) {
  ws.close(4429, 'rate limited')
  return
}

auditLog.write({
  actor: operator.id,
  action: 'ack_incident',
  incidentId: msg.payload.incidentId,
})

Run one hostile test for each row. A forged command from a viewer session should fail before it touches the database.

Minimum viable controls

You do not need a full SOC on day one. You do need message size limits, auth on privileged frames, and a connection cap per IP. Add audit logging before you add fancy ML anomaly detection.

Try this on your own project: list five abuse cases for your live feature, map each to a control, and run one test that proves the control fires.

Lesson completed