feat: support --mount=type=secret,id=foo,env=bar

Allow secrets specified with mount to result in env variable set as an
alternative to mounting a file.

This matches Docker behaviour:
https://docs.docker.com/reference/dockerfile/#example-mount-as-environment-variable

Signed-off-by: Adam Eijdenberg <adam@continusec.com>
This commit is contained in:
Adam Eijdenberg 2025-07-14 12:39:03 +10:00
parent 50d9584c1e
commit 3a2513cc84
7 changed files with 107 additions and 60 deletions

View File

@ -109,6 +109,17 @@ type Secret struct {
SourceType string
}
func (s Secret) ResolveValue() ([]byte, error) {
switch s.SourceType {
case "env":
return []byte(os.Getenv(s.Source)), nil
case "file":
return os.ReadFile(s.Source)
default:
return nil, errors.New("invalid secret type")
}
}
// BuildOutputOptions contains the the outcome of parsing the value of a build --output flag
type BuildOutputOption struct {
Path string // Only valid if !IsStdout

View File

@ -967,6 +967,10 @@ The location of the secret in the container can be overridden using the
`RUN --mount=type=secret,id=mysecret,target=/run/secrets/myothersecret cat /run/secrets/myothersecret`
The secret may alternatively be exposed as an environment variable to the process:
`RUN --mount=type=secret,id=mysecret,env=FOO sh -c 'echo "Hello $FOO"'`
Note: changing the contents of secret files will not trigger a rebuild of layers that use said secrets.
**--security-opt**=[]

2
run.go
View File

@ -203,6 +203,8 @@ type runMountArtifacts struct {
TargetLocks []*lockfile.LockFile
// Intermediate mount points, which should be Unmount()ed and Removed()d
IntermediateMounts []string
// Environment variables that should be set for RUN that contain secrets, each is name=value form
SecretEnvVars []string
}
// RunMountInfo are the available run mounts for this run

View File

@ -1433,6 +1433,12 @@ func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath st
mounts = append(mounts, mount)
}
// Some mounts require env vars to be set, do these here
spec.Process.Env = append(spec.Process.Env, mountArtifacts.SecretEnvVars...)
if mountArtifacts.SSHAuthSock != "" {
spec.Process.Env = append(spec.Process.Env, "SSH_AUTH_SOCK="+mountArtifacts.SSHAuthSock)
}
// Set the list in the spec.
spec.Mounts = mounts
succeeded = true
@ -1522,6 +1528,7 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
intermediateMounts := make([]string, 0, len(mounts))
finalMounts := make([]specs.Mount, 0, len(mounts))
agents := make([]*sshagent.AgentServer, 0, len(mounts))
var secretEnvVars []string
defaultSSHSock := ""
targetLocks := []*lockfile.LockFile{}
var overlayDirs []string
@ -1561,11 +1568,7 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
}
}()
for _, mount := range mounts {
var mountSpec *specs.Mount
var err error
var envFile, image, bundleMountsDir, overlayDir, intermediateMount string
var agent *sshagent.AgentServer
var tl *lockfile.LockFile
var bundleMountsDir string
tokens := strings.Split(mount, ",")
@ -1583,18 +1586,21 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
}
switch mountType {
case "secret":
mountSpec, envFile, err = b.getSecretMount(tokens, sources.Secrets, idMaps, sources.WorkDir)
mountOrEnvSpec, err := b.getSecretMount(tokens, sources.Secrets, idMaps, sources.WorkDir)
if err != nil {
return nil, nil, err
}
if mountSpec != nil {
finalMounts = append(finalMounts, *mountSpec)
if envFile != "" {
tmpFiles = append(tmpFiles, envFile)
}
if mountOrEnvSpec.Mount != nil {
finalMounts = append(finalMounts, *mountOrEnvSpec.Mount)
}
if mountOrEnvSpec.EnvFile != "" {
tmpFiles = append(tmpFiles, mountOrEnvSpec.EnvFile)
}
if mountOrEnvSpec.EnvVariable != "" {
secretEnvVars = append(secretEnvVars, mountOrEnvSpec.EnvVariable)
}
case "ssh":
mountSpec, agent, err = b.getSSHMount(tokens, len(agents), sources.SSHSources, idMaps)
mountSpec, agent, err := b.getSSHMount(tokens, len(agents), sources.SSHSources, idMaps)
if err != nil {
return nil, nil, err
}
@ -1607,11 +1613,12 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
}
case define.TypeBind:
if bundleMountsDir == "" {
var err error
if bundleMountsDir, err = os.MkdirTemp(bundlePath, "mounts"); err != nil {
return nil, nil, err
}
}
mountSpec, image, intermediateMount, overlayDir, err = b.getBindMount(tokens, sources.SystemContext, sources.ContextDir, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
mountSpec, image, intermediateMount, overlayDir, err := b.getBindMount(tokens, sources.SystemContext, sources.ContextDir, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
if err != nil {
return nil, nil, err
}
@ -1626,18 +1633,19 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
}
finalMounts = append(finalMounts, *mountSpec)
case "tmpfs":
mountSpec, err = b.getTmpfsMount(tokens, idMaps, sources.WorkDir)
mountSpec, err := b.getTmpfsMount(tokens, idMaps, sources.WorkDir)
if err != nil {
return nil, nil, err
}
finalMounts = append(finalMounts, *mountSpec)
case "cache":
if bundleMountsDir == "" {
var err error
if bundleMountsDir, err = os.MkdirTemp(bundlePath, "mounts"); err != nil {
return nil, nil, err
}
}
mountSpec, image, intermediateMount, overlayDir, tl, err = b.getCacheMount(tokens, sources.SystemContext, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
mountSpec, image, intermediateMount, overlayDir, tl, err := b.getCacheMount(tokens, sources.SystemContext, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
if err != nil {
return nil, nil, err
}
@ -1666,6 +1674,7 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
SSHAuthSock: defaultSSHSock,
TargetLocks: targetLocks,
IntermediateMounts: intermediateMounts,
SecretEnvVars: secretEnvVars,
}
return finalMounts, artifacts, nil
}
@ -1731,13 +1740,17 @@ func (b *Builder) getTmpfsMount(tokens []string, idMaps IDMaps, workDir string)
return &volumes[0], nil
}
func (b *Builder) getSecretMount(tokens []string, secrets map[string]define.Secret, idMaps IDMaps, workdir string) (_ *specs.Mount, _ string, retErr error) {
errInvalidSyntax := errors.New("secret should have syntax id=id[,target=path,required=bool,mode=uint,uid=uint,gid=uint")
func (b *Builder) getSecretMount(tokens []string, secrets map[string]define.Secret, idMaps IDMaps, workdir string) (rv struct {
Mount *specs.Mount // set if mount created
EnvFile string // set if caller mount created from temp created env file
EnvVariable string // set if caller should add to env variable list
}, retErr error,
) {
errInvalidSyntax := errors.New("secret should have syntax id=id[,target=path,required=bool,mode=uint,uid=uint,gid=uint,env=dstVarName")
if len(tokens) == 0 {
return nil, "", errInvalidSyntax
return rv, errInvalidSyntax
}
var err error
var id, target string
var id, target, env string
var required bool
var uid, gid uint32
var mode uint32 = 0o400
@ -1757,110 +1770,123 @@ func (b *Builder) getSecretMount(tokens []string, secrets map[string]define.Secr
case "required":
required = true
if len(kv) > 1 {
var err error
required, err = strconv.ParseBool(kv[1])
if err != nil {
return nil, "", errInvalidSyntax
return rv, errInvalidSyntax
}
}
case "mode":
mode64, err := strconv.ParseUint(kv[1], 8, 32)
if err != nil {
return nil, "", errInvalidSyntax
return rv, errInvalidSyntax
}
mode = uint32(mode64)
case "uid":
uid64, err := strconv.ParseUint(kv[1], 10, 32)
if err != nil {
return nil, "", errInvalidSyntax
return rv, errInvalidSyntax
}
uid = uint32(uid64)
case "gid":
gid64, err := strconv.ParseUint(kv[1], 10, 32)
if err != nil {
return nil, "", errInvalidSyntax
return rv, errInvalidSyntax
}
gid = uint32(gid64)
case "env":
if kv[1] == "" {
return rv, errInvalidSyntax
}
env = kv[1]
default:
return nil, "", errInvalidSyntax
return rv, errInvalidSyntax
}
}
if id == "" {
return nil, "", errInvalidSyntax
return rv, errInvalidSyntax
}
// first fetch the secret data
secr, ok := secrets[id]
if !ok {
if required {
return rv, fmt.Errorf("secret required but no secret with id %q found", id)
}
return rv, nil
}
data, err := secr.ResolveValue()
if err != nil {
return rv, err
}
// if env is set, then we can return now
if env != "" {
rv.EnvVariable = env + "=" + string(data)
return rv, nil
}
// else we fallback to default behaviour of creating mount
// Default location for secrets is /run/secrets/id
if target == "" {
target = "/run/secrets/" + id
}
secr, ok := secrets[id]
if !ok {
if required {
return nil, "", fmt.Errorf("secret required but no secret with id %q found", id)
}
return nil, "", nil
}
var data []byte
var envFile string
var ctrFileOnHost string
switch secr.SourceType {
case "env":
data = []byte(os.Getenv(secr.Source))
tmpFile, err := os.CreateTemp(tmpdir.GetTempDir(), "buildah*")
if err != nil {
return nil, "", err
return rv, err
}
defer func() {
if retErr != nil {
os.Remove(tmpFile.Name())
}
}()
envFile = tmpFile.Name()
rv.EnvFile = tmpFile.Name()
ctrFileOnHost = tmpFile.Name()
case "file":
containerWorkingDir, err := b.store.ContainerDirectory(b.ContainerID)
if err != nil {
return nil, "", err
}
data, err = os.ReadFile(secr.Source)
if err != nil {
return nil, "", err
return rv, err
}
ctrFileOnHost = filepath.Join(containerWorkingDir, "secrets", digest.FromString(id).Encoded()[:16])
default:
return nil, "", errors.New("invalid source secret type")
return rv, errors.New("invalid source secret type")
}
// Copy secrets to container working dir (or tmp dir if it's an env), since we need to chmod,
// chown and relabel it for the container user and we don't want to mess with the original file
if err := os.MkdirAll(filepath.Dir(ctrFileOnHost), 0o755); err != nil {
return nil, "", err
return rv, err
}
if err := os.WriteFile(ctrFileOnHost, data, 0o644); err != nil {
return nil, "", err
return rv, err
}
if err := relabel(ctrFileOnHost, b.MountLabel, false); err != nil {
return nil, "", err
return rv, err
}
hostUID, hostGID, err := util.GetHostIDs(idMaps.uidmap, idMaps.gidmap, uid, gid)
if err != nil {
return nil, "", err
return rv, err
}
if err := os.Lchown(ctrFileOnHost, int(hostUID), int(hostGID)); err != nil {
return nil, "", err
return rv, err
}
if err := os.Chmod(ctrFileOnHost, os.FileMode(mode)); err != nil {
return nil, "", err
return rv, err
}
newMount := specs.Mount{
rv.Mount = &specs.Mount{
Destination: target,
Type: define.TypeBind,
Source: ctrFileOnHost,
Options: append(define.BindOptions, "rprivate", "ro"),
}
return &newMount, envFile, nil
return rv, nil
}
// getSSHMount parses the --mount type=ssh flag in the Containerfile, checks if there's an ssh source provided, and creates and starts an ssh-agent to be forwarded into the container

View File

@ -276,10 +276,6 @@ func (b *Builder) Run(command []string, options RunOptions) error {
if err != nil {
return fmt.Errorf("resolving mountpoints for container %q: %w", b.ContainerID, err)
}
if runArtifacts.SSHAuthSock != "" {
sshenv := "SSH_AUTH_SOCK=" + runArtifacts.SSHAuthSock
spec.Process.Env = append(spec.Process.Env, sshenv)
}
// following run was called from `buildah run`
// and some images were mounted for this run

View File

@ -513,10 +513,6 @@ rootless=%d
if err != nil {
return fmt.Errorf("resolving mountpoints for container %q: %w", b.ContainerID, err)
}
if runArtifacts.SSHAuthSock != "" {
sshenv := "SSH_AUTH_SOCK=" + runArtifacts.SSHAuthSock
spec.Process.Env = append(spec.Process.Env, sshenv)
}
// Create any mount points that we need that aren't already present in
// the rootfs.

View File

@ -8888,3 +8888,15 @@ _EOF
run_buildah --root=${TEST_SCRATCH_DIR}/newroot --storage-opt=imagestore=${TEST_SCRATCH_DIR}/root build --pull=never ${contextdir}
run_buildah --root=${TEST_SCRATCH_DIR}/newroot --storage-opt=imagestore=${TEST_SCRATCH_DIR}/root build --pull=never --squash ${contextdir}
}
@test "use-secret-to-env-variable" {
local outpath="${TEST_SCRATCH_DIR}/timestamp-after-secret.tar"
BAR=baz run_buildah build --secret id=mysecret,env=BAR -f <(printf "FROM alpine\nRUN --mount=type=secret,id=mysecret,env=FOO,required sh -c 'echo "'"Hello $FOO"'"'") --tag=oci-archive:${outpath}.a --timestamp 0
expect_output --substring "Hello baz"
BAR=boz run_buildah build --secret id=mysecret,env=BAR -f <(printf "FROM alpine\nRUN --mount=type=secret,id=mysecret,env=FOO,required sh -c 'echo "'"Hello $FOO"'"'") --tag=oci-archive:${outpath}.b --timestamp 0
expect_output --substring "Hello boz"
# even though different secret was passed to each(baz vs boz), we expect the same result, ie should not affect build history
diff "${outpath}.a" "${outpath}.b"
}