Skip to content

Commit bbfa364

Browse files
committed
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 <[email protected]>
1 parent daf7143 commit bbfa364

File tree

7 files changed

+106
-59
lines changed

7 files changed

+106
-59
lines changed

define/types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,17 @@ type Secret struct {
109109
SourceType string
110110
}
111111

112+
func (s Secret) ResolveValue() ([]byte, error) {
113+
switch s.SourceType {
114+
case "env":
115+
return []byte(os.Getenv(s.Source)), nil
116+
case "file":
117+
return os.ReadFile(s.Source)
118+
default:
119+
return nil, errors.New("invalid secret type")
120+
}
121+
}
122+
112123
// BuildOutputOptions contains the the outcome of parsing the value of a build --output flag
113124
type BuildOutputOption struct {
114125
Path string // Only valid if !IsStdout

docs/buildah-build.1.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,10 @@ The location of the secret in the container can be overridden using the
967967

968968
`RUN --mount=type=secret,id=mysecret,target=/run/secrets/myothersecret cat /run/secrets/myothersecret`
969969

970+
The secret may alternatively be exposed as an environment variable to the process:
971+
972+
`RUN --mount=type=secret,id=mysecret,env=FOO sh -c 'echo "Hello $FOO"'`
973+
970974
Note: changing the contents of secret files will not trigger a rebuild of layers that use said secrets.
971975

972976
**--security-opt**=[]

run.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ type runMountArtifacts struct {
203203
TargetLocks []*lockfile.LockFile
204204
// Intermediate mount points, which should be Unmount()ed and Removed()d
205205
IntermediateMounts []string
206+
// Environment variables that should be set for RUN that contain secrets, each is name=value form
207+
SecretEnvVars []string
206208
}
207209

208210
// RunMountInfo are the available run mounts for this run

run_common.go

Lines changed: 77 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,12 @@ func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath st
14141414
mounts = append(mounts, mount)
14151415
}
14161416

1417+
// Some mounts require env vars to be set, do these here
1418+
spec.Process.Env = append(spec.Process.Env, mountArtifacts.SecretEnvVars...)
1419+
if mountArtifacts.SSHAuthSock != "" {
1420+
spec.Process.Env = append(spec.Process.Env, "SSH_AUTH_SOCK="+mountArtifacts.SSHAuthSock)
1421+
}
1422+
14171423
// Set the list in the spec.
14181424
spec.Mounts = mounts
14191425
succeeded = true
@@ -1503,6 +1509,7 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
15031509
intermediateMounts := make([]string, 0, len(mounts))
15041510
finalMounts := make([]specs.Mount, 0, len(mounts))
15051511
agents := make([]*sshagent.AgentServer, 0, len(mounts))
1512+
var secretEnvVars []string
15061513
defaultSSHSock := ""
15071514
targetLocks := []*lockfile.LockFile{}
15081515
var overlayDirs []string
@@ -1542,11 +1549,7 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
15421549
}
15431550
}()
15441551
for _, mount := range mounts {
1545-
var mountSpec *specs.Mount
1546-
var err error
1547-
var envFile, image, bundleMountsDir, overlayDir, intermediateMount string
1548-
var agent *sshagent.AgentServer
1549-
var tl *lockfile.LockFile
1552+
var bundleMountsDir string
15501553

15511554
tokens := strings.Split(mount, ",")
15521555

@@ -1564,18 +1567,21 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
15641567
}
15651568
switch mountType {
15661569
case "secret":
1567-
mountSpec, envFile, err = b.getSecretMount(tokens, sources.Secrets, idMaps, sources.WorkDir)
1570+
mountOrEnvSpec, err := b.getSecretMount(tokens, sources.Secrets, idMaps, sources.WorkDir)
15681571
if err != nil {
15691572
return nil, nil, err
15701573
}
1571-
if mountSpec != nil {
1572-
finalMounts = append(finalMounts, *mountSpec)
1573-
if envFile != "" {
1574-
tmpFiles = append(tmpFiles, envFile)
1575-
}
1574+
if mountOrEnvSpec.Mount != nil {
1575+
finalMounts = append(finalMounts, *mountOrEnvSpec.Mount)
1576+
}
1577+
if mountOrEnvSpec.EnvFile != "" {
1578+
tmpFiles = append(tmpFiles, mountOrEnvSpec.EnvFile)
1579+
}
1580+
if mountOrEnvSpec.EnvVariable != "" {
1581+
secretEnvVars = append(secretEnvVars, mountOrEnvSpec.EnvVariable)
15761582
}
15771583
case "ssh":
1578-
mountSpec, agent, err = b.getSSHMount(tokens, len(agents), sources.SSHSources, idMaps)
1584+
mountSpec, agent, err := b.getSSHMount(tokens, len(agents), sources.SSHSources, idMaps)
15791585
if err != nil {
15801586
return nil, nil, err
15811587
}
@@ -1588,11 +1594,12 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
15881594
}
15891595
case define.TypeBind:
15901596
if bundleMountsDir == "" {
1597+
var err error
15911598
if bundleMountsDir, err = os.MkdirTemp(bundlePath, "mounts"); err != nil {
15921599
return nil, nil, err
15931600
}
15941601
}
1595-
mountSpec, image, intermediateMount, overlayDir, err = b.getBindMount(tokens, sources.SystemContext, sources.ContextDir, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
1602+
mountSpec, image, intermediateMount, overlayDir, err := b.getBindMount(tokens, sources.SystemContext, sources.ContextDir, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
15961603
if err != nil {
15971604
return nil, nil, err
15981605
}
@@ -1607,18 +1614,19 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
16071614
}
16081615
finalMounts = append(finalMounts, *mountSpec)
16091616
case "tmpfs":
1610-
mountSpec, err = b.getTmpfsMount(tokens, idMaps, sources.WorkDir)
1617+
mountSpec, err := b.getTmpfsMount(tokens, idMaps, sources.WorkDir)
16111618
if err != nil {
16121619
return nil, nil, err
16131620
}
16141621
finalMounts = append(finalMounts, *mountSpec)
16151622
case "cache":
16161623
if bundleMountsDir == "" {
1624+
var err error
16171625
if bundleMountsDir, err = os.MkdirTemp(bundlePath, "mounts"); err != nil {
16181626
return nil, nil, err
16191627
}
16201628
}
1621-
mountSpec, image, intermediateMount, overlayDir, tl, err = b.getCacheMount(tokens, sources.SystemContext, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
1629+
mountSpec, image, intermediateMount, overlayDir, tl, err := b.getCacheMount(tokens, sources.SystemContext, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
16221630
if err != nil {
16231631
return nil, nil, err
16241632
}
@@ -1647,6 +1655,7 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
16471655
SSHAuthSock: defaultSSHSock,
16481656
TargetLocks: targetLocks,
16491657
IntermediateMounts: intermediateMounts,
1658+
SecretEnvVars: secretEnvVars,
16501659
}
16511660
return finalMounts, artifacts, nil
16521661
}
@@ -1712,13 +1721,17 @@ func (b *Builder) getTmpfsMount(tokens []string, idMaps IDMaps, workDir string)
17121721
return &volumes[0], nil
17131722
}
17141723

1715-
func (b *Builder) getSecretMount(tokens []string, secrets map[string]define.Secret, idMaps IDMaps, workdir string) (_ *specs.Mount, _ string, retErr error) {
1716-
errInvalidSyntax := errors.New("secret should have syntax id=id[,target=path,required=bool,mode=uint,uid=uint,gid=uint")
1724+
func (b *Builder) getSecretMount(tokens []string, secrets map[string]define.Secret, idMaps IDMaps, workdir string) (rv struct {
1725+
Mount *specs.Mount // set if mount created
1726+
EnvFile string // set if caller mount created from temp created env file
1727+
EnvVariable string // set if caller should add to env variable list
1728+
}, retErr error,
1729+
) {
1730+
errInvalidSyntax := errors.New("secret should have syntax id=id[,target=path,required=bool,mode=uint,uid=uint,gid=uint,env=dstVarName")
17171731
if len(tokens) == 0 {
1718-
return nil, "", errInvalidSyntax
1732+
return rv, errInvalidSyntax
17191733
}
1720-
var err error
1721-
var id, target string
1734+
var id, target, env string
17221735
var required bool
17231736
var uid, gid uint32
17241737
var mode uint32 = 0o400
@@ -1738,110 +1751,123 @@ func (b *Builder) getSecretMount(tokens []string, secrets map[string]define.Secr
17381751
case "required":
17391752
required = true
17401753
if len(kv) > 1 {
1754+
var err error
17411755
required, err = strconv.ParseBool(kv[1])
17421756
if err != nil {
1743-
return nil, "", errInvalidSyntax
1757+
return rv, errInvalidSyntax
17441758
}
17451759
}
17461760
case "mode":
17471761
mode64, err := strconv.ParseUint(kv[1], 8, 32)
17481762
if err != nil {
1749-
return nil, "", errInvalidSyntax
1763+
return rv, errInvalidSyntax
17501764
}
17511765
mode = uint32(mode64)
17521766
case "uid":
17531767
uid64, err := strconv.ParseUint(kv[1], 10, 32)
17541768
if err != nil {
1755-
return nil, "", errInvalidSyntax
1769+
return rv, errInvalidSyntax
17561770
}
17571771
uid = uint32(uid64)
17581772
case "gid":
17591773
gid64, err := strconv.ParseUint(kv[1], 10, 32)
17601774
if err != nil {
1761-
return nil, "", errInvalidSyntax
1775+
return rv, errInvalidSyntax
17621776
}
17631777
gid = uint32(gid64)
1778+
case "env":
1779+
if kv[1] == "" {
1780+
return rv, errInvalidSyntax
1781+
}
1782+
env = kv[1]
17641783
default:
1765-
return nil, "", errInvalidSyntax
1784+
return rv, errInvalidSyntax
17661785
}
17671786
}
17681787

17691788
if id == "" {
1770-
return nil, "", errInvalidSyntax
1771-
}
1772-
// Default location for secrets is /run/secrets/id
1773-
if target == "" {
1774-
target = "/run/secrets/" + id
1789+
return rv, errInvalidSyntax
17751790
}
17761791

1792+
// first fetch the secret data
17771793
secr, ok := secrets[id]
17781794
if !ok {
17791795
if required {
1780-
return nil, "", fmt.Errorf("secret required but no secret with id %q found", id)
1796+
return rv, fmt.Errorf("secret required but no secret with id %q found", id)
17811797
}
1782-
return nil, "", nil
1798+
return rv, nil
1799+
}
1800+
data, err := secr.ResolveValue()
1801+
if err != nil {
1802+
return rv, err
1803+
}
1804+
1805+
// if env is set, then we can return now
1806+
if env != "" {
1807+
rv.EnvVariable = env + "=" + string(data)
1808+
return rv, nil
1809+
}
1810+
// else we fallback to default behaviour of creating mount
1811+
1812+
// Default location for secrets is /run/secrets/id
1813+
if target == "" {
1814+
target = "/run/secrets/" + id
17831815
}
1784-
var data []byte
1785-
var envFile string
1816+
17861817
var ctrFileOnHost string
17871818

17881819
switch secr.SourceType {
17891820
case "env":
1790-
data = []byte(os.Getenv(secr.Source))
17911821
tmpFile, err := os.CreateTemp(tmpdir.GetTempDir(), "buildah*")
17921822
if err != nil {
1793-
return nil, "", err
1823+
return rv, err
17941824
}
17951825
defer func() {
17961826
if retErr != nil {
17971827
os.Remove(tmpFile.Name())
17981828
}
17991829
}()
1800-
envFile = tmpFile.Name()
1830+
rv.EnvFile = tmpFile.Name()
18011831
ctrFileOnHost = tmpFile.Name()
18021832
case "file":
18031833
containerWorkingDir, err := b.store.ContainerDirectory(b.ContainerID)
18041834
if err != nil {
1805-
return nil, "", err
1806-
}
1807-
data, err = os.ReadFile(secr.Source)
1808-
if err != nil {
1809-
return nil, "", err
1835+
return rv, err
18101836
}
18111837
ctrFileOnHost = filepath.Join(containerWorkingDir, "secrets", digest.FromString(id).Encoded()[:16])
18121838
default:
1813-
return nil, "", errors.New("invalid source secret type")
1839+
return rv, errors.New("invalid source secret type")
18141840
}
18151841

18161842
// Copy secrets to container working dir (or tmp dir if it's an env), since we need to chmod,
18171843
// chown and relabel it for the container user and we don't want to mess with the original file
18181844
if err := os.MkdirAll(filepath.Dir(ctrFileOnHost), 0o755); err != nil {
1819-
return nil, "", err
1845+
return rv, err
18201846
}
18211847
if err := os.WriteFile(ctrFileOnHost, data, 0o644); err != nil {
1822-
return nil, "", err
1848+
return rv, err
18231849
}
18241850

18251851
if err := relabel(ctrFileOnHost, b.MountLabel, false); err != nil {
1826-
return nil, "", err
1852+
return rv, err
18271853
}
18281854
hostUID, hostGID, err := util.GetHostIDs(idMaps.uidmap, idMaps.gidmap, uid, gid)
18291855
if err != nil {
1830-
return nil, "", err
1856+
return rv, err
18311857
}
18321858
if err := os.Lchown(ctrFileOnHost, int(hostUID), int(hostGID)); err != nil {
1833-
return nil, "", err
1859+
return rv, err
18341860
}
18351861
if err := os.Chmod(ctrFileOnHost, os.FileMode(mode)); err != nil {
1836-
return nil, "", err
1862+
return rv, err
18371863
}
1838-
newMount := specs.Mount{
1864+
rv.Mount = &specs.Mount{
18391865
Destination: target,
18401866
Type: define.TypeBind,
18411867
Source: ctrFileOnHost,
18421868
Options: append(define.BindOptions, "rprivate", "ro"),
18431869
}
1844-
return &newMount, envFile, nil
1870+
return rv, nil
18451871
}
18461872

18471873
// 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

run_freebsd.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,6 @@ func (b *Builder) Run(command []string, options RunOptions) error {
276276
if err != nil {
277277
return fmt.Errorf("resolving mountpoints for container %q: %w", b.ContainerID, err)
278278
}
279-
if runArtifacts.SSHAuthSock != "" {
280-
sshenv := "SSH_AUTH_SOCK=" + runArtifacts.SSHAuthSock
281-
spec.Process.Env = append(spec.Process.Env, sshenv)
282-
}
283279

284280
// following run was called from `buildah run`
285281
// and some images were mounted for this run

run_linux.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -513,10 +513,6 @@ rootless=%d
513513
if err != nil {
514514
return fmt.Errorf("resolving mountpoints for container %q: %w", b.ContainerID, err)
515515
}
516-
if runArtifacts.SSHAuthSock != "" {
517-
sshenv := "SSH_AUTH_SOCK=" + runArtifacts.SSHAuthSock
518-
spec.Process.Env = append(spec.Process.Env, sshenv)
519-
}
520516

521517
// Create any mount points that we need that aren't already present in
522518
// the rootfs.

tests/bud.bats

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8502,3 +8502,15 @@ _EOF
85028502
assert "$status" = "0"
85038503
assert "${#lines[*]}" = "1"
85048504
}
8505+
8506+
@test "use-secret-to-env-variable" {
8507+
local outpath="${TEST_SCRATCH_DIR}/timestamp-after-secret.tar"
8508+
8509+
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
8510+
expect_output --substring "Hello baz"
8511+
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
8512+
expect_output --substring "Hello boz"
8513+
8514+
# even though different secret was passed to each(baz vs boz), we expect the same result, ie should not affect build history
8515+
diff "${outpath}.a" "${outpath}.b"
8516+
}

0 commit comments

Comments
 (0)