Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement frontend support for RUN --security=insecure #1081

Merged
merged 1 commit into from
Jul 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions frontend/dockerfile/dockerfile2llb/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,11 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE
}
opt = append(opt, runMounts...)

err = dispatchRunSecurity(d, c)
if err != nil {
return err
}

shlex := *dopt.shlex
shlex.RawQuotes = true
shlex.SkipUnsetEnv = true
Expand Down
11 changes: 11 additions & 0 deletions frontend/dockerfile/dockerfile2llb/convert_norunsecurity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// +build !dfrunsecurity

package dockerfile2llb

import (
"github.com/moby/buildkit/frontend/dockerfile/instructions"
)

func dispatchRunSecurity(d *dispatchState, c *instructions.RunCommand) error {
return nil
}
27 changes: 27 additions & 0 deletions frontend/dockerfile/dockerfile2llb/convert_runsecurity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// +build dfrunsecurity

package dockerfile2llb

import (
"github.com/pkg/errors"

"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/moby/buildkit/solver/pb"
)

func dispatchRunSecurity(d *dispatchState, c *instructions.RunCommand) 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)
}
}

return nil
}
131 changes: 131 additions & 0 deletions frontend/dockerfile/dockerfile_runsecurity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// +build dfrunsecurity

package dockerfile

import (
"context"
"os"
"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/integration"
"github.com/stretchr/testify/require"
)

var runSecurityTests = []integration.Test{
testRunSecurityInsecure,
testRunSecuritySandbox,
testRunSecurityDefault,
}

func init() {
securityTests = append(securityTests, runSecurityTests...)

}

func testRunSecurityInsecure(t *testing.T, sb integration.Sandbox) {
f := getFrontend(t, sb)

dockerfile := []byte(`
FROM busybox
RUN --security=insecure [ "$(cat /proc/self/status | grep CapBnd)" == "CapBnd: 0000003fffffffff" ]
`)

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,
},
AllowedEntitlements: []entitlements.Entitlement{entitlements.EntitlementSecurityInsecure},
}, nil)

secMode := sb.Value("secmode")
switch secMode {
case securitySandbox:
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")
}
}

func testRunSecuritySandbox(t *testing.T, sb integration.Sandbox) {
f := getFrontend(t, sb)

dockerfile := []byte(`
FROM busybox
RUN --security=sandbox [ "$(cat /proc/self/status | grep CapBnd)" == "CapBnd: 00000000a80425fb" ]
`)

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 testRunSecurityDefault(t *testing.T, sb integration.Sandbox) {
f := getFrontend(t, sb)

dockerfile := []byte(`
FROM busybox
RUN [ "$(cat /proc/self/status | grep CapBnd)" == "CapBnd: 00000000a80425fb" ]
`)

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,
},
AllowedEntitlements: []entitlements.Entitlement{entitlements.EntitlementSecurityInsecure},
}, nil)

secMode := sb.Value("secmode")
switch secMode {
case securitySandbox:
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")
}
}
26 changes: 24 additions & 2 deletions frontend/dockerfile/dockerfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ var fileOpTests = []integration.Test{
testWorkdirUser,
}

var securityTests = []integration.Test{}

var opts []integration.TestOpt

type frontend interface {
Expand Down Expand Up @@ -151,6 +153,11 @@ func TestIntegration(t *testing.T) {
"true": true,
"false": false,
}))...)
integration.Run(t, securityTests, append(opts,
integration.WithMatrix("secmode", map[string]interface{}{
"sandbox": securitySandbox,
"insecure": securityInsecure,
}))...)
}

func testDefaultEnvWithArgs(t *testing.T, sb integration.Sandbox) {
Expand All @@ -160,7 +167,7 @@ func testDefaultEnvWithArgs(t *testing.T, sb integration.Sandbox) {
FROM busybox AS build
ARG my_arg
ENV my_arg "${my_arg:-def_val}"
COPY myscript.sh myscript.sh
COPY myscript.sh myscript.sh
RUN ./myscript.sh
FROM scratch
COPY --from=build /out /out
Expand Down Expand Up @@ -2921,7 +2928,7 @@ func testDockerfileFromGit(t *testing.T, sb integration.Sandbox) {

dockerfile := `
FROM busybox AS build
RUN echo -n fromgit > foo
RUN echo -n fromgit > foo
FROM scratch
COPY --from=build foo bar
`
Expand Down Expand Up @@ -4174,3 +4181,18 @@ type nopWriteCloser struct {
}

func (nopWriteCloser) Close() error { return nil }

type secModeSandbox struct{}

func (*secModeSandbox) UpdateConfigFile(in string) string {
return in
}

type secModeInsecure struct{}

func (*secModeInsecure) UpdateConfigFile(in string) string {
return in + "\n\ninsecure-entitlements = [\"security.insecure\"]\n"
}

var securitySandbox integration.ConfigUpdater = &secModeSandbox{}
var securityInsecure integration.ConfigUpdater = &secModeInsecure{}
22 changes: 22 additions & 0 deletions frontend/dockerfile/docs/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,25 @@ $ 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

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
to running `docker run --privileged`. In order to access this feature, entitlement
`security.insecure` should be enabled when starting the buildkitd daemon
(`--allow-insecure-entitlement security.insecure`) and for a build request
(`--allow security.insecure`).

Default sandbox mode can be activated via `--security=sandbox`, but that is no-op.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add: In order to build Dockerfiles with --security=insecure user needs to enable security.insecure entitlement when starting the buildkit daemon and allow it for a specific build request.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated!


#### Example: check entitlements

```dockerfile
# syntax = docker/dockerfile:experimental
FROM ubuntu
RUN --security=insecure cat /proc/self/status | grep CapEff
```

```
#84 0.093 CapEff: 0000003fffffffff
```
83 changes: 83 additions & 0 deletions frontend/dockerfile/instructions/commands_runsecurity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// +build dfrunsecurity

package instructions

import (
"encoding/csv"
"strings"

"github.com/pkg/errors"
)

const (
SecurityInsecure = "insecure"
SecuritySandbox = "sandbox"
)

var allowedSecurity = map[string]struct{}{
SecurityInsecure: {},
SecuritySandbox: {},
}

func isValidSecurity(value string) bool {
_, ok := allowedSecurity[value]
return ok
}

type securityKeyT string

var securityKey = securityKeyT("dockerfile/run/security")

func init() {
parseRunPreHooks = append(parseRunPreHooks, runSecurityPreHook)
parseRunPostHooks = append(parseRunPostHooks, runSecurityPostHook)
}

func runSecurityPreHook(cmd *RunCommand, req parseRequest) error {
st := &securityState{}
st.flag = req.flags.AddStrings("security")
cmd.setExternalValue(securityKey, st)
return nil
}

func runSecurityPostHook(cmd *RunCommand, req parseRequest) error {
st := getSecurityState(cmd)
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)
}
}

return nil
}

func getSecurityState(cmd *RunCommand) *securityState {
v := cmd.getExternalValue(securityKey)
if v == nil {
return nil
}
return v.(*securityState)
}

func GetSecurity(cmd *RunCommand) []string {
return getSecurityState(cmd).security
}

type securityState struct {
flag *Flag
security []string
}
2 changes: 1 addition & 1 deletion frontend/dockerfile/release/experimental/tags
Original file line number Diff line number Diff line change
@@ -1 +1 @@
dfrunmount dfsecrets dfssh
dfrunmount dfrunsecurity dfsecrets dfssh
AkihiroSuda marked this conversation as resolved.
Show resolved Hide resolved