From ca77468a32fb14a173a08e3c77ca81c423011863 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 8 Dec 2022 11:22:33 -0500 Subject: [PATCH] WIP: Add a `/etc/containers/auth.json` A long-running tension in the docker/podman land is around running as a system service versus being executed by a user. (Specifically a "login user", i.e. a Unix user that can be logged into via `ssh` etc.) For login users, it makes total sense to configure the container runtime in `$HOME`. But for system services (e.g. code executed by systemd) it is generally a bad idea to access or read the `/root` home directory. On image based systems, `/root` may be dynamically mutable state in contrast to `/etc` which may be managed by OS upgrades, or even be read-only. For these reasons, let's introduce `/etc/contaners/auth.json`. If it is present, and the current process is executing in systemd, it will be preferred. (There's some further logic around this that is explained in the manpage; please see that for details) cc https://github.com/coreos/rpm-ostree/issues/4180 Signed-off-by: Colin Walters --- docs/containers-auth.json.5.md | 13 +++++++---- pkg/docker/config/config.go | 41 +++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/docs/containers-auth.json.5.md b/docs/containers-auth.json.5.md index c5e22b087c..a08978f15f 100644 --- a/docs/containers-auth.json.5.md +++ b/docs/containers-auth.json.5.md @@ -5,17 +5,22 @@ containers-auth.json - syntax for the registry authentication file # DESCRIPTION -A file in JSON format controlling authentication against container image registries. -The primary (read/write) file is stored at `${XDG_RUNTIME_DIR}/containers/auth.json` on Linux; +A credentials file in JSON format used to authenticate against container image registries. +The primary (read/write) per-user file is stored at `${XDG_RUNTIME_DIR}/containers/auth.json` on Linux; on Windows and macOS, at `$HOME/.config/containers/auth.json`. -When searching for the credential for a registry, the following files will be read in sequence until the valid credential is found: +There is also a system-global `/etc/containers/auth.json` path. When the current process is executing inside systemd as root, this path will be preferred. + +When running as a user and searching for the credential for a registry, the following files will be read in sequence until the valid credential is found: first reading the primary (read/write) file, or the explicit override using an option of the calling application. If credentials are not present there, the search continues in `${XDG_CONFIG_HOME}/containers/auth.json` (usually `~/.config/containers/auth.json`), `$HOME/.docker/config.json`, `$HOME/.dockercfg`. -Except for the primary (read/write) file, other files are read-only unless the user, using an option of the calling application, explicitly points at it as an override. +If the current process is not running in systemd, but is running as root, the system global path will be read last. + +Except the primary (read/write) file, other files are read-only, unless the user use an option of the calling application explicitly points at it as an override. +Note that the `/etc/containers/auth.json` file must not be readable by group or world (i.e. mode `044`), or a fatal error will occur. ## FORMAT diff --git a/pkg/docker/config/config.go b/pkg/docker/config/config.go index da2238a0b6..0e0c6bdce1 100644 --- a/pkg/docker/config/config.go +++ b/pkg/docker/config/config.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/fs" "os" "os/exec" @@ -35,6 +36,8 @@ type dockerConfigFile struct { CredHelpers map[string]string `json:"credHelpers,omitempty"` } +// systemPath is the global auth path preferred for systemd services. +var systemPath = authPath{path: filepath.FromSlash("/etc/containers/auth.json"), legacyFormat: false, requireUserOnly: true} var ( defaultPerUIDPathFormat = filepath.FromSlash("/run/containers/%d/auth.json") xdgConfigHomePath = filepath.FromSlash("containers/auth.json") @@ -56,6 +59,8 @@ var ( type authPath struct { path string legacyFormat bool + // requireUserOnly will cause the file to be ignored if it is readable by group or other + requireUserOnly bool } // newAuthPathDefault constructs an authPath in non-legacy format. @@ -143,8 +148,21 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon // The homeDir parameter should always be homedir.Get(), and is only intended to be overridden // by tests. func getAuthFilePaths(sys *types.SystemContext, homeDir string) []authPath { + runningInSystemd := os.Getenv("INVOCATION_ID") != "" + runningAsRoot := os.Getuid() == 0 + runningSystemdPrivileged := runningInSystemd && runningAsRoot + paths := []authPath{} pathToAuth, userSpecifiedPath, err := getPathToAuth(sys) + + // If we're in systemd, prefer the global auth path first. + insertedGlobalPath := false + if !userSpecifiedPath && runningSystemdPrivileged { + paths = append(paths, systemPath) + insertedGlobalPath = true + } + + if err == nil { paths = append(paths, pathToAuth) } else { @@ -169,6 +187,12 @@ func getAuthFilePaths(sys *types.SystemContext, homeDir string) []authPath { paths = append(paths, authPath{path: filepath.Join(homeDir, dockerLegacyHomePath), legacyFormat: true}, ) + // If we didn't already insert the global path, do it at the end if we're running as root. + // This will ensure the same semantics for code executed as systemd units and run + // from an interactive shell (as root) as long as there's no user-root owned configs. + if !insertedGlobalPath && runningAsRoot { + paths = append(paths, systemPath) + } } return paths } @@ -596,7 +620,7 @@ func getPathToAuthWithOS(sys *types.SystemContext, goOS string) (authPath, bool, func (path authPath) parse() (dockerConfigFile, error) { var fileContents dockerConfigFile - raw, err := os.ReadFile(path.path) + f, err := os.Open(path.path) if err != nil { if os.IsNotExist(err) { fileContents.AuthConfigs = map[string]dockerAuthConfig{} @@ -604,6 +628,21 @@ func (path authPath) parse() (dockerConfigFile, error) { } return dockerConfigFile{}, err } + defer f.Close() + if path.requireUserOnly { + st, err := f.Stat() + if err != nil { + return dockerConfigFile{}, fmt.Errorf("stat %s: %w", path.path, err) + } + perms := st.Mode().Perm() + if (perms & 044) > 0 { + return dockerConfigFile{}, fmt.Errorf("refusing to process %s with group or world read permissions", path.path) + } + } + raw, err := io.ReadAll(f) + if err != nil { + return dockerConfigFile{}, fmt.Errorf("reading %s: %w", path.path, err) + } if path.legacyFormat { if err = json.Unmarshal(raw, &fileContents.AuthConfigs); err != nil {