Skip to content

Commit f369377

Browse files
authored
Merge pull request #2666 from tonistiigi/bake-entitlements
bake: enable support for entitlements
2 parents b7486e5 + 5ecff53 commit f369377

File tree

6 files changed

+324
-9
lines changed

6 files changed

+324
-9
lines changed

bake/bake.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/moby/buildkit/client"
2626
"github.com/moby/buildkit/client/llb"
2727
"github.com/moby/buildkit/session/auth/authprovider"
28+
"github.com/moby/buildkit/util/entitlements"
2829
"github.com/pkg/errors"
2930
"github.com/tonistiigi/go-csvvalue"
3031
"github.com/zclconf/go-cty/cty"
@@ -542,7 +543,7 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error)
542543
o := t[kk[1]]
543544

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

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

737+
if t.NetworkMode != nil && *t.NetworkMode == "host" {
738+
t.Entitlements = append(t.Entitlements, "network.host")
739+
}
740+
741+
t.Entitlements = removeDupes(t.Entitlements)
742+
735743
for k, v := range t.Contexts {
736744
if v == "" {
737745
delete(t.Contexts, k)
@@ -831,6 +839,9 @@ func (t *Target) Merge(t2 *Target) {
831839
if t2.Description != "" {
832840
t.Description = t2.Description
833841
}
842+
if t2.Entitlements != nil { // merge
843+
t.Entitlements = append(t.Entitlements, t2.Entitlements...)
844+
}
834845
t.Inherits = append(t.Inherits, t2.Inherits...)
835846
}
836847

@@ -885,6 +896,8 @@ func (t *Target) AddOverrides(overrides map[string]Override) error {
885896
t.Platforms = o.ArrValue
886897
case "output":
887898
t.Outputs = o.ArrValue
899+
case "entitlements":
900+
t.Entitlements = append(t.Entitlements, o.ArrValue...)
888901
case "annotations":
889902
t.Annotations = append(t.Annotations, o.ArrValue...)
890903
case "attest":
@@ -1368,6 +1381,10 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
13681381
}
13691382
bo.Ulimits = ulimits
13701383

1384+
for _, ent := range t.Entitlements {
1385+
bo.Allow = append(bo.Allow, entitlements.Entitlement(ent))
1386+
}
1387+
13711388
return bo, nil
13721389
}
13731390

bake/bake_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99
"testing"
1010

11+
"github.com/moby/buildkit/util/entitlements"
1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
1314
)
@@ -1726,3 +1727,70 @@ func TestAnnotations(t *testing.T) {
17261727
require.Len(t, bo["app"].Exports, 1)
17271728
require.Equal(t, "bar", bo["app"].Exports[0].Attrs["annotation-manifest[linux/amd64].foo"])
17281729
}
1730+
1731+
func TestHCLEntitlements(t *testing.T) {
1732+
fp := File{
1733+
Name: "docker-bake.hcl",
1734+
Data: []byte(
1735+
`target "app" {
1736+
entitlements = ["security.insecure", "network.host"]
1737+
}`),
1738+
}
1739+
ctx := context.TODO()
1740+
m, g, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
1741+
require.NoError(t, err)
1742+
1743+
bo, err := TargetsToBuildOpt(m, &Input{})
1744+
require.NoError(t, err)
1745+
1746+
require.Equal(t, 1, len(g))
1747+
require.Equal(t, []string{"app"}, g["default"].Targets)
1748+
1749+
require.Equal(t, 1, len(m))
1750+
require.Contains(t, m, "app")
1751+
require.Len(t, m["app"].Entitlements, 2)
1752+
require.Equal(t, "security.insecure", m["app"].Entitlements[0])
1753+
require.Equal(t, "network.host", m["app"].Entitlements[1])
1754+
1755+
require.Len(t, bo["app"].Allow, 2)
1756+
require.Equal(t, entitlements.EntitlementSecurityInsecure, bo["app"].Allow[0])
1757+
require.Equal(t, entitlements.EntitlementNetworkHost, bo["app"].Allow[1])
1758+
}
1759+
1760+
func TestEntitlementsForNetHost(t *testing.T) {
1761+
fp := File{
1762+
Name: "docker-bake.hcl",
1763+
Data: []byte(
1764+
`target "app" {
1765+
dockerfile = "app.Dockerfile"
1766+
}`),
1767+
}
1768+
1769+
fp2 := File{
1770+
Name: "docker-compose.yml",
1771+
Data: []byte(
1772+
`services:
1773+
app:
1774+
build:
1775+
network: "host"
1776+
`),
1777+
}
1778+
1779+
ctx := context.TODO()
1780+
m, g, err := ReadTargets(ctx, []File{fp, fp2}, []string{"app"}, nil, nil)
1781+
require.NoError(t, err)
1782+
1783+
bo, err := TargetsToBuildOpt(m, &Input{})
1784+
require.NoError(t, err)
1785+
1786+
require.Equal(t, 1, len(g))
1787+
require.Equal(t, []string{"app"}, g["default"].Targets)
1788+
1789+
require.Equal(t, 1, len(m))
1790+
require.Contains(t, m, "app")
1791+
require.Len(t, m["app"].Entitlements, 1)
1792+
require.Equal(t, "network.host", m["app"].Entitlements[0])
1793+
1794+
require.Len(t, bo["app"].Allow, 1)
1795+
require.Equal(t, entitlements.EntitlementNetworkHost, bo["app"].Allow[0])
1796+
}

bake/entitlements.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package bake
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"fmt"
7+
"io"
8+
"os"
9+
"slices"
10+
"strings"
11+
12+
"github.com/containerd/console"
13+
"github.com/docker/buildx/build"
14+
"github.com/moby/buildkit/util/entitlements"
15+
"github.com/pkg/errors"
16+
)
17+
18+
type EntitlementKey string
19+
20+
const (
21+
EntitlementKeyNetworkHost EntitlementKey = "network.host"
22+
EntitlementKeySecurityInsecure EntitlementKey = "security.insecure"
23+
EntitlementKeyFSRead EntitlementKey = "fs.read"
24+
EntitlementKeyFSWrite EntitlementKey = "fs.write"
25+
EntitlementKeyFS EntitlementKey = "fs"
26+
EntitlementKeyImagePush EntitlementKey = "image.push"
27+
EntitlementKeyImageLoad EntitlementKey = "image.load"
28+
EntitlementKeyImage EntitlementKey = "image"
29+
EntitlementKeySSH EntitlementKey = "ssh"
30+
)
31+
32+
type EntitlementConf struct {
33+
NetworkHost bool
34+
SecurityInsecure bool
35+
FSRead []string
36+
FSWrite []string
37+
ImagePush []string
38+
ImageLoad []string
39+
SSH bool
40+
}
41+
42+
func ParseEntitlements(in []string) (EntitlementConf, error) {
43+
var conf EntitlementConf
44+
for _, e := range in {
45+
switch e {
46+
case string(EntitlementKeyNetworkHost):
47+
conf.NetworkHost = true
48+
case string(EntitlementKeySecurityInsecure):
49+
conf.SecurityInsecure = true
50+
case string(EntitlementKeySSH):
51+
conf.SSH = true
52+
default:
53+
k, v, _ := strings.Cut(e, "=")
54+
switch k {
55+
case string(EntitlementKeyFSRead):
56+
conf.FSRead = append(conf.FSRead, v)
57+
case string(EntitlementKeyFSWrite):
58+
conf.FSWrite = append(conf.FSWrite, v)
59+
case string(EntitlementKeyFS):
60+
conf.FSRead = append(conf.FSRead, v)
61+
conf.FSWrite = append(conf.FSWrite, v)
62+
case string(EntitlementKeyImagePush):
63+
conf.ImagePush = append(conf.ImagePush, v)
64+
case string(EntitlementKeyImageLoad):
65+
conf.ImageLoad = append(conf.ImageLoad, v)
66+
case string(EntitlementKeyImage):
67+
conf.ImagePush = append(conf.ImagePush, v)
68+
conf.ImageLoad = append(conf.ImageLoad, v)
69+
default:
70+
return conf, errors.Errorf("uknown entitlement key %q", k)
71+
}
72+
73+
// TODO: dedupe slices and parent paths
74+
}
75+
}
76+
return conf, nil
77+
}
78+
79+
func (c EntitlementConf) Validate(m map[string]build.Options) (EntitlementConf, error) {
80+
var expected EntitlementConf
81+
82+
for _, v := range m {
83+
if err := c.check(v, &expected); err != nil {
84+
return EntitlementConf{}, err
85+
}
86+
}
87+
88+
return expected, nil
89+
}
90+
91+
func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) error {
92+
for _, e := range bo.Allow {
93+
switch e {
94+
case entitlements.EntitlementNetworkHost:
95+
if !c.NetworkHost {
96+
expected.NetworkHost = true
97+
}
98+
case entitlements.EntitlementSecurityInsecure:
99+
if !c.SecurityInsecure {
100+
expected.SecurityInsecure = true
101+
}
102+
}
103+
}
104+
return nil
105+
}
106+
107+
func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
108+
var term bool
109+
if _, err := console.ConsoleFromFile(os.Stdin); err == nil {
110+
term = true
111+
}
112+
113+
var msgs []string
114+
var flags []string
115+
116+
if c.NetworkHost {
117+
msgs = append(msgs, " - Running build containers that can access host network")
118+
flags = append(flags, "network.host")
119+
}
120+
if c.SecurityInsecure {
121+
msgs = append(msgs, " - Running privileged containers that can make system changes")
122+
flags = append(flags, "security.insecure")
123+
}
124+
125+
if len(msgs) == 0 {
126+
return nil
127+
}
128+
129+
fmt.Fprintf(out, "Your build is requesting privileges for following possibly insecure capabilities:\n\n")
130+
for _, m := range msgs {
131+
fmt.Fprintf(out, "%s\n", m)
132+
}
133+
134+
for i, f := range flags {
135+
flags[i] = "--allow=" + f
136+
}
137+
138+
if term {
139+
fmt.Fprintf(out, "\nIn order to not see this message in the future pass %q to grant requested privileges.\n", strings.Join(flags, " "))
140+
} else {
141+
fmt.Fprintf(out, "\nPass %q to grant requested privileges.\n", strings.Join(flags, " "))
142+
}
143+
144+
args := append([]string(nil), os.Args...)
145+
if v, ok := os.LookupEnv("DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"); ok && v != "" {
146+
args[0] = v
147+
}
148+
idx := slices.Index(args, "bake")
149+
150+
if idx != -1 {
151+
fmt.Fprintf(out, "\nYour full command with requested privileges:\n\n")
152+
fmt.Fprintf(out, "%s %s %s\n\n", strings.Join(args[:idx+1], " "), strings.Join(flags, " "), strings.Join(args[idx+1:], " "))
153+
}
154+
155+
if term {
156+
fmt.Fprintf(out, "Do you want to grant requested privileges and continue? [y/N] ")
157+
reader := bufio.NewReader(os.Stdin)
158+
answerCh := make(chan string, 1)
159+
go func() {
160+
answer, _, _ := reader.ReadLine()
161+
answerCh <- string(answer)
162+
close(answerCh)
163+
}()
164+
165+
select {
166+
case <-ctx.Done():
167+
case answer := <-answerCh:
168+
if strings.ToLower(string(answer)) == "y" {
169+
return nil
170+
}
171+
}
172+
}
173+
174+
return errors.Errorf("additional privileges requested")
175+
}

0 commit comments

Comments
 (0)