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 committed Jan 3, 2023
1 parent dc16147 commit 838b0f9
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 4 deletions.
9 changes: 7 additions & 2 deletions docs/containers-auth.json.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 have mode `0600` i.e. only readable by root, or it will be ignored.

## FORMAT

Expand Down
44 changes: 42 additions & 2 deletions 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"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -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")
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand All @@ -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
}
Expand Down Expand Up @@ -552,14 +577,29 @@ 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{}
return auths, 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, &auths.AuthConfigs); err != nil {
Expand Down

0 comments on commit 838b0f9

Please sign in to comment.