Keep the forge credential out of what a check's container sees, and redact what a check publishes (issue 462)
The toolchain container mounts the workspace as HOME, so the credential kept there, and a clone's recorded userinfo, were readable by any pull request; its output is kept on the bus for days. The credential now lives in a private directory outside the workspace only while cloning, clones record their URL without userinfo, and every line said to the build's log and the verdict passes a redactor copied from the journal tool (sharing it: issue 471).
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -159,13 +160,11 @@ func build(ctx context.Context, run Runner, publish Publisher,
|
||||
}
|
||||
// The credential is a file git reads, never an argument: a URL carrying a password in argv
|
||||
// would be readable by anything that can list processes for as long as a clone runs.
|
||||
credentials := ""
|
||||
if forge.URL != "" {
|
||||
credentials = filepath.Join(workspace, "git-credentials")
|
||||
if err := os.WriteFile(credentials, []byte(forge.URL+"\n"), 0o600); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
credentials, forget, err := storeCredential(workspace, forge)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
defer forget()
|
||||
tree := filepath.Join(workspace, "source")
|
||||
if err := os.RemoveAll(tree); err != nil {
|
||||
return Result{}, err
|
||||
@@ -467,6 +466,55 @@ func contextURL(from catalogue.ArtifactContext, seats map[string]string) (string
|
||||
return strings.TrimRight(base, "/") + "/" + strings.TrimSuffix(strings.Trim(from.Repository, "/"), ".git") + ".git", nil
|
||||
}
|
||||
|
||||
// storeCredential writes the forge credential where git's credential store reads it, and the function that
|
||||
// removes it again; "" and nothing to remove when the builder holds none.
|
||||
//
|
||||
// **Never inside the workspace** (novox/hq issue 462): a merge check's toolchain container mounts the
|
||||
// workspace as its HOME, so a credential kept there — even one a build left behind — is readable by any pull
|
||||
// request's merge-check.sh, and what it prints is kept on the bus. So it lives in a directory of its own
|
||||
// outside the workspace, made private, and a credential an older builder left in the workspace is removed.
|
||||
func storeCredential(workspace string, forge GitCredential) (string, func(), error) {
|
||||
if err := os.Remove(filepath.Join(workspace, "git-credentials")); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return "", nil, fmt.Errorf("a credential left in the workspace cannot be removed: %w", err)
|
||||
}
|
||||
if forge.URL == "" {
|
||||
return "", func() {}, nil
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "mesh-forge-credential-")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
forget := func() { os.RemoveAll(dir) }
|
||||
if withinDir(workspace, dir) {
|
||||
forget()
|
||||
return "", nil, fmt.Errorf("the temporary directory %s is inside the workspace %s, which a check's "+
|
||||
"container mounts: the forge credential is not written where it could read it", dir, workspace)
|
||||
}
|
||||
path := filepath.Join(dir, "git-credentials")
|
||||
if err := os.WriteFile(path, []byte(forge.URL+"\n"), 0o600); err != nil {
|
||||
forget()
|
||||
return "", nil, err
|
||||
}
|
||||
return path, forget, nil
|
||||
}
|
||||
|
||||
// withinDir is whether path is dir or under it, both made absolute and resolved.
|
||||
func withinDir(dir, path string) bool {
|
||||
d, err1 := filepath.Abs(dir)
|
||||
p, err2 := filepath.Abs(path)
|
||||
if err1 != nil || err2 != nil {
|
||||
return true
|
||||
}
|
||||
if r, err := filepath.EvalSymlinks(d); err == nil {
|
||||
d = r
|
||||
}
|
||||
if r, err := filepath.EvalSymlinks(p); err == nil {
|
||||
p = r
|
||||
}
|
||||
rel, err := filepath.Rel(d, p)
|
||||
return err != nil || rel == "." || filepath.IsLocal(rel)
|
||||
}
|
||||
|
||||
// cloneWith is a git invocation that may offer a stored credential.
|
||||
//
|
||||
// The first `-c credential.helper=` clears every helper the environment might carry, so exactly
|
||||
|
||||
Reference in New Issue
Block a user