feat(client,membershipd): TLS on the HTTP control plane (H5)
Audit H5 (Alto, public). The control plane was signed but plaintext, so a network MITM could read all metadata (subjects, endpoints, public keys, sealed keys, blob hashes, the social graph) and drop requests. Signing gives integrity, not confidentiality. - membershipd serves the control plane over TLS (ListenAndServeTLS, MinVersion 1.2) with the same CA-signed cert as the data plane when --tls-cert is set; the fail-open guard already requires --bus-auth enforce alongside it. - The client gets a separate Options.CtrlTLS so the HTTP client pins the bus CA, independent of the NATS data-plane TLS. Connect now sets both planes' TLS from the one CA and REFUSES a plaintext http:// control-plane URL when a CA is provided, so metadata is never sent in the clear when TLS is expected. Connect's signature is unchanged; callers (worker/chat --ca, mobile NewSession) must pass an https:// control-plane URL when they pass a CA. Documented for the deploy step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+26
-5
@@ -24,6 +24,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -67,10 +68,15 @@ type Options struct {
|
||||
// with an nkey to a server that does not advertise nkey auth ("nkeys not
|
||||
// supported by the server"), so this is opt-in rather than always-on.
|
||||
UseNkey bool
|
||||
// TLS, when non-nil, secures the NATS connection and pins the server to this
|
||||
// config's RootCAs (the bus's self-signed CA). Build it with
|
||||
// busauth.LoadCATLSConfig(caPath). Nil keeps the connection plaintext.
|
||||
// TLS, when non-nil, secures the NATS (data plane) connection and pins the
|
||||
// server to this config's RootCAs (the bus's self-signed CA). Build it with
|
||||
// busauth.LoadCATLSConfig(caPath). Nil keeps the data plane plaintext.
|
||||
TLS *tls.Config
|
||||
// CtrlTLS, when non-nil, secures the HTTP control-plane connection and pins it
|
||||
// to this config's RootCAs. It is separate from TLS so the two planes can be
|
||||
// secured independently (a test may TLS one and not the other); production
|
||||
// sets both to the same CA via Connect. Nil keeps the control plane plaintext.
|
||||
CtrlTLS *tls.Config
|
||||
}
|
||||
|
||||
// New connects to NATS and records the control-plane URL with default Options
|
||||
@@ -90,11 +96,19 @@ func Connect(natsURL, ctrlURL string, id cs.Identity, caPath string) (*Client, e
|
||||
if caPath == "" {
|
||||
return New(natsURL, ctrlURL, id)
|
||||
}
|
||||
// A CA implies the bus is TLS on BOTH planes. Refuse a plaintext control-plane
|
||||
// URL: signing gives integrity, not confidentiality, so sending metadata over
|
||||
// http:// when the operator provisioned a CA would silently leak it to a MITM
|
||||
// (audit H5). Force https rather than silently downgrade.
|
||||
if !strings.HasPrefix(ctrlURL, "https://") {
|
||||
return nil, fmt.Errorf("client: control-plane URL %q must be https:// when a CA is provided", ctrlURL)
|
||||
}
|
||||
tlsCfg, err := busauth.LoadCATLSConfig(caPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("client: load CA %q: %w", caPath, err)
|
||||
}
|
||||
return NewWithOptions(natsURL, ctrlURL, id, Options{UseNkey: true, TLS: tlsCfg})
|
||||
// Pin the same CA on both planes: nkey+TLS on NATS, TLS on the HTTP control plane.
|
||||
return NewWithOptions(natsURL, ctrlURL, id, Options{UseNkey: true, TLS: tlsCfg, CtrlTLS: tlsCfg})
|
||||
}
|
||||
|
||||
// NewWithOptions is New with explicit connection options (nkey auth, and, from
|
||||
@@ -125,13 +139,20 @@ func NewWithOptions(natsURL, ctrlURL string, id cs.Identity, opts Options) (*Cli
|
||||
nc.Close()
|
||||
return nil, fmt.Errorf("client: init jetstream: %w", err)
|
||||
}
|
||||
// The control-plane HTTP client pins the bus CA when CtrlTLS is set, so an
|
||||
// https:// control plane is verified against the bus's own CA rather than the
|
||||
// system roots (audit H5). Without it the client stays plaintext for dev.
|
||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||
if opts.CtrlTLS != nil {
|
||||
httpClient.Transport = &http.Transport{TLSClientConfig: opts.CtrlTLS.Clone()}
|
||||
}
|
||||
return &Client{
|
||||
id: id,
|
||||
endpoint: frame.EndpointID(id.SignPub),
|
||||
nc: nc,
|
||||
js: js,
|
||||
ctrlURL: ctrlURL,
|
||||
http: &http.Client{Timeout: 10 * time.Second},
|
||||
http: httpClient,
|
||||
keyCache: map[string]map[int][]byte{},
|
||||
signCache: map[string][]byte{},
|
||||
}, nil
|
||||
|
||||
Reference in New Issue
Block a user