diff options
author | Ashley Cui <acui@redhat.com> | 2021-05-05 10:34:13 -0400 |
---|---|---|
committer | Ashley Cui <acui@redhat.com> | 2021-05-06 14:00:57 -0400 |
commit | 2634cb234f1500b76a2fd89351b9ad8a737a24ea (patch) | |
tree | 10fb9e9dc38ef35ecd9390b43effe5dc667578b0 | |
parent | 476c76f580d5cd092ff958765af36857b2a68d6c (diff) | |
download | podman-2634cb234f1500b76a2fd89351b9ad8a737a24ea.tar.gz podman-2634cb234f1500b76a2fd89351b9ad8a737a24ea.tar.bz2 podman-2634cb234f1500b76a2fd89351b9ad8a737a24ea.zip |
Add support for environment variable secrets
Env var secrets are env vars that are set inside the container but not
commited to and image. Also support reading from env var when creating a
secret.
Signed-off-by: Ashley Cui <acui@redhat.com>
-rw-r--r-- | cmd/podman/common/specgen.go | 73 | ||||
-rw-r--r-- | cmd/podman/secrets/create.go | 15 | ||||
-rw-r--r-- | docs/source/markdown/podman-create.1.md | 17 | ||||
-rw-r--r-- | docs/source/markdown/podman-run.1.md | 17 | ||||
-rw-r--r-- | docs/source/markdown/podman-secret-create.1.md | 4 | ||||
-rw-r--r-- | libpod/container_config.go | 2 | ||||
-rw-r--r-- | libpod/container_internal_linux.go | 14 | ||||
-rw-r--r-- | libpod/options.go | 22 | ||||
-rw-r--r-- | pkg/specgen/generate/container_create.go | 5 | ||||
-rw-r--r-- | pkg/specgen/specgen.go | 3 | ||||
-rw-r--r-- | test/e2e/commit_test.go | 24 | ||||
-rw-r--r-- | test/e2e/run_test.go | 89 | ||||
-rw-r--r-- | test/e2e/secret_test.go | 23 |
13 files changed, 293 insertions, 15 deletions
diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index 310a07a00..ce7ca2b4b 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -639,11 +639,15 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string } s.RestartPolicy = splitRestart[0] } + + s.Secrets, s.EnvSecrets, err = parseSecrets(c.Secrets) + if err != nil { + return err + } s.Remove = c.Rm s.StopTimeout = &c.StopTimeout s.Timezone = c.Timezone s.Umask = c.Umask - s.Secrets = c.Secrets s.PidFile = c.PidFile return nil @@ -771,3 +775,70 @@ func parseThrottleIOPsDevices(iopsDevices []string) (map[string]specs.LinuxThrot } return td, nil } + +func parseSecrets(secrets []string) ([]string, map[string]string, error) { + secretParseError := errors.New("error parsing secret") + var mount []string + envs := make(map[string]string) + for _, val := range secrets { + source := "" + secretType := "" + target := "" + split := strings.Split(val, ",") + + // --secret mysecret + if len(split) == 1 { + source = val + mount = append(mount, source) + continue + } + // --secret mysecret,opt=opt + if !strings.Contains(split[0], "=") { + source = split[0] + split = split[1:] + } + // TODO: implement other secret options + for _, val := range split { + kv := strings.SplitN(val, "=", 2) + if len(kv) < 2 { + return nil, nil, errors.Wrapf(secretParseError, "option %s must be in form option=value", val) + } + switch kv[0] { + case "source": + source = kv[1] + case "type": + if secretType != "" { + return nil, nil, errors.Wrap(secretParseError, "cannot set more tha one secret type") + } + if kv[1] != "mount" && kv[1] != "env" { + return nil, nil, errors.Wrapf(secretParseError, "type %s is invalid", kv[1]) + } + secretType = kv[1] + case "target": + target = kv[1] + default: + return nil, nil, errors.Wrapf(secretParseError, "option %s invalid", val) + } + } + + if secretType == "" { + secretType = "mount" + } + if source == "" { + return nil, nil, errors.Wrapf(secretParseError, "no source found %s", val) + } + if secretType == "mount" { + if target != "" { + return nil, nil, errors.Wrapf(secretParseError, "target option is invalid for mounted secrets") + } + mount = append(mount, source) + } + if secretType == "env" { + if target == "" { + target = source + } + envs[target] = source + } + } + return mount, envs, nil +} diff --git a/cmd/podman/secrets/create.go b/cmd/podman/secrets/create.go index 7374b682b..4204f30b4 100644 --- a/cmd/podman/secrets/create.go +++ b/cmd/podman/secrets/create.go @@ -2,15 +2,16 @@ package secrets import ( "context" - "errors" "fmt" "io" "os" + "strings" "github.com/containers/common/pkg/completion" "github.com/containers/podman/v3/cmd/podman/common" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -29,6 +30,7 @@ var ( var ( createOpts = entities.SecretCreateOptions{} + env = false ) func init() { @@ -43,6 +45,9 @@ func init() { driverFlagName := "driver" flags.StringVar(&createOpts.Driver, driverFlagName, "file", "Specify secret driver") _ = createCmd.RegisterFlagCompletionFunc(driverFlagName, completion.AutocompleteNone) + + envFlagName := "env" + flags.BoolVar(&env, envFlagName, false, "Read secret data from environment variable") } func create(cmd *cobra.Command, args []string) error { @@ -52,7 +57,13 @@ func create(cmd *cobra.Command, args []string) error { path := args[1] var reader io.Reader - if path == "-" || path == "/dev/stdin" { + if env { + envValue := os.Getenv(path) + if envValue == "" { + return errors.Errorf("cannot create store secret data: environment variable %s is not set", path) + } + reader = strings.NewReader(envValue) + } else if path == "-" || path == "/dev/stdin" { stat, err := os.Stdin.Stat() if err != nil { return err diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index 229bb82f5..7e1d0be4a 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -840,7 +840,7 @@ Specify the policy to select the seccomp profile. If set to *image*, Podman will Note that this feature is experimental and may change in the future. -#### **\-\-secret**=*secret* +#### **\-\-secret**=*secret*[,opt=opt ...] Give the container access to a secret. Can be specified multiple times. @@ -848,12 +848,17 @@ A secret is a blob of sensitive data which a container needs at runtime but should not be stored in the image or in source control, such as usernames and passwords, TLS certificates and keys, SSH keys or other important generic strings or binary content (up to 500 kb in size). -Secrets are copied and mounted into the container when a container is created. If a secret is deleted using -`podman secret rm`, the container will still have access to the secret. If a secret is deleted and -another secret is created with the same name, the secret inside the container will not change; the old -secret value will still remain. +When secrets are specified as type `mount`, the secrets are copied and mounted into the container when a container is created. +When secrets are specified as type `env`, the secret will be set as an environment variable within the container. +Secrets are written in the container at the time of container creation, and modifying the secret using `podman secret` commands +after the container is created will not affect the secret inside the container. -Secrets are managed using the `podman secret` command. +Secrets and its storage are managed using the `podman secret` command. + +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. #### **\-\-security-opt**=*option* diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index 2e6d97a05..56ca8fde0 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -892,7 +892,7 @@ Specify the policy to select the seccomp profile. If set to *image*, Podman will Note that this feature is experimental and may change in the future. -#### **\-\-secret**=*secret* +#### **\-\-secret**=*secret*[,opt=opt ...] Give the container access to a secret. Can be specified multiple times. @@ -900,12 +900,17 @@ A secret is a blob of sensitive data which a container needs at runtime but should not be stored in the image or in source control, such as usernames and passwords, TLS certificates and keys, SSH keys or other important generic strings or binary content (up to 500 kb in size). -Secrets are copied and mounted into the container when a container is created. If a secret is deleted using -`podman secret rm`, the container will still have access to the secret. If a secret is deleted and -another secret is created with the same name, the secret inside the container will not change; the old -secret value will still remain. +When secrets are specified as type `mount`, the secrets are copied and mounted into the container when a container is created. +When secrets are specified as type `env`, the secret will be set as an environment variable within the container. +Secrets are written in the container at the time of container creation, and modifying the secret using `podman secret` commands +after the container is created will not affect the secret inside the container. -Secrets are managed using the `podman secret` command +Secrets and its storage are managed using the `podman secret` command. + +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. #### **\-\-security-opt**=*option* diff --git a/docs/source/markdown/podman-secret-create.1.md b/docs/source/markdown/podman-secret-create.1.md index f5a97a0f3..7aacca3fe 100644 --- a/docs/source/markdown/podman-secret-create.1.md +++ b/docs/source/markdown/podman-secret-create.1.md @@ -20,6 +20,10 @@ Secrets will not be committed to an image with `podman commit`, and will not be ## OPTIONS +#### **\-\-env**=*false* + +Read secret data from environment variable + #### **\-\-driver**=*driver* Specify the secret driver (default **file**, which is unencrypted). diff --git a/libpod/container_config.go b/libpod/container_config.go index d0572fbc2..ac17a2c4f 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -368,4 +368,6 @@ type ContainerMiscConfig struct { PidFile string `json:"pid_file,omitempty"` // CDIDevices contains devices that use the CDI CDIDevices []string `json:"cdiDevices,omitempty"` + // EnvSecrets are secrets that are set as environment variables + EnvSecrets map[string]*secrets.Secret `json:"secret_env,omitempty"` } diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index f4762b5ff..c6839ffd0 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -29,6 +29,7 @@ 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" @@ -763,6 +764,19 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { if c.state.ExtensionStageHooks, err = c.setupOCIHooks(ctx, g.Config); err != nil { return nil, errors.Wrapf(err, "error setting up OCI Hooks") } + if len(c.config.EnvSecrets) > 0 { + manager, err := secrets.NewManager(c.runtime.GetSecretsStorageDir()) + if err != nil { + return nil, err + } + for name, secr := range c.config.EnvSecrets { + _, data, err := manager.LookupSecretData(secr.Name) + if err != nil { + return nil, err + } + g.AddProcessEnv(name, string(data)) + } + } return g.Config, nil } diff --git a/libpod/options.go b/libpod/options.go index 103a9a80a..7c574df75 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1703,6 +1703,28 @@ func WithSecrets(secretNames []string) CtrCreateOption { } } +// WithSecrets adds environment variable secrets to the container +func WithEnvSecrets(envSecrets map[string]string) CtrCreateOption { + return func(ctr *Container) error { + ctr.config.EnvSecrets = make(map[string]*secrets.Secret) + if ctr.valid { + return define.ErrCtrFinalized + } + manager, err := secrets.NewManager(ctr.runtime.GetSecretsStorageDir()) + if err != nil { + return err + } + for target, src := range envSecrets { + secr, err := manager.Lookup(src) + if err != nil { + return err + } + ctr.config.EnvSecrets[target] = secr + } + return nil + } +} + // WithPidFile adds pidFile to the container func WithPidFile(pidFile string) CtrCreateOption { return func(ctr *Container) error { diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 2f623bf10..dcacb3780 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -397,6 +397,11 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. if len(s.Secrets) != 0 { options = append(options, libpod.WithSecrets(s.Secrets)) } + + if len(s.EnvSecrets) != 0 { + options = append(options, libpod.WithEnvSecrets(s.EnvSecrets)) + } + if len(s.DependencyContainers) > 0 { deps := make([]*libpod.Container, 0, len(s.DependencyContainers)) for _, ctr := range s.DependencyContainers { diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index e3d4b1436..4d89f72e4 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -175,6 +175,9 @@ type ContainerBasicConfig struct { // set tags as `json:"-"` for not supported remote // Optional. PidFile string `json:"-"` + // EnvSecrets are secrets that will be set as environment variables + // Optional. + EnvSecrets map[string]string `json:"secret_env,omitempty"` } // ContainerStorageConfig contains information on the storage configuration of a diff --git a/test/e2e/commit_test.go b/test/e2e/commit_test.go index 0d3f2bed7..70a66124a 100644 --- a/test/e2e/commit_test.go +++ b/test/e2e/commit_test.go @@ -304,4 +304,28 @@ var _ = Describe("Podman commit", func() { Expect(session.ExitCode()).To(Not(Equal(0))) }) + + It("podman commit should not commit env secret", 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", "source=mysecret,type=env", "--name", "secr", ALPINE, "printenv", "mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(Equal(secretsString)) + + session = podmanTest.Podman([]string{"commit", "secr", "foobar.com/test1-image:latest"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"run", "foobar.com/test1-image:latest", "printenv", "mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.OutputToString()).To(Not(ContainSubstring(secretsString))) + }) }) diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index 93505d742..4859db524 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -1589,6 +1589,95 @@ WORKDIR /madethis`, BB) }) + It("podman run --secret source=mysecret,type=mount", 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", "source=mysecret,type=mount", "--name", "secr", ALPINE, "cat", "/run/secrets/mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(Equal(secretsString)) + + session = podmanTest.Podman([]string{"inspect", "secr", "--format", " {{(index .Config.Secrets 0).Name}}"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("mysecret")) + + }) + + It("podman run --secret source=mysecret,type=env", 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", "source=mysecret,type=env", "--name", "secr", ALPINE, "printenv", "mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(Equal(secretsString)) + }) + + It("podman run --secret target option", 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)) + // target with mount type should fail + session = podmanTest.Podman([]string{"run", "--secret", "source=mysecret,type=mount,target=anotherplace", "--name", "secr", ALPINE, "cat", "/run/secrets/mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + + session = podmanTest.Podman([]string{"run", "--secret", "source=mysecret,type=env,target=anotherplace", "--name", "secr", ALPINE, "printenv", "anotherplace"}) + 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") + 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)) + + // Invalid type + session = podmanTest.Podman([]string{"run", "--secret", "source=mysecret,type=other", "--name", "secr", ALPINE, "printenv", "mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + + // Invalid option + session = podmanTest.Podman([]string{"run", "--secret", "source=mysecret,invalid=invalid", "--name", "secr", ALPINE, "printenv", "mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + + // Option syntax not valid + session = podmanTest.Podman([]string{"run", "--secret", "source=mysecret,type", "--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() + Expect(session.ExitCode()).To(Not(Equal(0))) + }) + It("podman run --requires", func() { depName := "ctr1" depContainer := podmanTest.Podman([]string{"create", "--name", depName, ALPINE, "top"}) diff --git a/test/e2e/secret_test.go b/test/e2e/secret_test.go index fbee18442..b54b959bf 100644 --- a/test/e2e/secret_test.go +++ b/test/e2e/secret_test.go @@ -199,4 +199,27 @@ var _ = Describe("Podman secret", func() { Expect(len(session.OutputToStringArray())).To(Equal(1)) }) + It("podman secret creates from environment variable", func() { + // no env variable set, should fail + session := podmanTest.Podman([]string{"secret", "create", "--env", "a", "MYENVVAR"}) + session.WaitWithDefaultTimeout() + secrID := session.OutputToString() + Expect(session.ExitCode()).To(Not(Equal(0))) + + os.Setenv("MYENVVAR", "somedata") + if IsRemote() { + podmanTest.RestartRemoteService() + } + + session = podmanTest.Podman([]string{"secret", "create", "--env", "a", "MYENVVAR"}) + session.WaitWithDefaultTimeout() + secrID = session.OutputToString() + Expect(session.ExitCode()).To(Equal(0)) + + inspect := podmanTest.Podman([]string{"secret", "inspect", "--format", "{{.ID}}", secrID}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(Equal(0)) + Expect(inspect.OutputToString()).To(Equal(secrID)) + }) + }) |