Skip to content

Commit

Permalink
Add support for secret mounts
Browse files Browse the repository at this point in the history
Add support for secrets. Secrets is a two-part flag that allows secret files to
be accessed for a certain RUN instruction, but not any other
instructions, as well as now showing up in the final image.

Signed-off-by: Ashley Cui <acui@redhat.com>
  • Loading branch information
ashley-cui committed Apr 22, 2021
1 parent 1c0e033 commit 99076ab
Show file tree
Hide file tree
Showing 15 changed files with 303 additions and 14 deletions.
2 changes: 2 additions & 0 deletions define/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ type CommonBuildOptions struct {
Ulimit []string
// Volumes to bind mount into the container
Volumes []string
// Secrets are the available secrets to use in a build
Secrets []string
}

// BuildOptions can be used to alter how an image is built.
Expand Down
9 changes: 9 additions & 0 deletions docs/buildah-bud.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,15 @@ consult the manpages of the selected container runtime.
Note: Do not pass the leading `--` to the flag. To pass the runc flag `--log-format json`
to buildah bud, the option given would be `--runtime-flag log-format=json`.

**--secret**=**id=id,src=path**
Pass secret information to be used in the Containerfile for building images
in a safe way that will not end up stored in the final image, or be seen in other stages.
The secret will be mounted in the container at the default location of `/run/secrets/id`.

To later use the secret, use the --mount flag in a `RUN` instruction:

`RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret`

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

Security Options
Expand Down
7 changes: 7 additions & 0 deletions imagebuildah/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ type Executor struct {
imageInfoCache map[string]imageTypeAndHistoryAndDiffIDs
fromOverride string
manifest string
secrets map[string]string
}

type imageTypeAndHistoryAndDiffIDs struct {
Expand Down Expand Up @@ -166,6 +167,11 @@ func NewExecutor(store storage.Store, options define.BuildOptions, mainNode *par
transientMounts = append([]Mount{Mount(mount)}, transientMounts...)
}

secrets, err := parse.Secrets(options.CommonBuildOpts.Secrets)
if err != nil {
return nil, err
}

jobs := 1
if options.Jobs != nil {
jobs = *options.Jobs
Expand Down Expand Up @@ -236,6 +242,7 @@ func NewExecutor(store storage.Store, options define.BuildOptions, mainNode *par
imageInfoCache: make(map[string]imageTypeAndHistoryAndDiffIDs),
fromOverride: options.From,
manifest: options.Manifest,
secrets: secrets,
}
if exec.err == nil {
exec.err = os.Stderr
Expand Down
2 changes: 2 additions & 0 deletions imagebuildah/stage_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,8 @@ func (s *StageExecutor) Run(run imagebuilder.Run, config docker.Config) error {
Quiet: s.executor.quiet,
NamespaceOptions: s.executor.namespaceOptions,
Terminal: buildah.WithoutTerminal,
Secrets: s.executor.secrets,
RunMounts: run.Mounts,
}
if config.NetworkDisabled {
options.ConfigureNetwork = buildah.NetworkDisabled
Expand Down
3 changes: 3 additions & 0 deletions pkg/cli/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type BudResults struct {
Rm bool
Runtime string
RuntimeFlags []string
Secrets []string
SignaturePolicy string
SignBy string
Squash bool
Expand Down Expand Up @@ -207,6 +208,7 @@ func GetBudFlags(flags *BudResults) pflag.FlagSet {
fs.BoolVar(&flags.Rm, "rm", true, "Remove intermediate containers after a successful build")
// "runtime" definition moved to avoid name collision in podman build. Defined in cmd/buildah/bud.go.
fs.StringSliceVar(&flags.RuntimeFlags, "runtime-flag", []string{}, "add global flags for the container runtime")
fs.StringArrayVar(&flags.Secrets, "secret", []string{}, "secret file to expose to the build")
fs.StringVar(&flags.SignBy, "sign-by", "", "sign the image using a GPG key with the specified `FINGERPRINT`")
fs.StringVar(&flags.SignaturePolicy, "signature-policy", "", "`pathname` of signature policy file (not usually used)")
if err := fs.MarkHidden("signature-policy"); err != nil {
Expand Down Expand Up @@ -245,6 +247,7 @@ func GetBudFlagsCompletions() commonComp.FlagCompletions {
flagCompletion["os"] = commonComp.AutocompleteNone
flagCompletion["platform"] = commonComp.AutocompleteNone
flagCompletion["runtime-flag"] = commonComp.AutocompleteNone
flagCompletion["secret"] = commonComp.AutocompleteNone
flagCompletion["sign-by"] = commonComp.AutocompleteNone
flagCompletion["signature-policy"] = commonComp.AutocompleteNone
flagCompletion["tag"] = commonComp.AutocompleteNone
Expand Down
37 changes: 37 additions & 0 deletions pkg/parse/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ func CommonBuildOptions(c *cobra.Command) (*define.CommonBuildOptions, error) {
ulimit, _ = c.Flags().GetStringSlice("ulimit")
}

secrets, _ := c.Flags().GetStringArray("secret")

commonOpts := &define.CommonBuildOptions{
AddHost: addHost,
CPUPeriod: cpuPeriod,
Expand All @@ -142,6 +144,7 @@ func CommonBuildOptions(c *cobra.Command) (*define.CommonBuildOptions, error) {
ShmSize: c.Flag("shm-size").Value.String(),
Ulimit: ulimit,
Volumes: volumes,
Secrets: secrets,
}
securityOpts, _ := c.Flags().GetStringArray("security-opt")
if err := parseSecurityOpts(securityOpts, commonOpts); err != nil {
Expand Down Expand Up @@ -1051,3 +1054,37 @@ func GetTempDir() string {
}
return "/var/tmp"
}

// Secrets parses the --secret flag
func Secrets(secrets []string) (map[string]string, error) {
parsed := make(map[string]string)
invalidSyntax := errors.Errorf("incorrect secret flag format: should be --secret id=foo,src=bar")
for _, secret := range secrets {
split := strings.Split(secret, ",")
if len(split) > 2 {
return nil, invalidSyntax
}
if len(split) == 2 {
id := strings.Split(split[0], "=")
src := strings.Split(split[1], "=")
if len(split) == 2 && strings.ToLower(id[0]) == "id" && strings.ToLower(src[0]) == "src" {
fullPath, err := filepath.Abs(src[1])
if err != nil {
return nil, err
}
_, err = os.Stat(fullPath)
if err == nil {
parsed[id[1]] = fullPath
}
if err != nil {
return nil, errors.Wrap(err, "could not parse secrets")
}
} else {
return nil, invalidSyntax
}
} else {
return nil, invalidSyntax
}
}
return parsed, nil
}
5 changes: 5 additions & 0 deletions run.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,9 @@ type RunOptions struct {
DropCapabilities []string
// Devices are the additional devices to add to the containers
Devices define.ContainerDevices
// Secrets are the available secrets to use in a RUN
Secrets map[string]string
// RunMounts are mounts for this run. RunMounts for this run
// will not show up in subsequent runs.
RunMounts []string
}
175 changes: 166 additions & 9 deletions run_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,17 @@ rootless=%d
bindFiles["/run/.containerenv"] = containerenvPath
}

err = b.setupMounts(mountPoint, spec, path, options.Mounts, bindFiles, volumes, b.CommonBuildOpts.Volumes, b.CommonBuildOpts.ShmSize, namespaceOptions)
runMountTargets, err := b.setupMounts(mountPoint, spec, path, options.Mounts, bindFiles, volumes, b.CommonBuildOpts.Volumes, b.CommonBuildOpts.ShmSize, namespaceOptions, options.Secrets, options.RunMounts)
if err != nil {
return errors.Wrapf(err, "error resolving mountpoints for container %q", b.ContainerID)
}

defer func() {
if err := cleanupRunMounts(runMountTargets, mountPoint); err != nil {
logrus.Errorf("unabe to cleanup run mounts %v", err)
}
}()

defer b.cleanupTempVolumes()

if options.CNIConfigDir == "" {
Expand Down Expand Up @@ -403,7 +410,7 @@ func runSetupBuiltinVolumes(mountLabel, mountPoint, containerDir string, builtin
return mounts, nil
}

func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath string, optionMounts []specs.Mount, bindFiles map[string]string, builtinVolumes, volumeMounts []string, shmSize string, namespaceOptions define.NamespaceOptions) error {
func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath string, optionMounts []specs.Mount, bindFiles map[string]string, builtinVolumes, volumeMounts []string, shmSize string, namespaceOptions define.NamespaceOptions, secrets map[string]string, runFileMounts []string) (runMountTargets []string, err error) {
// Start building a new list of mounts.
var mounts []specs.Mount
haveMount := func(destination string) bool {
Expand Down Expand Up @@ -497,39 +504,43 @@ func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath st
// After this point we need to know the per-container persistent storage directory.
cdir, err := b.store.ContainerDirectory(b.ContainerID)
if err != nil {
return errors.Wrapf(err, "error determining work directory for container %q", b.ContainerID)
return nil, errors.Wrapf(err, "error determining work directory for container %q", b.ContainerID)
}

// Figure out which UID and GID to tell the subscriptions package to use
// for files that it creates.
rootUID, rootGID, err := util.GetHostRootIDs(spec)
if err != nil {
return err
return nil, err
}

// Get the list of subscriptions mounts.
subscriptionMounts := subscriptions.MountsWithUIDGID(b.MountLabel, cdir, b.DefaultMountsFilePath, mountPoint, int(rootUID), int(rootGID), unshare.IsRootless(), false)

runMounts, runTargets, err := runSetupRunMounts(runFileMounts, secrets, b.MountLabel, cdir, spec.Linux.UIDMappings, spec.Linux.GIDMappings)
if err != nil {
return nil, err
}
// Add temporary copies of the contents of volume locations at the
// volume locations, unless we already have something there.
builtins, err := runSetupBuiltinVolumes(b.MountLabel, mountPoint, cdir, builtinVolumes, int(rootUID), int(rootGID))
if err != nil {
return err
return nil, err
}

// Get host UID and GID of the container process.
processUID, processGID, err := util.GetHostIDs(spec.Linux.UIDMappings, spec.Linux.GIDMappings, spec.Process.User.UID, spec.Process.User.GID)
if err != nil {
return err
return nil, err
}

// Get the list of explicitly-specified volume mounts.
volumes, err := b.runSetupVolumeMounts(spec.Linux.MountLabel, volumeMounts, optionMounts, int(rootUID), int(rootGID), int(processUID), int(processGID))
if err != nil {
return err
return nil, err
}

allMounts := util.SortMounts(append(append(append(append(append(volumes, builtins...), subscriptionMounts...), bindFileMounts...), specMounts...), sysfsMount...))
allMounts := util.SortMounts(append(append(append(append(append(append(volumes, builtins...), runMounts...), subscriptionMounts...), bindFileMounts...), specMounts...), sysfsMount...))
// Add them all, in the preferred order, except where they conflict with something that was previously added.
for _, mount := range allMounts {
if haveMount(mount.Destination) {
Expand All @@ -542,7 +553,7 @@ func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath st

// Set the list in the spec.
spec.Mounts = mounts
return nil
return runTargets, nil
}

// addNetworkConfig copies files from host and sets them up to bind mount into container
Expand Down Expand Up @@ -2248,3 +2259,149 @@ type runUsingRuntimeSubprocOptions struct {
func init() {
reexec.Register(runUsingRuntimeCommand, runUsingRuntimeMain)
}

// runSetupRunMounts sets up mounts that exist only in this RUN, not in subsequent runs
func runSetupRunMounts(mounts []string, secrets map[string]string, mountlabel string, containerWorkingDir string, uidmap []spec.LinuxIDMapping, gidmap []spec.LinuxIDMapping) ([]spec.Mount, []string, error) {
mountTargets := make([]string, 0, 10)
finalMounts := make([]specs.Mount, 0, len(mounts))
for _, mount := range mounts {
arr := strings.SplitN(mount, ",", 2)
if len(arr) < 2 {
return nil, nil, errors.New("invalid mount syntax")
}

kv := strings.Split(arr[0], "=")
if len(kv) != 2 || kv[0] != "type" {
return nil, nil, errors.New("invalid mount type")
}

tokens := strings.Split(arr[1], ",")
// For now, we only support type secret.
switch kv[1] {
case "secret":
mount, err := getSecretMount(tokens, secrets, mountlabel, containerWorkingDir, uidmap, gidmap)
if err != nil {
return nil, nil, err
}
if mount != nil {
finalMounts = append(finalMounts, *mount)
mountTargets = append(mountTargets, mount.Destination)

}
default:
return nil, nil, errors.Errorf("invalid filesystem type %q", kv[1])
}
}
return finalMounts, mountTargets, nil
}

func getSecretMount(tokens []string, secrets map[string]string, mountlabel string, containerWorkingDir string, uidmap []spec.LinuxIDMapping, gidmap []spec.LinuxIDMapping) (*spec.Mount, error) {
errInvalidSyntax := errors.New("secret should have syntax id=id[,target=path,required=bool,mode=uint,uid=uint,gid=uint")

var err error
var id, target string
var required bool
var uid, gid uint32
var mode uint32 = 400
for _, val := range tokens {
kv := strings.SplitN(val, "=", 2)
switch kv[0] {
case "id":
id = kv[1]
case "target":
target = kv[1]
case "required":
required, err = strconv.ParseBool(kv[1])
if err != nil {
return nil, errInvalidSyntax
}
case "mode":
mode64, err := strconv.ParseUint(kv[1], 8, 32)
if err != nil {
return nil, errInvalidSyntax
}
mode = uint32(mode64)
case "uid":
uid64, err := strconv.ParseUint(kv[1], 10, 32)
if err != nil {
return nil, errInvalidSyntax
}
uid = uint32(uid64)
case "gid":
gid64, err := strconv.ParseUint(kv[1], 10, 32)
if err != nil {
return nil, errInvalidSyntax
}
gid = uint32(gid64)
default:
return nil, errInvalidSyntax
}
}

if id == "" {
return nil, errInvalidSyntax
}
// Default location for secretis is /run/secrets/id
if target == "" {
target = "/run/secrets/" + id
}

src, ok := secrets[id]
if !ok {
if required {
return nil, errors.Errorf("secret required but no secret with id %s found", id)
}
return nil, nil
}

// Copy secrets to container working dir, since we need to chmod, chown and relabel it
// for the container user and we don't want to mess with the original file
ctrFileOnHost := filepath.Join(containerWorkingDir, "secrets", id)
_, err = os.Stat(ctrFileOnHost)
if os.IsNotExist(err) {
data, err := ioutil.ReadFile(src)
if err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Dir(ctrFileOnHost), 0644); err != nil {
return nil, err
}
if err := ioutil.WriteFile(ctrFileOnHost, data, 0644); err != nil {
return nil, err
}
}

if err := label.Relabel(ctrFileOnHost, mountlabel, false); err != nil {
return nil, err
}
hostUID, hostGID, err := util.GetHostIDs(uidmap, gidmap, uid, gid)
if err != nil {
return nil, err
}
if err := os.Lchown(ctrFileOnHost, int(hostUID), int(hostGID)); err != nil {
return nil, err
}
if err := os.Chmod(ctrFileOnHost, os.FileMode(mode)); err != nil {
return nil, err
}
newMount := specs.Mount{
Destination: target,
Type: "bind",
Source: ctrFileOnHost,
Options: []string{"bind", "rprivate", "ro"},
}
return &newMount, nil
}

func cleanupRunMounts(paths []string, mountpoint string) error {
opts := copier.RemoveOptions{
All: true,
}
for _, path := range paths {
err := copier.Remove(mountpoint, path, opts)
if err != nil {
return err
}
}
return nil
}
Loading

0 comments on commit 99076ab

Please sign in to comment.