Skip to content

Commit

Permalink
bake: enable support for entitlements
Browse files Browse the repository at this point in the history
Add support for security.insecure and network.host
entitlements via bake. User needs to confirm elevated
privileges through a prompt or CLI flags.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
  • Loading branch information
tonistiigi committed Aug 30, 2024
1 parent 96eb69a commit 203fd8a
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 9 deletions.
19 changes: 18 additions & 1 deletion bake/bake.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/util/entitlements"
"github.com/pkg/errors"
"github.com/tonistiigi/go-csvvalue"
"github.com/zclconf/go-cty/cty"
Expand Down Expand Up @@ -542,7 +543,7 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error)
o := t[kk[1]]

switch keys[1] {
case "output", "cache-to", "cache-from", "tags", "platform", "secrets", "ssh", "attest":
case "output", "cache-to", "cache-from", "tags", "platform", "secrets", "ssh", "attest", "entitlements":
if len(parts) == 2 {
o.ArrValue = append(o.ArrValue, parts[1])
}
Expand Down Expand Up @@ -708,6 +709,7 @@ type Target struct {
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional"`
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional"`
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"`
// IMPORTANT: if you add more fields here, do not forget to update newOverrides/AddOverrides and docs/bake-reference.md.

// linked is a private field to mark a target used as a linked one
Expand All @@ -732,6 +734,12 @@ func (t *Target) normalize() {
t.NoCacheFilter = removeDupes(t.NoCacheFilter)
t.Ulimits = removeDupes(t.Ulimits)

if t.NetworkMode != nil && *t.NetworkMode == "host" {
t.Entitlements = append(t.Entitlements, "network.host")
}

t.Entitlements = removeDupes(t.Entitlements)

for k, v := range t.Contexts {
if v == "" {
delete(t.Contexts, k)
Expand Down Expand Up @@ -831,6 +839,9 @@ func (t *Target) Merge(t2 *Target) {
if t2.Description != "" {
t.Description = t2.Description
}
if t2.Entitlements != nil { // merge
t.Entitlements = append(t.Entitlements, t2.Entitlements...)
}
t.Inherits = append(t.Inherits, t2.Inherits...)
}

Expand Down Expand Up @@ -885,6 +896,8 @@ func (t *Target) AddOverrides(overrides map[string]Override) error {
t.Platforms = o.ArrValue
case "output":
t.Outputs = o.ArrValue
case "entitlements":
t.Entitlements = append(t.Entitlements, o.ArrValue...)
case "annotations":
t.Annotations = append(t.Annotations, o.ArrValue...)
case "attest":
Expand Down Expand Up @@ -1368,6 +1381,10 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
}
bo.Ulimits = ulimits

for _, ent := range t.Entitlements {
bo.Allow = append(bo.Allow, entitlements.Entitlement(ent))
}

return bo, nil
}

Expand Down
68 changes: 68 additions & 0 deletions bake/bake_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"testing"

"github.com/moby/buildkit/util/entitlements"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -1726,3 +1727,70 @@ func TestAnnotations(t *testing.T) {
require.Len(t, bo["app"].Exports, 1)
require.Equal(t, "bar", bo["app"].Exports[0].Attrs["annotation-manifest[linux/amd64].foo"])
}

func TestHCLEntitlements(t *testing.T) {
fp := File{
Name: "docker-bake.hcl",
Data: []byte(
`target "app" {
entitlements = ["security.insecure", "network.host"]
}`),
}
ctx := context.TODO()
m, g, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
require.NoError(t, err)

bo, err := TargetsToBuildOpt(m, &Input{})
require.NoError(t, err)

require.Equal(t, 1, len(g))
require.Equal(t, []string{"app"}, g["default"].Targets)

require.Equal(t, 1, len(m))
require.Contains(t, m, "app")
require.Len(t, m["app"].Entitlements, 2)
require.Equal(t, "security.insecure", m["app"].Entitlements[0])
require.Equal(t, "network.host", m["app"].Entitlements[1])

require.Len(t, bo["app"].Allow, 2)
require.Equal(t, entitlements.EntitlementSecurityInsecure, bo["app"].Allow[0])
require.Equal(t, entitlements.EntitlementNetworkHost, bo["app"].Allow[1])
}

func TestEntitlementsForNetHost(t *testing.T) {
fp := File{
Name: "docker-bake.hcl",
Data: []byte(
`target "app" {
dockerfile = "app.Dockerfile"
}`),
}

fp2 := File{
Name: "docker-compose.yml",
Data: []byte(
`services:
app:
build:
network: "host"
`),
}

ctx := context.TODO()
m, g, err := ReadTargets(ctx, []File{fp, fp2}, []string{"app"}, nil, nil)
require.NoError(t, err)

bo, err := TargetsToBuildOpt(m, &Input{})
require.NoError(t, err)

require.Equal(t, 1, len(g))
require.Equal(t, []string{"app"}, g["default"].Targets)

require.Equal(t, 1, len(m))
require.Contains(t, m, "app")
require.Len(t, m["app"].Entitlements, 1)
require.Equal(t, "network.host", m["app"].Entitlements[0])

require.Len(t, bo["app"].Allow, 1)
require.Equal(t, entitlements.EntitlementNetworkHost, bo["app"].Allow[0])
}
176 changes: 176 additions & 0 deletions bake/entitlements.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package bake

import (
"bufio"
"context"
"fmt"
"io"
"os"
"path/filepath"
"slices"
"strings"

"github.com/containerd/console"
"github.com/docker/buildx/build"
"github.com/moby/buildkit/util/entitlements"
"github.com/pkg/errors"
)

type EntitlementKey string

const (
EntitlementKeyNetworkHost EntitlementKey = "network.host"
EntitlementKeySecurityInsecure EntitlementKey = "security.insecure"
EntitlementKeyFSRead EntitlementKey = "fs.read"
EntitlementKeyFSWrite EntitlementKey = "fs.write"
EntitlementKeyFS EntitlementKey = "fs"
EntitlementKeyImagePush EntitlementKey = "image.push"
EntitlementKeyImageLoad EntitlementKey = "image.load"
EntitlementKeyImage EntitlementKey = "image"
EntitlementKeySSH EntitlementKey = "ssh"
)

type EntitlementConf struct {
NetworkHost bool
SecurityInsecure bool
FSRead []string
FSWrite []string
ImagePush []string
ImageLoad []string
SSH bool
}

func ParseEntitlements(in []string) (EntitlementConf, error) {
var conf EntitlementConf
for _, e := range in {
switch e {
case string(EntitlementKeyNetworkHost):
conf.NetworkHost = true
case string(EntitlementKeySecurityInsecure):
conf.SecurityInsecure = true
case string(EntitlementKeySSH):
conf.SSH = true
default:
k, v, _ := strings.Cut(e, "=")
switch k {
case string(EntitlementKeyFSRead):
conf.FSRead = append(conf.FSRead, v)
case string(EntitlementKeyFSWrite):
conf.FSWrite = append(conf.FSWrite, v)
case string(EntitlementKeyFS):
conf.FSRead = append(conf.FSRead, v)
conf.FSWrite = append(conf.FSWrite, v)
case string(EntitlementKeyImagePush):
conf.ImagePush = append(conf.ImagePush, v)
case string(EntitlementKeyImageLoad):
conf.ImageLoad = append(conf.ImageLoad, v)
case string(EntitlementKeyImage):
conf.ImagePush = append(conf.ImagePush, v)
conf.ImageLoad = append(conf.ImageLoad, v)
default:
return conf, errors.Errorf("uknown entitlement key %q", k)
}

// TODO: dedupe slices and parent paths
}
}
return conf, nil
}

func (c EntitlementConf) Validate(m map[string]build.Options) (EntitlementConf, error) {
var expected EntitlementConf

for _, v := range m {
if err := c.check(v, &expected); err != nil {
return EntitlementConf{}, err
}
}

return expected, nil
}

func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) error {
for _, e := range bo.Allow {
switch e {
case entitlements.EntitlementNetworkHost:
if !c.NetworkHost {
expected.NetworkHost = true
}
case entitlements.EntitlementSecurityInsecure:
if !c.SecurityInsecure {
expected.SecurityInsecure = true
}
}
}
return nil
}

func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
var term bool
if _, err := console.ConsoleFromFile(os.Stdin); err == nil {
term = true
}

var msgs []string
var flags []string

if c.NetworkHost {
msgs = append(msgs, " - Running build containers that can access host network")
flags = append(flags, "network.host")
}
if c.SecurityInsecure {
msgs = append(msgs, " - Running privileged containers that can make system changes")
flags = append(flags, "security.insecure")
}

if len(msgs) == 0 {
return nil
}

fmt.Fprintf(out, "Your build is requesting privileges for following possibly insecure capabilities:\n\n")
for _, m := range msgs {
fmt.Fprintf(out, "%s\n", m)
}

for i, f := range flags {
flags[i] = "--allow=" + f
}

if term {
fmt.Fprintf(out, "\nIn order to not see this message in the future pass %q to grant requested privileges.\n", strings.Join(flags, " "))
} else {
fmt.Fprintf(out, "\nPass %q to grant requested privileges.\n", strings.Join(flags, " "))
}

args := append([]string(nil), os.Args...)
if filepath.Base(args[0]) == "docker-buildx" {
args[0] = "docker"
}
idx := slices.Index(args, "bake")

if idx != -1 {
fmt.Fprintf(out, "\nYour full command with requested privileges:\n\n")
fmt.Fprintf(out, "%s %s %s\n\n", strings.Join(args[:idx+1], " "), strings.Join(flags, " "), strings.Join(args[idx+1:], " "))
}

if term {
fmt.Fprintf(out, "Do you want to grant requested privileges and continue? [y/N] ")
reader := bufio.NewReader(os.Stdin)
answerCh := make(chan string, 1)
go func() {
answer, _, _ := reader.ReadLine()
answerCh <- string(answer)
close(answerCh)
}()

select {
case <-ctx.Done():
case answer := <-answerCh:
if strings.ToLower(string(answer)) == "y" {
return nil
}
}
}

return errors.Errorf("additional privileges requested")
}
Loading

0 comments on commit 203fd8a

Please sign in to comment.