Files
mesh-controller/internal/broker/broker.go
T
jschoubben b7da02e370 An address is read from the node's settings where it is used, never recorded with a port
Three readers did not follow a moved foundation port (novox/hq 04-ISSUES/102),
and each took the control-node down in its own way: the control plane's own
store and broker connections, sealed at genesis with the port inside; and every
build the mesh ever recorded, kept as `<registry>:<port>/<module>/<artifact>@…`.

The control plane cannot open its own sealed connections to move a port, and it
cannot bind the store as a consumer would — a binding mints a credential. So its
settings get a third twin: `NAME_PORT`, composed into its container from the
node's settings by a placeholder that names a seat, `${seat:mesh-store:5432}`,
and read on top of the sealed value by the store, the broker, the management API
and the bus connection. The answer is empty when the mesh has nothing to add,
so what genesis wrote stands until the node says otherwise.

A build is now recorded by digest and path — `artifact-store://<module>/<artifact>@…`
— and the store's address is composed in where a reference is used: the
declaration, the trust file, the bases a build is handed, a replay to the
catalogue. A reference recorded before this, with an address, is re-routed the
same way when the mesh built it. The trust file and every provider's address
now come from one derivation, with the node's given port over the mesh's
assignment over the manifest's number.

novox/hq 04-ISSUES/102
2026-09-23 23:26:43 +02:00

99 lines
3.7 KiB
Go

// Package broker is what the control plane knows about the broker nodes dial.
//
// Two facts, and a token needs both (novox/hq ADR 0004): where it is, and what certificate to
// expect there. They are the two parts of a token this control plane does not generate itself.
//
// The address is configuration. The fingerprint is **not** — it is derived from the certificate
// the broker is actually serving. Configuring a fingerprint separately would let it drift from
// the certificate it describes, and a drifted pin is worse than none: every node issued a token
// during the drift refuses to connect, and the failure looks like an attack.
package broker
import (
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"github.com/novox/mesh-controller/internal/envfile"
"os"
"strings"
)
// Where the two settings come from.
const (
AddressVar = "MESH_BROKER_ADDRESS"
CertificateVar = "MESH_BROKER_CERTIFICATE"
)
// Broker is what a token needs to say about it.
type Broker struct {
Address string
Fingerprint string
}
// ErrNotConfigured means this control plane has not been told where its broker is.
//
// Not a failure to start. A control plane can hold node records and a signing key without one;
// what it cannot do is issue a token anybody could use, and that is where this surfaces.
var ErrNotConfigured = errors.New("this control plane has not been told about its broker")
// FromEnvironment reads the two settings, if they are there.
//
// The address's port follows MESH_BROKER_ADDRESS_PORT when the node's settings moved the bus
// (novox/hq 04-ISSUES/102): the address genesis wrote is a public name and the port genesis
// chose, and only the port is the node's to move.
func FromEnvironment() (Broker, error) {
address, err := envfile.Placed(AddressVar)
if err != nil {
return Broker{}, err
}
path := strings.TrimSpace(os.Getenv(CertificateVar))
if address == "" && path == "" {
return Broker{}, ErrNotConfigured
}
// One without the other is worse than neither: a token with an address and no fingerprint
// invites a node to connect to something it cannot check.
if address == "" || path == "" {
return Broker{}, fmt.Errorf(
"%s and %s must be set together — an address with nothing to check the certificate "+
"against is a node connecting to whatever answers", AddressVar, CertificateVar)
}
fingerprint, err := FingerprintOf(path)
if err != nil {
return Broker{}, err
}
return Broker{Address: address, Fingerprint: fingerprint}, nil
}
// FingerprintOf reads a PEM certificate and returns what a client pins.
//
// SHA-256 over the DER bytes, which is what a TLS client can compute from the certificate the
// server presents — so the two are comparing the same thing. A digest over the PEM text would
// not be: the same certificate re-wrapped with different line endings would hash differently
// while being the same certificate.
func FingerprintOf(path string) (string, error) {
raw, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("cannot read the broker's certificate at %s: %w", path, err)
}
block, _ := pem.Decode(raw)
if block == nil || block.Type != "CERTIFICATE" {
return "", fmt.Errorf(
"%s does not contain a PEM certificate. If this is a private key, it is the wrong "+
"file — what a node pins is the certificate the broker presents", path)
}
// Parsed rather than hashed straight from the block, so a malformed certificate is caught
// here rather than becoming a pin that matches nothing.
if _, err := x509.ParseCertificate(block.Bytes); err != nil {
return "", fmt.Errorf("the certificate at %s could not be parsed: %w", path, err)
}
sum := sha256.Sum256(block.Bytes)
return "sha256:" + hex.EncodeToString(sum[:]), nil
}