A build machine is told what to check the broker against

The credential was a URL and nothing else, so the builder verified the broker
the ordinary way — against public roots. A mesh's broker presents a certificate
of the mesh's own, which is in no trust store anywhere, so the connection could
only ever succeed against a broker somebody else vouches for. It failed at TLS
with an error about an unknown authority rather than about a missing pin, and
the container sat there running: up, credential on disk, connected to nothing.

So the sealed credential now carries the URL and the broker's fingerprint —
the same two facts a node's token carries, for the same reason, delivered out
of band relative to the thing being trusted. The builder pins it: the standard
chain check is replaced rather than removed, and what replaces it is stricter,
accepting one certificate instead of every certificate a public authority
would sign.

A file holding only a URL still works, for a builder somebody runs by hand
against a broker with an ordinary certificate.
This commit is contained in:
2026-08-31 00:55:33 +02:00
parent 1eb1b69cae
commit a5b11fd6b2
3 changed files with 108 additions and 14 deletions
+61 -10
View File
@@ -17,7 +17,12 @@ package main
import (
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"os/signal"
@@ -65,7 +70,7 @@ func run() error {
}
}
amqpURL, err := brokerFrom()
credential, err := brokerFrom()
if err != nil {
return err
}
@@ -85,7 +90,7 @@ func run() error {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
conn, err := amqp.Dial(amqpURL)
conn, err := dial(credential)
if err != nil {
// Not quoted back: the URL carries this builder's broker password.
return fmt.Errorf("cannot reach the broker: %w", err)
@@ -270,25 +275,71 @@ func whereToPublish() (string, error) {
// the one copy that matters passing through a terminal and a process listing.
//
// The variable remains for a builder run by a person.
func brokerFrom() (string, error) {
func brokerFrom() (Credential, error) {
if path := strings.TrimSpace(os.Getenv("MESH_BROKER_FILE")); path != "" {
raw, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("cannot read this builder's credential: %w", err)
return Credential{}, fmt.Errorf("cannot read this builder's credential: %w", err)
}
url := strings.TrimSpace(string(raw))
if url == "" {
said := strings.TrimSpace(string(raw))
if said == "" {
// An empty credential file is a machine that will connect as nobody and be refused,
// with the reason three layers away.
return "", fmt.Errorf("%s is empty, so this builder has no credential", path)
return Credential{}, fmt.Errorf("%s is empty, so this builder has no credential", path)
}
return url, nil
var held Credential
if err := json.Unmarshal([]byte(said), &held); err == nil && held.URL != "" {
return held, nil
}
// A file holding only a URL, which is what a person writing one by hand produces. The
// broker is then verified against whatever this machine already trusts.
return Credential{URL: said}, nil
}
url := strings.TrimSpace(os.Getenv("MESH_BROKER_AMQP"))
if url == "" {
return "", fmt.Errorf(
return Credential{}, fmt.Errorf(
"neither MESH_BROKER_FILE nor MESH_BROKER_AMQP: a builder with no broker has " +
"nothing to build")
}
return url, nil
return Credential{URL: url}, nil
}
// Credential is what a build machine is given so it can reach the broker.
//
// Two things, because reaching a broker over TLS needs both: who to connect as, and what to check
// the certificate against. A mesh's broker presents a certificate of the mesh's own, which is in
// no public trust store, so a URL alone can only connect to a broker somebody else vouches for.
//
// **The same shape a node gets, for the same reason** (novox/hq ADR 0004): the fingerprint travels
// out of band — here, sealed with the credential — and the endpoint is verified once at connect.
type Credential struct {
URL string `json:"url"`
// Fingerprint is SHA-256 over the broker certificate's DER bytes, or empty to verify the
// ordinary way.
Fingerprint string `json:"fingerprint,omitempty"`
}
// dial opens the connection, pinning the broker's certificate when there is one to pin.
func dial(held Credential) (*amqp.Connection, error) {
if held.Fingerprint == "" {
return amqp.Dial(held.URL)
}
// InsecureSkipVerify with a VerifyPeerCertificate is **pinning, not skipping**: the standard
// chain check is replaced, not removed, and what replaces it is stricter — one certificate is
// accepted rather than every certificate a public authority would sign.
return amqp.DialTLS(held.URL, &tls.Config{
InsecureSkipVerify: true,
VerifyPeerCertificate: func(raw [][]byte, _ [][]*x509.Certificate) error {
if len(raw) == 0 {
return errors.New("the broker presented no certificate")
}
got := sha256.Sum256(raw[0])
if hex.EncodeToString(got[:]) != held.Fingerprint {
return fmt.Errorf(
"this is not the broker this builder was told about: it presented a "+
"certificate with fingerprint %s", hex.EncodeToString(got[:]))
}
return nil
},
})
}