Merge pull request #10221 from ashley-cui/envsec

Add support for environment variable secrets
This commit is contained in:
OpenShift Merge Robot
2021-05-07 05:34:26 -04:00
committed by GitHub
13 changed files with 293 additions and 15 deletions

View File

@ -639,12 +639,16 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string
} }
s.RestartPolicy = splitRestart[0] s.RestartPolicy = splitRestart[0]
} }
s.Secrets, s.EnvSecrets, err = parseSecrets(c.Secrets)
if err != nil {
return err
}
s.Remove = c.Rm s.Remove = c.Rm
s.StopTimeout = &c.StopTimeout s.StopTimeout = &c.StopTimeout
s.Timeout = c.Timeout s.Timeout = c.Timeout
s.Timezone = c.Timezone s.Timezone = c.Timezone
s.Umask = c.Umask s.Umask = c.Umask
s.Secrets = c.Secrets
s.PidFile = c.PidFile s.PidFile = c.PidFile
s.Volatile = c.Rm s.Volatile = c.Rm
@ -773,3 +777,70 @@ func parseThrottleIOPsDevices(iopsDevices []string) (map[string]specs.LinuxThrot
} }
return td, nil 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
}

View File

@ -2,15 +2,16 @@ package secrets
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"strings"
"github.com/containers/common/pkg/completion" "github.com/containers/common/pkg/completion"
"github.com/containers/podman/v3/cmd/podman/common" "github.com/containers/podman/v3/cmd/podman/common"
"github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/domain/entities"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -29,6 +30,7 @@ var (
var ( var (
createOpts = entities.SecretCreateOptions{} createOpts = entities.SecretCreateOptions{}
env = false
) )
func init() { func init() {
@ -43,6 +45,9 @@ func init() {
driverFlagName := "driver" driverFlagName := "driver"
flags.StringVar(&createOpts.Driver, driverFlagName, "file", "Specify secret driver") flags.StringVar(&createOpts.Driver, driverFlagName, "file", "Specify secret driver")
_ = createCmd.RegisterFlagCompletionFunc(driverFlagName, completion.AutocompleteNone) _ = 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 { func create(cmd *cobra.Command, args []string) error {
@ -52,7 +57,13 @@ func create(cmd *cobra.Command, args []string) error {
path := args[1] path := args[1]
var reader io.Reader 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() stat, err := os.Stdin.Stat()
if err != nil { if err != nil {
return err return err

View File

@ -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. 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. 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, 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). 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 When secrets are specified as type `mount`, the secrets are copied and mounted into the container when a container is created.
`podman secret rm`, the container will still have access to the secret. If a secret is deleted and When secrets are specified as type `env`, the secret will be set as an environment variable within the container.
another secret is created with the same name, the secret inside the container will not change; the old Secrets are written in the container at the time of container creation, and modifying the secret using `podman secret` commands
secret value will still remain. 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* #### **\-\-security-opt**=*option*

View File

@ -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. 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. 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, 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). 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 When secrets are specified as type `mount`, the secrets are copied and mounted into the container when a container is created.
`podman secret rm`, the container will still have access to the secret. If a secret is deleted and When secrets are specified as type `env`, the secret will be set as an environment variable within the container.
another secret is created with the same name, the secret inside the container will not change; the old Secrets are written in the container at the time of container creation, and modifying the secret using `podman secret` commands
secret value will still remain. 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* #### **\-\-security-opt**=*option*

View File

@ -20,6 +20,10 @@ Secrets will not be committed to an image with `podman commit`, and will not be
## OPTIONS ## OPTIONS
#### **\-\-env**=*false*
Read secret data from environment variable
#### **\-\-driver**=*driver* #### **\-\-driver**=*driver*
Specify the secret driver (default **file**, which is unencrypted). Specify the secret driver (default **file**, which is unencrypted).

View File

@ -373,4 +373,6 @@ type ContainerMiscConfig struct {
PidFile string `json:"pid_file,omitempty"` PidFile string `json:"pid_file,omitempty"`
// CDIDevices contains devices that use the CDI // CDIDevices contains devices that use the CDI
CDIDevices []string `json:"cdiDevices,omitempty"` CDIDevices []string `json:"cdiDevices,omitempty"`
// EnvSecrets are secrets that are set as environment variables
EnvSecrets map[string]*secrets.Secret `json:"secret_env,omitempty"`
} }

View File

@ -29,6 +29,7 @@ import (
"github.com/containers/common/pkg/apparmor" "github.com/containers/common/pkg/apparmor"
"github.com/containers/common/pkg/chown" "github.com/containers/common/pkg/chown"
"github.com/containers/common/pkg/config" "github.com/containers/common/pkg/config"
"github.com/containers/common/pkg/secrets"
"github.com/containers/common/pkg/subscriptions" "github.com/containers/common/pkg/subscriptions"
"github.com/containers/common/pkg/umask" "github.com/containers/common/pkg/umask"
"github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/define"
@ -757,6 +758,19 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
if c.state.ExtensionStageHooks, err = c.setupOCIHooks(ctx, g.Config); err != nil { if c.state.ExtensionStageHooks, err = c.setupOCIHooks(ctx, g.Config); err != nil {
return nil, errors.Wrapf(err, "error setting up OCI Hooks") 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 return g.Config, nil
} }

View File

@ -1716,6 +1716,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 // WithPidFile adds pidFile to the container
func WithPidFile(pidFile string) CtrCreateOption { func WithPidFile(pidFile string) CtrCreateOption {
return func(ctr *Container) error { return func(ctr *Container) error {

View File

@ -402,6 +402,11 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.
if len(s.Secrets) != 0 { if len(s.Secrets) != 0 {
options = append(options, libpod.WithSecrets(s.Secrets)) options = append(options, libpod.WithSecrets(s.Secrets))
} }
if len(s.EnvSecrets) != 0 {
options = append(options, libpod.WithEnvSecrets(s.EnvSecrets))
}
if len(s.DependencyContainers) > 0 { if len(s.DependencyContainers) > 0 {
deps := make([]*libpod.Container, 0, len(s.DependencyContainers)) deps := make([]*libpod.Container, 0, len(s.DependencyContainers))
for _, ctr := range s.DependencyContainers { for _, ctr := range s.DependencyContainers {

View File

@ -180,6 +180,9 @@ type ContainerBasicConfig struct {
// set tags as `json:"-"` for not supported remote // set tags as `json:"-"` for not supported remote
// Optional. // Optional.
PidFile string `json:"-"` 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 // ContainerStorageConfig contains information on the storage configuration of a

View File

@ -304,4 +304,28 @@ var _ = Describe("Podman commit", func() {
Expect(session.ExitCode()).To(Not(Equal(0))) 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)))
})
}) })

View File

@ -1611,6 +1611,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() { It("podman run --requires", func() {
depName := "ctr1" depName := "ctr1"
depContainer := podmanTest.Podman([]string{"create", "--name", depName, ALPINE, "top"}) depContainer := podmanTest.Podman([]string{"create", "--name", depName, ALPINE, "top"})

View File

@ -199,4 +199,27 @@ var _ = Describe("Podman secret", func() {
Expect(len(session.OutputToStringArray())).To(Equal(1)) 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))
})
}) })