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/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-policy.json.5.md b/image/docs/containers-policy.json.5.md index 1408e6510a..d9e485288d 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_JSON` is set, it specifies the only policy file to use. ## FORMAT 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 diff --git a/image/signature/policy_config.go b/image/signature/policy_config.go index 5e06531192..7d54b4f5f5 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,41 @@ 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 - } - if sys != nil && sys.RootForImplicitAbsolutePaths != "" { - return filepath.Join(sys.RootForImplicitAbsolutePaths, systemPolicyPath), nil + policyFiles := configfile.File{ + Name: "policy", + Extension: "json", + DoNotLoadDropInFiles: true, + EnvironmentName: "CONTAINERS_POLICY_JSON", + RootForImplicitAbsolutePaths: rootForImplicitAbsPaths, + ErrorIfNotFound: true, } - if err := fileutils.Exists(systemPolicyPath); err == nil { - return systemPolicyPath, nil + + var policy *Policy + for item, err := range configfile.Read(&policyFiles) { + if err != nil { + return nil, err + } + + 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) + } } - 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..355b7c796f 100644 --- a/image/signature/policy_config_test.go +++ b/image/signature/policy_config_test.go @@ -127,130 +127,204 @@ 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"}) - require.NoError(t, err) - assert.Equal(t, policyFixtureContents, policy) + // prReject + const rejectJSON = `{"default":[{"type":"reject"}]}` + // prInsecureAcceptAnything + const insecureJSON = `{"default":[{"type":"insecureAcceptAnything"}]}` + + type tc struct { + name string + setup func(t *testing.T, rootPrefix string) *types.SystemContext + expectPolicy any // *Policy, *prReject, *prInsecureAcceptAnything + expectErr bool + expectErrMatch string + } - 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. - } { - policy, err := DefaultPolicy(&types.SystemContext{SignaturePolicyPath: path}) - assert.Error(t, err) - assert.Nil(t, policy) + 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)) } -} -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 - }{ - // 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 + for _, test := range []tc{ + { + name: "signature policy path override success", + setup: func(t *testing.T, _ string) *types.SystemContext { + return &types.SystemContext{SignaturePolicyPath: "./fixtures/policy.json"} + }, + expectPolicy: policyFixtureContents, + }, + { + name: "signature policy path override read error", + setup: func(t *testing.T, _ string) *types.SystemContext { + return &types.SystemContext{SignaturePolicyPath: "/this/does/not/exist"} + }, + expectErr: true, + }, + { + name: "signature policy path override parse error", + setup: func(t *testing.T, _ string) *types.SystemContext { + return &types.SystemContext{SignaturePolicyPath: "/dev/null"} + }, + expectErr: true, + }, + { + name: "user wins over etc and usr", + 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, + } + }, + expectPolicy: &prInsecureAcceptAnything{}, + }, + { + name: "etc fallback when user missing", + 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, + } + }, + expectPolicy: &prReject{}, + }, + { + name: "usr fallback when only usr present", + 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, + } + }, + expectPolicy: &prInsecureAcceptAnything{}, + }, { - &types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix}, - false, - true, - filepath.Join(rootPrefix, tempsystemdefaultpath), - "", + name: "no policy file found", + setup: func(t *testing.T, rootPrefix string) *types.SystemContext { + t.Setenv("XDG_CONFIG_HOME", "/tmp") + return &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + } + }, + expectErr: true, + expectErrMatch: "no policy.json file found; searched paths: [\"/tmp/containers/policy.json\" ", }, - // 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, + name: "containers policy conf override base file", + 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, + } }, - true, - true, - nondefaultPath, - "", + expectPolicy: &prInsecureAcceptAnything{}, }, - // Root and user policy present { - &types.SystemContext{ - RootForImplicitAbsolutePaths: rootPrefix, + name: "containers policy conf read error", + 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{} }, - true, - true, - userDefaultPolicyPath, - "", + expectErr: true, }, - // Context and user policy file preset simultaneously { - &types.SystemContext{ - RootForImplicitAbsolutePaths: rootPrefix, - SignaturePolicyPath: nondefaultPath, + name: "containers policy conf parse error", + 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{} }, - true, - true, - nondefaultPath, - "", + expectErr: true, }, - // Root and path overrides present simultaneously, { - &types.SystemContext{ - RootForImplicitAbsolutePaths: rootPrefix, - SignaturePolicyPath: nondefaultPath, + name: "containers policy conf readall error", + 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{} }, - false, - true, - nondefaultPath, - "", + expectErr: true, + }, + { + name: "signature policy path wins over root for implicit absolute paths", + 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) + + return &types.SystemContext{ + SignaturePolicyPath: "./fixtures/policy.json", + RootForImplicitAbsolutePaths: rootPrefix, + } + }, + expectPolicy: policyFixtureContents, + }, + { + name: "root for implicit absolute paths is honored", + 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, + } + }, + expectPolicy: &prReject{}, }, - // 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(test.name, func(t *testing.T) { + rootPrefix := t.TempDir() + var sys *types.SystemContext + if test.setup != nil { + sys = test.setup(t, rootPrefix) } - } - path, err := defaultPolicyPathWithHomeDir(c.sys, tempHome, tempsystemdefaultpath) - if c.expectedError != "" { - assert.Empty(t, path) - assert.EqualError(t, err, c.expectedError) - } else { + + 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 + } + require.NoError(t, err) - assert.Equal(t, c.expected, path) - } + + 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") + case *prReject: + _, ok := policy.Default[0].(*prReject) + assert.True(t, ok, "expected reject policy requirement") + default: + t.Fatalf("unexpected expectedPolicy type %T", test.expectPolicy) + } + }) } } diff --git a/image/signature/policy_paths_common.go b/image/signature/policy_paths_common.go deleted file mode 100644 index 038351cb74..0000000000 --- a/image/signature/policy_paths_common.go +++ /dev/null @@ -1,7 +0,0 @@ -//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 deleted file mode 100644 index 6a45a78fa1..0000000000 --- a/image/signature/policy_paths_freebsd.go +++ /dev/null @@ -1,7 +0,0 @@ -//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..414dc245bf 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 @@ -55,6 +57,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 @@ -70,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]. @@ -91,6 +97,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] { configFileName := getConfName(conf.Name, conf.Extension, conf.DoNotUseExtensionForConfigName) @@ -113,10 +121,14 @@ func Read(conf *File) 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(), @@ -134,6 +146,7 @@ func Read(conf *File) iter.Seq2[*Item, error] { if conf.EnvironmentName != "" { if path := os.Getenv(conf.EnvironmentName); 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 { @@ -165,6 +178,7 @@ func Read(conf *File) iter.Seq2[*Item, error] { if path == "" { continue } + 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 +205,7 @@ func Read(conf *File) iter.Seq2[*Item, error] { return } for _, file := range files { + 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 +226,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 @@ -224,9 +239,10 @@ 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 != "" { + 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 { @@ -238,6 +254,11 @@ func Read(conf *File) iter.Seq2[*Item, error] { } } } + + if conf.ErrorIfNotFound && !foundAny { + yield(nil, fmt.Errorf("no %s file found; searched paths: %q", configFileName, usedPaths)) + return + } } } @@ -324,8 +345,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 +358,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..0d3975f6bc 100644 --- a/storage/pkg/configfile/parse_test.go +++ b/storage/pkg/configfile/parse_test.go @@ -113,8 +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 + // wantErrContains, if non-empty, asserts a substring of the error string instead of wantErr. + wantErrContains string } tests := []testcase{ @@ -126,6 +128,16 @@ func Test_Read(t *testing.T) { }, want: nil, }, + { + 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", arg: File{ @@ -555,7 +567,7 @@ func Test_Read(t *testing.T) { tt.setup(t, &tt) } seq := Read(&tt.arg) - if tt.wantErr == nil { + if tt.wantErr == nil && tt.wantErrContains == "" { confs := collectConfigs(t, seq) assert.Equal(t, tt.want, confs) @@ -570,7 +582,12 @@ func Test_Read(t *testing.T) { _, err, ok := next() assert.True(t, ok) - assert.ErrorIs(t, err, tt.wantErr) + if tt.wantErrContains != "" { + assert.ErrorContains(t, err, tt.wantErrContains) + assert.Contains(t, err.Error(), tt.arg.RootForImplicitAbsolutePaths) + } else { + assert.ErrorIs(t, err, tt.wantErr) + } // end of iterator _, _, ok = next()