bake: add fs entitlements for context paths

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi 2024-11-18 22:09:28 -08:00 committed by CrazyMax
parent 1af4f05ba4
commit 9a7b028bab
No known key found for this signature in database
GPG Key ID: ADE44D8C9D44FBE4
5 changed files with 101 additions and 77 deletions

View File

@ -1117,62 +1117,36 @@ func updateContext(t *build.Inputs, inp *Input) {
t.ContextState = &st t.ContextState = &st
} }
// validateContextsEntitlements is a basic check to ensure contexts do not func collectLocalPaths(t build.Inputs) []string {
// escape local directories when loaded from remote sources. This is to be var out []string
// replaced with proper entitlements support in the future.
func validateContextsEntitlements(t build.Inputs, inp *Input) error {
if inp == nil || inp.State == nil {
return nil
}
if v, ok := os.LookupEnv("BAKE_ALLOW_REMOTE_FS_ACCESS"); ok {
if vv, _ := strconv.ParseBool(v); vv {
return nil
}
}
if t.ContextState == nil { if t.ContextState == nil {
if err := checkPath(t.ContextPath); err != nil { if v, ok := isLocalPath(t.ContextPath); ok {
return err out = append(out, v)
}
if v, ok := isLocalPath(t.DockerfilePath); ok {
out = append(out, v)
}
} else {
if strings.HasPrefix(t.ContextPath, "cwd://") {
out = append(out, strings.TrimPrefix(t.ContextPath, "cwd://"))
} }
} }
for _, v := range t.NamedContexts { for _, v := range t.NamedContexts {
if v.State != nil { if v.State != nil {
continue continue
} }
if err := checkPath(v.Path); err != nil { if v, ok := isLocalPath(v.Path); ok {
return err out = append(out, v)
} }
} }
return nil return out
} }
func checkPath(p string) error { func isLocalPath(p string) (string, bool) {
if build.IsRemoteURL(p) || strings.HasPrefix(p, "target:") || strings.HasPrefix(p, "docker-image:") { if build.IsRemoteURL(p) || strings.HasPrefix(p, "target:") || strings.HasPrefix(p, "docker-image:") {
return nil return "", false
} }
p, err := filepath.EvalSymlinks(p) return strings.TrimPrefix(p, "cwd://"), true
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
p, err = filepath.Abs(p)
if err != nil {
return err
}
wd, err := os.Getwd()
if err != nil {
return err
}
rel, err := filepath.Rel(wd, p)
if err != nil {
return err
}
parts := strings.Split(rel, string(os.PathSeparator))
if parts[0] == ".." {
return errors.Errorf("path %s is outside of the working directory, please set BAKE_ALLOW_REMOTE_FS_ACCESS=1", p)
}
return nil
} }
func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
@ -1212,9 +1186,6 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
// it's not outside the working directory and then resolve it to an // it's not outside the working directory and then resolve it to an
// absolute path. // absolute path.
bi.DockerfilePath = path.Clean(strings.TrimPrefix(bi.DockerfilePath, "cwd://")) bi.DockerfilePath = path.Clean(strings.TrimPrefix(bi.DockerfilePath, "cwd://"))
if err := checkPath(bi.DockerfilePath); err != nil {
return nil, err
}
var err error var err error
bi.DockerfilePath, err = filepath.Abs(bi.DockerfilePath) bi.DockerfilePath, err = filepath.Abs(bi.DockerfilePath)
if err != nil { if err != nil {
@ -1251,10 +1222,6 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
} }
} }
if err := validateContextsEntitlements(bi, inp); err != nil {
return nil, err
}
t.Context = &bi.ContextPath t.Context = &bi.ContextPath
args := map[string]string{} args := map[string]string{}

View File

@ -108,6 +108,10 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro
rwPaths := map[string]struct{}{} rwPaths := map[string]struct{}{}
roPaths := map[string]struct{}{} roPaths := map[string]struct{}{}
for _, p := range collectLocalPaths(bo.Inputs) {
roPaths[p] = struct{}{}
}
for _, out := range bo.Exports { for _, out := range bo.Exports {
if out.Type == "local" { if out.Type == "local" {
if dest, ok := out.Attrs["dest"]; ok { if dest, ok := out.Attrs["dest"]; ok {
@ -167,7 +171,7 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro
return nil return nil
} }
func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error { func (c EntitlementConf) Prompt(ctx context.Context, isRemote bool, out io.Writer) error {
var term bool var term bool
if _, err := console.ConsoleFromFile(os.Stdin); err == nil { if _, err := console.ConsoleFromFile(os.Stdin); err == nil {
term = true term = true
@ -195,6 +199,17 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
} }
roPaths, rwPaths, commonPaths := groupSamePaths(c.FSRead, c.FSWrite) roPaths, rwPaths, commonPaths := groupSamePaths(c.FSRead, c.FSWrite)
wd, err := os.Getwd()
if err != nil {
return errors.Wrap(err, "failed to get current working directory")
}
wd, err = filepath.EvalSymlinks(wd)
if err != nil {
return errors.Wrap(err, "failed to evaluate working directory")
}
roPaths = toRelativePaths(roPaths, wd)
rwPaths = toRelativePaths(rwPaths, wd)
commonPaths = toRelativePaths(commonPaths, wd)
if len(commonPaths) > 0 { if len(commonPaths) > 0 {
for _, p := range commonPaths { for _, p := range commonPaths {
@ -251,6 +266,17 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
} }
fsEntitlementsEnabled := false fsEntitlementsEnabled := false
if isRemote {
if v, ok := os.LookupEnv("BAKE_ALLOW_REMOTE_FS_ACCESS"); ok {
vv, err := strconv.ParseBool(v)
if err != nil {
return errors.Wrapf(err, "failed to parse BAKE_ALLOW_REMOTE_FS_ACCESS value %q", v)
}
fsEntitlementsEnabled = !vv
} else {
fsEntitlementsEnabled = true
}
}
v, fsEntitlementsSet := os.LookupEnv("BUILDX_BAKE_ENTITLEMENTS_FS") v, fsEntitlementsSet := os.LookupEnv("BUILDX_BAKE_ENTITLEMENTS_FS")
if fsEntitlementsSet { if fsEntitlementsSet {
vv, err := strconv.ParseBool(v) vv, err := strconv.ParseBool(v)
@ -334,11 +360,6 @@ loop0:
} }
func dedupPaths(in map[string]struct{}) (map[string]struct{}, error) { func dedupPaths(in map[string]struct{}) (map[string]struct{}, error) {
wd, err := os.Getwd()
if err != nil {
return nil, err
}
arr := make([]string, 0, len(in)) arr := make([]string, 0, len(in))
for p := range in { for p := range in {
arr = append(arr, filepath.Clean(p)) arr = append(arr, filepath.Clean(p))
@ -358,18 +379,23 @@ loop0:
} }
m[p] = struct{}{} m[p] = struct{}{}
} }
return m, nil
}
for p := range m { func toRelativePaths(in []string, wd string) []string {
out := make([]string, 0, len(in))
for _, p := range in {
rel, err := filepath.Rel(wd, p) rel, err := filepath.Rel(wd, p)
if err == nil { if err == nil {
if !strings.HasPrefix(rel, ".."+string(filepath.Separator)) { // allow up to one level of ".." in the path
delete(m, p) if !strings.HasPrefix(rel, ".."+string(filepath.Separator)+"..") {
m[rel] = struct{}{} out = append(out, rel)
continue
} }
} }
out = append(out, p)
} }
return out
return m, nil
} }
func groupSamePaths(in1, in2 []string) ([]string, []string, []string) { func groupSamePaths(in1, in2 []string) ([]string, []string, []string) {

View File

@ -10,6 +10,7 @@ import (
"github.com/docker/buildx/build" "github.com/docker/buildx/build"
"github.com/docker/buildx/controller/pb" "github.com/docker/buildx/controller/pb"
"github.com/moby/buildkit/client" "github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/util/entitlements" "github.com/moby/buildkit/util/entitlements"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -137,10 +138,12 @@ func TestDedupePaths(t *testing.T) {
filepath.Join(wd, "a/b"): {}, filepath.Join(wd, "a/b"): {},
filepath.Join(wd, "a/b/d"): {}, filepath.Join(wd, "a/b/d"): {},
filepath.Join(wd, "../aa/b"): {}, filepath.Join(wd, "../aa/b"): {},
filepath.Join(wd, "../../bb"): {},
}, },
out: map[string]struct{}{ out: map[string]struct{}{
"a/b": {}, "a/b": {},
filepath.Join(wd, "../aa"): {}, "../aa": {},
filepath.Join(wd, "../../bb"): {},
}, },
}, },
} }
@ -151,7 +154,18 @@ func TestDedupePaths(t *testing.T) {
if err != nil { if err != nil {
require.NoError(t, err) require.NoError(t, err)
} }
require.Equal(t, tc.out, out) // convert to relative paths as that is shown to user
arr := make([]string, 0, len(out))
for k := range out {
arr = append(arr, k)
}
require.NoError(t, err)
arr = toRelativePaths(arr, wd)
m := make(map[string]struct{})
for _, v := range arr {
m[v] = struct{}{}
}
require.Equal(t, tc.out, m)
}) })
} }
} }
@ -164,6 +178,9 @@ func TestValidateEntitlements(t *testing.T) {
err := os.Symlink("../../aa", escapeLink) err := os.Symlink("../../aa", escapeLink)
require.NoError(t, err) require.NoError(t, err)
wd, err := os.Getwd()
require.NoError(t, err)
tcases := []struct { tcases := []struct {
name string name string
conf EntitlementConf conf EntitlementConf
@ -172,6 +189,11 @@ func TestValidateEntitlements(t *testing.T) {
}{ }{
{ {
name: "No entitlements", name: "No entitlements",
opt: build.Options{
Inputs: build.Inputs{
ContextState: &llb.State{},
},
},
}, },
{ {
name: "NetworkHostMissing", name: "NetworkHostMissing",
@ -182,6 +204,7 @@ func TestValidateEntitlements(t *testing.T) {
}, },
expected: EntitlementConf{ expected: EntitlementConf{
NetworkHost: true, NetworkHost: true,
FSRead: []string{wd},
}, },
}, },
{ {
@ -194,7 +217,9 @@ func TestValidateEntitlements(t *testing.T) {
entitlements.EntitlementNetworkHost, entitlements.EntitlementNetworkHost,
}, },
}, },
expected: EntitlementConf{}, expected: EntitlementConf{
FSRead: []string{wd},
},
}, },
{ {
name: "SecurityAndNetworkHostMissing", name: "SecurityAndNetworkHostMissing",
@ -207,6 +232,7 @@ func TestValidateEntitlements(t *testing.T) {
expected: EntitlementConf{ expected: EntitlementConf{
NetworkHost: true, NetworkHost: true,
SecurityInsecure: true, SecurityInsecure: true,
FSRead: []string{wd},
}, },
}, },
{ {
@ -222,6 +248,7 @@ func TestValidateEntitlements(t *testing.T) {
}, },
expected: EntitlementConf{ expected: EntitlementConf{
SecurityInsecure: true, SecurityInsecure: true,
FSRead: []string{wd},
}, },
}, },
{ {
@ -235,6 +262,7 @@ func TestValidateEntitlements(t *testing.T) {
}, },
expected: EntitlementConf{ expected: EntitlementConf{
SSH: true, SSH: true,
FSRead: []string{wd},
}, },
}, },
{ {
@ -267,6 +295,7 @@ func TestValidateEntitlements(t *testing.T) {
slices.Sort(exp) slices.Sort(exp)
return exp return exp
}(), }(),
FSRead: []string{wd},
}, },
}, },
{ {
@ -279,7 +308,7 @@ func TestValidateEntitlements(t *testing.T) {
}, },
}, },
conf: EntitlementConf{ conf: EntitlementConf{
FSRead: []string{dir1}, FSRead: []string{wd, dir1},
}, },
}, },
{ {
@ -292,7 +321,7 @@ func TestValidateEntitlements(t *testing.T) {
}, },
}, },
conf: EntitlementConf{ conf: EntitlementConf{
FSRead: []string{dir1}, FSRead: []string{wd, dir1},
}, },
expected: EntitlementConf{ expected: EntitlementConf{
FSRead: []string{filepath.Join(dir1, "../..")}, FSRead: []string{filepath.Join(dir1, "../..")},

View File

@ -257,7 +257,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
if err != nil { if err != nil {
return err return err
} }
if err := exp.Prompt(ctx, &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil { if err := exp.Prompt(ctx, url != "", &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil {
return err return err
} }
if printer.IsDone() { if printer.IsDone() {

View File

@ -508,7 +508,8 @@ EOT
withArgs(addr, "--set", "*.output=type=local,dest="+dirDest), withArgs(addr, "--set", "*.output=type=local,dest="+dirDest),
) )
require.Error(t, err, out) require.Error(t, err, out)
require.Contains(t, out, "outside of the working directory, please set BAKE_ALLOW_REMOTE_FS_ACCESS") require.Contains(t, out, "Your build is requesting privileges for following possibly insecure capabilities")
require.Contains(t, out, "Read access to path ../")
out, err = bakeCmd( out, err = bakeCmd(
sb, sb,
@ -555,7 +556,8 @@ EOT
withArgs(addr, "--set", "*.output=type=local,dest="+dirDest), withArgs(addr, "--set", "*.output=type=local,dest="+dirDest),
) )
require.Error(t, err, out) require.Error(t, err, out)
require.Contains(t, out, "outside of the working directory, please set BAKE_ALLOW_REMOTE_FS_ACCESS") require.Contains(t, out, "Your build is requesting privileges for following possibly insecure capabilities")
require.Contains(t, out, "Read access to path ..")
out, err = bakeCmd( out, err = bakeCmd(
sb, sb,