Files
mesh-controller/internal/link/protocol.go
T
jschoubben 646609c1b2 The mesh certifies names inside it
08-connectivity keeps two authorities apart on purpose: a public one for
names the outside world reaches, and the mesh's own for names only the
mesh knows. Nothing implemented the second, so anything between machines
was plaintext or trust-on-first-use — which the design refuses everywhere
else.

A node now generates a fourth key at enrolment and reports the public
half. A fourth, because a key used for two purposes is one rotation away
from breaking the other: the identity key signs messages to the mesh and
would do for TLS, and reusing it would mean rotating a node's identity
every time its certificate is replaced.

**Nothing secret travels and nothing is sealed.** A certificate authority
says "this name belongs to the holder of this key", so the mesh signs a
public half it cannot use, and the certificate it issues is public. A
module asks for one and is given the certificate and, if it wants,
the mesh's own — the private key is a path to a file the machine already
has, the same arrangement the private network's key uses.

Asserted by verifying rather than inspecting, because a certificate that
parses and does not chain fails at the moment something connects:

- what the mesh issues verifies against the mesh, for the name asked for
- the name is in the subject alternative names, since a certificate
  carrying it only in the common name is refused by every modern client
- it certifies the key the node generated and no other
- another mesh's certificate does not verify, which is the whole point of
  two authorities being separate
- the authority cannot sign another authority — one that could is one
  that can be delegated without anybody deciding to
- two control planes starting together agree on one authority, or a mesh
  has certificates half its machines refuse

Certificates last ten years, which is a choice: a short life needs
something to renew it, and a renewal that fails silently is a mesh that
stops trusting itself on a date nobody wrote down. What makes one
replaceable is that the mesh reissues on demand, not that it expires.
2026-08-31 00:09:13 +02:00

118 lines
5.2 KiB
Go

// Package link is the control plane's side of the connection nodes hold open.
//
// novox/hq ADR 0002: nodes communicate over a message broker, not over HTTP. One exchange, and
// the control plane is the single consumer behind it — ADR 0006 makes that a property worth
// having rather than an accident, because two consumers sharing a queue silently split the
// traffic between them, each receiving half of what it expects. That has happened here before.
package link
// Exchange is where nodes publish everything they have to say.
const Exchange = "mesh"
// ControlQueue is what the control plane consumes. One queue, one consumer.
const ControlQueue = "control"
// Routing keys. A node may publish these; it may not publish anything else, because its broker
// account is scoped to this exchange and its own queue.
const (
KeyEnrol = "enrol"
KeyReport = "report"
KeyAlive = "alive"
)
// QueueFor is the queue a node consumes from — the only one it may read.
func QueueFor(node string) string { return "node." + node }
// EnrolRequest is what a joining node says.
//
// It arrives on a connection the broker has already authenticated, because the account was
// created when the token was issued and the token's secret is its password. So this message is
// not how a node gets in — it is what it says once it is in.
type EnrolRequest struct {
// Node is what this machine believes it is called. Checked against the token, never trusted.
Node string `json:"node"`
// Secret is the one-time right to join. The account password and this are the same string,
// which is deliberate: the broker proves somebody holds the token, and this proves the same
// thing to the control plane without the control plane having to ask the broker who connected.
Secret string `json:"secret"`
// PublicKey is what the mesh will believe from now on. The node generated it; the private
// half has never left that machine (novox/hq ADR 0004).
PublicKey []byte `json:"public_key"`
// OverlayKey is the public half of this node's key on the private network — a different key
// from PublicKey, and the mesh only ever sees this half.
OverlayKey string `json:"overlay_key,omitempty"`
// SealingKey is the public half of the key this node's secrets are sealed to. A third key,
// and the reasoning is the same one twice over: the mesh must be able to send this node
// something nothing else can read, and it must never be able to read it either.
SealingKey string `json:"sealing_key,omitempty"`
// ServingKey is the public half of the key this node serves TLS with on its internal name.
// The mesh signs a certificate binding it; the private half never leaves the machine, so
// there is nothing to seal and a copy of what the mesh holds certifies nothing new.
ServingKey string `json:"serving_key,omitempty"`
// Profile is what this machine can be asked to do. The control plane cannot decide what a
// node should run without it, so it arrives with enrolment rather than being asked for after.
Profile map[string]any `json:"profile,omitempty"`
}
// Signed is a declaration and the signature over it.
//
// The signature is over Declaration exactly as it will arrive, bytes unchanged — a node verifies
// what it received rather than what it re-encoded, because any difference in key order or spacing
// would break a signature over the same meaning.
type Signed struct {
Declaration []byte `json:"declaration"`
Signature []byte `json:"signature"`
}
// Alive is a node saying nothing except that it is there.
//
// How long a node has been out of touch is a fact only the mesh can hold — nobody else is
// watching — and without it a node running last month's assignments looks exactly like one that
// is current.
type Alive struct {
Node string `json:"node"`
}
// Report is what a node states after applying. It states; the owning context writes.
type Report struct {
Node string `json:"node"`
Applied []string `json:"applied,omitempty"`
Failed map[string]string `json:"failed,omitempty"`
Refused string `json:"refused,omitempty"`
}
// EnrolReply is what the mesh says back.
type EnrolReply struct {
// Accepted says whether the node is now known.
Accepted bool `json:"accepted"`
// Node is the name the mesh has for this machine, which settles any disagreement: the token
// was issued for a node record, and that record's name wins over what the machine called
// itself.
Node string `json:"node,omitempty"`
// Queue is where this node listens from now on.
Queue string `json:"queue,omitempty"`
// Password is this node's own broker account from now on, replacing the token's secret.
// A credential that lives for as long as the node should not be the same string as one that
// was meant to be used once.
Password string `json:"password,omitempty"`
// Fingerprint and Signer are what the node keeps so it can reconnect and keep verifying
// without a person and a new token.
Fingerprint string `json:"fingerprint,omitempty"`
Signer []byte `json:"signer,omitempty"`
Broker string `json:"broker,omitempty"`
// Refusal says why not, in words for a person. Deliberately the same for every reason a
// token can fail — unknown, spent, expired — so that guessing learns nothing.
Refusal string `json:"refusal,omitempty"`
}