// mesh-provisioner-client: the provisioner's client, holder of the node-provisioner seat on every node (novox/hq // ADR 0349 clauses 3 to 5, design 57 P5, issue 671). A service of its own, outside the tool runner, on a bus account // of its own (`provisioner.`, mesh-controller internal/broker, design 57 P3): the vault hands a secret only to // an account of its own, and the bus grants this one its machine's keys and nothing else. It fills the templates of // each module handed to the provisioner on its node and has the node-engine write them (client.go). package main import ( "context" "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/hex" "encoding/json" "errors" "flag" "fmt" "os" "os/signal" "path/filepath" "syscall" "time" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" ) func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) } // Credential is the client's account as the controller delivers it, its own secret `provisioner-broker` // (mesh-controller cmd/mesh-controller issueProvisionerClient): the bus, its certificate by fingerprint, the // machine, the user and its password. type Credential struct { URL string `json:"url"` Fingerprint string `json:"fingerprint,omitempty"` Node string `json:"node"` User string `json:"user"` Password string `json:"password"` } func readCredential(path string) (Credential, error) { raw, err := os.ReadFile(path) if err != nil { return Credential{}, fmt.Errorf("the client's account cannot be read: %w", err) } var c Credential if err := json.Unmarshal(raw, &c); err != nil { return Credential{}, errors.New("the client's account is not the JSON the mesh delivers (url, fingerprint, " + "node, user, password)") } if c.URL == "" || c.Node == "" || c.User == "" || c.Password == "" { return Credential{}, errors.New("the client's account names no bus, machine, user or password") } if c.User != "provisioner."+c.Node { return Credential{}, fmt.Errorf("the account %s is not machine %s's provisioner client's", c.User, c.Node) } return c, nil } // pinnedTo accepts exactly the certificate with this SHA-256: the mesh's bus presents one of its own, in no trust // store (mesh-controller internal/broker PinnedToFingerprint). func pinnedTo(want string) *tls.Config { return &tls.Config{ InsecureSkipVerify: true, //nolint:gosec // replaced by the pin below, which is stricter MinVersion: tls.VersionTLS12, VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error { if len(rawCerts) == 0 { return errors.New("the bus presented no certificate") } sum := sha256.Sum256(rawCerts[0]) if got := "sha256:" + hex.EncodeToString(sum[:]); got != want { return fmt.Errorf("the bus presented a certificate this mesh does not know (%s)", got) } return nil }, } } func main() { credential := flag.String("credential", os.Getenv("MESH_PROVISIONER_CREDENTIAL"), "the client's account, its own secret provisioner-broker (MESH_PROVISIONER_CREDENTIAL)") socket := flag.String("socket", DefaultSocket, "the node-engine's socket for writing a filled file") stateDir := flag.String("state", os.Getenv("MESH_PROVISIONER_STATE"), "where the client keeps the templates it fetched (MESH_PROVISIONER_STATE)") flag.Parse() if *credential == "" || *stateDir == "" { logf("usage: mesh-provisioner-client -credential -state ") os.Exit(2) } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() if err := serve(ctx, *credential, *socket, *stateDir); err != nil && ctx.Err() == nil { logf("[provisioner-client] %v", err) os.Exit(1) } } // serve connects and runs the client until ctx ends. A run that stops — a watch ended, the bus came back — is run // again from the current values, after a wait that grows to a minute. func serve(ctx context.Context, credential, socket, stateDir string) error { c, err := readCredential(credential) if err != nil { return err } back := make(chan struct{}, 1) opts := []nats.Option{ nats.UserInfo(c.User, c.Password), nats.Name("mesh-provisioner-client " + c.Node), // Answers come to the account's own inbox and no other (novox/hq design 25 §4). nats.CustomInboxPrefix("_INBOX." + c.User), nats.MaxReconnects(-1), nats.ReconnectHandler(func(*nats.Conn) { select { case back <- struct{}{}: default: } }), } if c.Fingerprint != "" { opts = append(opts, nats.Secure(pinnedTo(c.Fingerprint))) } nc, err := nats.Connect(c.URL, opts...) if err != nil { return fmt.Errorf("cannot reach the bus at %s as %s: %w", c.URL, c.User, err) } defer nc.Close() js, err := jetstream.New(nc) if err != nil { return err } // Its own key, for the vault to seal each secret to: made now, kept in memory alone. secrets, err := NewVaultSecrets(nc, c.User) if err != nil { return err } client := &Client{Node: c.Node, JS: js, Conn: nc, Secrets: secrets, Fill: SocketFiller{Socket: socket, Busy: time.Second, Tries: 10}, Templates: &templateStore{dir: filepath.Join(stateDir, "templates"), fetch: HTTPFetch}} wait := time.Second for { run, cancel := context.WithCancel(ctx) go func() { select { case <-back: logf("[provisioner-client] the bus is back; filling from the current values") cancel() case <-run.Done(): } }() started := time.Now() err := client.Run(run) cancel() if ctx.Err() != nil { return nil } if time.Since(started) > time.Minute { wait = time.Second } if err != nil && !errors.Is(err, context.Canceled) { logf("[provisioner-client] stopped (%v); running again in %s", err, wait) } select { case <-ctx.Done(): return nil case <-time.After(wait): } wait = min(2*wait, time.Minute) } }