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 Dec 8, 2022
1 parent 72e90f7 commit d68af81
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 11 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
51 changes: 44 additions & 7 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 @@ -35,9 +36,12 @@ type dockerConfigFile struct {
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
}

var (
systemPath = filepath.FromSlash("/etc/containers/auth.json")
defaultPerUIDPathFormat = filepath.FromSlash("/run/containers/%d/auth.json")
xdgConfigHomePath = filepath.FromSlash("containers/auth.json")
xdgRuntimeDirPath = filepath.FromSlash("containers/auth.json")
Expand Down Expand Up @@ -147,7 +151,7 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon
case sysregistriesv2.AuthenticationFileHelper:
for _, path := range getAuthFilePaths(sys, homedir.Get()) {
// readJSONFile returns an empty map in case the path doesn't exist.
auths, err := readJSONFile(path.path, path.legacyFormat)
auths, err := readJSONFile(path.path, path.legacyFormat, path.requireUserOnly)
if err != nil {
return nil, fmt.Errorf("reading JSON file %q: %w", path.path, err)
}
Expand Down Expand Up @@ -204,7 +208,17 @@ 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{}

// If we're in systemd, prefer the global auth path first.
if runningSystemdPrivileged {
paths = append(paths, authPath{path: systemPath, legacyFormat: false})
}

pathToAuth, lf, err := getPathToAuth(sys)
if err == nil {
paths = append(paths, authPath{path: pathToAuth, legacyFormat: lf})
Expand All @@ -214,6 +228,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)
}

xdgCfgHome := os.Getenv("XDG_CONFIG_HOME")
if xdgCfgHome == "" {
xdgCfgHome = filepath.Join(homeDir, ".config")
Expand All @@ -231,6 +246,13 @@ func getAuthFilePaths(sys *types.SystemContext, homeDir string) []authPath {
paths = append(paths,
authPath{path: filepath.Join(homeDir, dockerLegacyHomePath), legacyFormat: true},
)

// If we're not in systemd, we prefer the per-user paths first. However, as
// a convenience we do read the global auth path if we're running as root.
if !runningSystemdPrivileged && runningAsRoot {
paths = append(paths, authPath{path: systemPath, legacyFormat: false})
}

return paths
}

Expand Down Expand Up @@ -276,7 +298,7 @@ func getCredentialsWithHomeDir(sys *types.SystemContext, key, homeDir string) (t
// Anonymous function to query credentials from auth files.
getCredentialsFromAuthFiles := func() (types.DockerAuthConfig, string, error) {
for _, path := range getAuthFilePaths(sys, homeDir) {
authConfig, err := findCredentialsInFile(key, registry, path.path, path.legacyFormat)
authConfig, err := findCredentialsInFile(key, registry, path.path, path.legacyFormat, path.requireUserOnly)
if err != nil {
return types.DockerAuthConfig{}, "", err
}
Expand Down Expand Up @@ -538,17 +560,32 @@ func getPathToAuthWithOS(sys *types.SystemContext, goOS string) (string, bool, e
// readJSONFile unmarshals the authentications stored in the auth.json file and returns it
// or returns an empty dockerConfigFile data structure if auth.json does not exist
// if the file exists and is empty, readJSONFile returns an error
func readJSONFile(path string, legacyFormat bool) (dockerConfigFile, error) {
func readJSONFile(path string, legacyFormat, requireUserOnly bool) (dockerConfigFile, error) {
var auths dockerConfigFile

raw, err := os.ReadFile(path)
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
auths.AuthConfigs = map[string]dockerAuthConfig{}
return auths, nil
}
return dockerConfigFile{}, err
}
defer f.Close()
if requireUserOnly {
st, err := f.Stat()
if err != nil {
return dockerConfigFile{}, fmt.Errorf("stat %s: %w", path, err)
}
perms := st.Mode().Perm()
if (perms & 04) > 0 {
return dockerConfigFile{}, fmt.Errorf("refusing to process %s with world read permissions", path)
}
}
raw, err := io.ReadAll(f)
if err != nil {
return dockerConfigFile{}, fmt.Errorf("reading %s: %w", path, err)
}

if legacyFormat {
if err = json.Unmarshal(raw, &auths.AuthConfigs); err != nil {
Expand Down Expand Up @@ -588,7 +625,7 @@ func modifyJSON(sys *types.SystemContext, editor func(auths *dockerConfigFile) (
return "", err
}

auths, err := readJSONFile(path, false)
auths, err := readJSONFile(path, false, false)
if err != nil {
return "", fmt.Errorf("reading JSON file %q: %w", path, err)
}
Expand Down Expand Up @@ -655,8 +692,8 @@ func deleteAuthFromCredHelper(credHelper, registry string) error {

// findCredentialsInFile looks for credentials matching "key"
// (which is "registry" or a namespace in "registry") in "path".
func findCredentialsInFile(key, registry, path string, legacyFormat bool) (types.DockerAuthConfig, error) {
auths, err := readJSONFile(path, legacyFormat)
func findCredentialsInFile(key, registry, path string, legacyFormat, requireUserOnly bool) (types.DockerAuthConfig, error) {
auths, err := readJSONFile(path, legacyFormat, requireUserOnly)
if err != nil {
return types.DockerAuthConfig{}, fmt.Errorf("reading JSON file %q: %w", path, err)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/docker/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ func TestSetCredentials(t *testing.T) {
}

// Read the resulting file and verify it contains the expected keys
auth, err := readJSONFile(tmpFile.Name(), false)
auth, err := readJSONFile(tmpFile.Name(), false, false)
require.NoError(t, err)
assert.Len(t, auth.AuthConfigs, len(writtenCredentials))
// auth.AuthConfigs and writtenCredentials are both maps, i.e. their keys are unique;
Expand Down Expand Up @@ -772,7 +772,7 @@ func TestRemoveAuthentication(t *testing.T) {
}
}

auth, err := readJSONFile(tmpFile.Name(), false)
auth, err := readJSONFile(tmpFile.Name(), false, false)
require.NoError(t, err)

tc.assert(auth)
Expand Down

0 comments on commit d68af81

Please sign in to comment.