Skip to content

Commit ff08a60

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 2c5d71f commit ff08a60

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
@@ -973,6 +973,10 @@ The location of the secret in the container can be overridden using the
973973

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

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

978982
**--security-opt**=[]

run.go

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

210212
// 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
@@ -1433,6 +1433,12 @@ func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath st
14331433
mounts = append(mounts, mount)
14341434
}
14351435

1436+
// Some mounts require env vars to be set, do these here
1437+
spec.Process.Env = append(spec.Process.Env, mountArtifacts.SecretEnvVars...)
1438+
if mountArtifacts.SSHAuthSock != "" {
1439+
spec.Process.Env = append(spec.Process.Env, "SSH_AUTH_SOCK="+mountArtifacts.SSHAuthSock)
1440+
}
1441+
14361442
// Set the list in the spec.
14371443
spec.Mounts = mounts
14381444
succeeded = true
@@ -1522,6 +1528,7 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
15221528
intermediateMounts := make([]string, 0, len(mounts))
15231529
finalMounts := make([]specs.Mount, 0, len(mounts))
15241530
agents := make([]*sshagent.AgentServer, 0, len(mounts))
1531+
var secretEnvVars []string
15251532
defaultSSHSock := ""
15261533
targetLocks := []*lockfile.LockFile{}
15271534
var overlayDirs []string
@@ -1561,11 +1568,7 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
15611568
}
15621569
}()
15631570
for _, mount := range mounts {
1564-
var mountSpec *specs.Mount
1565-
var err error
1566-
var envFile, image, bundleMountsDir, overlayDir, intermediateMount string
1567-
var agent *sshagent.AgentServer
1568-
var tl *lockfile.LockFile
1571+
var bundleMountsDir string
15691572

15701573
tokens := strings.Split(mount, ",")
15711574

@@ -1583,18 +1586,21 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
15831586
}
15841587
switch mountType {
15851588
case "secret":
1586-
mountSpec, envFile, err = b.getSecretMount(tokens, sources.Secrets, idMaps, sources.WorkDir)
1589+
mountOrEnvSpec, err := b.getSecretMount(tokens, sources.Secrets, idMaps, sources.WorkDir)
15871590
if err != nil {
15881591
return nil, nil, err
15891592
}
1590-
if mountSpec != nil {
1591-
finalMounts = append(finalMounts, *mountSpec)
1592-
if envFile != "" {
1593-
tmpFiles = append(tmpFiles, envFile)
1594-
}
1593+
if mountOrEnvSpec.Mount != nil {
1594+
finalMounts = append(finalMounts, *mountOrEnvSpec.Mount)
1595+
}
1596+
if mountOrEnvSpec.EnvFile != "" {
1597+
tmpFiles = append(tmpFiles, mountOrEnvSpec.EnvFile)
1598+
}
1599+
if mountOrEnvSpec.EnvVariable != "" {
1600+
secretEnvVars = append(secretEnvVars, mountOrEnvSpec.EnvVariable)
15951601
}
15961602
case "ssh":
1597-
mountSpec, agent, err = b.getSSHMount(tokens, len(agents), sources.SSHSources, idMaps)
1603+
mountSpec, agent, err := b.getSSHMount(tokens, len(agents), sources.SSHSources, idMaps)
15981604
if err != nil {
15991605
return nil, nil, err
16001606
}
@@ -1607,11 +1613,12 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
16071613
}
16081614
case define.TypeBind:
16091615
if bundleMountsDir == "" {
1616+
var err error
16101617
if bundleMountsDir, err = os.MkdirTemp(bundlePath, "mounts"); err != nil {
16111618
return nil, nil, err
16121619
}
16131620
}
1614-
mountSpec, image, intermediateMount, overlayDir, err = b.getBindMount(tokens, sources.SystemContext, sources.ContextDir, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
1621+
mountSpec, image, intermediateMount, overlayDir, err := b.getBindMount(tokens, sources.SystemContext, sources.ContextDir, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
16151622
if err != nil {
16161623
return nil, nil, err
16171624
}
@@ -1626,18 +1633,19 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
16261633
}
16271634
finalMounts = append(finalMounts, *mountSpec)
16281635
case "tmpfs":
1629-
mountSpec, err = b.getTmpfsMount(tokens, idMaps, sources.WorkDir)
1636+
mountSpec, err := b.getTmpfsMount(tokens, idMaps, sources.WorkDir)
16301637
if err != nil {
16311638
return nil, nil, err
16321639
}
16331640
finalMounts = append(finalMounts, *mountSpec)
16341641
case "cache":
16351642
if bundleMountsDir == "" {
1643+
var err error
16361644
if bundleMountsDir, err = os.MkdirTemp(bundlePath, "mounts"); err != nil {
16371645
return nil, nil, err
16381646
}
16391647
}
1640-
mountSpec, image, intermediateMount, overlayDir, tl, err = b.getCacheMount(tokens, sources.SystemContext, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
1648+
mountSpec, image, intermediateMount, overlayDir, tl, err := b.getCacheMount(tokens, sources.SystemContext, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
16411649
if err != nil {
16421650
return nil, nil, err
16431651
}
@@ -1666,6 +1674,7 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
16661674
SSHAuthSock: defaultSSHSock,
16671675
TargetLocks: targetLocks,
16681676
IntermediateMounts: intermediateMounts,
1677+
SecretEnvVars: secretEnvVars,
16691678
}
16701679
return finalMounts, artifacts, nil
16711680
}
@@ -1731,13 +1740,17 @@ func (b *Builder) getTmpfsMount(tokens []string, idMaps IDMaps, workDir string)
17311740
return &volumes[0], nil
17321741
}
17331742

1734-
func (b *Builder) getSecretMount(tokens []string, secrets map[string]define.Secret, idMaps IDMaps, workdir string) (_ *specs.Mount, _ string, retErr error) {
1735-
errInvalidSyntax := errors.New("secret should have syntax id=id[,target=path,required=bool,mode=uint,uid=uint,gid=uint")
1743+
func (b *Builder) getSecretMount(tokens []string, secrets map[string]define.Secret, idMaps IDMaps, workdir string) (rv struct {
1744+
Mount *specs.Mount // set if mount created
1745+
EnvFile string // set if caller mount created from temp created env file
1746+
EnvVariable string // set if caller should add to env variable list
1747+
}, retErr error,
1748+
) {
1749+
errInvalidSyntax := errors.New("secret should have syntax id=id[,target=path,required=bool,mode=uint,uid=uint,gid=uint,env=dstVarName")
17361750
if len(tokens) == 0 {
1737-
return nil, "", errInvalidSyntax
1751+
return rv, errInvalidSyntax
17381752
}
1739-
var err error
1740-
var id, target string
1753+
var id, target, env string
17411754
var required bool
17421755
var uid, gid uint32
17431756
var mode uint32 = 0o400
@@ -1757,110 +1770,123 @@ func (b *Builder) getSecretMount(tokens []string, secrets map[string]define.Secr
17571770
case "required":
17581771
required = true
17591772
if len(kv) > 1 {
1773+
var err error
17601774
required, err = strconv.ParseBool(kv[1])
17611775
if err != nil {
1762-
return nil, "", errInvalidSyntax
1776+
return rv, errInvalidSyntax
17631777
}
17641778
}
17651779
case "mode":
17661780
mode64, err := strconv.ParseUint(kv[1], 8, 32)
17671781
if err != nil {
1768-
return nil, "", errInvalidSyntax
1782+
return rv, errInvalidSyntax
17691783
}
17701784
mode = uint32(mode64)
17711785
case "uid":
17721786
uid64, err := strconv.ParseUint(kv[1], 10, 32)
17731787
if err != nil {
1774-
return nil, "", errInvalidSyntax
1788+
return rv, errInvalidSyntax
17751789
}
17761790
uid = uint32(uid64)
17771791
case "gid":
17781792
gid64, err := strconv.ParseUint(kv[1], 10, 32)
17791793
if err != nil {
1780-
return nil, "", errInvalidSyntax
1794+
return rv, errInvalidSyntax
17811795
}
17821796
gid = uint32(gid64)
1797+
case "env":
1798+
if kv[1] == "" {
1799+
return rv, errInvalidSyntax
1800+
}
1801+
env = kv[1]
17831802
default:
1784-
return nil, "", errInvalidSyntax
1803+
return rv, errInvalidSyntax
17851804
}
17861805
}
17871806

17881807
if id == "" {
1789-
return nil, "", errInvalidSyntax
1790-
}
1791-
// Default location for secrets is /run/secrets/id
1792-
if target == "" {
1793-
target = "/run/secrets/" + id
1808+
return rv, errInvalidSyntax
17941809
}
17951810

1811+
// first fetch the secret data
17961812
secr, ok := secrets[id]
17971813
if !ok {
17981814
if required {
1799-
return nil, "", fmt.Errorf("secret required but no secret with id %q found", id)
1815+
return rv, fmt.Errorf("secret required but no secret with id %q found", id)
18001816
}
1801-
return nil, "", nil
1817+
return rv, nil
1818+
}
1819+
data, err := secr.ResolveValue()
1820+
if err != nil {
1821+
return rv, err
1822+
}
1823+
1824+
// if env is set, then we can return now
1825+
if env != "" {
1826+
rv.EnvVariable = env + "=" + string(data)
1827+
return rv, nil
1828+
}
1829+
// else we fallback to default behaviour of creating mount
1830+
1831+
// Default location for secrets is /run/secrets/id
1832+
if target == "" {
1833+
target = "/run/secrets/" + id
18021834
}
1803-
var data []byte
1804-
var envFile string
1835+
18051836
var ctrFileOnHost string
18061837

18071838
switch secr.SourceType {
18081839
case "env":
1809-
data = []byte(os.Getenv(secr.Source))
18101840
tmpFile, err := os.CreateTemp(tmpdir.GetTempDir(), "buildah*")
18111841
if err != nil {
1812-
return nil, "", err
1842+
return rv, err
18131843
}
18141844
defer func() {
18151845
if retErr != nil {
18161846
os.Remove(tmpFile.Name())
18171847
}
18181848
}()
1819-
envFile = tmpFile.Name()
1849+
rv.EnvFile = tmpFile.Name()
18201850
ctrFileOnHost = tmpFile.Name()
18211851
case "file":
18221852
containerWorkingDir, err := b.store.ContainerDirectory(b.ContainerID)
18231853
if err != nil {
1824-
return nil, "", err
1825-
}
1826-
data, err = os.ReadFile(secr.Source)
1827-
if err != nil {
1828-
return nil, "", err
1854+
return rv, err
18291855
}
18301856
ctrFileOnHost = filepath.Join(containerWorkingDir, "secrets", digest.FromString(id).Encoded()[:16])
18311857
default:
1832-
return nil, "", errors.New("invalid source secret type")
1858+
return rv, errors.New("invalid source secret type")
18331859
}
18341860

18351861
// Copy secrets to container working dir (or tmp dir if it's an env), since we need to chmod,
18361862
// chown and relabel it for the container user and we don't want to mess with the original file
18371863
if err := os.MkdirAll(filepath.Dir(ctrFileOnHost), 0o755); err != nil {
1838-
return nil, "", err
1864+
return rv, err
18391865
}
18401866
if err := os.WriteFile(ctrFileOnHost, data, 0o644); err != nil {
1841-
return nil, "", err
1867+
return rv, err
18421868
}
18431869

18441870
if err := relabel(ctrFileOnHost, b.MountLabel, false); err != nil {
1845-
return nil, "", err
1871+
return rv, err
18461872
}
18471873
hostUID, hostGID, err := util.GetHostIDs(idMaps.uidmap, idMaps.gidmap, uid, gid)
18481874
if err != nil {
1849-
return nil, "", err
1875+
return rv, err
18501876
}
18511877
if err := os.Lchown(ctrFileOnHost, int(hostUID), int(hostGID)); err != nil {
1852-
return nil, "", err
1878+
return rv, err
18531879
}
18541880
if err := os.Chmod(ctrFileOnHost, os.FileMode(mode)); err != nil {
1855-
return nil, "", err
1881+
return rv, err
18561882
}
1857-
newMount := specs.Mount{
1883+
rv.Mount = &specs.Mount{
18581884
Destination: target,
18591885
Type: define.TypeBind,
18601886
Source: ctrFileOnHost,
18611887
Options: append(define.BindOptions, "rprivate", "ro"),
18621888
}
1863-
return &newMount, envFile, nil
1889+
return rv, nil
18641890
}
18651891

18661892
// 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
@@ -9059,3 +9059,15 @@ EOF
90599059
done
90609060
run_buildah build ${TEST_SCRATCH_DIR}/buildcontext
90619061
}
9062+
9063+
@test "use-secret-to-env-variable" {
9064+
local outpath="${TEST_SCRATCH_DIR}/timestamp-after-secret.tar"
9065+
9066+
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
9067+
expect_output --substring "Hello baz"
9068+
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
9069+
expect_output --substring "Hello boz"
9070+
9071+
# even though different secret was passed to each(baz vs boz), we expect the same result, ie should not affect build history
9072+
diff "${outpath}.a" "${outpath}.b"
9073+
}

0 commit comments

Comments
 (0)