mirror of
https://github.com/containers/podman.git
synced 2025-07-15 03:02:52 +08:00
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>
This commit is contained in:
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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*
|
||||
|
||||
|
@ -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*
|
||||
|
||||
|
@ -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).
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)))
|
||||
})
|
||||
})
|
||||
|
@ -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"})
|
||||
|
@ -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))
|
||||
})
|
||||
|
||||
})
|
||||
|
Reference in New Issue
Block a user