feat(membership): forbid cleartext rooms on public deployments (H4 min defense)

Audit H4 (Alto). The embedded NATS has a single account with no per-subject
permissions, so any registered peer can subscribe to any subject — a cleartext
(ModeNATS) room's payload is readable by anyone who knows the subject.

A complete per-subject ACL derived from membership does not fit here: NATS
evaluates a connection's permissions once at connect time and never re-evaluates
them, but unibus clients connect-then-create/join-then-publish on one connection
(TestSecureBusEndToEnd). Static permissions would forbid the owner from
publishing to a room it just created; the dynamic reconnection model belongs to
the 0003 decentralization redesign. See dev/0004d-dataplane-acl.md.

Minimum defense implemented: Server.RequireEncryptedRooms (set by membershipd on
any non-loopback bind) refuses to create cleartext rooms, so every room on a
public deployment is end-to-end encrypted. Message CONTENT stays confidential
even with no subject isolation; residual traffic-metadata exposure is documented
and tracked for 0003.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 14:26:45 +02:00
parent 47ff74d837
commit e502b16675
3 changed files with 106 additions and 0 deletions
+18
View File
@@ -61,6 +61,16 @@ type Server struct {
authMode AuthMode
nonces *nonceCache
limiter *ipRateLimiter
// RequireEncryptedRooms, when true, refuses to create cleartext (ModeNATS)
// rooms. It is the minimum-defensive control for the data plane (audit H4):
// the embedded NATS has no per-subject ACL, so a cleartext room is readable by
// any registered peer that knows (or guesses) its subject. Forcing every room
// to be end-to-end encrypted keeps message CONTENT confidential even when the
// transport offers no subject isolation. The command sets this on a public
// (non-loopback) bind. See dev/0004d-dataplane-acl.md for the full rationale
// and the residual metadata exposure this does NOT close.
RequireEncryptedRooms bool
}
// NewServer wires the membership store and blob store into an http.Handler. The
@@ -341,6 +351,14 @@ func (s *Server) handleCreateRoom(w http.ResponseWriter, r *http.Request) {
writeErr(w, http.StatusBadRequest, "subject and owner.endpoint required")
return
}
// Data-plane minimum defense (audit H4): on a public deployment cleartext
// rooms are disabled, so no message ever rides the un-ACL'd NATS subject in
// the clear for another registered peer to sniff.
if s.RequireEncryptedRooms && !req.Policy.Encrypt {
writeErr(w, http.StatusForbidden,
"cleartext rooms are disabled on this deployment; create an encrypted (Matrix-policy) room")
return
}
roomID := newULID()
info := RoomInfo{
RoomID: roomID,