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
}
// validateContextsEntitlements is a basic check to ensure contexts do not
// escape local directories when loaded from remote sources. This is to be
// 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
}
}
func collectLocalPaths(t build.Inputs) []string {
var out []string
if t.ContextState == nil {
if err := checkPath(t.ContextPath); err != nil {
return err
if v, ok := isLocalPath(t.ContextPath); ok {
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 {
if v.State != nil {
continue
}
if err := checkPath(v.Path); err != nil {
return err
if v, ok := isLocalPath(v.Path); ok {
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:") {
return nil
return "", false
}
p, err := filepath.EvalSymlinks(p)
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
return strings.TrimPrefix(p, "cwd://"), true
}
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
// absolute path.
bi.DockerfilePath = path.Clean(strings.TrimPrefix(bi.DockerfilePath, "cwd://"))
if err := checkPath(bi.DockerfilePath); err != nil {
return nil, err
}
var err error
bi.DockerfilePath, err = filepath.Abs(bi.DockerfilePath)
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
args := map[string]string{}

View File

@ -108,6 +108,10 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro
rwPaths := map[string]struct{}{}
roPaths := map[string]struct{}{}
for _, p := range collectLocalPaths(bo.Inputs) {
roPaths[p] = struct{}{}
}
for _, out := range bo.Exports {
if out.Type == "local" {
if dest, ok := out.Attrs["dest"]; ok {
@ -167,7 +171,7 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro
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
if _, err := console.ConsoleFromFile(os.Stdin); err == nil {
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)
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 {
for _, p := range commonPaths {
@ -251,6 +266,17 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
}
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")
if fsEntitlementsSet {
vv, err := strconv.ParseBool(v)
@ -334,11 +360,6 @@ loop0:
}
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))
for p := range in {
arr = append(arr, filepath.Clean(p))
@ -358,18 +379,23 @@ loop0:
}
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)
if err == nil {
if !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
delete(m, p)
m[rel] = struct{}{}
// allow up to one level of ".." in the path
if !strings.HasPrefix(rel, ".."+string(filepath.Separator)+"..") {
out = append(out, rel)
continue
}
}
out = append(out, p)
}
return m, nil
return out
}
func groupSamePaths(in1, in2 []string) ([]string, []string, []string) {

View File

@ -10,6 +10,7 @@ import (
"github.com/docker/buildx/build"
"github.com/docker/buildx/controller/pb"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/util/entitlements"
"github.com/stretchr/testify/require"
)
@ -132,15 +133,17 @@ func TestDedupePaths(t *testing.T) {
},
{
in: map[string]struct{}{
filepath.Join(wd, "a/b/c"): {},
filepath.Join(wd, "../aa"): {},
filepath.Join(wd, "a/b"): {},
filepath.Join(wd, "a/b/d"): {},
filepath.Join(wd, "../aa/b"): {},
filepath.Join(wd, "a/b/c"): {},
filepath.Join(wd, "../aa"): {},
filepath.Join(wd, "a/b"): {},
filepath.Join(wd, "a/b/d"): {},
filepath.Join(wd, "../aa/b"): {},
filepath.Join(wd, "../../bb"): {},
},
out: map[string]struct{}{
"a/b": {},
filepath.Join(wd, "../aa"): {},
"a/b": {},
"../aa": {},
filepath.Join(wd, "../../bb"): {},
},
},
}
@ -151,7 +154,18 @@ func TestDedupePaths(t *testing.T) {
if err != nil {
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)
require.NoError(t, err)
wd, err := os.Getwd()
require.NoError(t, err)
tcases := []struct {
name string
conf EntitlementConf
@ -172,6 +189,11 @@ func TestValidateEntitlements(t *testing.T) {
}{
{
name: "No entitlements",
opt: build.Options{
Inputs: build.Inputs{
ContextState: &llb.State{},
},
},
},
{
name: "NetworkHostMissing",
@ -182,6 +204,7 @@ func TestValidateEntitlements(t *testing.T) {
},
expected: EntitlementConf{
NetworkHost: true,
FSRead: []string{wd},
},
},
{
@ -194,7 +217,9 @@ func TestValidateEntitlements(t *testing.T) {
entitlements.EntitlementNetworkHost,
},
},
expected: EntitlementConf{},
expected: EntitlementConf{
FSRead: []string{wd},
},
},
{
name: "SecurityAndNetworkHostMissing",
@ -207,6 +232,7 @@ func TestValidateEntitlements(t *testing.T) {
expected: EntitlementConf{
NetworkHost: true,
SecurityInsecure: true,
FSRead: []string{wd},
},
},
{
@ -222,6 +248,7 @@ func TestValidateEntitlements(t *testing.T) {
},
expected: EntitlementConf{
SecurityInsecure: true,
FSRead: []string{wd},
},
},
{
@ -234,7 +261,8 @@ func TestValidateEntitlements(t *testing.T) {
},
},
expected: EntitlementConf{
SSH: true,
SSH: true,
FSRead: []string{wd},
},
},
{
@ -267,6 +295,7 @@ func TestValidateEntitlements(t *testing.T) {
slices.Sort(exp)
return exp
}(),
FSRead: []string{wd},
},
},
{
@ -279,7 +308,7 @@ func TestValidateEntitlements(t *testing.T) {
},
},
conf: EntitlementConf{
FSRead: []string{dir1},
FSRead: []string{wd, dir1},
},
},
{
@ -292,7 +321,7 @@ func TestValidateEntitlements(t *testing.T) {
},
},
conf: EntitlementConf{
FSRead: []string{dir1},
FSRead: []string{wd, dir1},
},
expected: EntitlementConf{
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 {
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
}
if printer.IsDone() {

View File

@ -508,7 +508,8 @@ EOT
withArgs(addr, "--set", "*.output=type=local,dest="+dirDest),
)
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(
sb,
@ -555,7 +556,8 @@ EOT
withArgs(addr, "--set", "*.output=type=local,dest="+dirDest),
)
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(
sb,