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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user