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
| Case | Control |
|---|---|
| Connection flood | Max connections per IP, global cap, fail closed |
| Oversized message | Reject frames above 32 KB |
| Forged ack command | Authorize role per command id |
| Stale credential | Close socket after revocation |
| Sensitive field leak | Separate 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