Merge pull request 'A first node gets as far as its own bus: three faults on the way' (#50) from fix/one-foundation-on-the-bus-the-mesh-runs-on into main

This commit was merged in pull request #50.
This commit is contained in:
2026-09-29 14:06:33 +00:00
4 changed files with 54 additions and 25 deletions
+21 -12
View File
@@ -706,15 +706,18 @@ func enrol(ctx context.Context, opts options) error {
fmt.Printf(" signing key %s\n", fmt.Printf(" signing key %s\n",
base64.StdEncoding.EncodeToString(token.Signer)[:16]+"...") base64.StdEncoding.EncodeToString(token.Signer)[:16]+"...")
// The check that has to happen before this machine says anything. // **The pin is checked by the connection that presents this token, not by a dial of our own**
conn, err := link.Dial(token.Broker, token.Fingerprint, opts.timeout) // (novox/hq 04-ISSUES/146). This opened a raw TLS connection to the bus first, which worked
if err != nil { // against the broker the mesh used to run and cannot work against the one it runs now: NATS
return err // speaks its own protocol before it upgrades to TLS, so an immediate handshake is answered
} // with a plaintext line and the enrolment failed with "first record does not look like a TLS
defer conn.Close() // handshake" — on every node that has tried to join since the bus changed, which is why this
fmt.Println("\nthe broker presented the certificate this token pins") // went unnoticed: none had.
//
conn.Close() // What ADR 0004 requires still holds, and holds better: the client that presents the token
// carries the same pinned configuration, reads the server's greeting, upgrades, and the
// verification runs inside that handshake — so the one-time secret is sent only after the
// certificate has been checked, and nothing of this node's reaches an impostor.
mine, err := identity.Generate(*name) mine, err := identity.Generate(*name)
if err != nil { if err != nil {
@@ -788,10 +791,16 @@ func enrol(ctx context.Context, opts options) error {
proof := mine.Sign(link.EnrolProof(token.Secret, mine.Public, mine.Overlay.Public, proof := mine.Sign(link.EnrolProof(token.Secret, mine.Public, mine.Overlay.Public,
sealing.Public, serving.Public)) sealing.Public, serving.Public))
// The token says where to go and which certificate that address must present. It says nothing // The token says where to go and which certificate that address must present. It says nothing
// about which bus is there, and does not need to: every token names the one the mesh runs on // about which bus is there, and does not need to: there is one, and this host knows which
// today until the rollout (novox/hq ADR 0116 step 5), and that is what an empty Transport is. // (novox/hq ADR 0131 — the mesh speaks to one seat and the old transport is gone).
//
// **It used to leave this empty** and mean "whatever the mesh runs today", which was true
// while two buses existed and became a refusal the moment one did: an empty transport is not
// the bus's name, so every enrolment ended at "this token is for the \"\" bus"
// (novox/hq 04-ISSUES/146). Nothing caught it because nothing had enrolled since the bus
// changed.
reply, err := link.Enrol(ctx, reply, err := link.Enrol(ctx,
link.Approach{Address: token.Broker, Fingerprint: token.Fingerprint}, link.Approach{Address: token.Broker, Fingerprint: token.Fingerprint, Transport: link.OnNATS},
*name, token.Secret, *name, token.Secret,
mine.Public, mine.Overlay.Public, sealing.Public, serving.Public, reported, proof, found, mine.Public, mine.Overlay.Public, sealing.Public, serving.Public, reported, proof, found,
opts.timeout) opts.timeout)
+15 -6
View File
@@ -127,12 +127,21 @@
{ {
"id": "bus-certificate", "id": "bus-certificate",
"type": "action", "type": "action",
"command": ["docker", "run", "--rm", "--entrypoint", "sh", "-v", "mesh-broker-tls:/tls", // **The mesh makes its own** (novox/hq 04-ISSUES/146). This ran `openssl` inside the
"192.0.2.250:5000/nats@sha256:b83efabe3e7def1e0a4a31ec6e078999bb17c80363f881df35edc70fcb6bb927", // broker's image while the broker was one that carried it; the bus that replaced it has a
"-c", "test -f /tls/tls.crt || (openssl req -x509 -newkey rsa:2048 -nodes -keyout /tls/tls.key -out /tls/tls.crt -days 3650 -subj '/CN=mesh-broker' -addext 'subjectAltName=DNS:mesh-broker,IP:127.0.0.1' >/dev/null 2>&1 && chmod 644 /tls/tls.crt && chmod 600 /tls/tls.key)"], // shell and no openssl, and no other image the bundle names has one either. So the program
"verify": ["docker", "run", "--rm", "--entrypoint", "sh", "-v", "mesh-broker-tls:/tls", // that needs the certificate writes it — already on this machine, since the schema step ran
"192.0.2.250:5000/nats@sha256:b83efabe3e7def1e0a4a31ec6e078999bb17c80363f881df35edc70fcb6bb927", // it, and asking nothing of the image it writes into. Self-signed on purpose: a host pins
"-c", "test -s /tls/tls.crt && openssl x509 -in /tls/tls.crt -noout"] // this server's exact certificate (novox/hq ADR 0004), and at this moment there is no mesh
// to ask an authority of.
// `--user 0:0` because the volume is root's and this image runs as nobody, which is right
// for the long-running control plane and wrong for a one-shot writing into a fresh volume.
"command": ["docker", "run", "--rm", "--user", "0:0", "-v", "mesh-broker-tls:/tls",
"192.0.2.250:5000/mesh-controller@sha256:c67db38439ff0aee242b467486765467bb95801f52175fc5727cc4e437338ace",
"broker", "certificate", "--into", "/tls"],
"verify": ["docker", "run", "--rm", "--user", "0:0", "-v", "mesh-broker-tls:/tls",
"192.0.2.250:5000/mesh-controller@sha256:c67db38439ff0aee242b467486765467bb95801f52175fc5727cc4e437338ace",
"broker", "certificate", "--check", "--into", "/tls"]
}, },
{ {
"id": "bus-conf-dir", "id": "bus-conf-dir",
+14 -3
View File
@@ -73,8 +73,19 @@ func PinnedConfig(pin string) (*tls.Config, error) {
}, nil }, nil
} }
// Dial opens a TLS connection to the broker, refusing anything but the pinned certificate. // dialPinned completes a TLS handshake against an address, refusing anything but the pinned
func Dial(address, pin string, timeout time.Duration) (*tls.Conn, error) { // certificate.
//
// **Not how the bus is reached, and it used to be** (novox/hq 04-ISSUES/146). Enrolment opened one
// of these before it said anything, which was right while the broker answered TLS immediately and
// wrong the moment the mesh moved to a bus that speaks its own protocol first. The pin itself was
// never the problem — PinnedConfig is what the NATS client is given, and the verification runs
// inside the handshake that client performs.
//
// It stays here because this is where the pin is proven: the tests beside it run a real TLS server
// and assert that a wrong certificate is refused before a byte of application data is sent. What it
// must not become again is something a caller uses to reach the bus.
func dialPinned(address, pin string, timeout time.Duration) (*tls.Conn, error) {
config, err := PinnedConfig(pin) config, err := PinnedConfig(pin)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -86,7 +97,7 @@ func Dial(address, pin string, timeout time.Duration) (*tls.Conn, error) {
if errors.Is(err, ErrWrongCertificate) { if errors.Is(err, ErrWrongCertificate) {
return nil, err return nil, err
} }
return nil, fmt.Errorf("cannot reach the broker at %s: %w", address, err) return nil, fmt.Errorf("cannot reach %s: %w", address, err)
} }
return conn, nil return conn, nil
} }
+4 -4
View File
@@ -63,7 +63,7 @@ func server(t *testing.T) (address string, fingerprint string) {
func TestTheRightBrokerIsAccepted(t *testing.T) { func TestTheRightBrokerIsAccepted(t *testing.T) {
address, pin := server(t) address, pin := server(t)
conn, err := Dial(address, pin, 5*time.Second) conn, err := dialPinned(address, pin, 5*time.Second)
if err != nil { if err != nil {
t.Fatalf("the broker its token describes was refused: %v", err) t.Fatalf("the broker its token describes was refused: %v", err)
} }
@@ -76,7 +76,7 @@ func TestADifferentBrokerIsRefused(t *testing.T) {
address, _ := server(t) address, _ := server(t)
_, other := server(t) _, other := server(t)
_, err := Dial(address, other, 5*time.Second) _, err := dialPinned(address, other, 5*time.Second)
if err == nil { if err == nil {
t.Fatal("a broker presenting a different certificate was accepted") t.Fatal("a broker presenting a different certificate was accepted")
} }
@@ -130,7 +130,7 @@ func TestNothingIsSentToTheWrongBroker(t *testing.T) {
// A pin for a certificate this server does not have. // A pin for a certificate this server does not have.
_, elsewhere := server(t) _, elsewhere := server(t)
if _, err := Dial(listener.Addr().String(), elsewhere, 5*time.Second); err == nil { if _, err := dialPinned(listener.Addr().String(), elsewhere, 5*time.Second); err == nil {
t.Fatal("the impostor was accepted") t.Fatal("the impostor was accepted")
} }
if n := <-received; n > 0 { if n := <-received; n > 0 {
@@ -160,7 +160,7 @@ func TestAnUnreachableBrokerIsAnOrdinaryFailure(t *testing.T) {
address := listener.Addr().String() address := listener.Addr().String()
listener.Close() listener.Close()
_, err = Dial(address, pin, 2*time.Second) _, err = dialPinned(address, pin, 2*time.Second)
if err == nil { if err == nil {
t.Fatal("dialling a closed port succeeded") t.Fatal("dialling a closed port succeeded")
} }