From b8b573417c5f831922d1e0888a8b3ba616515e80 Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Mon, 23 Mar 2026 13:17:59 +0100 Subject: [PATCH 01/12] Use unified configfile parser for policy.json Signed-off-by: Jan Kaluza --- common/pkg/config/config.go | 5 - common/pkg/config/config_bsd.go | 6 - common/pkg/config/config_darwin.go | 6 - common/pkg/config/config_linux.go | 6 - common/pkg/config/config_windows.go | 4 - common/pkg/config/default.go | 21 --- image/docs/containers-policy.json.5.md | 4 +- image/signature/policy_config.go | 73 ++++----- image/signature/policy_config_test.go | 205 ++++++++++++------------ image/signature/policy_paths_common.go | 4 - image/signature/policy_paths_freebsd.go | 4 - storage/pkg/configfile/parse.go | 2 +- 12 files changed, 142 insertions(+), 198 deletions(-) diff --git a/common/pkg/config/config.go b/common/pkg/config/config.go index f686744186..28f03f5f8d 100644 --- a/common/pkg/config/config.go +++ b/common/pkg/config/config.go @@ -488,11 +488,6 @@ type EngineConfig struct { // backwards compat with older version of libpod and Podman. SetOptions - // SignaturePolicyPath is the path to a signature policy to use for - // validating images. If left empty, the containers/image default signature - // policy will be used. - SignaturePolicyPath string `toml:"-"` - // SDNotify tells container engine to allow containers to notify the host systemd of // readiness using the SD_NOTIFY mechanism. SDNotify bool `toml:"-"` diff --git a/common/pkg/config/config_bsd.go b/common/pkg/config/config_bsd.go index fec17a72fe..c925675b8c 100644 --- a/common/pkg/config/config_bsd.go +++ b/common/pkg/config/config_bsd.go @@ -2,12 +2,6 @@ package config -const ( - // DefaultSignaturePolicyPath is the default value for the - // policy.json file. - DefaultSignaturePolicyPath = "/usr/local/etc/containers/policy.json" -) - var defaultHelperBinariesDir = []string{ "/usr/local/bin", "/usr/local/libexec/podman", diff --git a/common/pkg/config/config_darwin.go b/common/pkg/config/config_darwin.go index 858187b043..a544f5fed7 100644 --- a/common/pkg/config/config_darwin.go +++ b/common/pkg/config/config_darwin.go @@ -1,11 +1,5 @@ package config -const ( - // DefaultSignaturePolicyPath is the default value for the - // policy.json file. - DefaultSignaturePolicyPath = "/etc/containers/policy.json" -) - var defaultHelperBinariesDir = []string{ // Relative to the binary directory "$BINDIR/../libexec/podman", diff --git a/common/pkg/config/config_linux.go b/common/pkg/config/config_linux.go index 655837864c..b3386fdc7a 100644 --- a/common/pkg/config/config_linux.go +++ b/common/pkg/config/config_linux.go @@ -5,12 +5,6 @@ import ( "go.podman.io/common/pkg/capabilities" ) -const ( - // DefaultSignaturePolicyPath is the default value for the - // policy.json file. - DefaultSignaturePolicyPath = "/etc/containers/policy.json" -) - func selinuxEnabled() bool { return selinux.GetEnabled() } diff --git a/common/pkg/config/config_windows.go b/common/pkg/config/config_windows.go index 3319ffbbd0..8855b7049c 100644 --- a/common/pkg/config/config_windows.go +++ b/common/pkg/config/config_windows.go @@ -7,10 +7,6 @@ import ( ) const ( - // DefaultSignaturePolicyPath is the default value for the - // policy.json file. - DefaultSignaturePolicyPath = "/etc/containers/policy.json" - // Mount type for mounting host dir _typeBind = "bind" ) diff --git a/common/pkg/config/default.go b/common/pkg/config/default.go index e0006cc681..7ea362f6bc 100644 --- a/common/pkg/config/default.go +++ b/common/pkg/config/default.go @@ -15,7 +15,6 @@ import ( nettypes "go.podman.io/common/libnetwork/types" "go.podman.io/common/pkg/apparmor" "go.podman.io/storage/pkg/configfile" - "go.podman.io/storage/pkg/fileutils" "go.podman.io/storage/pkg/homedir" "go.podman.io/storage/pkg/unshare" "go.podman.io/storage/types" @@ -177,9 +176,6 @@ const ( // DefaultSubnet is the subnet that will be used for the default // network. DefaultSubnet = "10.88.0.0/16" - // DefaultRootlessSignaturePolicyPath is the location within - // XDG_CONFIG_HOME of the rootless policy.json file. - DefaultRootlessSignaturePolicyPath = "containers/policy.json" // DefaultShmSize is the default upper limit on the size of tmpfs mounts. DefaultShmSize = "65536k" // DefaultUserNSSize indicates the default number of UIDs allocated for user namespace within a container. @@ -205,23 +201,6 @@ func defaultConfig() (*Config, error) { return nil, err } - defaultEngineConfig.SignaturePolicyPath = DefaultSignaturePolicyPath - // NOTE: For now we want Windows to use system locations. - // GetRootlessUID == -1 on Windows, so exclude negative range - if unshare.GetRootlessUID() > 0 { - configHome, err := homedir.GetConfigHome() - if err != nil { - return nil, err - } - sigPath := filepath.Join(configHome, DefaultRootlessSignaturePolicyPath) - defaultEngineConfig.SignaturePolicyPath = sigPath - if err := fileutils.Exists(sigPath); err != nil { - if err := fileutils.Exists(DefaultSignaturePolicyPath); err == nil { - defaultEngineConfig.SignaturePolicyPath = DefaultSignaturePolicyPath - } - } - } - return &Config{ Containers: ContainersConfig{ Annotations: configfile.Slice{}, diff --git a/image/docs/containers-policy.json.5.md b/image/docs/containers-policy.json.5.md index 1408e6510a..5667f7193b 100644 --- a/image/docs/containers-policy.json.5.md +++ b/image/docs/containers-policy.json.5.md @@ -10,7 +10,9 @@ containers-policy.json - syntax for the signature verification policy file Signature verification policy files are used to specify policy, e.g. trusted keys, applicable when deciding whether to accept an image, or individual signatures of that image, as valid. -By default, the policy is read from `$HOME/.config/containers/policy.json`, if it exists, otherwise from `/etc/containers/policy.json`; applications performing verification may allow using a different policy instead. +By default, the policy is read from `$XDG_CONFIG_HOME/containers/policy.json` (or from `$HOME/.config/containers/policy.json` if `$XDG_CONFIG_HOME` is unset), if it exists; otherwise from `/etc/containers/policy.json`; otherwise from `/usr/share/containers/policy.json`. Applications performing verification may allow using a different policy instead. + +If `CONTAINERS_POLICY_CONF` is set, it specifies the only policy file to use. ## FORMAT diff --git a/image/signature/policy_config.go b/image/signature/policy_config.go index 5e06531192..ca75c4995f 100644 --- a/image/signature/policy_config.go +++ b/image/signature/policy_config.go @@ -17,26 +17,17 @@ import ( "encoding/json" "errors" "fmt" + "io" "os" - "path/filepath" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/signature/internal" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/types" - "go.podman.io/storage/pkg/fileutils" - "go.podman.io/storage/pkg/homedir" + "go.podman.io/storage/pkg/configfile" "go.podman.io/storage/pkg/regexp" ) -// systemDefaultPolicyPath is the policy path used for DefaultPolicy(). -// You can override this at build time with -// -ldflags '-X go.podman.io/image/v5/signature.systemDefaultPolicyPath=$your_path' -var systemDefaultPolicyPath = builtinDefaultPolicyPath - -// userPolicyFile is the path to the per user policy path. -var userPolicyFile = filepath.FromSlash(".config/containers/policy.json") - // InvalidPolicyFormatError is returned when parsing an invalid policy configuration. type InvalidPolicyFormatError string @@ -51,39 +42,45 @@ func (err InvalidPolicyFormatError) Error() string { // NOTE: When this function returns an error, report it to the user and abort. // DO NOT hard-code fallback policies in your application. func DefaultPolicy(sys *types.SystemContext) (*Policy, error) { - policyPath, err := defaultPolicyPath(sys) - if err != nil { - return nil, err + if sys != nil && sys.SignaturePolicyPath != "" { + return NewPolicyFromFile(sys.SignaturePolicyPath) } - return NewPolicyFromFile(policyPath) -} - -// defaultPolicyPath returns a path to the relevant policy of the system, or an error if the policy is missing. -func defaultPolicyPath(sys *types.SystemContext) (string, error) { - policyFilePath, err := defaultPolicyPathWithHomeDir(sys, homedir.Get(), systemDefaultPolicyPath) - if err != nil { - return "", err + var rootForImplicitAbsPaths string + if sys != nil { + rootForImplicitAbsPaths = sys.RootForImplicitAbsolutePaths } - return policyFilePath, nil -} -// defaultPolicyPathWithHomeDir is an internal implementation detail of defaultPolicyPath, -// it exists only to allow testing it with artificial paths. -func defaultPolicyPathWithHomeDir(sys *types.SystemContext, homeDir string, systemPolicyPath string) (string, error) { - if sys != nil && sys.SignaturePolicyPath != "" { - return sys.SignaturePolicyPath, nil - } - userPolicyFilePath := filepath.Join(homeDir, userPolicyFile) - if err := fileutils.Exists(userPolicyFilePath); err == nil { - return userPolicyFilePath, nil + policyFiles := configfile.File{ + Name: "policy", + Extension: "json", + DoNotLoadDropInFiles: true, + EnvironmentName: "CONTAINERS_POLICY_CONF", + RootForImplicitAbsolutePaths: rootForImplicitAbsPaths, } - if sys != nil && sys.RootForImplicitAbsolutePaths != "" { - return filepath.Join(sys.RootForImplicitAbsolutePaths, systemPolicyPath), nil + + var policy *Policy + found := false + for item, err := range configfile.Read(&policyFiles) { + if err != nil { + return nil, err + } + found = true + + contents, err := io.ReadAll(item.Reader) + if err != nil { + return nil, err + } + policy, err = NewPolicyFromBytes(contents) + if err != nil { + return nil, fmt.Errorf("invalid policy in %q: %w", item.Name, err) + } } - if err := fileutils.Exists(systemPolicyPath); err == nil { - return systemPolicyPath, nil + + if !found { + return nil, fmt.Errorf("no policy.json file found") } - return "", fmt.Errorf("no policy.json file found at any of the following: %q, %q", userPolicyFilePath, systemPolicyPath) + + return policy, nil } // NewPolicyFromFile returns a policy configured in the specified file. diff --git a/image/signature/policy_config_test.go b/image/signature/policy_config_test.go index fd19a4d270..ae60df6678 100644 --- a/image/signature/policy_config_test.go +++ b/image/signature/policy_config_test.go @@ -127,12 +127,9 @@ func TestInvalidPolicyFormatError(t *testing.T) { } func TestDefaultPolicy(t *testing.T) { - // We can't test the actual systemDefaultPolicyPath, so override. - // TestDefaultPolicyPath below tests that we handle the overrides and defaults - // correctly. - - // Success - policy, err := DefaultPolicy(&types.SystemContext{SignaturePolicyPath: "./fixtures/policy.json"}) + // Success via environment override. + t.Setenv("CONTAINERS_POLICY_CONF", "./fixtures/policy.json") + policy, err := DefaultPolicy(nil) require.NoError(t, err) assert.Equal(t, policyFixtureContents, policy) @@ -140,120 +137,124 @@ func TestDefaultPolicy(t *testing.T) { "/this/does/not/exist", // Error reading file "/dev/null", // A failure case; most are tested in the individual method unit tests. } { - policy, err := DefaultPolicy(&types.SystemContext{SignaturePolicyPath: path}) + t.Setenv("CONTAINERS_POLICY_CONF", path) + policy, err := DefaultPolicy(nil) assert.Error(t, err) assert.Nil(t, policy) } } -func TestDefaultPolicyPath(t *testing.T) { - const nondefaultPath = "/this/is/not/the/default/path.json" - const variableReference = "$HOME" - const rootPrefix = "/root/prefix" - tempHome := t.TempDir() - userDefaultPolicyPath := filepath.Join(tempHome, userPolicyFile) - tempsystemdefaultpath := filepath.Join(tempHome, systemDefaultPolicyPath) - for _, c := range []struct { - sys *types.SystemContext - userfilePresent bool - systemfilePresent bool - expected string - expectedError string +func TestDefaultPolicyLookupOrder(t *testing.T) { + const rejectJSON = `{"default":[{"type":"reject"}]}` + const insecureJSON = `{"default":[{"type":"insecureAcceptAnything"}]}` + + for _, tc := range []struct { + name string + userPresent bool + etcPresent bool + usrPresent bool + expectedPolicy any // *prInsecureAcceptAnything or *prReject }{ - // The common case - {nil, false, true, tempsystemdefaultpath, ""}, - // There is a context, but it does not override the path. - {&types.SystemContext{}, false, true, tempsystemdefaultpath, ""}, - // Path overridden - {&types.SystemContext{SignaturePolicyPath: nondefaultPath}, false, true, nondefaultPath, ""}, - // Root overridden - { - &types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix}, - false, - true, - filepath.Join(rootPrefix, tempsystemdefaultpath), - "", - }, - // Empty context and user policy present - {&types.SystemContext{}, true, true, userDefaultPolicyPath, ""}, - // Only user policy present - {nil, true, true, userDefaultPolicyPath, ""}, - // Context signature path and user policy present - { - &types.SystemContext{ - SignaturePolicyPath: nondefaultPath, - }, - true, - true, - nondefaultPath, - "", - }, - // Root and user policy present { - &types.SystemContext{ - RootForImplicitAbsolutePaths: rootPrefix, - }, - true, - true, - userDefaultPolicyPath, - "", + name: "user wins", + userPresent: true, + etcPresent: true, + usrPresent: true, + expectedPolicy: &prInsecureAcceptAnything{}, }, - // Context and user policy file preset simultaneously { - &types.SystemContext{ - RootForImplicitAbsolutePaths: rootPrefix, - SignaturePolicyPath: nondefaultPath, - }, - true, - true, - nondefaultPath, - "", + name: "etc fallback", + userPresent: false, + etcPresent: true, + usrPresent: true, + expectedPolicy: &prReject{}, }, - // Root and path overrides present simultaneously, { - &types.SystemContext{ - RootForImplicitAbsolutePaths: rootPrefix, - SignaturePolicyPath: nondefaultPath, - }, - false, - true, - nondefaultPath, - "", + name: "usr fallback", + userPresent: false, + etcPresent: false, + usrPresent: true, + expectedPolicy: &prInsecureAcceptAnything{}, }, - // No environment expansion happens in the overridden paths - {&types.SystemContext{SignaturePolicyPath: variableReference}, false, true, variableReference, ""}, - // No policy.json file is present in userfilePath and systemfilePath - {nil, false, false, "", fmt.Sprintf("no policy.json file found at any of the following: %q, %q", userDefaultPolicyPath, tempsystemdefaultpath)}, } { - paths := []struct { - condition bool - path string - }{ - {c.userfilePresent, userDefaultPolicyPath}, - {c.systemfilePresent, tempsystemdefaultpath}, - } - for _, p := range paths { - if p.condition { - err := os.MkdirAll(filepath.Dir(p.path), os.ModePerm) - require.NoError(t, err) - f, err := os.Create(p.path) - require.NoError(t, err) - f.Close() - } else { - os.Remove(p.path) + t.Run(tc.name, func(t *testing.T) { + tempHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempHome) + userPolicyPath := filepath.Join(tempHome, "containers", "policy.json") + + rootPrefix := t.TempDir() + systemEtcPolicyPath := filepath.Join(rootPrefix, "etc", "containers", "policy.json") + systemUsrPolicyPath := filepath.Join(rootPrefix, "usr", "share", "containers", "policy.json") + + mustWritePolicy := func(path string, contents string) { + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) + require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) } - } - path, err := defaultPolicyPathWithHomeDir(c.sys, tempHome, tempsystemdefaultpath) - if c.expectedError != "" { - assert.Empty(t, path) - assert.EqualError(t, err, c.expectedError) - } else { + + if tc.userPresent { + mustWritePolicy(userPolicyPath, insecureJSON) + } + if tc.etcPresent { + mustWritePolicy(systemEtcPolicyPath, rejectJSON) + } + if tc.usrPresent { + mustWritePolicy(systemUsrPolicyPath, insecureJSON) + } + + policy, err := DefaultPolicy(&types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + }) require.NoError(t, err) - assert.Equal(t, c.expected, path) - } + require.NotEmpty(t, policy.Default) + + switch tc.expectedPolicy.(type) { + case *prInsecureAcceptAnything: + _, ok := policy.Default[0].(*prInsecureAcceptAnything) + assert.True(t, ok, "expected insecureAcceptAnything policy requirement") + case *prReject: + _, ok := policy.Default[0].(*prReject) + assert.True(t, ok, "expected reject policy requirement") + default: + t.Fatalf("unexpected expectedPolicy type %T", tc.expectedPolicy) + } + }) } } +func TestDefaultPolicyEnvOverride(t *testing.T) { + const rejectJSON = `{"default":[{"type":"reject"}]}` + const insecureJSON = `{"default":[{"type":"insecureAcceptAnything"}]}` + + tempHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempHome) + + rootPrefix := t.TempDir() + systemEtcPolicyPath := filepath.Join(rootPrefix, "etc", "containers", "policy.json") + systemUsrPolicyPath := filepath.Join(rootPrefix, "usr", "share", "containers", "policy.json") + require.NoError(t, os.MkdirAll(filepath.Dir(systemEtcPolicyPath), 0o755)) + require.NoError(t, os.WriteFile(systemEtcPolicyPath, []byte(rejectJSON), 0o600)) + require.NoError(t, os.MkdirAll(filepath.Dir(systemUsrPolicyPath), 0o755)) + require.NoError(t, os.WriteFile(systemUsrPolicyPath, []byte(rejectJSON), 0o600)) + + envBasePath := filepath.Join(t.TempDir(), "env-base.json") + envOverridePath := filepath.Join(t.TempDir(), "env-override.json") + require.NoError(t, os.WriteFile(envBasePath, []byte(insecureJSON), 0o600)) + require.NoError(t, os.WriteFile(envOverridePath, []byte(rejectJSON), 0o600)) + + t.Setenv("CONTAINERS_POLICY_CONF", envBasePath) + t.Setenv("CONTAINERS_POLICY_CONF_OVERRIDE", envOverridePath) + + policy, err := DefaultPolicy(&types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + }) + require.NoError(t, err) + require.NotEmpty(t, policy.Default) + + // _OVERRIDE is appended after all other selected policy files, so it wins. + _, ok := policy.Default[0].(*prReject) + assert.True(t, ok, "expected override policy requirement") +} + func TestNewPolicyFromFile(t *testing.T) { // Success policy, err := NewPolicyFromFile("./fixtures/policy.json") diff --git a/image/signature/policy_paths_common.go b/image/signature/policy_paths_common.go index 038351cb74..6c250349d5 100644 --- a/image/signature/policy_paths_common.go +++ b/image/signature/policy_paths_common.go @@ -1,7 +1,3 @@ //go:build !freebsd package signature - -// builtinDefaultPolicyPath is the policy path used for DefaultPolicy(). -// DO NOT change this, instead see systemDefaultPolicyPath above. -const builtinDefaultPolicyPath = "/etc/containers/policy.json" diff --git a/image/signature/policy_paths_freebsd.go b/image/signature/policy_paths_freebsd.go index 6a45a78fa1..81acbaa9b9 100644 --- a/image/signature/policy_paths_freebsd.go +++ b/image/signature/policy_paths_freebsd.go @@ -1,7 +1,3 @@ //go:build freebsd package signature - -// builtinDefaultPolicyPath is the policy path used for DefaultPolicy(). -// DO NOT change this, instead see systemDefaultPolicyPath above. -const builtinDefaultPolicyPath = "/usr/local/etc/containers/policy.json" diff --git a/storage/pkg/configfile/parse.go b/storage/pkg/configfile/parse.go index f54f926926..1555e9060e 100644 --- a/storage/pkg/configfile/parse.go +++ b/storage/pkg/configfile/parse.go @@ -224,7 +224,7 @@ func Read(conf *File) iter.Seq2[*Item, error] { conf.Modules = resolvedModules } - if conf.EnvironmentName != "" { + if conf.EnvironmentName != "" && !conf.DoNotLoadDropInFiles { // The _OVERRIDE env must be appended after loading all files, even modules. if path := os.Getenv(conf.EnvironmentName + "_OVERRIDE"); path != "" { f, err := os.Open(path) From 6e253a3c561641e652d2fa7a99c2254ec5ad7b86 Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Tue, 24 Mar 2026 12:22:00 +0100 Subject: [PATCH 02/12] Unify the DefaultPolicy tests into single test. Signed-off-by: Jan Kaluza --- image/signature/policy_config_test.go | 243 ++++++++++++++++---------- 1 file changed, 146 insertions(+), 97 deletions(-) diff --git a/image/signature/policy_config_test.go b/image/signature/policy_config_test.go index ae60df6678..aed147aa11 100644 --- a/image/signature/policy_config_test.go +++ b/image/signature/policy_config_test.go @@ -127,87 +127,170 @@ func TestInvalidPolicyFormatError(t *testing.T) { } func TestDefaultPolicy(t *testing.T) { - // Success via environment override. - t.Setenv("CONTAINERS_POLICY_CONF", "./fixtures/policy.json") - policy, err := DefaultPolicy(nil) - require.NoError(t, err) - assert.Equal(t, policyFixtureContents, policy) - - for _, path := range []string{ - "/this/does/not/exist", // Error reading file - "/dev/null", // A failure case; most are tested in the individual method unit tests. - } { - t.Setenv("CONTAINERS_POLICY_CONF", path) - policy, err := DefaultPolicy(nil) - assert.Error(t, err) - assert.Nil(t, policy) - } -} - -func TestDefaultPolicyLookupOrder(t *testing.T) { + // prReject const rejectJSON = `{"default":[{"type":"reject"}]}` + // prInsecureAcceptAnything const insecureJSON = `{"default":[{"type":"insecureAcceptAnything"}]}` - for _, tc := range []struct { + type tc struct { name string - userPresent bool - etcPresent bool - usrPresent bool - expectedPolicy any // *prInsecureAcceptAnything or *prReject - }{ + setup func(t *testing.T, rootPrefix string) + sys *types.SystemContext + useRootPrefix bool + expectPolicy any // *Policy, *prReject, *prInsecureAcceptAnything + expectErr bool + expectErrMatch string + } + + mustWritePolicy := func(t *testing.T, path, contents string) { + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) + require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) + } + + for _, test := range []tc{ + { + name: "signature policy path override success", + setup: func(t *testing.T, _ string) { + // no-op + }, + sys: &types.SystemContext{SignaturePolicyPath: "./fixtures/policy.json"}, + expectPolicy: policyFixtureContents, + }, + { + name: "signature policy path override read error", + setup: func(t *testing.T, _ string) { + // no-op + }, + sys: &types.SystemContext{SignaturePolicyPath: "/this/does/not/exist"}, + expectErr: true, + }, + { + name: "signature policy path override parse error", + setup: func(t *testing.T, _ string) { + // no-op + }, + sys: &types.SystemContext{SignaturePolicyPath: "/dev/null"}, + expectErr: true, + }, { - name: "user wins", - userPresent: true, - etcPresent: true, - usrPresent: true, - expectedPolicy: &prInsecureAcceptAnything{}, + name: "user wins over etc and usr", + setup: func(t *testing.T, rootPrefix string) { + tempHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempHome) + mustWritePolicy(t, filepath.Join(tempHome, "containers", "policy.json"), insecureJSON) + mustWritePolicy(t, filepath.Join(rootPrefix, "etc", "containers", "policy.json"), rejectJSON) + mustWritePolicy(t, filepath.Join(rootPrefix, "usr", "share", "containers", "policy.json"), rejectJSON) + }, + sys: &types.SystemContext{}, + useRootPrefix: true, + expectPolicy: &prInsecureAcceptAnything{}, }, { - name: "etc fallback", - userPresent: false, - etcPresent: true, - usrPresent: true, - expectedPolicy: &prReject{}, + name: "etc fallback when user missing", + setup: func(t *testing.T, rootPrefix string) { + tempHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempHome) + mustWritePolicy(t, filepath.Join(rootPrefix, "etc", "containers", "policy.json"), rejectJSON) + mustWritePolicy(t, filepath.Join(rootPrefix, "usr", "share", "containers", "policy.json"), insecureJSON) + }, + sys: &types.SystemContext{}, + useRootPrefix: true, + expectPolicy: &prReject{}, }, { - name: "usr fallback", - userPresent: false, - etcPresent: false, - usrPresent: true, - expectedPolicy: &prInsecureAcceptAnything{}, + name: "usr fallback when only usr present", + setup: func(t *testing.T, rootPrefix string) { + tempHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempHome) + mustWritePolicy(t, filepath.Join(rootPrefix, "usr", "share", "containers", "policy.json"), insecureJSON) + }, + sys: &types.SystemContext{}, + useRootPrefix: true, + expectPolicy: &prInsecureAcceptAnything{}, + }, + { + name: "no policy file found", + setup: func(t *testing.T, _ string) { + tempHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempHome) + }, + sys: &types.SystemContext{}, + useRootPrefix: true, + expectErr: true, + expectErrMatch: "no policy.json file found", + }, + { + name: "containers policy conf override base file", + setup: func(t *testing.T, rootPrefix string) { + tempHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempHome) + mustWritePolicy(t, filepath.Join(rootPrefix, "etc", "containers", "policy.json"), rejectJSON) + base := filepath.Join(t.TempDir(), "env-base.json") + mustWritePolicy(t, base, insecureJSON) + t.Setenv("CONTAINERS_POLICY_CONF", base) + }, + sys: &types.SystemContext{}, + expectPolicy: &prInsecureAcceptAnything{}, + }, + { + name: "containers policy conf read error", + setup: func(t *testing.T, _ string) { + tempHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempHome) + t.Setenv("CONTAINERS_POLICY_CONF", "/this/does/not/exist") + }, + sys: &types.SystemContext{}, + expectErr: true, + }, + { + name: "containers policy conf parse error", + setup: func(t *testing.T, _ string) { + tempHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempHome) + t.Setenv("CONTAINERS_POLICY_CONF", "/dev/null") + }, + sys: &types.SystemContext{}, + expectErr: true, + }, + { + name: "root for implicit absolute paths is honored", + setup: func(t *testing.T, rootPrefix string) { + tempHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempHome) + mustWritePolicy(t, filepath.Join(rootPrefix, "etc", "containers", "policy.json"), rejectJSON) + }, + sys: &types.SystemContext{}, + useRootPrefix: true, + expectPolicy: &prReject{}, }, } { - t.Run(tc.name, func(t *testing.T) { - tempHome := t.TempDir() - t.Setenv("XDG_CONFIG_HOME", tempHome) - userPolicyPath := filepath.Join(tempHome, "containers", "policy.json") - + t.Run(test.name, func(t *testing.T) { rootPrefix := t.TempDir() - systemEtcPolicyPath := filepath.Join(rootPrefix, "etc", "containers", "policy.json") - systemUsrPolicyPath := filepath.Join(rootPrefix, "usr", "share", "containers", "policy.json") - - mustWritePolicy := func(path string, contents string) { - require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) - require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) + if test.setup != nil { + test.setup(t, rootPrefix) } - - if tc.userPresent { - mustWritePolicy(userPolicyPath, insecureJSON) + sys := test.sys + if test.useRootPrefix && sys != nil && sys.RootForImplicitAbsolutePaths == "" && sys.SignaturePolicyPath == "" { + sys = &types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix} } - if tc.etcPresent { - mustWritePolicy(systemEtcPolicyPath, rejectJSON) - } - if tc.usrPresent { - mustWritePolicy(systemUsrPolicyPath, insecureJSON) + + policy, err := DefaultPolicy(sys) + if test.expectErr { + require.Error(t, err) + assert.Nil(t, policy) + if test.expectErrMatch != "" { + assert.Contains(t, err.Error(), test.expectErrMatch) + } + return } - policy, err := DefaultPolicy(&types.SystemContext{ - RootForImplicitAbsolutePaths: rootPrefix, - }) require.NoError(t, err) + require.NotNil(t, policy) require.NotEmpty(t, policy.Default) - switch tc.expectedPolicy.(type) { + switch expected := test.expectPolicy.(type) { + case *Policy: + assert.Equal(t, expected, policy) case *prInsecureAcceptAnything: _, ok := policy.Default[0].(*prInsecureAcceptAnything) assert.True(t, ok, "expected insecureAcceptAnything policy requirement") @@ -215,46 +298,12 @@ func TestDefaultPolicyLookupOrder(t *testing.T) { _, ok := policy.Default[0].(*prReject) assert.True(t, ok, "expected reject policy requirement") default: - t.Fatalf("unexpected expectedPolicy type %T", tc.expectedPolicy) + t.Fatalf("unexpected expectedPolicy type %T", test.expectPolicy) } }) } } -func TestDefaultPolicyEnvOverride(t *testing.T) { - const rejectJSON = `{"default":[{"type":"reject"}]}` - const insecureJSON = `{"default":[{"type":"insecureAcceptAnything"}]}` - - tempHome := t.TempDir() - t.Setenv("XDG_CONFIG_HOME", tempHome) - - rootPrefix := t.TempDir() - systemEtcPolicyPath := filepath.Join(rootPrefix, "etc", "containers", "policy.json") - systemUsrPolicyPath := filepath.Join(rootPrefix, "usr", "share", "containers", "policy.json") - require.NoError(t, os.MkdirAll(filepath.Dir(systemEtcPolicyPath), 0o755)) - require.NoError(t, os.WriteFile(systemEtcPolicyPath, []byte(rejectJSON), 0o600)) - require.NoError(t, os.MkdirAll(filepath.Dir(systemUsrPolicyPath), 0o755)) - require.NoError(t, os.WriteFile(systemUsrPolicyPath, []byte(rejectJSON), 0o600)) - - envBasePath := filepath.Join(t.TempDir(), "env-base.json") - envOverridePath := filepath.Join(t.TempDir(), "env-override.json") - require.NoError(t, os.WriteFile(envBasePath, []byte(insecureJSON), 0o600)) - require.NoError(t, os.WriteFile(envOverridePath, []byte(rejectJSON), 0o600)) - - t.Setenv("CONTAINERS_POLICY_CONF", envBasePath) - t.Setenv("CONTAINERS_POLICY_CONF_OVERRIDE", envOverridePath) - - policy, err := DefaultPolicy(&types.SystemContext{ - RootForImplicitAbsolutePaths: rootPrefix, - }) - require.NoError(t, err) - require.NotEmpty(t, policy.Default) - - // _OVERRIDE is appended after all other selected policy files, so it wins. - _, ok := policy.Default[0].(*prReject) - assert.True(t, ok, "expected override policy requirement") -} - func TestNewPolicyFromFile(t *testing.T) { // Success policy, err := NewPolicyFromFile("./fixtures/policy.json") From ae6b09f0dbc386404e6e684bfb1c189dc8537623 Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Wed, 25 Mar 2026 12:36:28 +0100 Subject: [PATCH 03/12] Fix formatting and use CONTAINERS_POLICY_JSON. Signed-off-by: Jan Kaluza --- image/docs/containers-policy.json.5.md | 2 +- image/signature/policy_config.go | 7 +++---- image/signature/policy_config_test.go | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/image/docs/containers-policy.json.5.md b/image/docs/containers-policy.json.5.md index 5667f7193b..d9e485288d 100644 --- a/image/docs/containers-policy.json.5.md +++ b/image/docs/containers-policy.json.5.md @@ -12,7 +12,7 @@ applicable when deciding whether to accept an image, or individual signatures of By default, the policy is read from `$XDG_CONFIG_HOME/containers/policy.json` (or from `$HOME/.config/containers/policy.json` if `$XDG_CONFIG_HOME` is unset), if it exists; otherwise from `/etc/containers/policy.json`; otherwise from `/usr/share/containers/policy.json`. Applications performing verification may allow using a different policy instead. -If `CONTAINERS_POLICY_CONF` is set, it specifies the only policy file to use. +If `CONTAINERS_POLICY_JSON` is set, it specifies the only policy file to use. ## FORMAT diff --git a/image/signature/policy_config.go b/image/signature/policy_config.go index ca75c4995f..dede25a605 100644 --- a/image/signature/policy_config.go +++ b/image/signature/policy_config.go @@ -45,6 +45,7 @@ func DefaultPolicy(sys *types.SystemContext) (*Policy, error) { if sys != nil && sys.SignaturePolicyPath != "" { return NewPolicyFromFile(sys.SignaturePolicyPath) } + var rootForImplicitAbsPaths string if sys != nil { rootForImplicitAbsPaths = sys.RootForImplicitAbsolutePaths @@ -54,17 +55,15 @@ func DefaultPolicy(sys *types.SystemContext) (*Policy, error) { Name: "policy", Extension: "json", DoNotLoadDropInFiles: true, - EnvironmentName: "CONTAINERS_POLICY_CONF", + EnvironmentName: "CONTAINERS_POLICY_JSON", RootForImplicitAbsolutePaths: rootForImplicitAbsPaths, } var policy *Policy - found := false for item, err := range configfile.Read(&policyFiles) { if err != nil { return nil, err } - found = true contents, err := io.ReadAll(item.Reader) if err != nil { @@ -76,7 +75,7 @@ func DefaultPolicy(sys *types.SystemContext) (*Policy, error) { } } - if !found { + if policy == nil { return nil, fmt.Errorf("no policy.json file found") } diff --git a/image/signature/policy_config_test.go b/image/signature/policy_config_test.go index aed147aa11..a47f605ab2 100644 --- a/image/signature/policy_config_test.go +++ b/image/signature/policy_config_test.go @@ -227,7 +227,7 @@ func TestDefaultPolicy(t *testing.T) { mustWritePolicy(t, filepath.Join(rootPrefix, "etc", "containers", "policy.json"), rejectJSON) base := filepath.Join(t.TempDir(), "env-base.json") mustWritePolicy(t, base, insecureJSON) - t.Setenv("CONTAINERS_POLICY_CONF", base) + t.Setenv("CONTAINERS_POLICY_JSON", base) }, sys: &types.SystemContext{}, expectPolicy: &prInsecureAcceptAnything{}, @@ -237,7 +237,7 @@ func TestDefaultPolicy(t *testing.T) { setup: func(t *testing.T, _ string) { tempHome := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tempHome) - t.Setenv("CONTAINERS_POLICY_CONF", "/this/does/not/exist") + t.Setenv("CONTAINERS_POLICY_JSON", "/this/does/not/exist") }, sys: &types.SystemContext{}, expectErr: true, @@ -247,7 +247,7 @@ func TestDefaultPolicy(t *testing.T) { setup: func(t *testing.T, _ string) { tempHome := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tempHome) - t.Setenv("CONTAINERS_POLICY_CONF", "/dev/null") + t.Setenv("CONTAINERS_POLICY_JSON", "/dev/null") }, sys: &types.SystemContext{}, expectErr: true, From bc3e62787af8895068356c68aa1b20a192d47395 Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Mon, 30 Mar 2026 13:34:39 +0200 Subject: [PATCH 04/12] include searched paths in policy.json lookup errors When no policy.json is found, `DefaultPolicy()`` previously returned a generic error without indicating where the system looked for the file. This commit introduces `configfile.ReadWithPaths()` to track all attempted config file locations during iteration. It uses this in DefaultPolicy() to include the searched paths in the error message when no policy file is found. Signed-off-by: Jan Kaluza --- image/signature/policy_config.go | 7 +- image/signature/policy_config_test.go | 7 ++ storage/pkg/configfile/parse.go | 29 +++++- storage/pkg/configfile/parse_test.go | 142 ++++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 3 deletions(-) diff --git a/image/signature/policy_config.go b/image/signature/policy_config.go index dede25a605..302e61009e 100644 --- a/image/signature/policy_config.go +++ b/image/signature/policy_config.go @@ -19,6 +19,7 @@ import ( "fmt" "io" "os" + "strings" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/signature/internal" @@ -60,7 +61,8 @@ func DefaultPolicy(sys *types.SystemContext) (*Policy, error) { } var policy *Policy - for item, err := range configfile.Read(&policyFiles) { + var usedPaths []string + for item, err := range configfile.ReadWithPaths(&policyFiles, &usedPaths) { if err != nil { return nil, err } @@ -76,6 +78,9 @@ func DefaultPolicy(sys *types.SystemContext) (*Policy, error) { } if policy == nil { + if len(usedPaths) > 0 { + return nil, fmt.Errorf("no policy.json file found; searched paths: %s", strings.Join(usedPaths, ", ")) + } return nil, fmt.Errorf("no policy.json file found") } diff --git a/image/signature/policy_config_test.go b/image/signature/policy_config_test.go index a47f605ab2..d28a53d8ca 100644 --- a/image/signature/policy_config_test.go +++ b/image/signature/policy_config_test.go @@ -281,6 +281,13 @@ func TestDefaultPolicy(t *testing.T) { if test.expectErrMatch != "" { assert.Contains(t, err.Error(), test.expectErrMatch) } + if test.name == "no policy file found" { + assert.Contains(t, err.Error(), "; searched paths:") + // The search paths include user config (XDG_CONFIG_HOME), etc and usr. + assert.Contains(t, err.Error(), filepath.Join(os.Getenv("XDG_CONFIG_HOME"), "containers", "policy.json")) + assert.Contains(t, err.Error(), filepath.Join(rootPrefix, "etc", "containers", "policy.json")) + assert.Contains(t, err.Error(), filepath.Join(rootPrefix, "usr", "share", "containers", "policy.json")) + } return } diff --git a/storage/pkg/configfile/parse.go b/storage/pkg/configfile/parse.go index 1555e9060e..22631b3560 100644 --- a/storage/pkg/configfile/parse.go +++ b/storage/pkg/configfile/parse.go @@ -92,6 +92,13 @@ func getConfName(name, extension string, noExtension bool) string { // Expected ENOENT errors are already ignored in this function and must not be handled again by callers. // The given File options must not be nil and populated with valid options. func Read(conf *File) iter.Seq2[*Item, error] { + return ReadWithPaths(conf, nil) +} + +// ReadWithPaths behaves like [Read] but also records every file path it tried to open. +// +// usedPaths is populated during iteration (if not nil). +func ReadWithPaths(conf *File, usedPaths *[]string) iter.Seq2[*Item, error] { configFileName := getConfName(conf.Name, conf.Extension, conf.DoNotUseExtensionForConfigName) // Note this can be empty which is a valid case and should be simply ignored then. @@ -134,6 +141,9 @@ func Read(conf *File) iter.Seq2[*Item, error] { if conf.EnvironmentName != "" { if path := os.Getenv(conf.EnvironmentName); path != "" { + if usedPaths != nil { + *usedPaths = append(*usedPaths, path) + } f, err := os.Open(path) // Do not ignore ErrNotExist here, we want to hard error if users set a wrong path here. if err != nil { @@ -165,6 +175,9 @@ func Read(conf *File) iter.Seq2[*Item, error] { if path == "" { continue } + if usedPaths != nil { + *usedPaths = append(*usedPaths, path) + } f, err := os.Open(path) // only ignore ErrNotExist, all other errors get return to the caller via yield if err != nil { @@ -191,6 +204,9 @@ func Read(conf *File) iter.Seq2[*Item, error] { return } for _, file := range files { + if usedPaths != nil { + *usedPaths = append(*usedPaths, file) + } f, err := os.Open(file) // only ignore ErrNotExist, all other errors get return to the caller via yield if err != nil { @@ -211,7 +227,7 @@ func Read(conf *File) iter.Seq2[*Item, error] { dirs := moduleDirectories(defaultConfig, overrideConfig, userConfig) resolvedModules := make([]string, 0, len(conf.Modules)) for _, module := range conf.Modules { - f, err := resolveModule(module, dirs) + f, err := resolveModule(module, dirs, usedPaths) if err != nil { yield(nil, fmt.Errorf("could not resolve module: %w", err)) return @@ -227,6 +243,9 @@ func Read(conf *File) iter.Seq2[*Item, error] { if conf.EnvironmentName != "" && !conf.DoNotLoadDropInFiles { // The _OVERRIDE env must be appended after loading all files, even modules. if path := os.Getenv(conf.EnvironmentName + "_OVERRIDE"); path != "" { + if usedPaths != nil { + *usedPaths = append(*usedPaths, path) + } f, err := os.Open(path) // Do not ignore ErrNotExist here, we want to hard error if users set a wrong path here. if err != nil { @@ -324,8 +343,11 @@ func moduleDirectories(defaultConfig, overrideConfig, userConfig string) []strin } // Resolve the specified path to a module. -func resolveModule(path string, dirs []string) (*os.File, error) { +func resolveModule(path string, dirs []string, usedPaths *[]string) (*os.File, error) { if filepath.IsAbs(path) { + if usedPaths != nil { + *usedPaths = append(*usedPaths, path) + } return os.Open(path) } @@ -334,6 +356,9 @@ func resolveModule(path string, dirs []string) (*os.File, error) { var multiErr error for _, d := range dirs { candidate := filepath.Join(d, path) + if usedPaths != nil { + *usedPaths = append(*usedPaths, candidate) + } f, err := os.Open(candidate) if err == nil { diff --git a/storage/pkg/configfile/parse_test.go b/storage/pkg/configfile/parse_test.go index 245c77385e..a742bfe3f3 100644 --- a/storage/pkg/configfile/parse_test.go +++ b/storage/pkg/configfile/parse_test.go @@ -580,6 +580,148 @@ func Test_Read(t *testing.T) { } } +func Test_ReadWithPaths(t *testing.T) { + t.Run("main stop after first existing main file", func(t *testing.T) { + rootPrefix := t.TempDir() + + writeTestFiles(t, rootPrefix, testfiles{ + etc: map[string]string{ + "policy.json": "etc-policy", + }, + }) + + conf := File{ + Name: "policy", + Extension: "json", + DoNotLoadDropInFiles: true, + RootForImplicitAbsolutePaths: rootPrefix, + } + + var usedPaths []string + seq := ReadWithPaths(&conf, &usedPaths) + _ = collectConfigs(t, seq) + + configFileName := getConfName(conf.Name, conf.Extension, conf.DoNotUseExtensionForConfigName) + + userBase, err := UserConfigPath() + require.NoError(t, err) + expectedUserConfig := "" + if userBase != "" { + expectedUserConfig = filepath.Join(userBase, configFileName) + } + + expectedOverrideConfig := "" + if adminOverrideConfigPath != "" { + expectedOverrideConfig = filepath.Join(adminOverrideConfigPath, configFileName) + expectedOverrideConfig = filepath.Join(rootPrefix, expectedOverrideConfig) + } + + expectedDefaultConfig := "" + if systemConfigPath != "" { + expectedDefaultConfig = filepath.Join(systemConfigPath, configFileName) + expectedDefaultConfig = filepath.Join(rootPrefix, expectedDefaultConfig) + } + + require.NotEmpty(t, expectedUserConfig) + require.NotEmpty(t, expectedOverrideConfig) + assert.Equal(t, []string{expectedUserConfig, expectedOverrideConfig}, usedPaths) + if expectedDefaultConfig != "" { + assert.NotContains(t, usedPaths, expectedDefaultConfig) + } + }) + + t.Run("partial iteration only records attempted paths so far", func(t *testing.T) { + rootPrefix := t.TempDir() + tempHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempHome) + + etcBase := filepath.Join(rootPrefix, adminOverrideConfigPath) + require.NoError(t, os.MkdirAll(etcBase, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(etcBase, "containers.conf"), []byte("etc-main"), 0o600)) + + usrBase := filepath.Join(rootPrefix, systemConfigPath) + require.NoError(t, os.MkdirAll(filepath.Join(usrBase, "containers.conf.d"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(usrBase, "containers.conf.d", "01.conf"), []byte("drop-in"), 0o600)) + + conf := File{ + Name: "containers", + Extension: "conf", + RootForImplicitAbsolutePaths: rootPrefix, + } + + var usedPaths []string + seq := ReadWithPaths(&conf, &usedPaths) + + // Stop iteration immediately after the first yielded item (the main file). + seq(func(item *Item, err error) bool { + require.NoError(t, err) + require.NotNil(t, item) + return false + }) + + // The main candidates are attempted in order; since /etc exists it stops before consulting /usr. + userConfig := filepath.Join(tempHome, "containers", "containers.conf") + etcConfig := filepath.Join(etcBase, "containers.conf") + usrConfig := filepath.Join(usrBase, "containers.conf") + + assert.Equal(t, []string{userConfig, etcConfig}, usedPaths) + assert.NotContains(t, usedPaths, usrConfig) + assert.NotContains(t, usedPaths, filepath.Join(usrBase, "containers.conf.d", "01.conf")) + }) + + t.Run("env config skips main/drop-ins; ignores _OVERRIDE when drop-ins disabled", func(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + + envPath := filepath.Join(t.TempDir(), "env.json") + require.NoError(t, os.WriteFile(envPath, []byte("{}"), 0o600)) + + overridePath := filepath.Join(t.TempDir(), "override.json") + require.NoError(t, os.WriteFile(overridePath, []byte("{}"), 0o600)) + + t.Setenv("CONTAINERS_POLICY_JSON", envPath) + t.Setenv("CONTAINERS_POLICY_JSON_OVERRIDE", overridePath) + + conf := File{ + Name: "policy", + Extension: "json", + EnvironmentName: "CONTAINERS_POLICY_JSON", + DoNotLoadDropInFiles: true, + } + + var usedPaths []string + seq := ReadWithPaths(&conf, &usedPaths) + _ = collectConfigs(t, seq) + + assert.Equal(t, []string{envPath}, usedPaths) + }) + + t.Run("env config includes _OVERRIDE when drop-ins enabled", func(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + + envPath := filepath.Join(t.TempDir(), "env.json") + require.NoError(t, os.WriteFile(envPath, []byte("{}"), 0o600)) + + overridePath := filepath.Join(t.TempDir(), "override.json") + require.NoError(t, os.WriteFile(overridePath, []byte("{}"), 0o600)) + + t.Setenv("CONTAINERS_POLICY_JSON", envPath) + t.Setenv("CONTAINERS_POLICY_JSON_OVERRIDE", overridePath) + + conf := File{ + Name: "policy", + Extension: "json", + EnvironmentName: "CONTAINERS_POLICY_JSON", + DoNotLoadDropInFiles: false, + } + + var usedPaths []string + seq := ReadWithPaths(&conf, &usedPaths) + _ = collectConfigs(t, seq) + + assert.Equal(t, []string{envPath, overridePath}, usedPaths) + }) +} + func writeTestFiles(t *testing.T, tmpdir string, files testfiles) { t.Helper() usr := filepath.Join(tmpdir, systemConfigPath) From acc9008c6b6f4f06bb35577fc50781d4fbd39dd2 Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Mon, 30 Mar 2026 13:45:06 +0200 Subject: [PATCH 05/12] Add test for root + SignaturePolicyPath. Signed-off-by: Jan Kaluza --- image/signature/policy_config_test.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/image/signature/policy_config_test.go b/image/signature/policy_config_test.go index d28a53d8ca..b45363422a 100644 --- a/image/signature/policy_config_test.go +++ b/image/signature/policy_config_test.go @@ -147,6 +147,8 @@ func TestDefaultPolicy(t *testing.T) { require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) } + signaturePathWithRootSys := &types.SystemContext{} + for _, test := range []tc{ { name: "signature policy path override success", @@ -230,7 +232,8 @@ func TestDefaultPolicy(t *testing.T) { t.Setenv("CONTAINERS_POLICY_JSON", base) }, sys: &types.SystemContext{}, - expectPolicy: &prInsecureAcceptAnything{}, + useRootPrefix: true, + expectPolicy: &prInsecureAcceptAnything{}, }, { name: "containers policy conf read error", @@ -252,6 +255,26 @@ func TestDefaultPolicy(t *testing.T) { sys: &types.SystemContext{}, expectErr: true, }, + { + name: "signature policy path wins over root for implicit absolute paths", + setup: func(t *testing.T, rootPrefix string) { + tempHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempHome) + + // If SignaturePolicyPath were ignored, this would be used due to RootForImplicitAbsolutePaths. + mustWritePolicy(t, filepath.Join(rootPrefix, "etc", "containers", "policy.json"), rejectJSON) + + // SignaturePolicyPath is used as-is (not interpreted relative to RootForImplicitAbsolutePaths). + sigPath := filepath.Join(t.TempDir(), "signature-policy.json") + mustWritePolicy(t, sigPath, insecureJSON) + + signaturePathWithRootSys.RootForImplicitAbsolutePaths = rootPrefix + signaturePathWithRootSys.SignaturePolicyPath = sigPath + }, + sys: signaturePathWithRootSys, + useRootPrefix: true, + expectPolicy: &prInsecureAcceptAnything{}, + }, { name: "root for implicit absolute paths is honored", setup: func(t *testing.T, rootPrefix string) { @@ -293,7 +316,6 @@ func TestDefaultPolicy(t *testing.T) { require.NoError(t, err) require.NotNil(t, policy) - require.NotEmpty(t, policy.Default) switch expected := test.expectPolicy.(type) { case *Policy: From 63d0fc227d35a4d24fc94ece3d123c281ae0b1c9 Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Mon, 30 Mar 2026 13:58:05 +0200 Subject: [PATCH 06/12] Document DoNotLoadDropInFiles ignoring _OVERRIDE variables Signed-off-by: Jan Kaluza --- image/signature/policy_config_test.go | 4 ++-- storage/pkg/configfile/parse.go | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/image/signature/policy_config_test.go b/image/signature/policy_config_test.go index b45363422a..b465b4b283 100644 --- a/image/signature/policy_config_test.go +++ b/image/signature/policy_config_test.go @@ -231,7 +231,7 @@ func TestDefaultPolicy(t *testing.T) { mustWritePolicy(t, base, insecureJSON) t.Setenv("CONTAINERS_POLICY_JSON", base) }, - sys: &types.SystemContext{}, + sys: &types.SystemContext{}, useRootPrefix: true, expectPolicy: &prInsecureAcceptAnything{}, }, @@ -271,7 +271,7 @@ func TestDefaultPolicy(t *testing.T) { signaturePathWithRootSys.RootForImplicitAbsolutePaths = rootPrefix signaturePathWithRootSys.SignaturePolicyPath = sigPath }, - sys: signaturePathWithRootSys, + sys: signaturePathWithRootSys, useRootPrefix: true, expectPolicy: &prInsecureAcceptAnything{}, }, diff --git a/storage/pkg/configfile/parse.go b/storage/pkg/configfile/parse.go index 22631b3560..4222d4af76 100644 --- a/storage/pkg/configfile/parse.go +++ b/storage/pkg/configfile/parse.go @@ -55,6 +55,7 @@ type File struct { DoNotLoadMainFiles bool // DoNotLoadDropInFiles should be set if only the main files should be loaded. + // If DoNotLoadDropInFiles is set, the _OVERRIDE environment variable is ignored. DoNotLoadDropInFiles bool // DoNotUseExtensionForConfigName makes it so that the extension is only consulted for the drop in @@ -91,6 +92,8 @@ func getConfName(name, extension string, noExtension bool) string { // If an error is returned by the iterator then this must be treated as fatal error and must fail the config file parsing. // Expected ENOENT errors are already ignored in this function and must not be handled again by callers. // The given File options must not be nil and populated with valid options. +// +// The _OVERRIDE environment is ignored if DoNotLoadDropInFiles is set. func Read(conf *File) iter.Seq2[*Item, error] { return ReadWithPaths(conf, nil) } From 988bbf926230461c5b309567a0810e829e88a0d3 Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Tue, 31 Mar 2026 10:44:46 +0200 Subject: [PATCH 07/12] Add test for policy.json readall error Signed-off-by: Jan Kaluza --- image/signature/policy_config_test.go | 13 ++++++++++++- storage/pkg/configfile/parse_test.go | 3 --- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/image/signature/policy_config_test.go b/image/signature/policy_config_test.go index b465b4b283..3cd680828b 100644 --- a/image/signature/policy_config_test.go +++ b/image/signature/policy_config_test.go @@ -255,6 +255,18 @@ func TestDefaultPolicy(t *testing.T) { sys: &types.SystemContext{}, expectErr: true, }, + { + name: "containers policy conf readall error", + setup: func(t *testing.T, _ string) { + tempHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempHome) + // Point the env to a directory so io.ReadAll fails when reading it. + dir := t.TempDir() + t.Setenv("CONTAINERS_POLICY_JSON", dir) + }, + sys: &types.SystemContext{}, + expectErr: true, + }, { name: "signature policy path wins over root for implicit absolute paths", setup: func(t *testing.T, rootPrefix string) { @@ -315,7 +327,6 @@ func TestDefaultPolicy(t *testing.T) { } require.NoError(t, err) - require.NotNil(t, policy) switch expected := test.expectPolicy.(type) { case *Policy: diff --git a/storage/pkg/configfile/parse_test.go b/storage/pkg/configfile/parse_test.go index a742bfe3f3..69dd36a1cb 100644 --- a/storage/pkg/configfile/parse_test.go +++ b/storage/pkg/configfile/parse_test.go @@ -662,11 +662,8 @@ func Test_ReadWithPaths(t *testing.T) { // The main candidates are attempted in order; since /etc exists it stops before consulting /usr. userConfig := filepath.Join(tempHome, "containers", "containers.conf") etcConfig := filepath.Join(etcBase, "containers.conf") - usrConfig := filepath.Join(usrBase, "containers.conf") assert.Equal(t, []string{userConfig, etcConfig}, usedPaths) - assert.NotContains(t, usedPaths, usrConfig) - assert.NotContains(t, usedPaths, filepath.Join(usrBase, "containers.conf.d", "01.conf")) }) t.Run("env config skips main/drop-ins; ignores _OVERRIDE when drop-ins disabled", func(t *testing.T) { From 81f5682c18148970a04fe0ab6f48f5434ff36b92 Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Tue, 31 Mar 2026 11:44:22 +0200 Subject: [PATCH 08/12] Single test for both Read and ReadWithPaths. Signed-off-by: Jan Kaluza --- storage/pkg/configfile/parse.go | 2 + storage/pkg/configfile/parse_test.go | 325 ++++++++++++++------------- 2 files changed, 168 insertions(+), 159 deletions(-) diff --git a/storage/pkg/configfile/parse.go b/storage/pkg/configfile/parse.go index 4222d4af76..5854e3c4ed 100644 --- a/storage/pkg/configfile/parse.go +++ b/storage/pkg/configfile/parse.go @@ -43,6 +43,8 @@ type File struct { Extension string // EnvironmentName is the name of environment variable that can be set to specify the override. + // If EnvironmentName is set, the variable with _OVERRIDE suffix is also checked for an override + // unless DoNotLoadDropInFiles is set. // Optional. EnvironmentName string diff --git a/storage/pkg/configfile/parse_test.go b/storage/pkg/configfile/parse_test.go index 69dd36a1cb..2738554944 100644 --- a/storage/pkg/configfile/parse_test.go +++ b/storage/pkg/configfile/parse_test.go @@ -6,6 +6,7 @@ import ( "iter" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -115,6 +116,8 @@ func Test_Read(t *testing.T) { want []string // wantErr is the error type matched with errors.Is() is the function should error instead wantErr error + // wantPaths contains the expected list of filesystem paths that ReadWithPaths should record. + wantPaths []string } tests := []testcase{ @@ -125,6 +128,11 @@ func Test_Read(t *testing.T) { Extension: "conf", }, want: nil, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + }, }, { name: "simple main file", @@ -140,6 +148,11 @@ func Test_Read(t *testing.T) { }, }, want: []string{"content1"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + }, }, { name: "etc overrides usr file", @@ -156,6 +169,10 @@ func Test_Read(t *testing.T) { }, }, want: []string{"file2"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + }, }, { name: "home overrides etc and usr file", @@ -175,6 +192,9 @@ func Test_Read(t *testing.T) { }, }, want: []string{"home"}, + wantPaths: []string{ + "/home/containers/containers.conf", + }, }, { name: "single drop in", @@ -188,6 +208,12 @@ func Test_Read(t *testing.T) { }, }, want: []string{"content1"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + "/usr/share/containers/containers.conf.d/10-myconf.conf", + }, }, { name: "drop in and main file", @@ -203,6 +229,12 @@ func Test_Read(t *testing.T) { }, }, want: []string{"file1", "file2"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + "/usr/share/containers/containers.conf.d/10-myconf.conf", + }, }, { name: "drop in and main file on different paths", @@ -220,6 +252,11 @@ func Test_Read(t *testing.T) { }, }, want: []string{"etc", "usr"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf.d/10-myconf.conf", + }, }, { name: "drop in order", @@ -241,6 +278,15 @@ func Test_Read(t *testing.T) { }, }, want: []string{"1", "2", "3", "4"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + "/etc/containers/containers.conf.d/10-conf1.conf", + "/usr/share/containers/containers.conf.d/20-conf2.conf", + "/home/containers/containers.conf.d/30-conf3.conf", + "/usr/share/containers/containers.conf.d/40-conf4.conf", + }, }, { name: "drop in override", @@ -260,6 +306,13 @@ func Test_Read(t *testing.T) { }, }, want: []string{"etc-override", "usr-content-2"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + "/etc/containers/containers.conf.d/10-settings.conf", + "/usr/share/containers/containers.conf.d/20-settings.conf", + }, }, { name: "drop in ignores wrong extensions", @@ -275,6 +328,12 @@ func Test_Read(t *testing.T) { }, }, want: []string{"valid"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + "/usr/share/containers/containers.conf.d/10-valid.conf", + }, }, { name: "policy.json main files only (ignore drop-ins)", @@ -290,6 +349,11 @@ func Test_Read(t *testing.T) { }, }, want: []string{"main"}, + wantPaths: []string{ + "/home/containers/policy.json", + "/etc/containers/policy.json", + "/usr/share/containers/policy.json", + }, }, { name: "registries.d drop ins only (ignore main)", @@ -306,6 +370,9 @@ func Test_Read(t *testing.T) { }, }, want: []string{"drop-in"}, + wantPaths: []string{ + "/usr/share/containers/registries.d/10-extra.yaml", + }, }, { name: "rootless specific drop-ins", @@ -322,6 +389,13 @@ func Test_Read(t *testing.T) { }, }, want: []string{"global", "rootless-specific"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + "/usr/share/containers/containers.conf.d/01-global.conf", + "/usr/share/containers/containers.rootless.conf.d/02-user.conf", + }, }, { name: "rootless uid specific drop-ins", @@ -337,6 +411,12 @@ func Test_Read(t *testing.T) { }, }, want: []string{"uid-1000"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + "/usr/share/containers/containers.rootless.conf.d/1000/settings.conf", + }, }, { name: "containers.conf env var not being set", @@ -351,6 +431,11 @@ func Test_Read(t *testing.T) { }, }, want: []string{"content1"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + }, }, { name: "containers.conf env var must override all files", @@ -373,6 +458,9 @@ func Test_Read(t *testing.T) { t.Setenv("CONTAINERS_CONF", file) }, want: []string{"env"}, + wantPaths: []string{ + "/somepath", + }, }, { name: "containers.conf override env var should be appended", @@ -394,6 +482,13 @@ func Test_Read(t *testing.T) { t.Setenv("CONTAINERS_CONF_OVERRIDE", file) }, want: []string{"content1", "01", "env"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + "/usr/share/containers/containers.conf.d/01.conf", + "/somepath", + }, }, { name: "containers.conf both env var should be appended", @@ -420,6 +515,10 @@ func Test_Read(t *testing.T) { t.Setenv("CONTAINERS_CONF_OVERRIDE", file2) }, want: []string{"env1", "env2"}, + wantPaths: []string{ + "/path1", + "/path1", + }, }, { name: "env var should error on non existing file", @@ -432,7 +531,8 @@ func Test_Read(t *testing.T) { file := filepath.Join(t.TempDir(), "123") t.Setenv("CONTAINERS_CONF", file) }, - wantErr: fs.ErrNotExist, + wantErr: fs.ErrNotExist, + wantPaths: nil, }, { name: "override env var should error on non existing file", @@ -446,6 +546,10 @@ func Test_Read(t *testing.T) { t.Setenv("CONTAINERS_CONF_OVERRIDE", file) }, wantErr: fs.ErrNotExist, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + }, }, { name: "containers.conf with modules", @@ -471,6 +575,13 @@ func Test_Read(t *testing.T) { tc.arg.Modules = append(tc.arg.Modules, file) }, want: []string{"content1", "relative module", "absolute module"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + "/home/containers/containers.conf.modules/module.abc", + "/somepath", + }, }, { name: "containers.conf with module override", @@ -492,6 +603,13 @@ func Test_Read(t *testing.T) { }, }, want: []string{"home", "etc"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + "/home/containers/containers.conf.modules/module.conf", + "/etc/containers/containers.conf.modules/different.conf", + }, }, { // same as above except we switch the module order to ensure we read the files in the proper order as given @@ -514,6 +632,13 @@ func Test_Read(t *testing.T) { }, }, want: []string{"etc", "home"}, + wantPaths: []string{ + "/home/containers/containers.conf", + "/etc/containers/containers.conf", + "/usr/share/containers/containers.conf", + "/etc/containers/containers.conf.modules/different.conf", + "/home/containers/containers.conf.modules/module.conf", + }, }, { name: "containers.conf env and modules order", @@ -544,6 +669,11 @@ func Test_Read(t *testing.T) { }, // CONTAINERS_CONF, then modules, then CONTAINERS_CONF_OVERRIDE want: []string{"env1", "mod", "env2"}, + wantPaths: []string{ + "/path1", + "/usr/share/containers/containers.conf.modules/module.conf", + "/path1", + }, }, } @@ -554,171 +684,48 @@ func Test_Read(t *testing.T) { if tt.setup != nil { tt.setup(t, &tt) } - seq := Read(&tt.arg) - if tt.wantErr == nil { - confs := collectConfigs(t, seq) - assert.Equal(t, tt.want, confs) - // ensure the modules all get resolves to absolute paths and are valid - for _, module := range tt.arg.Modules { - assert.FileExists(t, module) - assert.True(t, filepath.IsAbs(module)) + for _, useReadWithPaths := range []bool{false, true} { + var usedPaths []string + var seq iter.Seq2[*Item, error] + if useReadWithPaths { + seq = ReadWithPaths(&tt.arg, &usedPaths) + } else { + seq = Read(&tt.arg) + } + if tt.wantErr == nil { + confs := collectConfigs(t, seq) + assert.Equal(t, tt.want, confs) + + if useReadWithPaths && tt.wantErr == nil { + require.Equal(t, len(tt.wantPaths), len(usedPaths)) + for i, want := range tt.wantPaths { + assert.Truef(t, strings.HasSuffix(usedPaths[i], want), "path %q does not end with %q", usedPaths[i], want) + } + } + + // ensure the modules all get resolves to absolute paths and are valid + for _, module := range tt.arg.Modules { + assert.FileExists(t, module) + assert.True(t, filepath.IsAbs(module)) + } + } else { + next, stop := iter.Pull2(seq) + defer stop() + + _, err, ok := next() + assert.True(t, ok) + assert.ErrorIs(t, err, tt.wantErr) + + // end of iterator + _, _, ok = next() + assert.False(t, ok) } - } else { - next, stop := iter.Pull2(seq) - defer stop() - - _, err, ok := next() - assert.True(t, ok) - assert.ErrorIs(t, err, tt.wantErr) - - // end of iterator - _, _, ok = next() - assert.False(t, ok) } }) } } -func Test_ReadWithPaths(t *testing.T) { - t.Run("main stop after first existing main file", func(t *testing.T) { - rootPrefix := t.TempDir() - - writeTestFiles(t, rootPrefix, testfiles{ - etc: map[string]string{ - "policy.json": "etc-policy", - }, - }) - - conf := File{ - Name: "policy", - Extension: "json", - DoNotLoadDropInFiles: true, - RootForImplicitAbsolutePaths: rootPrefix, - } - - var usedPaths []string - seq := ReadWithPaths(&conf, &usedPaths) - _ = collectConfigs(t, seq) - - configFileName := getConfName(conf.Name, conf.Extension, conf.DoNotUseExtensionForConfigName) - - userBase, err := UserConfigPath() - require.NoError(t, err) - expectedUserConfig := "" - if userBase != "" { - expectedUserConfig = filepath.Join(userBase, configFileName) - } - - expectedOverrideConfig := "" - if adminOverrideConfigPath != "" { - expectedOverrideConfig = filepath.Join(adminOverrideConfigPath, configFileName) - expectedOverrideConfig = filepath.Join(rootPrefix, expectedOverrideConfig) - } - - expectedDefaultConfig := "" - if systemConfigPath != "" { - expectedDefaultConfig = filepath.Join(systemConfigPath, configFileName) - expectedDefaultConfig = filepath.Join(rootPrefix, expectedDefaultConfig) - } - - require.NotEmpty(t, expectedUserConfig) - require.NotEmpty(t, expectedOverrideConfig) - assert.Equal(t, []string{expectedUserConfig, expectedOverrideConfig}, usedPaths) - if expectedDefaultConfig != "" { - assert.NotContains(t, usedPaths, expectedDefaultConfig) - } - }) - - t.Run("partial iteration only records attempted paths so far", func(t *testing.T) { - rootPrefix := t.TempDir() - tempHome := t.TempDir() - t.Setenv("XDG_CONFIG_HOME", tempHome) - - etcBase := filepath.Join(rootPrefix, adminOverrideConfigPath) - require.NoError(t, os.MkdirAll(etcBase, 0o755)) - require.NoError(t, os.WriteFile(filepath.Join(etcBase, "containers.conf"), []byte("etc-main"), 0o600)) - - usrBase := filepath.Join(rootPrefix, systemConfigPath) - require.NoError(t, os.MkdirAll(filepath.Join(usrBase, "containers.conf.d"), 0o755)) - require.NoError(t, os.WriteFile(filepath.Join(usrBase, "containers.conf.d", "01.conf"), []byte("drop-in"), 0o600)) - - conf := File{ - Name: "containers", - Extension: "conf", - RootForImplicitAbsolutePaths: rootPrefix, - } - - var usedPaths []string - seq := ReadWithPaths(&conf, &usedPaths) - - // Stop iteration immediately after the first yielded item (the main file). - seq(func(item *Item, err error) bool { - require.NoError(t, err) - require.NotNil(t, item) - return false - }) - - // The main candidates are attempted in order; since /etc exists it stops before consulting /usr. - userConfig := filepath.Join(tempHome, "containers", "containers.conf") - etcConfig := filepath.Join(etcBase, "containers.conf") - - assert.Equal(t, []string{userConfig, etcConfig}, usedPaths) - }) - - t.Run("env config skips main/drop-ins; ignores _OVERRIDE when drop-ins disabled", func(t *testing.T) { - t.Setenv("XDG_CONFIG_HOME", t.TempDir()) - - envPath := filepath.Join(t.TempDir(), "env.json") - require.NoError(t, os.WriteFile(envPath, []byte("{}"), 0o600)) - - overridePath := filepath.Join(t.TempDir(), "override.json") - require.NoError(t, os.WriteFile(overridePath, []byte("{}"), 0o600)) - - t.Setenv("CONTAINERS_POLICY_JSON", envPath) - t.Setenv("CONTAINERS_POLICY_JSON_OVERRIDE", overridePath) - - conf := File{ - Name: "policy", - Extension: "json", - EnvironmentName: "CONTAINERS_POLICY_JSON", - DoNotLoadDropInFiles: true, - } - - var usedPaths []string - seq := ReadWithPaths(&conf, &usedPaths) - _ = collectConfigs(t, seq) - - assert.Equal(t, []string{envPath}, usedPaths) - }) - - t.Run("env config includes _OVERRIDE when drop-ins enabled", func(t *testing.T) { - t.Setenv("XDG_CONFIG_HOME", t.TempDir()) - - envPath := filepath.Join(t.TempDir(), "env.json") - require.NoError(t, os.WriteFile(envPath, []byte("{}"), 0o600)) - - overridePath := filepath.Join(t.TempDir(), "override.json") - require.NoError(t, os.WriteFile(overridePath, []byte("{}"), 0o600)) - - t.Setenv("CONTAINERS_POLICY_JSON", envPath) - t.Setenv("CONTAINERS_POLICY_JSON_OVERRIDE", overridePath) - - conf := File{ - Name: "policy", - Extension: "json", - EnvironmentName: "CONTAINERS_POLICY_JSON", - DoNotLoadDropInFiles: false, - } - - var usedPaths []string - seq := ReadWithPaths(&conf, &usedPaths) - _ = collectConfigs(t, seq) - - assert.Equal(t, []string{envPath, overridePath}, usedPaths) - }) -} - func writeTestFiles(t *testing.T, tmpdir string, files testfiles) { t.Helper() usr := filepath.Join(tmpdir, systemConfigPath) From 4ad0836d143fadd3a074cde9f56358be3adfabb7 Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Tue, 31 Mar 2026 14:05:23 +0200 Subject: [PATCH 09/12] Improve the TestDefaultPolicy tests. This commit does the following: - The `setup()` function now returns the SystemContext. - The hardcoded check for "no policy file found" has moved to the test itself. - Fixture is used for SignaturePolicyPath. Signed-off-by: Jan Kaluza --- image/signature/policy_config_test.go | 120 +++++++++++--------------- 1 file changed, 52 insertions(+), 68 deletions(-) diff --git a/image/signature/policy_config_test.go b/image/signature/policy_config_test.go index 3cd680828b..47e58590d2 100644 --- a/image/signature/policy_config_test.go +++ b/image/signature/policy_config_test.go @@ -134,9 +134,7 @@ func TestDefaultPolicy(t *testing.T) { type tc struct { name string - setup func(t *testing.T, rootPrefix string) - sys *types.SystemContext - useRootPrefix bool + setup func(t *testing.T, rootPrefix string) *types.SystemContext expectPolicy any // *Policy, *prReject, *prInsecureAcceptAnything expectErr bool expectErrMatch string @@ -147,166 +145,159 @@ func TestDefaultPolicy(t *testing.T) { require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) } - signaturePathWithRootSys := &types.SystemContext{} - for _, test := range []tc{ { name: "signature policy path override success", - setup: func(t *testing.T, _ string) { - // no-op + setup: func(t *testing.T, _ string) *types.SystemContext { + return &types.SystemContext{SignaturePolicyPath: "./fixtures/policy.json"} }, - sys: &types.SystemContext{SignaturePolicyPath: "./fixtures/policy.json"}, expectPolicy: policyFixtureContents, }, { name: "signature policy path override read error", - setup: func(t *testing.T, _ string) { - // no-op + setup: func(t *testing.T, _ string) *types.SystemContext { + return &types.SystemContext{SignaturePolicyPath: "/this/does/not/exist"} }, - sys: &types.SystemContext{SignaturePolicyPath: "/this/does/not/exist"}, expectErr: true, }, { name: "signature policy path override parse error", - setup: func(t *testing.T, _ string) { - // no-op + setup: func(t *testing.T, _ string) *types.SystemContext { + return &types.SystemContext{SignaturePolicyPath: "/dev/null"} }, - sys: &types.SystemContext{SignaturePolicyPath: "/dev/null"}, expectErr: true, }, { name: "user wins over etc and usr", - setup: func(t *testing.T, rootPrefix string) { + setup: func(t *testing.T, rootPrefix string) *types.SystemContext { tempHome := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tempHome) mustWritePolicy(t, filepath.Join(tempHome, "containers", "policy.json"), insecureJSON) mustWritePolicy(t, filepath.Join(rootPrefix, "etc", "containers", "policy.json"), rejectJSON) mustWritePolicy(t, filepath.Join(rootPrefix, "usr", "share", "containers", "policy.json"), rejectJSON) + return &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + } }, - sys: &types.SystemContext{}, - useRootPrefix: true, - expectPolicy: &prInsecureAcceptAnything{}, + expectPolicy: &prInsecureAcceptAnything{}, }, { name: "etc fallback when user missing", - setup: func(t *testing.T, rootPrefix string) { + setup: func(t *testing.T, rootPrefix string) *types.SystemContext { tempHome := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tempHome) mustWritePolicy(t, filepath.Join(rootPrefix, "etc", "containers", "policy.json"), rejectJSON) mustWritePolicy(t, filepath.Join(rootPrefix, "usr", "share", "containers", "policy.json"), insecureJSON) + return &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + } }, - sys: &types.SystemContext{}, - useRootPrefix: true, - expectPolicy: &prReject{}, + expectPolicy: &prReject{}, }, { name: "usr fallback when only usr present", - setup: func(t *testing.T, rootPrefix string) { + setup: func(t *testing.T, rootPrefix string) *types.SystemContext { tempHome := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tempHome) mustWritePolicy(t, filepath.Join(rootPrefix, "usr", "share", "containers", "policy.json"), insecureJSON) + return &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + } }, - sys: &types.SystemContext{}, - useRootPrefix: true, - expectPolicy: &prInsecureAcceptAnything{}, + expectPolicy: &prInsecureAcceptAnything{}, }, { name: "no policy file found", - setup: func(t *testing.T, _ string) { - tempHome := t.TempDir() - t.Setenv("XDG_CONFIG_HOME", tempHome) + setup: func(t *testing.T, rootPrefix string) *types.SystemContext { + t.Setenv("XDG_CONFIG_HOME", "/tmp") + return &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + } }, - sys: &types.SystemContext{}, - useRootPrefix: true, expectErr: true, - expectErrMatch: "no policy.json file found", + expectErrMatch: "no policy.json file found; searched paths: /tmp/containers/policy.json, ", }, { name: "containers policy conf override base file", - setup: func(t *testing.T, rootPrefix string) { + setup: func(t *testing.T, rootPrefix string) *types.SystemContext { tempHome := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tempHome) mustWritePolicy(t, filepath.Join(rootPrefix, "etc", "containers", "policy.json"), rejectJSON) base := filepath.Join(t.TempDir(), "env-base.json") mustWritePolicy(t, base, insecureJSON) t.Setenv("CONTAINERS_POLICY_JSON", base) + return &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + } }, - sys: &types.SystemContext{}, - useRootPrefix: true, - expectPolicy: &prInsecureAcceptAnything{}, + expectPolicy: &prInsecureAcceptAnything{}, }, { name: "containers policy conf read error", - setup: func(t *testing.T, _ string) { + setup: func(t *testing.T, _ string) *types.SystemContext { tempHome := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tempHome) t.Setenv("CONTAINERS_POLICY_JSON", "/this/does/not/exist") + return &types.SystemContext{} }, - sys: &types.SystemContext{}, expectErr: true, }, { name: "containers policy conf parse error", - setup: func(t *testing.T, _ string) { + setup: func(t *testing.T, _ string) *types.SystemContext { tempHome := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tempHome) t.Setenv("CONTAINERS_POLICY_JSON", "/dev/null") + return &types.SystemContext{} }, - sys: &types.SystemContext{}, expectErr: true, }, { name: "containers policy conf readall error", - setup: func(t *testing.T, _ string) { + setup: func(t *testing.T, _ string) *types.SystemContext { tempHome := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tempHome) // Point the env to a directory so io.ReadAll fails when reading it. dir := t.TempDir() t.Setenv("CONTAINERS_POLICY_JSON", dir) + return &types.SystemContext{} }, - sys: &types.SystemContext{}, expectErr: true, }, { name: "signature policy path wins over root for implicit absolute paths", - setup: func(t *testing.T, rootPrefix string) { + setup: func(t *testing.T, rootPrefix string) *types.SystemContext { tempHome := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tempHome) // If SignaturePolicyPath were ignored, this would be used due to RootForImplicitAbsolutePaths. mustWritePolicy(t, filepath.Join(rootPrefix, "etc", "containers", "policy.json"), rejectJSON) - // SignaturePolicyPath is used as-is (not interpreted relative to RootForImplicitAbsolutePaths). - sigPath := filepath.Join(t.TempDir(), "signature-policy.json") - mustWritePolicy(t, sigPath, insecureJSON) - - signaturePathWithRootSys.RootForImplicitAbsolutePaths = rootPrefix - signaturePathWithRootSys.SignaturePolicyPath = sigPath + return &types.SystemContext{ + SignaturePolicyPath: "./fixtures/policy.json", + RootForImplicitAbsolutePaths: rootPrefix, + } }, - sys: signaturePathWithRootSys, - useRootPrefix: true, - expectPolicy: &prInsecureAcceptAnything{}, + expectPolicy: policyFixtureContents, }, { name: "root for implicit absolute paths is honored", - setup: func(t *testing.T, rootPrefix string) { + setup: func(t *testing.T, rootPrefix string) *types.SystemContext { tempHome := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tempHome) mustWritePolicy(t, filepath.Join(rootPrefix, "etc", "containers", "policy.json"), rejectJSON) + return &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + } }, - sys: &types.SystemContext{}, - useRootPrefix: true, - expectPolicy: &prReject{}, + expectPolicy: &prReject{}, }, } { t.Run(test.name, func(t *testing.T) { rootPrefix := t.TempDir() + var sys *types.SystemContext if test.setup != nil { - test.setup(t, rootPrefix) - } - sys := test.sys - if test.useRootPrefix && sys != nil && sys.RootForImplicitAbsolutePaths == "" && sys.SignaturePolicyPath == "" { - sys = &types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix} + sys = test.setup(t, rootPrefix) } policy, err := DefaultPolicy(sys) @@ -316,13 +307,6 @@ func TestDefaultPolicy(t *testing.T) { if test.expectErrMatch != "" { assert.Contains(t, err.Error(), test.expectErrMatch) } - if test.name == "no policy file found" { - assert.Contains(t, err.Error(), "; searched paths:") - // The search paths include user config (XDG_CONFIG_HOME), etc and usr. - assert.Contains(t, err.Error(), filepath.Join(os.Getenv("XDG_CONFIG_HOME"), "containers", "policy.json")) - assert.Contains(t, err.Error(), filepath.Join(rootPrefix, "etc", "containers", "policy.json")) - assert.Contains(t, err.Error(), filepath.Join(rootPrefix, "usr", "share", "containers", "policy.json")) - } return } From b57552602577e0cc4d352b514436637e990d7a82 Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Tue, 31 Mar 2026 14:14:56 +0200 Subject: [PATCH 10/12] Remove useless policy_paths_*.go files. Signed-off-by: Jan Kaluza --- image/signature/policy_paths_common.go | 3 --- image/signature/policy_paths_freebsd.go | 3 --- 2 files changed, 6 deletions(-) delete mode 100644 image/signature/policy_paths_common.go delete mode 100644 image/signature/policy_paths_freebsd.go diff --git a/image/signature/policy_paths_common.go b/image/signature/policy_paths_common.go deleted file mode 100644 index 6c250349d5..0000000000 --- a/image/signature/policy_paths_common.go +++ /dev/null @@ -1,3 +0,0 @@ -//go:build !freebsd - -package signature diff --git a/image/signature/policy_paths_freebsd.go b/image/signature/policy_paths_freebsd.go deleted file mode 100644 index 81acbaa9b9..0000000000 --- a/image/signature/policy_paths_freebsd.go +++ /dev/null @@ -1,3 +0,0 @@ -//go:build freebsd - -package signature From ff4e0c001724648fc997514ec719c46910b77c7d Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Thu, 2 Apr 2026 13:01:23 +0200 Subject: [PATCH 11/12] return error in Read() if ErrorIfNotFound is true This commit introduces `File.ErrorIfNotFound`. If it is true, the Read returns an error which contains all the paths it tried when searching for a config file. The ReadWithPaths is replaced by this new logic. Signed-off-by: Jan Kaluza --- image/signature/policy_config.go | 12 +- image/signature/policy_config_test.go | 2 +- storage/pkg/configfile/parse.go | 37 +++-- storage/pkg/configfile/parse_test.go | 195 +++++--------------------- 4 files changed, 53 insertions(+), 193 deletions(-) diff --git a/image/signature/policy_config.go b/image/signature/policy_config.go index 302e61009e..7d54b4f5f5 100644 --- a/image/signature/policy_config.go +++ b/image/signature/policy_config.go @@ -19,7 +19,6 @@ import ( "fmt" "io" "os" - "strings" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/signature/internal" @@ -58,11 +57,11 @@ func DefaultPolicy(sys *types.SystemContext) (*Policy, error) { DoNotLoadDropInFiles: true, EnvironmentName: "CONTAINERS_POLICY_JSON", RootForImplicitAbsolutePaths: rootForImplicitAbsPaths, + ErrorIfNotFound: true, } var policy *Policy - var usedPaths []string - for item, err := range configfile.ReadWithPaths(&policyFiles, &usedPaths) { + for item, err := range configfile.Read(&policyFiles) { if err != nil { return nil, err } @@ -77,13 +76,6 @@ func DefaultPolicy(sys *types.SystemContext) (*Policy, error) { } } - if policy == nil { - if len(usedPaths) > 0 { - return nil, fmt.Errorf("no policy.json file found; searched paths: %s", strings.Join(usedPaths, ", ")) - } - return nil, fmt.Errorf("no policy.json file found") - } - return policy, nil } diff --git a/image/signature/policy_config_test.go b/image/signature/policy_config_test.go index 47e58590d2..355b7c796f 100644 --- a/image/signature/policy_config_test.go +++ b/image/signature/policy_config_test.go @@ -215,7 +215,7 @@ func TestDefaultPolicy(t *testing.T) { } }, expectErr: true, - expectErrMatch: "no policy.json file found; searched paths: /tmp/containers/policy.json, ", + expectErrMatch: "no policy.json file found; searched paths: [\"/tmp/containers/policy.json\" ", }, { name: "containers policy conf override base file", diff --git a/storage/pkg/configfile/parse.go b/storage/pkg/configfile/parse.go index 5854e3c4ed..414dc245bf 100644 --- a/storage/pkg/configfile/parse.go +++ b/storage/pkg/configfile/parse.go @@ -73,6 +73,9 @@ type File struct { // For compatibility reasons this field is written to with the fully resolved paths // of each module as this is what podman expects today. Modules []string + + // ErrorIfNotFound is true if an error should be returned if no file is found. + ErrorIfNotFound bool } // Item is a single config file that is being read once at a time and returned by the iterator from [Read]. @@ -97,13 +100,6 @@ func getConfName(name, extension string, noExtension bool) string { // // The _OVERRIDE environment is ignored if DoNotLoadDropInFiles is set. func Read(conf *File) iter.Seq2[*Item, error] { - return ReadWithPaths(conf, nil) -} - -// ReadWithPaths behaves like [Read] but also records every file path it tried to open. -// -// usedPaths is populated during iteration (if not nil). -func ReadWithPaths(conf *File, usedPaths *[]string) iter.Seq2[*Item, error] { configFileName := getConfName(conf.Name, conf.Extension, conf.DoNotUseExtensionForConfigName) // Note this can be empty which is a valid case and should be simply ignored then. @@ -125,10 +121,14 @@ func ReadWithPaths(conf *File, usedPaths *[]string) iter.Seq2[*Item, error] { } return func(yield func(*Item, error) bool) { + usedPaths := make([]string, 0, 8) + foundAny := false + shouldLoadMainFile := !conf.DoNotLoadMainFiles shouldLoadDropIns := !conf.DoNotLoadDropInFiles yieldAndClose := func(f *os.File) bool { + foundAny = true ok := yield(&Item{ Reader: f, Name: f.Name(), @@ -146,9 +146,7 @@ func ReadWithPaths(conf *File, usedPaths *[]string) iter.Seq2[*Item, error] { if conf.EnvironmentName != "" { if path := os.Getenv(conf.EnvironmentName); path != "" { - if usedPaths != nil { - *usedPaths = append(*usedPaths, path) - } + usedPaths = append(usedPaths, path) f, err := os.Open(path) // Do not ignore ErrNotExist here, we want to hard error if users set a wrong path here. if err != nil { @@ -180,9 +178,7 @@ func ReadWithPaths(conf *File, usedPaths *[]string) iter.Seq2[*Item, error] { if path == "" { continue } - if usedPaths != nil { - *usedPaths = append(*usedPaths, path) - } + usedPaths = append(usedPaths, path) f, err := os.Open(path) // only ignore ErrNotExist, all other errors get return to the caller via yield if err != nil { @@ -209,9 +205,7 @@ func ReadWithPaths(conf *File, usedPaths *[]string) iter.Seq2[*Item, error] { return } for _, file := range files { - if usedPaths != nil { - *usedPaths = append(*usedPaths, file) - } + usedPaths = append(usedPaths, file) f, err := os.Open(file) // only ignore ErrNotExist, all other errors get return to the caller via yield if err != nil { @@ -232,7 +226,7 @@ func ReadWithPaths(conf *File, usedPaths *[]string) iter.Seq2[*Item, error] { dirs := moduleDirectories(defaultConfig, overrideConfig, userConfig) resolvedModules := make([]string, 0, len(conf.Modules)) for _, module := range conf.Modules { - f, err := resolveModule(module, dirs, usedPaths) + f, err := resolveModule(module, dirs, &usedPaths) if err != nil { yield(nil, fmt.Errorf("could not resolve module: %w", err)) return @@ -248,9 +242,7 @@ func ReadWithPaths(conf *File, usedPaths *[]string) iter.Seq2[*Item, error] { if conf.EnvironmentName != "" && !conf.DoNotLoadDropInFiles { // The _OVERRIDE env must be appended after loading all files, even modules. if path := os.Getenv(conf.EnvironmentName + "_OVERRIDE"); path != "" { - if usedPaths != nil { - *usedPaths = append(*usedPaths, path) - } + usedPaths = append(usedPaths, path) f, err := os.Open(path) // Do not ignore ErrNotExist here, we want to hard error if users set a wrong path here. if err != nil { @@ -262,6 +254,11 @@ func ReadWithPaths(conf *File, usedPaths *[]string) iter.Seq2[*Item, error] { } } } + + if conf.ErrorIfNotFound && !foundAny { + yield(nil, fmt.Errorf("no %s file found; searched paths: %q", configFileName, usedPaths)) + return + } } } diff --git a/storage/pkg/configfile/parse_test.go b/storage/pkg/configfile/parse_test.go index 2738554944..0d3975f6bc 100644 --- a/storage/pkg/configfile/parse_test.go +++ b/storage/pkg/configfile/parse_test.go @@ -6,7 +6,6 @@ import ( "iter" "os" "path/filepath" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -114,10 +113,10 @@ func Test_Read(t *testing.T) { setup func(t *testing.T, tc *testcase) // Expected result, file content in right order. want []string - // wantErr is the error type matched with errors.Is() is the function should error instead + // wantErr is matched with errors.Is() when the function should error instead. wantErr error - // wantPaths contains the expected list of filesystem paths that ReadWithPaths should record. - wantPaths []string + // wantErrContains, if non-empty, asserts a substring of the error string instead of wantErr. + wantErrContains string } tests := []testcase{ @@ -128,11 +127,16 @@ func Test_Read(t *testing.T) { Extension: "conf", }, want: nil, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", + }, + { + name: "no files error if not found", + arg: File{ + Name: "containers", + Extension: "conf", + ErrorIfNotFound: true, }, + // Read records real paths (under RootForImplicitAbsolutePaths / XDG); the message is fmt.Errorf(..., %q, usedPaths). + wantErrContains: "no containers.conf file found; searched paths:", }, { name: "simple main file", @@ -148,11 +152,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"content1"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", - }, }, { name: "etc overrides usr file", @@ -169,10 +168,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"file2"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - }, }, { name: "home overrides etc and usr file", @@ -192,9 +187,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"home"}, - wantPaths: []string{ - "/home/containers/containers.conf", - }, }, { name: "single drop in", @@ -208,12 +200,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"content1"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", - "/usr/share/containers/containers.conf.d/10-myconf.conf", - }, }, { name: "drop in and main file", @@ -229,12 +215,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"file1", "file2"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", - "/usr/share/containers/containers.conf.d/10-myconf.conf", - }, }, { name: "drop in and main file on different paths", @@ -252,11 +232,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"etc", "usr"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf.d/10-myconf.conf", - }, }, { name: "drop in order", @@ -278,15 +253,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"1", "2", "3", "4"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", - "/etc/containers/containers.conf.d/10-conf1.conf", - "/usr/share/containers/containers.conf.d/20-conf2.conf", - "/home/containers/containers.conf.d/30-conf3.conf", - "/usr/share/containers/containers.conf.d/40-conf4.conf", - }, }, { name: "drop in override", @@ -306,13 +272,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"etc-override", "usr-content-2"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", - "/etc/containers/containers.conf.d/10-settings.conf", - "/usr/share/containers/containers.conf.d/20-settings.conf", - }, }, { name: "drop in ignores wrong extensions", @@ -328,12 +287,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"valid"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", - "/usr/share/containers/containers.conf.d/10-valid.conf", - }, }, { name: "policy.json main files only (ignore drop-ins)", @@ -349,11 +302,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"main"}, - wantPaths: []string{ - "/home/containers/policy.json", - "/etc/containers/policy.json", - "/usr/share/containers/policy.json", - }, }, { name: "registries.d drop ins only (ignore main)", @@ -370,9 +318,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"drop-in"}, - wantPaths: []string{ - "/usr/share/containers/registries.d/10-extra.yaml", - }, }, { name: "rootless specific drop-ins", @@ -389,13 +334,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"global", "rootless-specific"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", - "/usr/share/containers/containers.conf.d/01-global.conf", - "/usr/share/containers/containers.rootless.conf.d/02-user.conf", - }, }, { name: "rootless uid specific drop-ins", @@ -411,12 +349,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"uid-1000"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", - "/usr/share/containers/containers.rootless.conf.d/1000/settings.conf", - }, }, { name: "containers.conf env var not being set", @@ -431,11 +363,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"content1"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", - }, }, { name: "containers.conf env var must override all files", @@ -458,9 +385,6 @@ func Test_Read(t *testing.T) { t.Setenv("CONTAINERS_CONF", file) }, want: []string{"env"}, - wantPaths: []string{ - "/somepath", - }, }, { name: "containers.conf override env var should be appended", @@ -482,13 +406,6 @@ func Test_Read(t *testing.T) { t.Setenv("CONTAINERS_CONF_OVERRIDE", file) }, want: []string{"content1", "01", "env"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", - "/usr/share/containers/containers.conf.d/01.conf", - "/somepath", - }, }, { name: "containers.conf both env var should be appended", @@ -515,10 +432,6 @@ func Test_Read(t *testing.T) { t.Setenv("CONTAINERS_CONF_OVERRIDE", file2) }, want: []string{"env1", "env2"}, - wantPaths: []string{ - "/path1", - "/path1", - }, }, { name: "env var should error on non existing file", @@ -531,8 +444,7 @@ func Test_Read(t *testing.T) { file := filepath.Join(t.TempDir(), "123") t.Setenv("CONTAINERS_CONF", file) }, - wantErr: fs.ErrNotExist, - wantPaths: nil, + wantErr: fs.ErrNotExist, }, { name: "override env var should error on non existing file", @@ -546,10 +458,6 @@ func Test_Read(t *testing.T) { t.Setenv("CONTAINERS_CONF_OVERRIDE", file) }, wantErr: fs.ErrNotExist, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - }, }, { name: "containers.conf with modules", @@ -575,13 +483,6 @@ func Test_Read(t *testing.T) { tc.arg.Modules = append(tc.arg.Modules, file) }, want: []string{"content1", "relative module", "absolute module"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", - "/home/containers/containers.conf.modules/module.abc", - "/somepath", - }, }, { name: "containers.conf with module override", @@ -603,13 +504,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"home", "etc"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", - "/home/containers/containers.conf.modules/module.conf", - "/etc/containers/containers.conf.modules/different.conf", - }, }, { // same as above except we switch the module order to ensure we read the files in the proper order as given @@ -632,13 +526,6 @@ func Test_Read(t *testing.T) { }, }, want: []string{"etc", "home"}, - wantPaths: []string{ - "/home/containers/containers.conf", - "/etc/containers/containers.conf", - "/usr/share/containers/containers.conf", - "/etc/containers/containers.conf.modules/different.conf", - "/home/containers/containers.conf.modules/module.conf", - }, }, { name: "containers.conf env and modules order", @@ -669,11 +556,6 @@ func Test_Read(t *testing.T) { }, // CONTAINERS_CONF, then modules, then CONTAINERS_CONF_OVERRIDE want: []string{"env1", "mod", "env2"}, - wantPaths: []string{ - "/path1", - "/usr/share/containers/containers.conf.modules/module.conf", - "/path1", - }, }, } @@ -684,43 +566,32 @@ func Test_Read(t *testing.T) { if tt.setup != nil { tt.setup(t, &tt) } + seq := Read(&tt.arg) + if tt.wantErr == nil && tt.wantErrContains == "" { + confs := collectConfigs(t, seq) + assert.Equal(t, tt.want, confs) - for _, useReadWithPaths := range []bool{false, true} { - var usedPaths []string - var seq iter.Seq2[*Item, error] - if useReadWithPaths { - seq = ReadWithPaths(&tt.arg, &usedPaths) - } else { - seq = Read(&tt.arg) + // ensure the modules all get resolves to absolute paths and are valid + for _, module := range tt.arg.Modules { + assert.FileExists(t, module) + assert.True(t, filepath.IsAbs(module)) } - if tt.wantErr == nil { - confs := collectConfigs(t, seq) - assert.Equal(t, tt.want, confs) - - if useReadWithPaths && tt.wantErr == nil { - require.Equal(t, len(tt.wantPaths), len(usedPaths)) - for i, want := range tt.wantPaths { - assert.Truef(t, strings.HasSuffix(usedPaths[i], want), "path %q does not end with %q", usedPaths[i], want) - } - } + } else { + next, stop := iter.Pull2(seq) + defer stop() - // ensure the modules all get resolves to absolute paths and are valid - for _, module := range tt.arg.Modules { - assert.FileExists(t, module) - assert.True(t, filepath.IsAbs(module)) - } + _, err, ok := next() + assert.True(t, ok) + if tt.wantErrContains != "" { + assert.ErrorContains(t, err, tt.wantErrContains) + assert.Contains(t, err.Error(), tt.arg.RootForImplicitAbsolutePaths) } else { - next, stop := iter.Pull2(seq) - defer stop() - - _, err, ok := next() - assert.True(t, ok) assert.ErrorIs(t, err, tt.wantErr) - - // end of iterator - _, _, ok = next() - assert.False(t, ok) } + + // end of iterator + _, _, ok = next() + assert.False(t, ok) } }) } From 470e8ba383582ef8e1cb33175499bf0bd37aba19 Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Thu, 9 Apr 2026 11:30:49 +0200 Subject: [PATCH 12/12] image/docker: use unified configfile for registries.d Switch registries.d loading to use `configfile.Read()`, enabling unified drop-in search across /usr, /etc, and user config directories. Files are merged with standard precedence, with higher-priority paths masking lower ones. Preserve explicit RegistriesDirPath override behavior. Signed-off-by: Jan Kaluza --- image/docker/registries_d.go | 121 +++++++-------- image/docker/registries_d_test.go | 187 +++++++++++++++++------- image/docs/containers-registries.d.5.md | 16 +- 3 files changed, 212 insertions(+), 112 deletions(-) diff --git a/image/docker/registries_d.go b/image/docker/registries_d.go index 53bbb53cb1..f309a1d901 100644 --- a/image/docker/registries_d.go +++ b/image/docker/registries_d.go @@ -3,6 +3,7 @@ package docker import ( "errors" "fmt" + "io" "io/fs" "net/url" "os" @@ -15,30 +16,19 @@ import ( "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/internal/rootless" "go.podman.io/image/v5/types" - "go.podman.io/storage/pkg/fileutils" + "go.podman.io/storage/pkg/configfile" "go.podman.io/storage/pkg/homedir" + "go.podman.io/storage/pkg/unshare" "gopkg.in/yaml.v3" ) -// systemRegistriesDirPath is the path to registries.d, used for locating lookaside Docker signature storage. -// You can override this at build time with -// -ldflags '-X go.podman.io/image/v5/docker.systemRegistriesDirPath=$your_path' -var systemRegistriesDirPath = builtinRegistriesDirPath - -// builtinRegistriesDirPath is the path to registries.d. -// DO NOT change this, instead see systemRegistriesDirPath above. -const builtinRegistriesDirPath = etcDir + "/containers/registries.d" - -// userRegistriesDirPath is the path to the per user registries.d. -var userRegistriesDir = filepath.FromSlash(".config/containers/registries.d") - // defaultUserDockerDir is the default lookaside directory for unprivileged user var defaultUserDockerDir = filepath.FromSlash(".local/share/containers/sigstore") // defaultDockerDir is the default lookaside directory for root var defaultDockerDir = "/var/lib/containers/sigstore" -// registryConfiguration is one of the files in registriesDirPath configuring lookaside locations, or the result of merging them all. +// registryConfiguration is one of the registries signature storage YAML fragments, or the result of merging them all. // NOTE: Keep this in sync with docs/registries.d.md! type registryConfiguration struct { DefaultDocker *registryNamespace `yaml:"default-docker"` @@ -78,31 +68,40 @@ func SignatureStorageBaseURL(sys *types.SystemContext, ref types.ImageReference, // loadRegistryConfiguration returns a registryConfiguration appropriate for sys. func loadRegistryConfiguration(sys *types.SystemContext) (*registryConfiguration, error) { - dirPath := registriesDirPath(sys) - logrus.Debugf(`Using registries.d directory %s`, dirPath) - return loadAndMergeConfig(dirPath) -} - -// registriesDirPath returns a path to registries.d -func registriesDirPath(sys *types.SystemContext) string { - return registriesDirPathWithHomeDir(sys, homedir.Get()) -} - -// registriesDirPathWithHomeDir is an internal implementation detail of registriesDirPath, -// it exists only to allow testing it with an artificial home directory. -func registriesDirPathWithHomeDir(sys *types.SystemContext, homeDir string) string { if sys != nil && sys.RegistriesDirPath != "" { - return sys.RegistriesDirPath + logrus.Debugf(`Using registries.d directory %s`, sys.RegistriesDirPath) + return loadAndMergeConfig(sys.RegistriesDirPath) } - userRegistriesDirPath := filepath.Join(homeDir, userRegistriesDir) - if err := fileutils.Exists(userRegistriesDirPath); err == nil { - return userRegistriesDirPath + var rootForImplicitAbsPaths string + if sys != nil { + rootForImplicitAbsPaths = sys.RootForImplicitAbsolutePaths } - if sys != nil && sys.RootForImplicitAbsolutePaths != "" { - return filepath.Join(sys.RootForImplicitAbsolutePaths, systemRegistriesDirPath) + registriesFiles := configfile.File{ + Name: "registries", + Extension: "yaml", + DoNotLoadMainFiles: true, + DoNotUseExtensionForConfigName: true, + RootForImplicitAbsolutePaths: rootForImplicitAbsPaths, + UserId: unshare.GetRootlessUID(), + ErrorIfNotFound: false, } - - return systemRegistriesDirPath + mergedConfig := registryConfiguration{Docker: map[string]registryNamespace{}} + dockerDefaultMergedFrom := "" + nsMergedFrom := map[string]string{} + for item, err := range configfile.Read(®istriesFiles) { + if err != nil { + return nil, err + } + contents, err := io.ReadAll(item.Reader) + if err != nil { + return nil, err + } + logrus.Debugf(`Reading registries signature storage configuration from %q`, item.Name) + if err := mergeRegistriesYAMLFragment(&mergedConfig, item.Name, contents, &dockerDefaultMergedFrom, nsMergedFrom); err != nil { + return nil, err + } + } + return &mergedConfig, nil } // loadAndMergeConfig loads configuration files in dirPath @@ -119,6 +118,7 @@ func loadAndMergeConfig(dirPath string) (*registryConfiguration, error) { } return nil, err } + defer dir.Close() configNames, err := dir.Readdirnames(0) if err != nil { return nil, err @@ -131,39 +131,44 @@ func loadAndMergeConfig(dirPath string) (*registryConfiguration, error) { configBytes, err := os.ReadFile(configPath) if err != nil { if errors.Is(err, fs.ErrNotExist) { - // file must have been removed between the directory listing - // and the open call, ignore that as it is a expected race continue } return nil, err } - - var config registryConfiguration - err = yaml.Unmarshal(configBytes, &config) - if err != nil { - return nil, fmt.Errorf("parsing %s: %w", configPath, err) + if err := mergeRegistriesYAMLFragment(&mergedConfig, configPath, configBytes, &dockerDefaultMergedFrom, nsMergedFrom); err != nil { + return nil, err } + } - if config.DefaultDocker != nil { - if mergedConfig.DefaultDocker != nil { - return nil, fmt.Errorf(`Error parsing signature storage configuration: "default-docker" defined both in %q and %q`, - dockerDefaultMergedFrom, configPath) - } - mergedConfig.DefaultDocker = config.DefaultDocker - dockerDefaultMergedFrom = configPath - } + return &mergedConfig, nil +} - for nsName, nsConfig := range config.Docker { // includes config.Docker == nil - if _, ok := mergedConfig.Docker[nsName]; ok { - return nil, fmt.Errorf(`Error parsing signature storage configuration: "docker" namespace %q defined both in %q and %q`, - nsName, nsMergedFrom[nsName], configPath) - } - mergedConfig.Docker[nsName] = nsConfig - nsMergedFrom[nsName] = configPath +// mergeRegistriesYAMLFragment parses configBytes as a single registries.d YAML fragment and merges it into merged. +func mergeRegistriesYAMLFragment(merged *registryConfiguration, configPath string, configBytes []byte, dockerDefaultMergedFrom *string, nsMergedFrom map[string]string) error { + var config registryConfiguration + err := yaml.Unmarshal(configBytes, &config) + if err != nil { + return fmt.Errorf("parsing %s: %w", configPath, err) + } + + if config.DefaultDocker != nil { + if merged.DefaultDocker != nil { + return fmt.Errorf(`Error parsing signature storage configuration: "default-docker" defined both in %q and %q`, + *dockerDefaultMergedFrom, configPath) } + merged.DefaultDocker = config.DefaultDocker + *dockerDefaultMergedFrom = configPath } - return &mergedConfig, nil + for nsName, nsConfig := range config.Docker { + if _, ok := merged.Docker[nsName]; ok { + return fmt.Errorf(`Error parsing signature storage configuration: "docker" namespace %q defined both in %q and %q`, + nsName, nsMergedFrom[nsName], configPath) + } + merged.Docker[nsName] = nsConfig + nsMergedFrom[nsName] = configPath + } + return nil } // lookasideStorageBaseURL returns an appropriate signature storage URL for ref, for write access if “write”. diff --git a/image/docker/registries_d_test.go b/image/docker/registries_d_test.go index 3d54e933f3..cac5ea0018 100644 --- a/image/docker/registries_d_test.go +++ b/image/docker/registries_d_test.go @@ -21,6 +21,16 @@ func dockerRefFromString(t *testing.T, s string) dockerReference { return dockerRef } +func writeDockerLookaside(t *testing.T, dir, filename, registry, lookaside string) { + t.Helper() + require.NoError(t, os.WriteFile(filepath.Join(dir, filename), []byte(fmt.Sprintf("docker:\n %s:\n lookaside: %s\n", registry, lookaside)), 0o644)) +} + +func writeDefaultDockerLookaside(t *testing.T, dir, filename, lookaside string) { + t.Helper() + require.NoError(t, os.WriteFile(filepath.Join(dir, filename), []byte(fmt.Sprintf("default-docker:\n lookaside: %s\n", lookaside)), 0o644)) +} + func TestSignatureStorageBaseURL(t *testing.T) { emptyDir := t.TempDir() for _, c := range []struct { @@ -66,64 +76,137 @@ func TestSignatureStorageBaseURL(t *testing.T) { } } -func TestRegistriesDirPath(t *testing.T) { - const nondefaultPath = "/this/is/not/the/default/registries.d" - const variableReference = "$HOME" - const rootPrefix = "/root/prefix" - tempHome := t.TempDir() - userRegistriesDir := filepath.FromSlash(".config/containers/registries.d") - userRegistriesDirPath := filepath.Join(tempHome, userRegistriesDir) - for _, c := range []struct { - sys *types.SystemContext - userFilePresent bool - expected string - }{ - // The common case - {nil, false, systemRegistriesDirPath}, - // There is a context, but it does not override the path. - {&types.SystemContext{}, false, systemRegistriesDirPath}, - // Path overridden - {&types.SystemContext{RegistriesDirPath: nondefaultPath}, false, nondefaultPath}, - // Root overridden - { - &types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix}, - false, - filepath.Join(rootPrefix, systemRegistriesDirPath), +func TestLoadRegistryConfiguration(t *testing.T) { + type testcase struct { + setup func(t *testing.T) *types.SystemContext + wantLookaside string + forbiddenDockerKey string + expectErr bool + } + tests := []testcase{ + { // Explicit override directory: only load from there. + setup: func(t *testing.T) *types.SystemContext { + dir := t.TempDir() + writeDockerLookaside(t, dir, "01.yaml", "example.com", "https://override.example.com") + return &types.SystemContext{RegistriesDirPath: dir} + }, + wantLookaside: "https://override.example.com", }, - // Root and path overrides present simultaneously, - { - &types.SystemContext{ - RootForImplicitAbsolutePaths: rootPrefix, - RegistriesDirPath: nondefaultPath, + { // Default configfile search: drop-ins from /usr + /etc (under RootForImplicitAbsolutePaths); main registries.yaml ignored. + setup: func(t *testing.T) *types.SystemContext { + root := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", filepath.Join(root, "xdg")) + + usrRegistriesD := filepath.Join(root, "usr/share/containers/registries.d") + etcRegistriesD := filepath.Join(root, "etc/containers/registries.d") + require.NoError(t, os.MkdirAll(usrRegistriesD, 0o755)) + require.NoError(t, os.MkdirAll(etcRegistriesD, 0o755)) + + writeDockerLookaside(t, usrRegistriesD, "10-vendor.yaml", "example.com", "https://vendor.example.com") + writeDockerLookaside(t, etcRegistriesD, "10-vendor.yaml", "example.com", "https://admin.example.com") + writeDockerLookaside(t, filepath.Join(root, "etc/containers"), "registries.yaml", "should.not.be.loaded", "https://wrong.example.com") + + return &types.SystemContext{RootForImplicitAbsolutePaths: root} }, - false, - nondefaultPath, + wantLookaside: "https://admin.example.com", + forbiddenDockerKey: "should.not.be.loaded", }, - // User registries.d present, not overridden - {&types.SystemContext{}, true, userRegistriesDirPath}, - // Context and user User registries.d preset simultaneously - {&types.SystemContext{RegistriesDirPath: nondefaultPath}, true, nondefaultPath}, - // Root and user registries.d overrides present simultaneously, - { - &types.SystemContext{ - RootForImplicitAbsolutePaths: rootPrefix, - RegistriesDirPath: nondefaultPath, + { // Explicit RegistriesDirPath bypasses configfile search completelz. + setup: func(t *testing.T) *types.SystemContext { + root := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", filepath.Join(root, "xdg")) + userRegistriesD := filepath.Join(root, "xdg/containers/registries.d") + require.NoError(t, os.MkdirAll(userRegistriesD, 0o755)) + writeDockerLookaside(t, userRegistriesD, "10-user.yaml", "example.com", "https://user.example.com") + + overrideDir := t.TempDir() + writeDockerLookaside(t, overrideDir, "01.yaml", "example.com", "https://explicit.example.com") + + return &types.SystemContext{ + RegistriesDirPath: overrideDir, + RootForImplicitAbsolutePaths: root, // should not matter for the explicit override + } }, - true, - nondefaultPath, + wantLookaside: "https://explicit.example.com", }, - // No environment expansion happens in the overridden paths - {&types.SystemContext{RegistriesDirPath: variableReference}, false, variableReference}, - } { - if c.userFilePresent { - err := os.MkdirAll(userRegistriesDirPath, 0o700) - require.NoError(t, err) - } else { - err := os.RemoveAll(userRegistriesDirPath) - require.NoError(t, err) + { // RootForImplicitAbsolutePaths does not affect explicit RegistriesDirPath. + setup: func(t *testing.T) *types.SystemContext { + overrideDir := t.TempDir() + writeDockerLookaside(t, overrideDir, "01.yaml", "example.com", "https://explicit.example.com") + + root := t.TempDir() + // If RootForImplicitAbsolutePaths were incorrectly applied to RegistriesDirPath, + // we'd look under root+overrideDir which doesn't exist. + return &types.SystemContext{RegistriesDirPath: overrideDir, RootForImplicitAbsolutePaths: root} + }, + wantLookaside: "https://explicit.example.com", + }, + { // Explicit RegistriesDirPath is not env-expanded. + setup: func(t *testing.T) *types.SystemContext { + parent := t.TempDir() + literalHomeDir := filepath.Join(parent, "$HOME") + require.NoError(t, os.MkdirAll(literalHomeDir, 0o755)) + writeDockerLookaside(t, literalHomeDir, "01.yaml", "example.com", "https://literal.example.com") + return &types.SystemContext{RegistriesDirPath: literalHomeDir} + }, + wantLookaside: "https://literal.example.com", + }, + { // user XDG_CONFIG_HOME/.../registries.d has higher priority than /etc for same filename. + setup: func(t *testing.T) *types.SystemContext { + root := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", filepath.Join(root, "xdg")) + + etcRegistriesD := filepath.Join(root, "etc/containers/registries.d") + require.NoError(t, os.MkdirAll(etcRegistriesD, 0o755)) + writeDockerLookaside(t, etcRegistriesD, "10-same.yaml", "example.com", "https://etc.example.com") + + userRegistriesD := filepath.Join(root, "xdg/containers/registries.d") + require.NoError(t, os.MkdirAll(userRegistriesD, 0o755)) + writeDockerLookaside(t, userRegistriesD, "10-same.yaml", "example.com", "https://user.example.com") + + return &types.SystemContext{RootForImplicitAbsolutePaths: root} + }, + wantLookaside: "https://user.example.com", + }, + { // Duplicate docker namespace across distinct filenames still errors. + setup: func(t *testing.T) *types.SystemContext { + root := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", filepath.Join(root, "xdg")) + etcRegistriesD := filepath.Join(root, "etc/containers/registries.d") + require.NoError(t, os.MkdirAll(etcRegistriesD, 0o755)) + writeDockerLookaside(t, etcRegistriesD, "10-a.yaml", "example.com", "https://a.example.com") + writeDockerLookaside(t, etcRegistriesD, "20-b.yaml", "example.com", "https://b.example.com") + return &types.SystemContext{RootForImplicitAbsolutePaths: root} + }, + expectErr: true, + }, + { // Duplicate default-docker across distinct filenames still errors. + setup: func(t *testing.T) *types.SystemContext { + root := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", filepath.Join(root, "xdg")) + etcRegistriesD := filepath.Join(root, "etc/containers/registries.d") + require.NoError(t, os.MkdirAll(etcRegistriesD, 0o755)) + writeDefaultDockerLookaside(t, etcRegistriesD, "10-a.yaml", "https://a.example.com") + writeDefaultDockerLookaside(t, etcRegistriesD, "20-b.yaml", "https://b.example.com") + return &types.SystemContext{RootForImplicitAbsolutePaths: root} + }, + expectErr: true, + }, + } + + for _, tt := range tests { + sys := tt.setup(t) + cfg, err := loadRegistryConfiguration(sys) + if tt.expectErr { + assert.Error(t, err) + continue + } + require.NoError(t, err) + assert.Equal(t, tt.wantLookaside, cfg.Docker["example.com"].Lookaside) + if tt.forbiddenDockerKey != "" { + _, ok := cfg.Docker[tt.forbiddenDockerKey] + assert.False(t, ok) } - path := registriesDirPathWithHomeDir(c.sys, tempHome) - assert.Equal(t, c.expected, path) } } diff --git a/image/docs/containers-registries.d.5.md b/image/docs/containers-registries.d.5.md index 04434de4b6..2d558c27ed 100644 --- a/image/docs/containers-registries.d.5.md +++ b/image/docs/containers-registries.d.5.md @@ -12,8 +12,20 @@ The registries configuration directory contains configuration for various regist so that the configuration does not have to be provided in command-line options over and over for every command, and so that it can be shared by all users of containers/image. -By default, the registries configuration directory is `$HOME/.config/containers/registries.d` if it exists, otherwise `/etc/containers/registries.d` (unless overridden at compile-time); -applications may allow using a different directory instead. +By default, registries.d configuration is loaded from drop-in directories following the same search locations and precedence rules as other containers configuration files. +In particular, configuration may be provided by a vendor in `/usr/share/containers/registries.d/`, overridden by an administrator in `/etc/containers/registries.d/`, +and overridden per-user in `$XDG_CONFIG_HOME/containers/registries.d/` (or `$HOME/.config/containers/registries.d/` if `$XDG_CONFIG_HOME` is not set). + +Rootless/rootful specific drop-in directories are also consulted where applicable: + +- `/usr/share/containers/registries.rootful.d/` (UID == 0) +- `/usr/share/containers/registries.rootless.d/` (UID > 0) +- `/usr/share/containers/registries.rootless.d//` (UID > 0) +- `/etc/containers/registries.rootful.d/` (UID == 0) +- `/etc/containers/registries.rootless.d/` (UID > 0) +- `/etc/containers/registries.rootless.d//` (UID > 0) + +Only files with the `.yaml` extension are read. Applications may allow using a different directory instead (e.g. via an explicit override). ## Directory Structure