From 18c1735f7c624365cb3203d4cb5a661f0e9381f3 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:07:34 +0100 Subject: [PATCH] ENG-5: the bootstrap makes a one-off push credential for the holders' first images, writes it for the store's holder and forgets it when its run ends (design 51, issue 1966) Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- cmd/mesh-bootstrap/main.go | 4 +- internal/bootstrap/bootstrap.go | 27 +++++++++++-- internal/bootstrap/push_credential.go | 55 +++++++++++++++++++++++++++ internal/bootstrap/trust_test.go | 17 +++++++++ 4 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 internal/bootstrap/push_credential.go diff --git a/cmd/mesh-bootstrap/main.go b/cmd/mesh-bootstrap/main.go index 369a744..46ccad0 100644 --- a/cmd/mesh-bootstrap/main.go +++ b/cmd/mesh-bootstrap/main.go @@ -188,7 +188,9 @@ func parseArgs(args []string) (string, bootstrap.Options, bool, error) { Out: defaultOut, State: store.DefaultPath, Registry: defaultRegistry, - Host: defaultHost, + // Where the store's holder reads the one-off credential the holders' first images are pushed with (design 51, ENG-5). + PushCredentialFile: "/var/lib/mesh/registry/bootstrap-push", + Host: defaultHost, // The machine's own name, because that is what a person already calls it and an installer // inventing a different one would leave the mesh naming a machine nobody recognises. It is // read here rather than inside the bootstrap so that --node overrides a fact rather than a diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 425c9a4..2d4b2ff 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -157,6 +157,12 @@ type Options struct { // an address the others can reach. Registry string + // PushCredentialFile is where the bootstrap writes its one-off push credential for the store's holder to find, the + // holder's state file `bootstrap-push` on the node that holds it (design 51 *The move*, issue 1966 ENG-5); empty writes + // none. PushCredentialOwner is the user id and group id the file is given when the holder's account is known. + PushCredentialFile string + PushCredentialOwner *[2]int + // Host is the `mesh-host` binary on this machine — the program that enrols and then holds the // machine to what the mesh says. The installer runs it; it does not contain it. Host string @@ -296,10 +302,12 @@ type Result struct { Agent string `json:"host-agent,omitempty"` // Registry is the mesh's own artifact store, once it answers. - Registry string `json:"registry,omitempty"` - RegistryReplied int `json:"registry-replied,omitempty"` - RegistryKnown bool `json:"registry-already-registered,omitempty"` - RegistryRunning string `json:"registry-container,omitempty"` + Registry string `json:"registry,omitempty"` + // PushCredentialFile is where this run left its one-off push credential (never the credential itself). + PushCredentialFile string `json:"push-credential-file,omitempty"` + RegistryReplied int `json:"registry-replied,omitempty"` + RegistryKnown bool `json:"registry-already-registered,omitempty"` + RegistryRunning string `json:"registry-container,omitempty"` // RegistryTrusted are the names the runtime was told to trust the registry by, before the push. RegistryTrusted []string `json:"registry-trusted,omitempty"` PublishedAs string `json:"control-plane-image,omitempty"` @@ -815,6 +823,17 @@ func RegistryAndPublish(ctx context.Context, o Options, d Deps, sys system.Syste if err != nil { return Published{}, failed(StepRegistry, err) } + // The one-off credential the holders' first images are pushed to the store's front with, written for its holder to find + // and forgotten when this run ends; the holder discards the file once it answers its first publish (ENG-5). + if o.PushCredentialFile != "" { + cred, err := MintPushCredential(o.PushCredentialFile, o.PushCredentialOwner) + if err != nil { + return Published{}, failed(StepRegistry, err) + } + defer cred.Forget() + result.PushCredentialFile = cred.File + say(" a one-off credential for the holders' first images is at " + cred.File + "; the store's holder discards it once it serves publish") + } // ---- 8. publish ----------------------------------------------------------------------- say("publish — the control plane's image gets its first manifest digest") diff --git a/internal/bootstrap/push_credential.go b/internal/bootstrap/push_credential.go new file mode 100644 index 0000000..5331609 --- /dev/null +++ b/internal/bootstrap/push_credential.go @@ -0,0 +1,55 @@ +package bootstrap + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "os" + "path/filepath" +) + +// PushCredential is the bootstrap's one-off credential for the holders' first images (design 51 *The move*; issue 1966 ENG-5): +// a bearer token of 32 random bytes, hex, on one line, made for one fresh-mesh run. No vault and no bus holds it: this run +// holds it and writes it once, to the file the store's holder reads on each write (`Authorization: Bearer ` on an OCI +// write to the store's port, from any node); the holder removes the file when it answers its first `publish`, and this run +// forgets the token when it ends. On the holder's own node the bootstrap may push to the registry container's loopback with +// no credential at all. +type PushCredential struct { + // File is where the token was written. + File string + token string +} + +// Token is the bearer token, for a push through the store's front from another node. +func (c *PushCredential) Token() string { return c.token } + +// Forget drops the token from this run's memory. The file is the holder's to remove. +func (c *PushCredential) Forget() { c.token = "" } + +// MintPushCredential makes a token and writes it to file, mode 0600, owned by owner (user id, group id) when given; the +// directory is made if it is not there. A file that is there is replaced: a credential left by an earlier run is never reused. +func MintPushCredential(file string, owner *[2]int) (*PushCredential, error) { + raw := make([]byte, 32) + if _, err := rand.Read(raw); err != nil { + return nil, fmt.Errorf("a push credential cannot be made: %w", err) + } + token := hex.EncodeToString(raw) + if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil { + return nil, fmt.Errorf("the push credential's directory %s cannot be made: %w", filepath.Dir(file), err) + } + tmp := file + ".new" + if err := os.WriteFile(tmp, []byte(token+"\n"), 0o600); err != nil { + return nil, fmt.Errorf("the push credential cannot be written to %s: %w", file, err) + } + if owner != nil { + if err := os.Chown(tmp, owner[0], owner[1]); err != nil { + os.Remove(tmp) + return nil, fmt.Errorf("the push credential cannot be given to the store's holder: %w", err) + } + } + if err := os.Rename(tmp, file); err != nil { + os.Remove(tmp) + return nil, fmt.Errorf("the push credential cannot be put at %s: %w", file, err) + } + return &PushCredential{File: file, token: token}, nil +} diff --git a/internal/bootstrap/trust_test.go b/internal/bootstrap/trust_test.go index 5e406b8..4ca6007 100644 --- a/internal/bootstrap/trust_test.go +++ b/internal/bootstrap/trust_test.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "path/filepath" + "regexp" "strings" "testing" "time" @@ -263,6 +264,9 @@ func TestNothingIsPushedToTheRegistryBeforeTheRuntimeTrustsIt(t *testing.T) { return http.StatusNotFound, "", nil }} var result Result + // And the one-off credential for the holders' first images (ENG-5): written once, mode 0600, one line of 64 hex digits, + // named in the result and never put in it. + o.PushCredentialFile = filepath.Join(t.TempDir(), "registry", "bootstrap-push") published, err := RegistryAndPublish(context.Background(), o, d, sys, controlPlane{container: "temp-mesh-controller", run: run, timeout: time.Second}, held, &result, func(string) {}) if err != nil { @@ -287,6 +291,19 @@ func TestNothingIsPushedToTheRegistryBeforeTheRuntimeTrustsIt(t *testing.T) { t.Errorf("the trust was not written (%d) and confirmed (%d) before the first tag or push (%d): %v", reloaded, confirmed, firstPush, runtime.commands) } + if raw, err := os.ReadFile(o.PushCredentialFile); err != nil || !regexp.MustCompile(`^[0-9a-f]{64}\n$`).Match(raw) { + t.Errorf("the push credential is %q (%v)", raw, err) + } else if st, _ := os.Stat(o.PushCredentialFile); st.Mode().Perm() != 0o600 { + t.Errorf("the push credential is mode %v", st.Mode().Perm()) + } + if result.PushCredentialFile != o.PushCredentialFile { + t.Errorf("the result names %q", result.PushCredentialFile) + } + if again, err := MintPushCredential(o.PushCredentialFile, nil); err != nil || again.Token() == "" { + t.Errorf("a credential of an earlier run was not replaced: %v", err) + } else if again.Forget(); again.Token() != "" { + t.Errorf("a forgotten credential still holds its token") + } if fmt.Sprint(result.RegistryTrusted) != "[192.0.2.10:5100 anchor.internal:5100]" { t.Errorf("the result says it trusted %v", result.RegistryTrusted) }