diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index 5dc2ec864c..2f45e559d0 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -778,20 +778,30 @@ func parseThrottleIOPsDevices(iopsDevices []string) (map[string]specs.LinuxThrot return td, nil } -func parseSecrets(secrets []string) ([]string, map[string]string, error) { +func parseSecrets(secrets []string) ([]specgen.Secret, map[string]string, error) { secretParseError := errors.New("error parsing secret") - var mount []string + var mount []specgen.Secret envs := make(map[string]string) for _, val := range secrets { + // mount only tells if user has set an option that can only be used with mount secret type + mountOnly := false source := "" secretType := "" target := "" + var uid, gid uint32 + // default mode 444 octal = 292 decimal + var mode uint32 = 292 split := strings.Split(val, ",") // --secret mysecret if len(split) == 1 { - source = val - mount = append(mount, source) + mountSecret := specgen.Secret{ + Source: val, + UID: uid, + GID: gid, + Mode: mode, + } + mount = append(mount, mountSecret) continue } // --secret mysecret,opt=opt @@ -799,7 +809,7 @@ func parseSecrets(secrets []string) ([]string, map[string]string, error) { source = split[0] split = split[1:] } - // TODO: implement other secret options + for _, val := range split { kv := strings.SplitN(val, "=", 2) if len(kv) < 2 { @@ -818,6 +828,28 @@ func parseSecrets(secrets []string) ([]string, map[string]string, error) { secretType = kv[1] case "target": target = kv[1] + case "mode": + mountOnly = true + mode64, err := strconv.ParseUint(kv[1], 8, 32) + if err != nil { + return nil, nil, errors.Wrapf(secretParseError, "mode %s invalid", kv[1]) + } + mode = uint32(mode64) + case "uid", "UID": + mountOnly = true + uid64, err := strconv.ParseUint(kv[1], 10, 32) + if err != nil { + return nil, nil, errors.Wrapf(secretParseError, "UID %s invalid", kv[1]) + } + uid = uint32(uid64) + case "gid", "GID": + mountOnly = true + gid64, err := strconv.ParseUint(kv[1], 10, 32) + if err != nil { + return nil, nil, errors.Wrapf(secretParseError, "GID %s invalid", kv[1]) + } + gid = uint32(gid64) + default: return nil, nil, errors.Wrapf(secretParseError, "option %s invalid", val) } @@ -833,9 +865,18 @@ func parseSecrets(secrets []string) ([]string, map[string]string, error) { if target != "" { return nil, nil, errors.Wrapf(secretParseError, "target option is invalid for mounted secrets") } - mount = append(mount, source) + mountSecret := specgen.Secret{ + Source: source, + UID: uid, + GID: gid, + Mode: mode, + } + mount = append(mount, mountSecret) } if secretType == "env" { + if mountOnly { + return nil, nil, errors.Wrap(secretParseError, "UID, GID, Mode options cannot be set with secret type env") + } if target == "" { target = source } diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index d03d485064..2c51b312d7 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -859,6 +859,9 @@ Secret Options - `type=mount|env` : How the secret will be exposed to the container. Default mount. - `target=target` : Target of secret. Defauts to secret name. +- `uid=0` : UID of secret. Defaults to 0. Mount secret type only. +- `gid=0` : GID of secret. Defaults to 0. Mount secret type only. +- `mode=0` : Mode of secret. Defaults to 0444. Mount secret type only. #### **--security-opt**=*option* diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index b9cfb01d13..46e15d62ff 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -911,6 +911,9 @@ Secret Options - `type=mount|env` : How the secret will be exposed to the container. Default mount. - `target=target` : Target of secret. Defauts to secret name. +- `uid=0` : UID of secret. Defaults to 0. Mount secret type only. +- `gid=0` : GID of secret. Defaults to 0. Mount secret type only. +- `mode=0` : Mode of secret. Defaults to 0444. Mount secret type only. #### **--security-opt**=*option* diff --git a/libpod/container.go b/libpod/container.go index e14051f804..6cc64f3fd3 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -237,6 +237,18 @@ type ContainerImageVolume struct { ReadWrite bool `json:"rw"` } +// ContainerSecret is a secret that is mounted in a container +type ContainerSecret struct { + // Secret is the secret + *secrets.Secret + // UID is tbe UID of the secret file + UID uint32 + // GID is the GID of the secret file + GID uint32 + // Mode is the mode of the secret file + Mode uint32 +} + // ContainerNetworkDescriptions describes the relationship between the CNI // network and the ethN where N is an integer type ContainerNetworkDescriptions map[string]int @@ -1136,7 +1148,7 @@ func (c *Container) Umask() string { } //Secrets return the secrets in the container -func (c *Container) Secrets() []*secrets.Secret { +func (c *Container) Secrets() []*ContainerSecret { return c.config.Secrets } diff --git a/libpod/container_config.go b/libpod/container_config.go index 904c03f9b4..0de79fde35 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -148,7 +148,7 @@ type ContainerRootFSConfig struct { // default, but others do not. CreateWorkingDir bool `json:"createWorkingDir,omitempty"` // Secrets lists secrets to mount into the container - Secrets []*secrets.Secret `json:"secrets,omitempty"` + Secrets []*ContainerSecret `json:"secrets,omitempty"` // SecretPath is the secrets location in storage SecretsPath string `json:"secretsPath"` // Volatile specifies whether the container storage can be optimized diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index b38c356976..638e0b756e 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -343,11 +343,13 @@ func (c *Container) generateInspectContainerConfig(spec *spec.Spec) *define.Insp ctrConfig.CreateCommand = c.config.CreateCommand ctrConfig.Timezone = c.config.Timezone - for _, secret := range c.config.Secrets { newSec := define.InspectSecret{} newSec.Name = secret.Name newSec.ID = secret.ID + newSec.UID = secret.UID + newSec.GID = secret.GID + newSec.Mode = secret.Mode ctrConfig.Secrets = append(ctrConfig.Secrets, &newSec) } diff --git a/libpod/container_internal.go b/libpod/container_internal.go index f79edfbcac..2555f15ec4 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -15,7 +15,7 @@ import ( metadata "github.com/checkpoint-restore/checkpointctl/lib" "github.com/containers/buildah/copier" - "github.com/containers/common/pkg/secrets" + butil "github.com/containers/buildah/util" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/events" "github.com/containers/podman/v3/pkg/cgroups" @@ -24,6 +24,7 @@ import ( "github.com/containers/podman/v3/pkg/hooks/exec" "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/podman/v3/pkg/selinux" + "github.com/containers/podman/v3/pkg/util" "github.com/containers/storage" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/idtools" @@ -2188,21 +2189,31 @@ func (c *Container) hasNamespace(namespace spec.LinuxNamespaceType) bool { } // extractSecretToStorage copies a secret's data from the secrets manager to the container's static dir -func (c *Container) extractSecretToCtrStorage(name string) error { - manager, err := secrets.NewManager(c.runtime.GetSecretsStorageDir()) +func (c *Container) extractSecretToCtrStorage(secr *ContainerSecret) error { + manager, err := c.runtime.SecretsManager() if err != nil { return err } - secr, data, err := manager.LookupSecretData(name) + _, data, err := manager.LookupSecretData(secr.Name) if err != nil { return err } secretFile := filepath.Join(c.config.SecretsPath, secr.Name) + hostUID, hostGID, err := butil.GetHostIDs(util.IDtoolsToRuntimeSpec(c.config.IDMappings.UIDMap), util.IDtoolsToRuntimeSpec(c.config.IDMappings.GIDMap), secr.UID, secr.GID) + if err != nil { + return errors.Wrap(err, "unable to extract secret") + } err = ioutil.WriteFile(secretFile, data, 0644) if err != nil { return errors.Wrapf(err, "unable to create %s", secretFile) } + if err := os.Lchown(secretFile, int(hostUID), int(hostGID)); err != nil { + return err + } + if err := os.Chmod(secretFile, os.FileMode(secr.Mode)); err != nil { + return err + } if err := label.Relabel(secretFile, c.config.MountLabel, false); err != nil { return err } diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index b31d90a6e1..eeeeec86a7 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -29,7 +29,6 @@ import ( "github.com/containers/common/pkg/apparmor" "github.com/containers/common/pkg/chown" "github.com/containers/common/pkg/config" - "github.com/containers/common/pkg/secrets" "github.com/containers/common/pkg/subscriptions" "github.com/containers/common/pkg/umask" "github.com/containers/podman/v3/libpod/define" @@ -759,7 +758,10 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { return nil, errors.Wrapf(err, "error setting up OCI Hooks") } if len(c.config.EnvSecrets) > 0 { - manager, err := secrets.NewManager(c.runtime.GetSecretsStorageDir()) + manager, err := c.runtime.SecretsManager() + if err != nil { + return nil, err + } if err != nil { return nil, err } @@ -2413,7 +2415,7 @@ func (c *Container) createSecretMountDir() error { oldUmask := umask.Set(0) defer umask.Set(oldUmask) - if err := os.MkdirAll(src, 0644); err != nil { + if err := os.MkdirAll(src, 0755); err != nil { return err } if err := label.Relabel(src, c.config.MountLabel, false); err != nil { diff --git a/libpod/define/container_inspect.go b/libpod/define/container_inspect.go index 5283946fac..af8ba6ecfe 100644 --- a/libpod/define/container_inspect.go +++ b/libpod/define/container_inspect.go @@ -713,13 +713,16 @@ type DriverData struct { Data map[string]string `json:"Data"` } -// InspectHostPort provides information on a port on the host that a container's -// port is bound to. +// InspectSecret contains information on secrets mounted inside the container type InspectSecret struct { - // IP on the host we are bound to. "" if not specified (binding to all - // IPs). + // Name is the name of the secret Name string `json:"Name"` - // Port on the host we are bound to. No special formatting - just an - // integer stuffed into a string. + // ID is the ID of the secret ID string `json:"ID"` + // ID is the UID of the mounted secret file + UID uint32 `json:"UID"` + // ID is the GID of the mounted secret file + GID uint32 `json:"GID"` + // ID is the ID of the mode of the mounted secret file + Mode uint32 `json:"Mode"` } diff --git a/libpod/options.go b/libpod/options.go index deb78a9e02..f2468e41ba 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1712,23 +1712,12 @@ func WithUmask(umask string) CtrCreateOption { } // WithSecrets adds secrets to the container -func WithSecrets(secretNames []string) CtrCreateOption { +func WithSecrets(containerSecrets []*ContainerSecret) CtrCreateOption { return func(ctr *Container) error { if ctr.valid { return define.ErrCtrFinalized } - manager, err := secrets.NewManager(ctr.runtime.GetSecretsStorageDir()) - if err != nil { - return err - } - for _, name := range secretNames { - secr, err := manager.Lookup(name) - if err != nil { - return err - } - ctr.config.Secrets = append(ctr.config.Secrets, secr) - } - + ctr.config.Secrets = containerSecrets return nil } } @@ -1740,7 +1729,7 @@ func WithEnvSecrets(envSecrets map[string]string) CtrCreateOption { if ctr.valid { return define.ErrCtrFinalized } - manager, err := secrets.NewManager(ctr.runtime.GetSecretsStorageDir()) + manager, err := ctr.runtime.SecretsManager() if err != nil { return err } diff --git a/libpod/runtime.go b/libpod/runtime.go index 7099f55f20..bae012635b 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -18,6 +18,7 @@ import ( "github.com/containers/buildah/pkg/parse" "github.com/containers/common/libimage" "github.com/containers/common/pkg/config" + "github.com/containers/common/pkg/secrets" "github.com/containers/image/v5/pkg/sysregistriesv2" is "github.com/containers/image/v5/storage" "github.com/containers/image/v5/types" @@ -106,6 +107,8 @@ type Runtime struct { // noStore indicates whether we need to interact with a store or not noStore bool + // secretsManager manages secrets + secretsManager *secrets.SecretsManager } // SetXdgDirs ensures the XDG_RUNTIME_DIR env and XDG_CONFIG_HOME variables are set. @@ -1091,6 +1094,18 @@ func (r *Runtime) GetSecretsStorageDir() string { return filepath.Join(r.store.GraphRoot(), "secrets") } +// SecretsManager returns the directory that the secrets manager should take +func (r *Runtime) SecretsManager() (*secrets.SecretsManager, error) { + if r.secretsManager == nil { + manager, err := secrets.NewManager(r.GetSecretsStorageDir()) + if err != nil { + return nil, err + } + r.secretsManager = manager + } + return r.secretsManager, nil +} + func graphRootMounted() bool { f, err := os.OpenFile("/run/.containerenv", os.O_RDONLY, os.ModePerm) if err != nil { diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 7d31e392ff..4e4b2a8ab3 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -366,7 +366,7 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai return nil, err } for _, secr := range ctr.config.Secrets { - err = ctr.extractSecretToCtrStorage(secr.Name) + err = ctr.extractSecretToCtrStorage(secr) if err != nil { return nil, err } diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index a94c5f5c57..0ac9b5d8d7 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -12,7 +12,6 @@ import ( "github.com/containers/common/libimage" "github.com/containers/common/pkg/config" - "github.com/containers/common/pkg/secrets" "github.com/containers/image/v5/types" "github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod/define" @@ -161,7 +160,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY ) // Create the secret manager before hand - secretsManager, err := secrets.NewManager(ic.Libpod.GetSecretsStorageDir()) + secretsManager, err := ic.Libpod.SecretsManager() if err != nil { return nil, err } diff --git a/pkg/domain/infra/abi/secrets.go b/pkg/domain/infra/abi/secrets.go index 764f4a9dca..1e1cbc70fa 100644 --- a/pkg/domain/infra/abi/secrets.go +++ b/pkg/domain/infra/abi/secrets.go @@ -6,7 +6,6 @@ import ( "io/ioutil" "path/filepath" - "github.com/containers/common/pkg/secrets" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/pkg/errors" ) @@ -14,7 +13,7 @@ import ( func (ic *ContainerEngine) SecretCreate(ctx context.Context, name string, reader io.Reader, options entities.SecretCreateOptions) (*entities.SecretCreateReport, error) { data, _ := ioutil.ReadAll(reader) secretsPath := ic.Libpod.GetSecretsStorageDir() - manager, err := secrets.NewManager(secretsPath) + manager, err := ic.Libpod.SecretsManager() if err != nil { return nil, err } @@ -36,8 +35,7 @@ func (ic *ContainerEngine) SecretCreate(ctx context.Context, name string, reader } func (ic *ContainerEngine) SecretInspect(ctx context.Context, nameOrIDs []string) ([]*entities.SecretInfoReport, []error, error) { - secretsPath := ic.Libpod.GetSecretsStorageDir() - manager, err := secrets.NewManager(secretsPath) + manager, err := ic.Libpod.SecretsManager() if err != nil { return nil, nil, err } @@ -71,8 +69,7 @@ func (ic *ContainerEngine) SecretInspect(ctx context.Context, nameOrIDs []string } func (ic *ContainerEngine) SecretList(ctx context.Context) ([]*entities.SecretInfoReport, error) { - secretsPath := ic.Libpod.GetSecretsStorageDir() - manager, err := secrets.NewManager(secretsPath) + manager, err := ic.Libpod.SecretsManager() if err != nil { return nil, err } @@ -105,8 +102,7 @@ func (ic *ContainerEngine) SecretRm(ctx context.Context, nameOrIDs []string, opt toRemove []string reports = []*entities.SecretRmReport{} ) - secretsPath := ic.Libpod.GetSecretsStorageDir() - manager, err := secrets.NewManager(secretsPath) + manager, err := ic.Libpod.SecretsManager() if err != nil { return nil, err } diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index e53032ebea..087ff59dfd 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -401,7 +401,24 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. } if len(s.Secrets) != 0 { - options = append(options, libpod.WithSecrets(s.Secrets)) + manager, err := rt.SecretsManager() + if err != nil { + return nil, err + } + var secrs []*libpod.ContainerSecret + for _, s := range s.Secrets { + secr, err := manager.Lookup(s.Source) + if err != nil { + return nil, err + } + secrs = append(secrs, &libpod.ContainerSecret{ + Secret: secr, + UID: s.UID, + GID: s.GID, + Mode: s.Mode, + }) + } + options = append(options, libpod.WithSecrets(secrs)) } if len(s.EnvSecrets) != 0 { diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 2e01d15355..2815bdebb4 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -258,7 +258,7 @@ type ContainerStorageConfig struct { RootfsPropagation string `json:"rootfs_propagation,omitempty"` // Secrets are the secrets that will be added to the container // Optional. - Secrets []string `json:"secrets,omitempty"` + Secrets []Secret `json:"secrets,omitempty"` // Volatile specifies whether the container storage can be optimized // at the cost of not syncing all the dirty files in memory. Volatile bool `json:"volatile,omitempty"` @@ -521,6 +521,13 @@ type PortMapping struct { Protocol string `json:"protocol,omitempty"` } +type Secret struct { + Source string + UID uint32 + GID uint32 + Mode uint32 +} + var ( // ErrNoStaticIPRootless is used when a rootless user requests to assign a static IP address // to a pod or container diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index 174714cacf..cae1b5aad5 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -1681,6 +1681,49 @@ WORKDIR /madethis`, BB) Expect(session.OutputToString()).To(Equal(secretsString)) }) + It("podman run --secret mount with uid, gid, mode options", func() { + secretsString := "somesecretdata" + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte(secretsString), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"secret", "create", "mysecret", secretFilePath}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + // check default permissions + session = podmanTest.Podman([]string{"run", "--secret", "mysecret", "--name", "secr", ALPINE, "ls", "-l", "/run/secrets/mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + output := session.OutputToString() + Expect(output).To(ContainSubstring("-r--r--r--")) + Expect(output).To(ContainSubstring("root")) + + session = podmanTest.Podman([]string{"run", "--secret", "source=mysecret,type=mount,uid=1000,gid=1001,mode=777", "--name", "secr2", ALPINE, "ls", "-ln", "/run/secrets/mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + output = session.OutputToString() + Expect(output).To(ContainSubstring("-rwxrwxrwx")) + Expect(output).To(ContainSubstring("1000")) + Expect(output).To(ContainSubstring("1001")) + }) + + It("podman run --secret with --user", func() { + secretsString := "somesecretdata" + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte(secretsString), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"secret", "create", "mysecret", secretFilePath}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"run", "--secret", "mysecret", "--name", "nonroot", "--user", "200:200", ALPINE, "cat", "/run/secrets/mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(Equal(secretsString)) + }) + It("podman run invalid secret option", func() { secretsString := "somesecretdata" secretFilePath := filepath.Join(podmanTest.TempDir, "secret") @@ -1706,6 +1749,11 @@ WORKDIR /madethis`, BB) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Not(Equal(0))) + // mount option with env type + session = podmanTest.Podman([]string{"run", "--secret", "source=mysecret,type=env,uid=1000", "--name", "secr", ALPINE, "printenv", "mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + // No source given session = podmanTest.Podman([]string{"run", "--secret", "type=env", "--name", "secr", ALPINE, "printenv", "mysecret"}) session.WaitWithDefaultTimeout()