diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go index 8cec181ed5a1..840520e8cc90 100644 --- a/frontend/dockerfile/dockerfile2llb/convert.go +++ b/frontend/dockerfile/dockerfile2llb/convert.go @@ -640,10 +640,21 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE } opt = append(opt, runMounts...) - err = dispatchRunSecurity(d, c) + securityOpt, err := dispatchRunSecurity(c) if err != nil { return err } + if securityOpt != nil { + opt = append(opt, securityOpt) + } + + networkOpt, err := dispatchRunNetwork(c) + if err != nil { + return err + } + if networkOpt != nil { + opt = append(opt, networkOpt) + } shlex := *dopt.shlex shlex.RawQuotes = true diff --git a/frontend/dockerfile/dockerfile2llb/convert_norunnetwork.go b/frontend/dockerfile/dockerfile2llb/convert_norunnetwork.go new file mode 100644 index 000000000000..300b9d85f1d7 --- /dev/null +++ b/frontend/dockerfile/dockerfile2llb/convert_norunnetwork.go @@ -0,0 +1,12 @@ +// +build !dfrunnetwork + +package dockerfile2llb + +import ( + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/frontend/dockerfile/instructions" +) + +func dispatchRunNetwork(c *instructions.RunCommand) (llb.RunOption, error) { + return nil, nil +} diff --git a/frontend/dockerfile/dockerfile2llb/convert_norunsecurity.go b/frontend/dockerfile/dockerfile2llb/convert_norunsecurity.go index bc37ff43c850..10184b42e075 100644 --- a/frontend/dockerfile/dockerfile2llb/convert_norunsecurity.go +++ b/frontend/dockerfile/dockerfile2llb/convert_norunsecurity.go @@ -3,9 +3,10 @@ package dockerfile2llb import ( + "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/frontend/dockerfile/instructions" ) -func dispatchRunSecurity(d *dispatchState, c *instructions.RunCommand) error { - return nil +func dispatchRunSecurity(c *instructions.RunCommand) (llb.RunOption, error) { + return nil, nil } diff --git a/frontend/dockerfile/dockerfile2llb/convert_runnetwork.go b/frontend/dockerfile/dockerfile2llb/convert_runnetwork.go new file mode 100644 index 000000000000..01313e23be19 --- /dev/null +++ b/frontend/dockerfile/dockerfile2llb/convert_runnetwork.go @@ -0,0 +1,26 @@ +// +build dfrunnetwork + +package dockerfile2llb + +import ( + "github.com/pkg/errors" + + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/frontend/dockerfile/instructions" + "github.com/moby/buildkit/solver/pb" +) + +func dispatchRunNetwork(c *instructions.RunCommand) (llb.RunOption, error) { + network := instructions.GetNetwork(c) + + switch network { + case instructions.NetworkDefault: + return nil, nil + case instructions.NetworkNone: + return llb.Network(pb.NetMode_NONE), nil + case instructions.NetworkHost: + return llb.Network(pb.NetMode_HOST), nil + default: + return nil, errors.Errorf("unsupported network mode %q", network) + } +} diff --git a/frontend/dockerfile/dockerfile2llb/convert_runsecurity.go b/frontend/dockerfile/dockerfile2llb/convert_runsecurity.go index 7b1f0994647b..764424a2c1bc 100644 --- a/frontend/dockerfile/dockerfile2llb/convert_runsecurity.go +++ b/frontend/dockerfile/dockerfile2llb/convert_runsecurity.go @@ -5,23 +5,20 @@ package dockerfile2llb import ( "github.com/pkg/errors" + "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/frontend/dockerfile/instructions" "github.com/moby/buildkit/solver/pb" ) -func dispatchRunSecurity(d *dispatchState, c *instructions.RunCommand) error { +func dispatchRunSecurity(c *instructions.RunCommand) (llb.RunOption, error) { security := instructions.GetSecurity(c) - for _, sec := range security { - switch sec { - case instructions.SecurityInsecure: - d.state = d.state.Security(pb.SecurityMode_INSECURE) - case instructions.SecuritySandbox: - d.state = d.state.Security(pb.SecurityMode_SANDBOX) - default: - return errors.Errorf("unsupported security mode %q", sec) - } + switch security { + case instructions.SecurityInsecure: + return llb.Security(pb.SecurityMode_INSECURE), nil + case instructions.SecuritySandbox: + return llb.Security(pb.SecurityMode_SANDBOX), nil + default: + return nil, errors.Errorf("unsupported security mode %q", security) } - - return nil } diff --git a/frontend/dockerfile/dockerfile_runnetwork_test.go b/frontend/dockerfile/dockerfile_runnetwork_test.go new file mode 100644 index 000000000000..dc33961427e0 --- /dev/null +++ b/frontend/dockerfile/dockerfile_runnetwork_test.go @@ -0,0 +1,190 @@ +// +build dfrunnetwork + +package dockerfile + +import ( + "context" + "fmt" + "os" + "strings" + "testing" + + "github.com/containerd/continuity/fs/fstest" + "github.com/moby/buildkit/client" + "github.com/moby/buildkit/frontend/dockerfile/builder" + "github.com/moby/buildkit/util/entitlements" + "github.com/moby/buildkit/util/testutil/echoserver" + "github.com/moby/buildkit/util/testutil/integration" + "github.com/stretchr/testify/require" +) + +var runNetworkTests = []integration.Test{ + testRunDefaultNetwork, + testRunNoNetwork, + testRunHostNetwork, + testRunGlobalNetwork, +} + +func init() { + networkTests = append(networkTests, runNetworkTests...) +} + +func testRunDefaultNetwork(t *testing.T, sb integration.Sandbox) { + if os.Getenv("BUILDKIT_RUN_NETWORK_INTEGRATION_TESTS") == "" { + t.SkipNow() + } + + f := getFrontend(t, sb) + + dockerfile := []byte(` +FROM busybox +RUN ip link show eth0 +`) + + dir, err := tmpdir( + fstest.CreateFile("Dockerfile", dockerfile, 0600), + ) + require.NoError(t, err) + defer os.RemoveAll(dir) + + c, err := client.New(context.TODO(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + _, err = f.Solve(context.TODO(), c, client.SolveOpt{ + LocalDirs: map[string]string{ + builder.DefaultLocalNameDockerfile: dir, + builder.DefaultLocalNameContext: dir, + }, + }, nil) + + require.NoError(t, err) +} + +func testRunNoNetwork(t *testing.T, sb integration.Sandbox) { + if os.Getenv("BUILDKIT_RUN_NETWORK_INTEGRATION_TESTS") == "" { + t.SkipNow() + } + + f := getFrontend(t, sb) + + dockerfile := []byte(` +FROM busybox +RUN --network=none ! ip link show eth0 +RUN ip link show eth0 +`) + + dir, err := tmpdir( + fstest.CreateFile("Dockerfile", dockerfile, 0600), + ) + require.NoError(t, err) + defer os.RemoveAll(dir) + + c, err := client.New(context.TODO(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + _, err = f.Solve(context.TODO(), c, client.SolveOpt{ + LocalDirs: map[string]string{ + builder.DefaultLocalNameDockerfile: dir, + builder.DefaultLocalNameContext: dir, + }, + }, nil) + + require.NoError(t, err) +} + +func testRunHostNetwork(t *testing.T, sb integration.Sandbox) { + if os.Getenv("BUILDKIT_RUN_NETWORK_INTEGRATION_TESTS") == "" { + t.SkipNow() + } + + f := getFrontend(t, sb) + + s, err := echoserver.NewTestServer("foo") + require.NoError(t, err) + addrParts := strings.Split(s.Addr().String(), ":") + port := addrParts[len(addrParts)-1] + + dockerfile := fmt.Sprintf(` +FROM busybox +RUN --network=host nc 127.0.0.1 %s | grep foo +RUN ! nc 127.0.0.1 %s | grep foo +`, port, port) + + dir, err := tmpdir( + fstest.CreateFile("Dockerfile", []byte(dockerfile), 0600), + ) + require.NoError(t, err) + defer os.RemoveAll(dir) + + c, err := client.New(context.TODO(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + _, err = f.Solve(context.TODO(), c, client.SolveOpt{ + LocalDirs: map[string]string{ + builder.DefaultLocalNameDockerfile: dir, + builder.DefaultLocalNameContext: dir, + }, + AllowedEntitlements: []entitlements.Entitlement{entitlements.EntitlementNetworkHost}, + }, nil) + + hostAllowed := sb.Value("network.host") + switch hostAllowed { + case networkHostGranted: + require.NoError(t, err) + case networkHostDenied: + require.Error(t, err) + require.Contains(t, err.Error(), "entitlement network.host is not allowed") + default: + require.Fail(t, "unexpected network.host mode %q", hostAllowed) + } +} + +func testRunGlobalNetwork(t *testing.T, sb integration.Sandbox) { + f := getFrontend(t, sb) + + s, err := echoserver.NewTestServer("foo") + require.NoError(t, err) + addrParts := strings.Split(s.Addr().String(), ":") + port := addrParts[len(addrParts)-1] + + dockerfile := fmt.Sprintf(` +FROM busybox +RUN nc 127.0.0.1 %s | grep foo +RUN --network=none ! nc -z 127.0.0.1 %s +`, port, port) + + dir, err := tmpdir( + fstest.CreateFile("Dockerfile", []byte(dockerfile), 0600), + ) + require.NoError(t, err) + defer os.RemoveAll(dir) + + c, err := client.New(context.TODO(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + _, err = f.Solve(context.TODO(), c, client.SolveOpt{ + LocalDirs: map[string]string{ + builder.DefaultLocalNameDockerfile: dir, + builder.DefaultLocalNameContext: dir, + }, + AllowedEntitlements: []entitlements.Entitlement{entitlements.EntitlementNetworkHost}, + FrontendAttrs: map[string]string{ + "force-network-mode": "host", + }, + }, nil) + + hostAllowed := sb.Value("network.host") + switch hostAllowed { + case networkHostGranted: + require.NoError(t, err) + case networkHostDenied: + require.Error(t, err) + require.Contains(t, err.Error(), "entitlement network.host is not allowed") + default: + require.Fail(t, "unexpected network.host mode %q", hostAllowed) + } +} diff --git a/frontend/dockerfile/dockerfile_runsecurity_test.go b/frontend/dockerfile/dockerfile_runsecurity_test.go index 2115ede97958..32bf3c14ba13 100644 --- a/frontend/dockerfile/dockerfile_runsecurity_test.go +++ b/frontend/dockerfile/dockerfile_runsecurity_test.go @@ -32,6 +32,7 @@ func testRunSecurityInsecure(t *testing.T, sb integration.Sandbox) { dockerfile := []byte(` FROM busybox RUN --security=insecure [ "$(cat /proc/self/status | grep CapBnd)" == "CapBnd: 0000003fffffffff" ] +RUN [ "$(cat /proc/self/status | grep CapBnd)" == "CapBnd: 00000000a80425fb" ] `) dir, err := tmpdir( @@ -52,13 +53,13 @@ RUN --security=insecure [ "$(cat /proc/self/status | grep CapBnd)" == "CapBnd: 0 AllowedEntitlements: []entitlements.Entitlement{entitlements.EntitlementSecurityInsecure}, }, nil) - secMode := sb.Value("secmode") + secMode := sb.Value("security.insecure") switch secMode { - case securitySandbox: + case securityInsecureGranted: + require.NoError(t, err) + case securityInsecureDenied: require.Error(t, err) require.Contains(t, err.Error(), "entitlement security.insecure is not allowed") - case securityInsecure: - require.NoError(t, err) default: require.Fail(t, "unexpected secmode") } @@ -118,13 +119,13 @@ RUN [ "$(cat /proc/self/status | grep CapBnd)" == "CapBnd: 00000000a80425fb" ] AllowedEntitlements: []entitlements.Entitlement{entitlements.EntitlementSecurityInsecure}, }, nil) - secMode := sb.Value("secmode") + secMode := sb.Value("security.insecure") switch secMode { - case securitySandbox: + case securityInsecureGranted: + require.NoError(t, err) + case securityInsecureDenied: require.Error(t, err) require.Contains(t, err.Error(), "entitlement security.insecure is not allowed") - case securityInsecure: - require.NoError(t, err) default: require.Fail(t, "unexpected secmode") } diff --git a/frontend/dockerfile/dockerfile_test.go b/frontend/dockerfile/dockerfile_test.go index 703b58eef0ac..5bd04663503c 100644 --- a/frontend/dockerfile/dockerfile_test.go +++ b/frontend/dockerfile/dockerfile_test.go @@ -112,8 +112,12 @@ var fileOpTests = []integration.Test{ testWorkdirCopyIgnoreRelative, } +// Tests that depend on the `security.*` entitlements var securityTests = []integration.Test{} +// Tests that depend on the `network.*` entitlements +var networkTests = []integration.Test{} + var opts []integration.TestOpt type frontend interface { @@ -156,9 +160,14 @@ func TestIntegration(t *testing.T) { "false": false, }))...) integration.Run(t, securityTests, append(opts, - integration.WithMatrix("secmode", map[string]interface{}{ - "sandbox": securitySandbox, - "insecure": securityInsecure, + integration.WithMatrix("security.insecure", map[string]interface{}{ + "granted": securityInsecureGranted, + "denied": securityInsecureDenied, + }))...) + integration.Run(t, networkTests, append(opts, + integration.WithMatrix("network.host", map[string]interface{}{ + "granted": networkHostGranted, + "denied": networkHostDenied, }))...) } @@ -4278,8 +4287,23 @@ func (*secModeInsecure) UpdateConfigFile(in string) string { return in + "\n\ninsecure-entitlements = [\"security.insecure\"]\n" } -var securitySandbox integration.ConfigUpdater = &secModeSandbox{} -var securityInsecure integration.ConfigUpdater = &secModeInsecure{} +var securityInsecureGranted integration.ConfigUpdater = &secModeInsecure{} +var securityInsecureDenied integration.ConfigUpdater = &secModeSandbox{} + +type networkModeHost struct{} + +func (*networkModeHost) UpdateConfigFile(in string) string { + return in + "\n\ninsecure-entitlements = [\"network.host\"]\n" +} + +type networkModeSandbox struct{} + +func (*networkModeSandbox) UpdateConfigFile(in string) string { + return in +} + +var networkHostGranted integration.ConfigUpdater = &networkModeHost{} +var networkHostDenied integration.ConfigUpdater = &networkModeSandbox{} func fixedWriteCloser(wc io.WriteCloser) func(map[string]string) (io.WriteCloser, error) { return func(map[string]string) (io.WriteCloser, error) { diff --git a/frontend/dockerfile/docs/experimental.md b/frontend/dockerfile/docs/experimental.md index f4c1af34c969..876dc1061d74 100644 --- a/frontend/dockerfile/docs/experimental.md +++ b/frontend/dockerfile/docs/experimental.md @@ -138,7 +138,7 @@ $ buildctl build --frontend=dockerfile.v0 --local context=. --local dockerfile=. You can also specify a path to `*.pem` file on the host directly instead of `$SSH_AUTH_SOCK`. However, pem files with passphrases are not supported. -### RUN --security=insecure|sandbox +### `RUN --security=insecure|sandbox` With `--security=insecure`, this runs the command without sandbox in insecure mode, which allows to run flows requiring elevated privileges (e.g. containerd). This is equivalent @@ -160,3 +160,33 @@ RUN --security=insecure cat /proc/self/status | grep CapEff ``` #84 0.093 CapEff: 0000003fffffffff ``` + +### `RUN --network=none|host|default` + +This allows control over which networking environment the command is run in. + +The allowed values are: + +* `none` - The command is run with no network access (`lo` is still available, + but is isolated to this process) +* `host` - The command is run in the host's network environment (similar to + `docker build --network=host`, but on a per-instruction basis) +* `default` - Equivalent to not supplying a flag at all, the command is run in + the default network for the build + +The use of `--network=host` is protected by the `network.host` entitlement, +which needs to be enabled when starting the buildkitd daemon +(`--allow-insecure-entitlement network.host`) and on the build request +(`--allow network.host`). + +#### Example: isolating external effects + +```dockerfile +# syntax = docker/dockerfile:experimental +FROM python:3.6 +ADD mypackage.tgz wheels/ +RUN --network=none pip install --find-links wheels mypackage +``` + +`pip` will only be able to install the packages provided in the tarfile, which +can be controlled by an earlier build stage. diff --git a/frontend/dockerfile/instructions/commands_runnetwork.go b/frontend/dockerfile/instructions/commands_runnetwork.go new file mode 100644 index 000000000000..adef3fd7aa97 --- /dev/null +++ b/frontend/dockerfile/instructions/commands_runnetwork.go @@ -0,0 +1,63 @@ +// +build dfrunnetwork + +package instructions + +import ( + "github.com/pkg/errors" +) + +const ( + NetworkDefault = "default" + NetworkNone = "none" + NetworkHost = "host" +) + +var allowedNetwork = map[string]struct{}{ + NetworkDefault: {}, + NetworkNone: {}, + NetworkHost: {}, +} + +func isValidNetwork(value string) bool { + _, ok := allowedNetwork[value] + return ok +} + +var networkKey = "dockerfile/run/network" + +func init() { + parseRunPreHooks = append(parseRunPreHooks, runNetworkPreHook) + parseRunPostHooks = append(parseRunPostHooks, runNetworkPostHook) +} + +func runNetworkPreHook(cmd *RunCommand, req parseRequest) error { + st := &networkState{} + st.flag = req.flags.AddString("network", NetworkDefault) + cmd.setExternalValue(networkKey, st) + return nil +} + +func runNetworkPostHook(cmd *RunCommand, req parseRequest) error { + st := cmd.getExternalValue(networkKey).(*networkState) + if st == nil { + return errors.Errorf("no network state") + } + + value := st.flag.Value + if !isValidNetwork(value) { + return errors.Errorf("invalid network mode %q", value) + } + + st.networkMode = value + + return nil +} + +func GetNetwork(cmd *RunCommand) string { + return cmd.getExternalValue(networkKey).(*networkState).networkMode +} + +type networkState struct { + flag *Flag + networkMode string +} diff --git a/frontend/dockerfile/instructions/commands_runsecurity.go b/frontend/dockerfile/instructions/commands_runsecurity.go index b83b6f2f85eb..0c0be80664f4 100644 --- a/frontend/dockerfile/instructions/commands_runsecurity.go +++ b/frontend/dockerfile/instructions/commands_runsecurity.go @@ -3,9 +3,6 @@ package instructions import ( - "encoding/csv" - "strings" - "github.com/pkg/errors" ) @@ -24,9 +21,7 @@ func isValidSecurity(value string) bool { return ok } -type securityKeyT string - -var securityKey = securityKeyT("dockerfile/run/security") +var securityKey = "dockerfile/run/security" func init() { parseRunPreHooks = append(parseRunPreHooks, runSecurityPreHook) @@ -35,49 +30,32 @@ func init() { func runSecurityPreHook(cmd *RunCommand, req parseRequest) error { st := &securityState{} - st.flag = req.flags.AddStrings("security") + st.flag = req.flags.AddString("security", SecuritySandbox) cmd.setExternalValue(securityKey, st) return nil } func runSecurityPostHook(cmd *RunCommand, req parseRequest) error { - st := getSecurityState(cmd) + st := cmd.getExternalValue(securityKey).(*securityState) if st == nil { return errors.Errorf("no security state") } - for _, value := range st.flag.StringValues { - csvReader := csv.NewReader(strings.NewReader(value)) - fields, err := csvReader.Read() - if err != nil { - return errors.Wrap(err, "failed to parse csv security") - } - - for _, field := range fields { - if !isValidSecurity(field) { - return errors.Errorf("security %q is not valid", field) - } - - st.security = append(st.security, field) - } + value := st.flag.Value + if !isValidSecurity(value) { + return errors.Errorf("security %q is not valid", value) } - return nil -} + st.security = value -func getSecurityState(cmd *RunCommand) *securityState { - v := cmd.getExternalValue(securityKey) - if v == nil { - return nil - } - return v.(*securityState) + return nil } -func GetSecurity(cmd *RunCommand) []string { - return getSecurityState(cmd).security +func GetSecurity(cmd *RunCommand) string { + return cmd.getExternalValue(securityKey).(*securityState).security } type securityState struct { flag *Flag - security []string + security string } diff --git a/frontend/dockerfile/release/experimental/tags b/frontend/dockerfile/release/experimental/tags index f14d3af43750..f1cc9fb52e98 100644 --- a/frontend/dockerfile/release/experimental/tags +++ b/frontend/dockerfile/release/experimental/tags @@ -1 +1 @@ -dfrunmount dfrunsecurity dfsecrets dfssh +dfrunmount dfrunsecurity dfsecrets dfssh dfrunnetwork