From a0a5cc778760fe66cc54701a404e0f5c1fa8379a Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 26 Jul 2017 14:26:31 -0400 Subject: [PATCH] Add user namespace support to client Signed-off-by: Michael Crosby --- apparmor.go | 13 +++ container_test.go | 78 ++++++++++++++++ helpers_unix_test.go | 4 + helpers_windows_test.go | 12 +++ linux/bundle.go | 6 +- linux/runtime.go | 2 +- linux/shim/init.go | 1 - server/server.go | 4 +- snapshot/overlay/overlay.go | 4 +- spec.go | 2 + spec_unix.go | 173 ++++++++++++++++++++++++++++++++---- spec_unix_test.go | 1 - 12 files changed, 271 insertions(+), 29 deletions(-) create mode 100644 apparmor.go diff --git a/apparmor.go b/apparmor.go new file mode 100644 index 000000000..117b52bb1 --- /dev/null +++ b/apparmor.go @@ -0,0 +1,13 @@ +// +build linux + +package containerd + +import specs "github.com/opencontainers/runtime-spec/specs-go" + +// WithApparmor sets the provided apparmor profile to the spec +func WithApparmorProfile(profile string) SpecOpts { + return func(s *specs.Spec) error { + s.Process.ApparmorProfile = profile + return nil + } +} diff --git a/container_test.go b/container_test.go index 38e8b699f..2b423285f 100644 --- a/container_test.go +++ b/container_test.go @@ -889,3 +889,81 @@ func TestContainerExecNoBinaryExists(t *testing.T) { } <-finished } + +func TestUserNamespaces(t *testing.T) { + client, err := newClient(t, address) + if err != nil { + t.Fatal(err) + } + defer client.Close() + + var ( + image Image + ctx, cancel = testContext() + id = t.Name() + ) + defer cancel() + + if runtime.GOOS != "windows" { + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Error(err) + return + } + } + + spec, err := generateSpec( + withImageConfig(ctx, image), + withExitStatus(7), + withUserNamespace(0, 1000, 10000), + ) + if err != nil { + t.Error(err) + return + } + container, err := client.NewContainer(ctx, id, + WithSpec(spec), + withRemappedSnapshot(id, image, 1000, 1000), + ) + if err != nil { + t.Error(err) + return + } + defer container.Delete(ctx, WithSnapshotCleanup) + + task, err := container.NewTask(ctx, Stdio) + if err != nil { + t.Error(err) + return + } + defer task.Delete(ctx) + + statusC := make(chan uint32, 1) + go func() { + status, err := task.Wait(ctx) + if err != nil { + t.Error(err) + } + statusC <- status + }() + + if pid := task.Pid(); pid <= 0 { + t.Errorf("invalid task pid %d", pid) + } + if err := task.Start(ctx); err != nil { + t.Error(err) + task.Delete(ctx) + return + } + status := <-statusC + if status != 7 { + t.Errorf("expected status 7 from wait but received %d", status) + } + if status, err = task.Delete(ctx); err != nil { + t.Error(err) + return + } + if status != 7 { + t.Errorf("expected status 7 from delete but received %d", status) + } +} diff --git a/helpers_unix_test.go b/helpers_unix_test.go index 7186d70fe..ea93953ef 100644 --- a/helpers_unix_test.go +++ b/helpers_unix_test.go @@ -49,3 +49,7 @@ func withImageConfig(ctx context.Context, i Image) SpecOpts { func withNewSnapshot(id string, i Image) NewContainerOpts { return WithNewSnapshot(id, i) } + +var withUserNamespace = WithUserNamespace + +var withRemappedSnapshot = WithRemappedSnapshot diff --git a/helpers_windows_test.go b/helpers_windows_test.go index 9a6e632a2..7a148d90c 100644 --- a/helpers_windows_test.go +++ b/helpers_windows_test.go @@ -63,3 +63,15 @@ func withNewSnapshot(id string, i Image) NewContainerOpts { return nil } } + +func withUserNamespace(u, g, s uint32) SpecOpts { + return func(s *specs.Spec) error { + return nil + } +} + +func withRemappedSnapshot(id string, i Image, u, g uint32) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + return nil + } +} diff --git a/linux/bundle.go b/linux/bundle.go index b4b672342..42465c3d3 100644 --- a/linux/bundle.go +++ b/linux/bundle.go @@ -21,7 +21,7 @@ func loadBundle(path, namespace string) *bundle { // newBundle creates a new bundle on disk at the provided path for the given id func newBundle(path, namespace, id string, spec []byte) (b *bundle, err error) { - if err := os.MkdirAll(path, 0700); err != nil { + if err := os.MkdirAll(path, 0711); err != nil { return nil, err } path = filepath.Join(path, id) @@ -30,10 +30,10 @@ func newBundle(path, namespace, id string, spec []byte) (b *bundle, err error) { os.RemoveAll(path) } }() - if err := os.Mkdir(path, 0700); err != nil { + if err := os.Mkdir(path, 0711); err != nil { return nil, err } - if err := os.Mkdir(filepath.Join(path, "rootfs"), 0700); err != nil { + if err := os.Mkdir(filepath.Join(path, "rootfs"), 0711); err != nil { return nil, err } f, err := os.Create(filepath.Join(path, configFilename)) diff --git a/linux/runtime.go b/linux/runtime.go index b32e3f24c..7179c2929 100644 --- a/linux/runtime.go +++ b/linux/runtime.go @@ -68,7 +68,7 @@ type Config struct { } func New(ic *plugin.InitContext) (interface{}, error) { - if err := os.MkdirAll(ic.Root, 0700); err != nil { + if err := os.MkdirAll(ic.Root, 0711); err != nil { return nil, err } monitor, err := ic.Get(plugin.TaskMonitorPlugin) diff --git a/linux/shim/init.go b/linux/shim/init.go index f34defe70..d1eb0a86e 100644 --- a/linux/shim/init.go +++ b/linux/shim/init.go @@ -120,7 +120,6 @@ func newInitProcess(context context.Context, path, namespace string, r *shimapi. } defer os.Remove(socket.Path()) } else { - // TODO: get uid/gid if io, err = runc.NewPipeIO(0, 0); err != nil { return nil, errors.Wrap(err, "failed to create OCI runtime io pipes") } diff --git a/server/server.go b/server/server.go index 1d205261f..940f40eaf 100644 --- a/server/server.go +++ b/server/server.go @@ -36,7 +36,7 @@ func New(ctx context.Context, config *Config) (*Server, error) { if config.Root == "" { return nil, errors.New("root must be specified") } - if err := os.MkdirAll(config.Root, 0700); err != nil { + if err := os.MkdirAll(config.Root, 0711); err != nil { return nil, err } if err := apply(ctx, config); err != nil { @@ -168,7 +168,7 @@ func loadPlugins(config *Config) ([]*plugin.Registration, error) { Type: plugin.MetadataPlugin, ID: "bolt", Init: func(ic *plugin.InitContext) (interface{}, error) { - if err := os.MkdirAll(ic.Root, 0700); err != nil { + if err := os.MkdirAll(ic.Root, 0711); err != nil { return nil, err } return bolt.Open(filepath.Join(ic.Root, "meta.db"), 0644, nil) diff --git a/snapshot/overlay/overlay.go b/snapshot/overlay/overlay.go index c759829d5..7d1616bc6 100644 --- a/snapshot/overlay/overlay.go +++ b/snapshot/overlay/overlay.go @@ -255,12 +255,12 @@ func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshot.Kind, ke } }() - if err = os.MkdirAll(filepath.Join(td, "fs"), 0711); err != nil { + if err = os.MkdirAll(filepath.Join(td, "fs"), 0755); err != nil { return nil, err } if kind == snapshot.KindActive { - if err = os.MkdirAll(filepath.Join(td, "work"), 0700); err != nil { + if err = os.MkdirAll(filepath.Join(td, "work"), 0711); err != nil { return nil, err } } diff --git a/spec.go b/spec.go index 53e1fd5f5..3d52b9cc3 100644 --- a/spec.go +++ b/spec.go @@ -2,8 +2,10 @@ package containerd import specs "github.com/opencontainers/runtime-spec/specs-go" +// SpecOpts sets spec specific information to a newly generated OCI spec type SpecOpts func(s *specs.Spec) error +// WithProcessArgs replaces the args on the generated spec func WithProcessArgs(args ...string) SpecOpts { return func(s *specs.Spec) error { s.Process.Args = args diff --git a/spec_unix.go b/spec_unix.go index b8e1171b6..0addded1b 100644 --- a/spec_unix.go +++ b/spec_unix.go @@ -6,12 +6,21 @@ import ( "context" "encoding/json" "fmt" + "io/ioutil" + "os" + "path/filepath" "strconv" "strings" + "syscall" + + "golang.org/x/sys/unix" "github.com/containerd/containerd/containers" + "github.com/containerd/containerd/fs" "github.com/containerd/containerd/images" + "github.com/containerd/containerd/mount" "github.com/containerd/containerd/typeurl" + "github.com/opencontainers/image-spec/identity" "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/runtime-spec/specs-go" ) @@ -78,7 +87,6 @@ func createDefaultSpec() (*specs.Spec, error) { Permitted: defaltCaps(), Inheritable: defaltCaps(), Effective: defaltCaps(), - Ambient: defaltCaps(), }, Rlimits: []specs.POSIXRlimit{ { @@ -130,24 +138,6 @@ func createDefaultSpec() (*specs.Spec, error) { Source: "tmpfs", Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}, }, - { - Destination: "/etc/resolv.conf", - Type: "bind", - Source: "/etc/resolv.conf", - Options: []string{"rbind", "ro"}, - }, - { - Destination: "/etc/hosts", - Type: "bind", - Source: "/etc/hosts", - Options: []string{"rbind", "ro"}, - }, - { - Destination: "/etc/localtime", - Type: "bind", - Source: "/etc/localtime", - Options: []string{"rbind", "ro"}, - }, }, Linux: &specs.Linux{ // TODO (@crosbymichael) make sure we don't have have two containers in the same cgroup @@ -272,6 +262,7 @@ func WithImageConfig(ctx context.Context, i Image) SpecOpts { } } +// WithSpec sets the provided spec for a new container func WithSpec(spec *specs.Spec) NewContainerOpts { return func(ctx context.Context, client *Client, c *containers.Container) error { any, err := typeurl.MarshalAny(spec) @@ -283,9 +274,153 @@ func WithSpec(spec *specs.Spec) NewContainerOpts { } } +// WithResources sets the provided resources on the spec for task updates func WithResources(resources *specs.LinuxResources) UpdateTaskOpts { return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error { r.Resources = resources return nil } } + +// WithNoNewPrivileges sets no_new_privileges on the process for the container +func WithNoNewPrivileges(s *specs.Spec) error { + s.Process.NoNewPrivileges = true + return nil +} + +func WithHostHosts(s *specs.Spec) error { + s.Mounts = append(s.Mounts, specs.Mount{ + Destination: "/etc/hosts", + Type: "bind", + Source: "/etc/hosts", + Options: []string{"rbind", "ro"}, + }) + return nil +} + +func WithHostResoveconf(s *specs.Spec) error { + s.Mounts = append(s.Mounts, specs.Mount{ + Destination: "/etc/resolv.conf", + Type: "bind", + Source: "/etc/resolv.conf", + Options: []string{"rbind", "ro"}, + }) + return nil +} + +func WithHostLocaltime(s *specs.Spec) error { + s.Mounts = append(s.Mounts, specs.Mount{ + Destination: "/etc/localtime", + Type: "bind", + Source: "/etc/localtime", + Options: []string{"rbind", "ro"}, + }) + return nil +} + +// WithUserNamespace sets the uid and gid mappings for the task +// this can be called multiple times to add more mappings to the generated spec +func WithUserNamespace(container, host, size uint32) SpecOpts { + return func(s *specs.Spec) error { + var hasUserns bool + for _, ns := range s.Linux.Namespaces { + if ns.Type == specs.UserNamespace { + hasUserns = true + break + } + } + if !hasUserns { + s.Linux.Namespaces = append(s.Linux.Namespaces, specs.LinuxNamespace{ + Type: specs.UserNamespace, + }) + } + mapping := specs.LinuxIDMapping{ + ContainerID: container, + HostID: host, + Size: size, + } + s.Linux.UIDMappings = append(s.Linux.UIDMappings, mapping) + s.Linux.GIDMappings = append(s.Linux.GIDMappings, mapping) + return nil + } +} + +// WithRemappedSnapshot creates a new snapshot and remaps the uid/gid for the +// filesystem to be used by a container with user namespaces +func WithRemappedSnapshot(id string, i Image, uid, gid uint32) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + diffIDs, err := i.(*image).i.RootFS(ctx, client.ContentStore()) + if err != nil { + return err + } + var ( + snapshotter = client.SnapshotService(c.Snapshotter) + parent = identity.ChainID(diffIDs).String() + usernsID = fmt.Sprintf("%s-%d-%d", parent, uid, gid) + ) + if _, err := snapshotter.Stat(ctx, usernsID); err == nil { + if _, err := snapshotter.Prepare(ctx, id, usernsID); err != nil { + return err + } + c.RootFS = id + c.Image = i.Name() + return nil + } + mounts, err := snapshotter.Prepare(ctx, usernsID+"-remap", parent) + if err != nil { + return err + } + if err := remapRootFS(mounts, uid, gid); err != nil { + snapshotter.Remove(ctx, usernsID) + return err + } + if err := snapshotter.Commit(ctx, usernsID, usernsID+"-remap"); err != nil { + return err + } + if _, err := snapshotter.Prepare(ctx, id, usernsID); err != nil { + return err + } + c.RootFS = id + c.Image = i.Name() + return nil + } +} + +func remapRootFS(mounts []mount.Mount, uid, gid uint32) error { + root, err := ioutil.TempDir("", "ctd-remap") + if err != nil { + return err + } + defer os.RemoveAll(root) + for _, m := range mounts { + if err := m.Mount(root); err != nil { + return err + } + } + defer unix.Unmount(root, 0) + return filepath.Walk(root, incrementFS(root, uid, gid)) +} + +func incrementFS(root string, uidInc, gidInc uint32) filepath.WalkFunc { + return func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if root == path { + return nil + } + var ( + stat = info.Sys().(*syscall.Stat_t) + u, g = int(stat.Uid + uidInc), int(stat.Gid + gidInc) + symlink = info.Mode()&os.ModeSymlink != 0 + ) + // make sure we resolve links inside the root for symlinks + if path, err = fs.RootPath(root, strings.TrimPrefix(path, root)); err != nil { + return err + } + if err := os.Chown(path, u, g); err != nil && !symlink { + return err + } + return nil + } +} diff --git a/spec_unix_test.go b/spec_unix_test.go index 394771ea3..bd23b77ea 100644 --- a/spec_unix_test.go +++ b/spec_unix_test.go @@ -20,7 +20,6 @@ func TestGenerateSpec(t *testing.T) { // check for matching caps defaults := defaltCaps() for _, cl := range [][]string{ - s.Process.Capabilities.Ambient, s.Process.Capabilities.Bounding, s.Process.Capabilities.Permitted, s.Process.Capabilities.Inheritable,