Skip to content

Commit

Permalink
WIP: Add a /etc/containers/auth.json
Browse files Browse the repository at this point in the history
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 coreos/rpm-ostree#4180

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters authored and ipanova committed Oct 23, 2024
1 parent a66152c commit ca77468
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 5 deletions.
13 changes: 9 additions & 4 deletions docs/containers-auth.json.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
41 changes: 40 additions & 1 deletion pkg/docker/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
Expand Down Expand Up @@ -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")
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -596,14 +620,29 @@ 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{}
return fileContents, nil
}
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 {
Expand Down

0 comments on commit ca77468

Please sign in to comment.