From d6ce04640fb9bee13233eebb29d8f88ebaedd67c Mon Sep 17 00:00:00 2001 From: Laura Brehm Date: Tue, 15 Oct 2024 11:38:46 +0100 Subject: [PATCH] Support plaintext credentials as multi-call binary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Docker CLI supports storing/managing credentials without a credential-helper, in which case credentials are fetched from/saved to the CLI config file (`~/.docker/config.json`). This is all managed entirely by the CLI itself, without resort to a separate binary. There are a few issues with this approach – for one, saving the credentials together with all the configurations make it impossible to share one without the other, so one can't for example bind mount the config file into a container without also including all configured credentials. Another issue is that this has made it so that any other clients accessing registry credentials (such as https://github.com/google/go-containerregistry) all have to both: - read/parse the CLI `config.json`, to check for credentials there, which also means they're dependent on this type and might break if the type changes/we need to be careful not to break other codebases parsing this file, and can't change the location where plaintext credentials are stored. - support the credential helper protocol, so that they can access credentials when users do have configured credential helpers. This means that if we want to do something like support oauth credentials by having credential-helpers refresh oauth tokens before returning them, we have to both implement that in each credential-helper and in the CLI itself, and any client directly reading `config.json` will also need to implement this logic. This commit turns the Docker CLI binary into a multicall binary, acting as a standalone credentials helper when invoked as `docker-credential-file`, while still storing/fetching credentials from the configuration file (`~/.docker/config.json`), and without any further changes. This represents a first step into aligning the "no credhelper"/plaintext flow with the "credhelper" flow, meaning that instead of this being an exception where credentials must be read directly from the config file, credentials can now be accessed in the exact same way as with other credential helpers – by invoking `docker-credential-[credhelper name]`, such as `docker-credential-pass`, `docker-credential-osxkeychain` or `docker-credential-wincred`. This would also make it possible for any other clients accessing credentials to untangle themselves from things like the location of the credentials, parsing credentials from `config.json`, etc. and instead simply support the credential-helper protocol, and call the `docker-credential-file` binary as they do others. Signed-off-by: Laura Brehm --- cmd/docker/docker.go | 6 ++ cmd/docker/file_helper.go | 69 +++++++++++++++++++ .../docker/docker/pkg/reexec/command_linux.go | 26 +++++++ .../docker/docker/pkg/reexec/command_other.go | 19 +++++ .../docker/pkg/reexec/command_unsupported.go | 12 ++++ .../docker/docker/pkg/reexec/reexec.go | 64 +++++++++++++++++ vendor/modules.txt | 1 + 7 files changed, 197 insertions(+) create mode 100644 cmd/docker/file_helper.go create mode 100644 vendor/github.com/docker/docker/pkg/reexec/command_linux.go create mode 100644 vendor/github.com/docker/docker/pkg/reexec/command_other.go create mode 100644 vendor/github.com/docker/docker/pkg/reexec/command_unsupported.go create mode 100644 vendor/github.com/docker/docker/pkg/reexec/reexec.go diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index 8b15b76f8e..051e226175 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -19,8 +19,10 @@ import ( cliflags "github.com/docker/cli/cli/flags" "github.com/docker/cli/cli/version" platformsignals "github.com/docker/cli/cmd/docker/internal/signals" + "github.com/docker/docker/api/types/versions" "github.com/docker/docker/errdefs" + "github.com/docker/docker/pkg/reexec" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -29,6 +31,10 @@ import ( ) func main() { + if reexec.Init() { + return + } + err := dockerMain(context.Background()) if err != nil && !errdefs.IsCancelled(err) { _, _ = fmt.Fprintln(os.Stderr, err) diff --git a/cmd/docker/file_helper.go b/cmd/docker/file_helper.go new file mode 100644 index 0000000000..08fca8bd56 --- /dev/null +++ b/cmd/docker/file_helper.go @@ -0,0 +1,69 @@ +package main + +import ( + "os" + + credhelpers "github.com/docker/docker-credential-helpers/credentials" + "github.com/docker/docker/pkg/reexec" + + "github.com/docker/cli/cli/config" + "github.com/docker/cli/cli/config/credentials" + "github.com/docker/cli/cli/config/types" +) + +//nolint:gosec // ignore G101: Potential hardcoded credentials +const fileCredsHelperBinary = "docker-credential-file" + +func init() { + reexec.Register(fileCredsHelperBinary, serveFileCredHelper) +} + +func serveFileCredHelper() { + configfile := config.LoadDefaultConfigFile(os.Stderr) + store := credentials.NewFileStore(configfile) + credhelpers.Serve(&FileHelper{ + fileStore: store, + }) +} + +var _ credhelpers.Helper = &FileHelper{} + +type FileHelper struct { + fileStore credentials.Store +} + +func (f *FileHelper) Add(creds *credhelpers.Credentials) error { + return f.fileStore.Store(types.AuthConfig{ + Username: creds.Username, + Password: creds.Secret, + ServerAddress: creds.ServerURL, + }) +} + +func (f *FileHelper) Delete(serverAddress string) error { + return f.fileStore.Erase(serverAddress) +} + +func (f *FileHelper) Get(serverAddress string) (string, string, error) { + authConfig, err := f.fileStore.Get(serverAddress) + if err != nil { + return "", "", err + } + + return authConfig.Username, authConfig.Password, nil +} + +func (f *FileHelper) List() (map[string]string, error) { + creds := make(map[string]string) + + authConfig, err := f.fileStore.GetAll() + if err != nil { + return nil, err + } + + for k, v := range authConfig { + creds[k] = v.Username + } + + return creds, nil +} diff --git a/vendor/github.com/docker/docker/pkg/reexec/command_linux.go b/vendor/github.com/docker/docker/pkg/reexec/command_linux.go new file mode 100644 index 0000000000..952633c864 --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/reexec/command_linux.go @@ -0,0 +1,26 @@ +package reexec + +import ( + "os/exec" + "syscall" +) + +// Command returns an [*exec.Cmd] which has Path as current binary which, +// on Linux, is set to the in-memory version (/proc/self/exe) of the current +// binary, it is thus safe to delete or replace the on-disk binary (os.Args[0]). +// +// On Linux, the Pdeathsig of [*exec.Cmd.SysProcAttr] is set to SIGTERM. +// This signal will be sent to the process when the OS thread which created +// the process dies. +// +// It is the caller's responsibility to ensure that the creating thread is +// not terminated prematurely. See https://go.dev/issue/27505 for more details. +func Command(args ...string) *exec.Cmd { + return &exec.Cmd{ + Path: Self(), + Args: args, + SysProcAttr: &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + }, + } +} diff --git a/vendor/github.com/docker/docker/pkg/reexec/command_other.go b/vendor/github.com/docker/docker/pkg/reexec/command_other.go new file mode 100644 index 0000000000..b458ef2d20 --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/reexec/command_other.go @@ -0,0 +1,19 @@ +//go:build freebsd || darwin || windows + +package reexec + +import ( + "os/exec" +) + +// Command returns *exec.Cmd with its Path set to the path of the current +// binary using the result of [Self]. For example if current binary is +// "my-binary" at "/usr/bin/" (or "my-binary.exe" at "C:\" on Windows), +// then cmd.Path is set to "/usr/bin/my-binary" and "C:\my-binary.exe" +// respectively. +func Command(args ...string) *exec.Cmd { + return &exec.Cmd{ + Path: Self(), + Args: args, + } +} diff --git a/vendor/github.com/docker/docker/pkg/reexec/command_unsupported.go b/vendor/github.com/docker/docker/pkg/reexec/command_unsupported.go new file mode 100644 index 0000000000..3e98b989a3 --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/reexec/command_unsupported.go @@ -0,0 +1,12 @@ +//go:build !linux && !windows && !freebsd && !darwin + +package reexec + +import ( + "os/exec" +) + +// Command is unsupported on operating systems apart from Linux, Windows, and Darwin. +func Command(args ...string) *exec.Cmd { + return nil +} diff --git a/vendor/github.com/docker/docker/pkg/reexec/reexec.go b/vendor/github.com/docker/docker/pkg/reexec/reexec.go new file mode 100644 index 0000000000..b9d11a2a58 --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/reexec/reexec.go @@ -0,0 +1,64 @@ +// Package reexec facilitates the busybox style reexec of a binary. +// +// Handlers can be registered with a name and the argv 0 of the exec of +// the binary will be used to find and execute custom init paths. +// +// It is used in dockerd to work around forking limitations when using Go. +package reexec + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" +) + +var registeredInitializers = make(map[string]func()) + +// Register adds an initialization func under the specified name. It panics +// if the given name is already registered. +func Register(name string, initializer func()) { + if _, exists := registeredInitializers[name]; exists { + panic(fmt.Sprintf("reexec func already registered under name %q", name)) + } + + registeredInitializers[name] = initializer +} + +// Init is called as the first part of the exec process and returns true if an +// initialization function was called. +func Init() bool { + if initializer, ok := registeredInitializers[os.Args[0]]; ok { + initializer() + return true + } + return false +} + +// Self returns the path to the current process's binary. On Linux, it +// returns "/proc/self/exe", which provides the in-memory version of the +// current binary, whereas on other platforms it attempts to looks up the +// absolute path for os.Args[0], or otherwise returns os.Args[0] as-is. +func Self() string { + if runtime.GOOS == "linux" { + return "/proc/self/exe" + } + return naiveSelf() +} + +func naiveSelf() string { + name := os.Args[0] + if filepath.Base(name) == name { + if lp, err := exec.LookPath(name); err == nil { + return lp + } + } + // handle conversion of relative paths to absolute + if absName, err := filepath.Abs(name); err == nil { + return absName + } + // if we couldn't get absolute name, return original + // (NOTE: Go only errors on Abs() if os.Getwd fails) + return name +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4bedede2e7..2e235ee65e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -91,6 +91,7 @@ github.com/docker/docker/pkg/longpath github.com/docker/docker/pkg/pools github.com/docker/docker/pkg/process github.com/docker/docker/pkg/progress +github.com/docker/docker/pkg/reexec github.com/docker/docker/pkg/stdcopy github.com/docker/docker/pkg/streamformatter github.com/docker/docker/pkg/stringid