c90f145a05
Add high-availability cluster support to the embedded NATS server (issue 0003a, first phase of decentralization). pkg/embeddednats: - ServerConfig gains ServerName (unique per node, required by JetStream RAFT) and an optional *ClusterConfig (cluster name, route host/port, peer route URLs, shared-secret Username/Password, and a mutual-TLS *tls.Config). applyClusterOpts maps it onto server.Options.Cluster + Routes. Nil Cluster keeps the legacy standalone server. pkg/busauth: - RouteTLSConfig builds the route layer's mutual-TLS config: the node presents its CA-signed certificate AND verifies the peer's certificate against the bus CA (RequireAndVerifyClientCert), reusing the issue-0001 CA. Routes authenticate NODES, never the client nkey authenticator. cmd/membershipd: - Cluster flags (--cluster-name/--server-name/--cluster-port/--routes/ --cluster-user/--cluster-pass/--route-tls-cert/-key/-ca) wire a node into the cluster. validateClusterConfig refuses a public cluster without a route secret and complete mutual route TLS, and rejects partial route-TLS flags (all-or-nothing). splitRoutes parses the CSV. Tests (DoD: golden + 2 edge + error path): - TestClusterForwardsAcrossNodes: 2-node cluster forwards a client subject from one node to a subscriber on the other. - TestClusterThreeNodesForward: 3-node (HA shape) cross-node forwarding. - TestClusterMutualTLSForwards: forwarding over mutual-TLS routes. - TestClusterRejectsBadRouteAuth: wrong cluster password -> no route. - TestClusterRejectsUnsignedNode: cert not signed by the bus CA -> no route. - TestClusterConfigPolicy / TestSplitRoutes: boot-guard + CSV parsing. Master stays green: standalone (no --cluster-name) is unchanged.
76 lines
3.1 KiB
Go
76 lines
3.1 KiB
Go
package busauth
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
// LoadCATLSConfig builds a *tls.Config that trusts ONLY the given CA certificate
|
|
// (PEM file), for a bus client pinning the project's self-signed CA. Because the
|
|
// bus uses a private CA rather than a public one, clients must pin it explicitly;
|
|
// trusting the system roots would reject the server cert. This is the single
|
|
// helper every client (Go peers, the mobile binding, the gateway) uses to turn a
|
|
// ca.crt path into a connection config.
|
|
func LoadCATLSConfig(caPEMPath string) (*tls.Config, error) {
|
|
pem, err := os.ReadFile(caPEMPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("busauth: read CA %q: %w", caPEMPath, err)
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if !pool.AppendCertsFromPEM(pem) {
|
|
return nil, fmt.Errorf("busauth: CA %q contains no valid PEM certificate", caPEMPath)
|
|
}
|
|
return &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12}, nil
|
|
}
|
|
|
|
// ServerTLSConfig loads the bus NATS server's certificate and private key (PEM
|
|
// files) into a *tls.Config to present to clients. The private key never leaves
|
|
// the host; only the CA cert travels to clients.
|
|
func ServerTLSConfig(certPEMPath, keyPEMPath string) (*tls.Config, error) {
|
|
cert, err := tls.LoadX509KeyPair(certPEMPath, keyPEMPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("busauth: load server keypair: %w", err)
|
|
}
|
|
return &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12}, nil
|
|
}
|
|
|
|
// RouteTLSConfig builds the mutual-TLS config for the NATS CLUSTER route layer
|
|
// (issue 0003a). Unlike the client data plane, where the server presents a cert
|
|
// and only the client verifies it, routes are server-to-server: each node both
|
|
// presents its own node certificate AND verifies the connecting node's
|
|
// certificate against the bus CA. So this single config carries:
|
|
//
|
|
// - Certificates: this node's CA-signed certificate (presented in both the
|
|
// server and the client role of a route handshake),
|
|
// - RootCAs: the bus CA, to verify the certificate of a node we dial out to,
|
|
// - ClientCAs + ClientAuth=RequireAndVerifyClientCert: the bus CA, to verify
|
|
// the certificate of a node dialing in.
|
|
//
|
|
// The effect: a node that lacks a certificate signed by the bus CA cannot
|
|
// establish a route in either direction, even if it knows the cluster password.
|
|
// Reuse the same CA as the client data plane (deploy/tls) but a per-node cert
|
|
// whose SAN covers that node's route address.
|
|
func RouteTLSConfig(certPEMPath, keyPEMPath, caPEMPath string) (*tls.Config, error) {
|
|
cert, err := tls.LoadX509KeyPair(certPEMPath, keyPEMPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("busauth: load route keypair: %w", err)
|
|
}
|
|
pem, err := os.ReadFile(caPEMPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("busauth: read route CA %q: %w", caPEMPath, err)
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if !pool.AppendCertsFromPEM(pem) {
|
|
return nil, fmt.Errorf("busauth: route CA %q contains no valid PEM certificate", caPEMPath)
|
|
}
|
|
return &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
RootCAs: pool,
|
|
ClientCAs: pool,
|
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
|
MinVersion: tls.VersionTLS12,
|
|
}, nil
|
|
}
|