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 ` 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 }