diff --git a/components/engine/api/swagger.yaml b/components/engine/api/swagger.yaml index af3bd6d4847..3cdfe2988ca 100644 --- a/components/engine/api/swagger.yaml +++ b/components/engine/api/swagger.yaml @@ -772,6 +772,16 @@ definitions: - "default" - "process" - "hyperv" + MaskedPaths: + type: "array" + description: "The list of paths to be masked inside the container (this overrides the default set of paths)" + items: + type: "string" + ReadonlyPaths: + type: "array" + description: "The list of paths to be set as read-only inside the container (this overrides the default set of paths)" + items: + type: "string" ContainerConfig: description: "Configuration for a container that is portable between hosts" diff --git a/components/engine/api/types/container/host_config.go b/components/engine/api/types/container/host_config.go index 02271ecd985..4ef26fa6c87 100644 --- a/components/engine/api/types/container/host_config.go +++ b/components/engine/api/types/container/host_config.go @@ -401,6 +401,12 @@ type HostConfig struct { // Mounts specs used by the container Mounts []mount.Mount `json:",omitempty"` + // MaskedPaths is the list of paths to be masked inside the container (this overrides the default set of paths) + MaskedPaths []string + + // ReadonlyPaths is the list of paths to be set as read-only inside the container (this overrides the default set of paths) + ReadonlyPaths []string + // Run a custom init inside the container, if null, use the daemon's configured settings Init *bool `json:",omitempty"` } diff --git a/components/engine/daemon/create_unix.go b/components/engine/daemon/create_unix.go index 9ea74e7c1f9..eb9b6537300 100644 --- a/components/engine/daemon/create_unix.go +++ b/components/engine/daemon/create_unix.go @@ -11,6 +11,7 @@ import ( containertypes "github.com/docker/docker/api/types/container" mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/container" + "github.com/docker/docker/oci" "github.com/docker/docker/pkg/stringid" volumeopts "github.com/docker/docker/volume/service/opts" "github.com/opencontainers/selinux/go-selinux/label" @@ -29,6 +30,16 @@ func (daemon *Daemon) createContainerOSSpecificSettings(container *container.Con return err } + // Set the default masked and readonly paths with regard to the host config options if they are not set. + if hostConfig.MaskedPaths == nil && !hostConfig.Privileged { + hostConfig.MaskedPaths = oci.DefaultSpec().Linux.MaskedPaths // Set it to the default if nil + container.HostConfig.MaskedPaths = hostConfig.MaskedPaths + } + if hostConfig.ReadonlyPaths == nil && !hostConfig.Privileged { + hostConfig.ReadonlyPaths = oci.DefaultSpec().Linux.ReadonlyPaths // Set it to the default if nil + container.HostConfig.ReadonlyPaths = hostConfig.ReadonlyPaths + } + for spec := range config.Volumes { name := stringid.GenerateNonCryptoID() destination := filepath.Clean(spec) diff --git a/components/engine/daemon/oci_linux.go b/components/engine/daemon/oci_linux.go index b675eaf406c..9b39a64ee73 100644 --- a/components/engine/daemon/oci_linux.go +++ b/components/engine/daemon/oci_linux.go @@ -903,6 +903,14 @@ func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, e s.Process.OOMScoreAdj = &c.HostConfig.OomScoreAdj s.Linux.MountLabel = c.MountLabel + // Set the masked and readonly paths with regard to the host config options if they are set. + if c.HostConfig.MaskedPaths != nil { + s.Linux.MaskedPaths = c.HostConfig.MaskedPaths + } + if c.HostConfig.ReadonlyPaths != nil { + s.Linux.ReadonlyPaths = c.HostConfig.ReadonlyPaths + } + return &s, nil } diff --git a/components/engine/integration/container/create_test.go b/components/engine/integration/container/create_test.go index cea8c059cc7..9c466448903 100644 --- a/components/engine/integration/container/create_test.go +++ b/components/engine/integration/container/create_test.go @@ -2,14 +2,21 @@ package container // import "github.com/docker/docker/integration/container" import ( "context" + "encoding/json" + "fmt" "strconv" "testing" + "time" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" + ctr "github.com/docker/docker/integration/internal/container" "github.com/docker/docker/internal/test/request" + "github.com/docker/docker/oci" "github.com/gotestyourself/gotestyourself/assert" is "github.com/gotestyourself/gotestyourself/assert/cmp" + "github.com/gotestyourself/gotestyourself/poll" "github.com/gotestyourself/gotestyourself/skip" ) @@ -137,3 +144,160 @@ func TestCreateTmpfsMountsTarget(t *testing.T) { assert.Check(t, is.ErrorContains(err, tc.expectedError)) } } +func TestCreateWithCustomMaskedPaths(t *testing.T) { + skip.If(t, testEnv.DaemonInfo.OSType != "linux") + + defer setupTest(t)() + client := request.NewAPIClient(t) + ctx := context.Background() + + testCases := []struct { + maskedPaths []string + expected []string + }{ + { + maskedPaths: []string{}, + expected: []string{}, + }, + { + maskedPaths: nil, + expected: oci.DefaultSpec().Linux.MaskedPaths, + }, + { + maskedPaths: []string{"/proc/kcore", "/proc/keys"}, + expected: []string{"/proc/kcore", "/proc/keys"}, + }, + } + + checkInspect := func(t *testing.T, ctx context.Context, name string, expected []string) { + _, b, err := client.ContainerInspectWithRaw(ctx, name, false) + assert.NilError(t, err) + + var inspectJSON map[string]interface{} + err = json.Unmarshal(b, &inspectJSON) + assert.NilError(t, err) + + cfg, ok := inspectJSON["HostConfig"].(map[string]interface{}) + assert.Check(t, is.Equal(true, ok), name) + + maskedPaths, ok := cfg["MaskedPaths"].([]interface{}) + assert.Check(t, is.Equal(true, ok), name) + + mps := []string{} + for _, mp := range maskedPaths { + mps = append(mps, mp.(string)) + } + + assert.DeepEqual(t, expected, mps) + } + + for i, tc := range testCases { + name := fmt.Sprintf("create-masked-paths-%d", i) + config := container.Config{ + Image: "busybox", + Cmd: []string{"true"}, + } + hc := container.HostConfig{} + if tc.maskedPaths != nil { + hc.MaskedPaths = tc.maskedPaths + } + + // Create the container. + c, err := client.ContainerCreate(context.Background(), + &config, + &hc, + &network.NetworkingConfig{}, + name, + ) + assert.NilError(t, err) + + checkInspect(t, ctx, name, tc.expected) + + // Start the container. + err = client.ContainerStart(ctx, c.ID, types.ContainerStartOptions{}) + assert.NilError(t, err) + + poll.WaitOn(t, ctr.IsInState(ctx, client, c.ID, "exited"), poll.WithDelay(100*time.Millisecond)) + + checkInspect(t, ctx, name, tc.expected) + } +} + +func TestCreateWithCustomReadonlyPaths(t *testing.T) { + skip.If(t, testEnv.DaemonInfo.OSType != "linux") + + defer setupTest(t)() + client := request.NewAPIClient(t) + ctx := context.Background() + + testCases := []struct { + doc string + readonlyPaths []string + expected []string + }{ + { + readonlyPaths: []string{}, + expected: []string{}, + }, + { + readonlyPaths: nil, + expected: oci.DefaultSpec().Linux.ReadonlyPaths, + }, + { + readonlyPaths: []string{"/proc/asound", "/proc/bus"}, + expected: []string{"/proc/asound", "/proc/bus"}, + }, + } + + checkInspect := func(t *testing.T, ctx context.Context, name string, expected []string) { + _, b, err := client.ContainerInspectWithRaw(ctx, name, false) + assert.NilError(t, err) + + var inspectJSON map[string]interface{} + err = json.Unmarshal(b, &inspectJSON) + assert.NilError(t, err) + + cfg, ok := inspectJSON["HostConfig"].(map[string]interface{}) + assert.Check(t, is.Equal(true, ok), name) + + readonlyPaths, ok := cfg["ReadonlyPaths"].([]interface{}) + assert.Check(t, is.Equal(true, ok), name) + + rops := []string{} + for _, rop := range readonlyPaths { + rops = append(rops, rop.(string)) + } + assert.DeepEqual(t, expected, rops) + } + + for i, tc := range testCases { + name := fmt.Sprintf("create-readonly-paths-%d", i) + config := container.Config{ + Image: "busybox", + Cmd: []string{"true"}, + } + hc := container.HostConfig{} + if tc.readonlyPaths != nil { + hc.ReadonlyPaths = tc.readonlyPaths + } + + // Create the container. + c, err := client.ContainerCreate(context.Background(), + &config, + &hc, + &network.NetworkingConfig{}, + name, + ) + assert.NilError(t, err) + + checkInspect(t, ctx, name, tc.expected) + + // Start the container. + err = client.ContainerStart(ctx, c.ID, types.ContainerStartOptions{}) + assert.NilError(t, err) + + poll.WaitOn(t, ctr.IsInState(ctx, client, c.ID, "exited"), poll.WithDelay(100*time.Millisecond)) + + checkInspect(t, ctx, name, tc.expected) + } +}