diff --git a/docs/containers-auth.json.5.md b/docs/containers-auth.json.5.md index 4030a06c05..5a15d310e4 100644 --- a/docs/containers-auth.json.5.md +++ b/docs/containers-auth.json.5.md @@ -6,15 +6,20 @@ containers-auth.json - syntax for the registry authentication file # DESCRIPTION A credentials file in JSON format used to authenticate against container image registries. -The primary (read/write) file is stored at `${XDG_RUNTIME_DIR}/containers/auth.json` on Linux; +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, search in `${XDG_CONFIG_HOME}/containers/auth.json` (usually `~/.config/containers/auth.json`), `$HOME/.docker/config.json`, `$HOME/.dockercfg`. +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 6c9bca04eb..961ab8bd03 100644 --- a/pkg/docker/config/config.go +++ b/pkg/docker/config/config.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -32,6 +33,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") @@ -53,6 +56,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. @@ -215,7 +220,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{} + + haveExplicitConfig := sys != nil && (sys.AuthFilePath != "" || sys.LegacyFormatAuthFilePath != "") + + // If we're in systemd, prefer the global auth path first. + insertedGlobalPath := false + if !haveExplicitConfig && runningSystemdPrivileged { + paths = append(paths, systemPath) + insertedGlobalPath = true + } + pathToAuth, err := getPathToAuth(sys) if err == nil { paths = append(paths, pathToAuth) @@ -225,7 +244,7 @@ func getAuthFilePaths(sys *types.SystemContext, homeDir string) []authPath { // Logging the error as a warning instead and moving on to pulling the image logrus.Warnf("%v: Trying to pull image in the event that it is a public image.", err) } - if sys == nil || (sys.AuthFilePath == "" && sys.LegacyFormatAuthFilePath == "") { + if !haveExplicitConfig { xdgCfgHome := os.Getenv("XDG_CONFIG_HOME") if xdgCfgHome == "" { xdgCfgHome = filepath.Join(homeDir, ".config") @@ -241,6 +260,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 } @@ -552,7 +577,7 @@ func getPathToAuthWithOS(sys *types.SystemContext, goOS string) (authPath, error func (path authPath) parse() (dockerConfigFile, error) { var auths dockerConfigFile - raw, err := os.ReadFile(path.path) + f, err := os.Open(path.path) if err != nil { if os.IsNotExist(err) { auths.AuthConfigs = map[string]dockerAuthConfig{} @@ -560,6 +585,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, &auths.AuthConfigs); err != nil {