The broker settings take a _FILE twin like the store connections; the catalogue engine refuses a secret placeholder in a container's env and a secret-carrying env-file unless the container says why with secrets-in-environment, which stays in the catalogue and never reaches the machine.
95 lines
3.5 KiB
Go
95 lines
3.5 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.
|
|
func FromEnvironment() (Broker, error) {
|
|
address, err := envfile.Value(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
|
|
}
|