Files
mesh-host/internal/bootstrap/push_credential.go
T
jschoubben 20614822df F-host 2, ENG-5: the bootstrap's one-off push credential goes to distribution's account, mode 0600, only once the node-engine made that account and its state directory, so the holder's program can read it (review of issue 1966)
The owner is the account the manifest names for the directory that holds the file; the bootstrap no longer makes that
directory itself, and a run that reaches it first is refused naming what it waits on.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:24:14 +01:00

115 lines
4.6 KiB
Go

package bootstrap
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"os/user"
"path/filepath"
"strconv"
"syscall"
)
// 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 <token>` 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 = "" }
// lookupAccount gives an account's user id and group id. A variable so a test can name an account this machine has.
var lookupAccount = func(name string) (uid, gid int, err error) {
u, err := user.Lookup(name)
if err != nil {
return 0, 0, err
}
if uid, err = strconv.Atoi(u.Uid); err != nil {
return 0, 0, err
}
gid, err = strconv.Atoi(u.Gid)
return uid, gid, err
}
// WritePushCredential writes the one-off credential for the store's holder, read from its manifest in catalogue: the file
// goes into the holder's directory resource that holds file's path, given to the account that directory names as owner,
// and only once the node-engine has made that account and that directory, owned by it. Until then the holder's program,
// which runs as that account, could not read it, so it is refused rather than written as root's.
func WritePushCredential(catalogue, file string) (*PushCredential, error) {
manifest, err := readManifest(catalogue, RegistryModule)
if err != nil {
return nil, err
}
var m struct {
Resources []struct {
Type string `json:"type"`
Name string `json:"name"`
Path string `json:"path"`
Owner string `json:"owner"`
} `json:"resources"`
}
if err := json.Unmarshal(manifest, &m); err != nil {
return nil, fmt.Errorf("the %s manifest is not readable as JSON: %w", RegistryModule, err)
}
dir, owner, account := filepath.Dir(file), "", false
for _, r := range m.Resources {
if r.Type == "directory" && filepath.Clean(r.Path) == dir {
owner = r.Owner
}
}
for _, r := range m.Resources {
account = account || (owner != "" && r.Type == "user" && r.Name == owner)
}
if !account {
return nil, fmt.Errorf("the push credential %s has no account to go to: the %s manifest declares no directory %s "+
"owned by an account it declares, so its program could not read the file", file, RegistryModule, dir)
}
uid, gid, err := lookupAccount(owner)
if err != nil {
return nil, fmt.Errorf("the push credential waits on the account %s, which the node-engine has not made: %w", owner, err)
}
st, err := os.Stat(dir)
if err != nil {
return nil, fmt.Errorf("the push credential waits on %s's state directory %s, which the node-engine has not made: %w", owner, dir, err)
}
if sys, ok := st.Sys().(*syscall.Stat_t); !ok || int(sys.Uid) != uid {
return nil, fmt.Errorf("the push credential waits on %s owning its state directory %s, which the node-engine has not yet given it", owner, dir)
}
return MintPushCredential(file, uid, gid)
}
// MintPushCredential makes a token and writes it to file, mode 0600, owned by the user id and group id given, into a
// directory that must be there. A file that is there is replaced: a credential left by an earlier run is never reused.
func MintPushCredential(file string, uid, gid 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)
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 err := os.Chown(tmp, uid, gid); 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
}