The Socket.IO /terminal namespace validated the JWT but discarded the
role, so a read-only viewer (403'd on every REST write) could connect
and open an interactive SSH shell on any managed device using the
device's stored router-admin credentials — a full bypass of the role
model (reported CVSS 9.6).
- Gate the namespace handshake by role: only admin/operator may connect;
viewers are rejected with "Console access denied for this role".
- Re-check the role at the start handler (defense in depth) and attach
the verified payload to socket.data.
- Audit-log terminal session opens (and rate-limit hits) to audit_log.
- Rate-limit shell starts per user (5 / 60s, in-memory sliding window).
- Frontend: gate the Terminal button (DeviceDetailPage) and dashboard
"Open terminal" quick action behind canWrite so viewers don't see it.
Validated on the local docker stack: viewer rejected from /terminal,
admin still connects; viewer remains 403 on POST /api/devices.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>