A node holds its link open, and applies what the mesh signs

The loop the whole thing exists for: told, apply, report.

`run` holds one outbound connection open and consumes the node's own queue.
Every declaration is verified against the control plane's signing key before a
byte of it is read as an instruction -- not once at connect, every time. The
transport being pinned is a different question from the instruction being
genuine, and pinning only the first would make the second transitive: a
compromised broker could forge declarations, and this host applies whatever the
link delivers.

Malformed and forged are reported differently, because ADR 0004 requires a host
to tell "this is not from the mesh I joined" from "this is broken". One means
somebody is trying and the other means something needs fixing.

A node now keeps what it needs to come back on its own: the broker's address
and fingerprint, the signing key it believes, and its own broker password --
which the mesh issues at enrolment to replace the token's secret, so the
one-time thing stays one-time and the credential it holds for years is not the
one that was pasted into a terminal.

Verified in the lab end to end. The node enrolled, held its link, received a
signed declaration and applied it -- the file is on the machine with the right
contents, and the host's own record lists both resources.

That run also found issue 010, which is recorded in novox/hq: the declaration
removed every container on the machine, including the control plane that sent
it. Correct reconciliation, shared store, and the first thing that happens.
This commit is contained in:
2026-08-29 16:23:27 +02:00
parent a4445f5c0a
commit a488c76b5e
7 changed files with 497 additions and 6 deletions
+47 -3
View File
@@ -28,14 +28,51 @@ func Path(statePath string) string {
return filepath.Join(filepath.Dir(statePath), FileName)
}
// Identity is this node's own keypair, and the name the mesh knows it by.
// Identity is this node's own keypair, the name the mesh knows it by, and what it needs to get
// back to that mesh without a person.
//
// The keypair is the node's own and was never anybody else's. Everything under Membership was
// learned at enrolment and is the mesh's answer rather than this machine's — kept here because a
// node that could not reconnect after a restart without a new token would make disconnection a
// crisis instead of an ordinary situation (novox/hq ADR 0004).
type Identity struct {
// Node is the name in the mesh's records. Learned at enrolment, from the mesh — it is the one
// thing here the node does not decide for itself.
// Node is the name in the mesh's records. Learned at enrolment — the one thing here the node
// does not decide for itself.
Node string `json:"node"`
Public []byte `json:"public"`
Private []byte `json:"private"`
Membership Membership `json:"membership"`
}
// Membership is how this node reaches the mesh it belongs to, and who it believes.
type Membership struct {
// Broker is an address, not a name: there is no resolution before the link.
Broker string `json:"broker"`
// Fingerprint is checked before anything is sent, on every connection and not only the first.
Fingerprint string `json:"fingerprint"`
// Signer is the control plane's public signing key. Kept because **each declaration is
// verified by its signature, every time** (novox/hq ADR 0004) — a node that only pinned the
// broker would make the control plane's authority transitive, and a compromised broker could
// then forge declarations, which is the whole machine.
Signer []byte `json:"signer"`
// Password is this node's own broker account, issued at enrolment and belonging to it alone.
// Not the token's secret: that is spent, and a credential that lives for ever should not be
// the same string as one that was meant to be used once.
Password string `json:"password"`
}
// Queue is where this node listens. Its account may read this and nothing else.
func (i Identity) Queue() string { return "node." + i.Node }
// Joined reports whether this identity can reach its mesh unaided.
func (m Membership) Joined() bool {
return m.Broker != "" && m.Fingerprint != "" && len(m.Signer) == ed25519.PublicKeySize &&
m.Password != ""
}
// ErrNoIdentity means this machine has not enrolled.
@@ -92,6 +129,13 @@ func Load(path string) (Identity, error) {
if strings.TrimSpace(i.Node) == "" {
return Identity{}, fmt.Errorf("the identity at %s names no node", path)
}
// Checked here rather than at the moment it is used, which would be while trying to
// reconnect on a machine nobody is watching.
if !i.Membership.Joined() {
return Identity{}, fmt.Errorf(
"the identity at %s does not say how to reach its mesh, so this node cannot "+
"reconnect. It needs a new token", path)
}
return i, nil
}