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:
@@ -224,6 +224,9 @@ func run(ctx context.Context, command string, opts options) error {
|
||||
case "enrol", "enroll":
|
||||
return enrol(ctx, opts)
|
||||
|
||||
case "run":
|
||||
return runLink(ctx, opts)
|
||||
|
||||
case "version":
|
||||
fmt.Println(version)
|
||||
return nil
|
||||
@@ -455,6 +458,20 @@ func enrol(ctx context.Context, opts options) error {
|
||||
// The mesh's name for this node wins over what the machine called itself: the token was
|
||||
// issued for a node record, and that record is what the identity binds to.
|
||||
mine.Node = reply.Node
|
||||
mine.Membership = identity.Membership{
|
||||
Broker: firstNonEmpty(reply.Broker, token.Broker),
|
||||
Fingerprint: firstNonEmpty(reply.Fingerprint, token.Fingerprint),
|
||||
Signer: firstNonEmpty2(reply.Signer, token.Signer),
|
||||
Password: reply.Password,
|
||||
}
|
||||
if mine.Membership.Password == "" {
|
||||
// The mesh did not replace the token's secret, so it is still this node's broker
|
||||
// password. Said rather than silently kept: a one-time secret living on as a credential
|
||||
// is worth knowing about.
|
||||
mine.Membership.Password = token.Secret
|
||||
fmt.Println("\nnote: the mesh issued no separate broker password, so the token's secret " +
|
||||
"remains this node's credential")
|
||||
}
|
||||
|
||||
// Saved only now, and only once the mesh has said it knows this node. A node holding an
|
||||
// identity the mesh has never recorded would believe it had joined and be believed by
|
||||
@@ -465,8 +482,108 @@ func enrol(ctx context.Context, opts options) error {
|
||||
"That token is spent, so getting back needs a new one", reply.Node, err)
|
||||
}
|
||||
|
||||
if !mine.Membership.Joined() {
|
||||
return fmt.Errorf(
|
||||
"the mesh accepted this node as %q but did not say how to reach it again, so this "+
|
||||
"identity could not be used after a restart. Nothing was saved", reply.Node)
|
||||
}
|
||||
|
||||
fmt.Printf("\nenrolled as %s\n", reply.Node)
|
||||
fmt.Printf(" identity %s\n", identityPath)
|
||||
fmt.Printf(" queue %s\n", reply.Queue)
|
||||
return nil
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, v := range values {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstNonEmpty2(values ...[]byte) []byte {
|
||||
for _, v := range values {
|
||||
if len(v) > 0 {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runLink holds this node's link to the mesh open, applying what arrives.
|
||||
//
|
||||
// One outbound connection and nothing listening. While it is up this node is enrolled; while it
|
||||
// is down it is disconnected, which is an ordinary situation rather than a failure — the machine
|
||||
// keeps running whatever it was last told, from its own store.
|
||||
func runLink(ctx context.Context, opts options) error {
|
||||
mine, err := identity.Load(identity.Path(opts.state))
|
||||
if errors.Is(err, identity.ErrNoIdentity) {
|
||||
return errors.New("this machine has not joined a mesh. Enrol it first: " +
|
||||
"mesh-host enrol --token <token> --name <name>")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("node %s, linking to %s\n", mine.Node, mine.Membership.Broker)
|
||||
|
||||
apply := func(ctx context.Context, raw []byte) link.Report {
|
||||
return applyDeclared(ctx, opts, raw)
|
||||
}
|
||||
return link.Run(ctx, link.Membership{
|
||||
Node: mine.Node,
|
||||
Broker: mine.Membership.Broker,
|
||||
Fingerprint: mine.Membership.Fingerprint,
|
||||
Password: mine.Membership.Password,
|
||||
Signer: mine.Membership.Signer,
|
||||
}, apply, opts.timeout)
|
||||
}
|
||||
|
||||
// applyDeclared applies a declaration that has already been proved to come from the mesh.
|
||||
//
|
||||
// Signature checking happens before this is called, in the link. By the time anything here runs,
|
||||
// the question "is this from the mesh I joined" is settled — which is why this can treat the
|
||||
// bytes as instructions.
|
||||
func applyDeclared(ctx context.Context, opts options, raw []byte) link.Report {
|
||||
declared, err := declaration.Parse(raw)
|
||||
if err != nil {
|
||||
return link.Report{Refused: err.Error()}
|
||||
}
|
||||
|
||||
built, err := system.For(builtFor)
|
||||
if err != nil {
|
||||
return link.Report{Refused: err.Error()}
|
||||
}
|
||||
if err := system.Check(built, declared); err != nil {
|
||||
return link.Report{Refused: err.Error()}
|
||||
}
|
||||
|
||||
known, err := store.Load(opts.state)
|
||||
if err != nil {
|
||||
return link.Report{Refused: err.Error()}
|
||||
}
|
||||
|
||||
if err := built.Confirm(ctx, apply.ExecRunner); err != nil {
|
||||
return link.Report{Refused: err.Error()}
|
||||
}
|
||||
|
||||
outcome, updated, applyErr := apply.Apply(ctx, built, declared, known, apply.ExecRunner, nil)
|
||||
|
||||
// Saved whichever way it went. Recording only on success would lose the footprint of a
|
||||
// failed apply, and that footprint is on the machine either way.
|
||||
if saveErr := store.Save(opts.state, updated); saveErr != nil {
|
||||
return link.Report{Refused: "applied, and the node's state could not be saved: " +
|
||||
saveErr.Error()}
|
||||
}
|
||||
|
||||
report := link.Report{}
|
||||
for _, change := range outcome.Outcomes {
|
||||
report.Applied = append(report.Applied, change.ID)
|
||||
}
|
||||
if applyErr != nil {
|
||||
report.Failed = map[string]string{"apply": applyErr.Error()}
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user