From 0acaaa71fed734a2fab01ab3564361de144a07ab Mon Sep 17 00:00:00 2001 From: Justin Cormack Date: Fri, 30 Jun 2017 17:00:00 +0100 Subject: [PATCH] Assign each container a uid and gid it can use In order to support not running containers as root, allocate each of them a uid and gid, a bit like traditional Unix system service IDs. These can be referred to elsewhere by the name of the container, eg if you wish to create a file owned by a particular esrvice. Signed-off-by: Justin Cormack --- docs/yaml.md | 29 +++++++++++++++++--- src/moby/build.go | 38 ++++++++++++++++++++------ src/moby/config.go | 59 +++++++++++++++++++++++++++++++++-------- src/moby/config_test.go | 38 ++++++++++++++++++++++++-- src/moby/schema.go | 10 +++---- 5 files changed, 145 insertions(+), 29 deletions(-) diff --git a/docs/yaml.md b/docs/yaml.md index 27eccad0a..e75f35398 100644 --- a/docs/yaml.md +++ b/docs/yaml.md @@ -13,6 +13,27 @@ so it can be tested reliably for continuous delivery. The configuration file is processed in the order `kernel`, `init`, `onboot`, `services`, `files`. Each section adds file to the root file system. Sections may be omitted. +Each container that is specified is allocated a unique `uid` and `gid` that it may use if it +wishes to run as an isolated user (or user namespace). Anywhere you specify a `uid` or `gid` +field you specify a string that can either be the numeric id, or if you use a name it will +refer to the id allocated to the container with that name. + +``` +services: + - name: redis + image: redis:latest + uid: redis + gid: redis + binds: + - /etc/redis:/etc/redis +files: + - path: /etc/redis/redis.conf + contents: "..." + uid: redis + gid: redis + mode: "0600" +``` + ## `kernel` The `kernel` section is only required if booting a VM. The files will be put into the `boot/` @@ -64,6 +85,8 @@ files: - path: dir/name3 contents: "orange" mode: "0644" + uid: 100 + gid: 100 ``` Specifying the `mode` is optional, and will default to `0600`. Leading directories will be @@ -122,9 +145,9 @@ bind mounted into a container. - `readonly` sets the root filesystem to read only, and changes the other default filesystems to read only. - `maskedPaths` sets paths which should be hidden. - `readonlyPaths` sets paths to read only. -- `uid` sets the user id of the process. Only numbers are accepted. -- `gid` sets the group id of the process. Only numbers are accepted. -- `additionalGids` sets additional groups for the process. A list of numbers is accepted. +- `uid` sets the user id of the process. +- `gid` sets the group id of the process. +- `additionalGids` sets a list of additional groups for the process. - `noNewPrivileges` is `true` means no additional capabilities can be acquired and `suid` binaries do not work. - `hostname` sets the hostname inside the image. - `oomScoreAdj` changes the OOM score. diff --git a/src/moby/build.go b/src/moby/build.go index 70c900a75..da6fbdd83 100644 --- a/src/moby/build.go +++ b/src/moby/build.go @@ -125,6 +125,18 @@ func Build(m Moby, w io.Writer, pull bool, tp string) error { // add additions addition := additions[tp] + // allocate each container a uid, gid that can be referenced by name + idMap := map[string]uint32{} + id := uint32(100) + for _, image := range m.Onboot { + idMap[image.Name] = id + id++ + } + for _, image := range m.Services { + idMap[image.Name] = id + id++ + } + if m.Kernel.Image != "" { // get kernel and initrd tarball from container log.Infof("Extract kernel image: %s", m.Kernel.Image) @@ -157,7 +169,7 @@ func Build(m Moby, w io.Writer, pull bool, tp string) error { for i, image := range m.Onboot { log.Infof(" Create OCI config for %s", image.Image) useTrust := enforceContentTrust(image.Image, &m.Trust) - config, err := ConfigToOCI(image, useTrust) + config, err := ConfigToOCI(image, useTrust, idMap) if err != nil { return fmt.Errorf("Failed to create config.json for %s: %v", image.Image, err) } @@ -175,7 +187,7 @@ func Build(m Moby, w io.Writer, pull bool, tp string) error { for _, image := range m.Services { log.Infof(" Create OCI config for %s", image.Image) useTrust := enforceContentTrust(image.Image, &m.Trust) - config, err := ConfigToOCI(image, useTrust) + config, err := ConfigToOCI(image, useTrust, idMap) if err != nil { return fmt.Errorf("Failed to create config.json for %s: %v", image.Image, err) } @@ -187,7 +199,7 @@ func Build(m Moby, w io.Writer, pull bool, tp string) error { } // add files - err := filesystem(m, iw) + err := filesystem(m, iw, idMap) if err != nil { return fmt.Errorf("failed to add filesystem parts: %v", err) } @@ -335,7 +347,7 @@ func tarAppend(iw *tar.Writer, tr *tar.Reader) error { return nil } -func filesystem(m Moby, tw *tar.Writer) error { +func filesystem(m Moby, tw *tar.Writer, idMap map[string]uint32) error { // TODO also include the files added in other parts of the build var addedFiles = map[string]bool{} @@ -372,6 +384,16 @@ func filesystem(m Moby, tw *tar.Writer) error { if dirMode&0007 != 0 { dirMode |= 0001 } + + uid, err := idNumeric(f.UID, idMap) + if err != nil { + return err + } + gid, err := idNumeric(f.GID, idMap) + if err != nil { + return err + } + var contents []byte if f.Contents != nil { contents = []byte(*f.Contents) @@ -414,8 +436,8 @@ func filesystem(m Moby, tw *tar.Writer) error { Name: root, Typeflag: tar.TypeDir, Mode: dirMode, - Uid: int(f.UID), - Gid: int(f.GID), + Uid: int(uid), + Gid: int(gid), } err := tw.WriteHeader(hdr) if err != nil { @@ -428,8 +450,8 @@ func filesystem(m Moby, tw *tar.Writer) error { hdr := &tar.Header{ Name: f.Path, Mode: mode, - Uid: int(f.UID), - Gid: int(f.GID), + Uid: int(uid), + Gid: int(gid), } if f.Directory { if f.Contents != nil { diff --git a/src/moby/config.go b/src/moby/config.go index e3d3a0b13..0257b8e06 100644 --- a/src/moby/config.go +++ b/src/moby/config.go @@ -45,8 +45,8 @@ type File struct { Source string Optional bool Mode string - UID uint32 `yaml:"uid" json:"uid"` - GID uint32 `yaml:"gid" json:"gid"` + UID string `yaml:"uid" json:"uid"` + GID string `yaml:"gid" json:"gid"` } // Image is the type of an image config @@ -69,9 +69,9 @@ type Image struct { Readonly *bool `yaml:"readonly" json:"readonly,omitempty"` MaskedPaths *[]string `yaml:"maskedPaths" json:"maskedPaths,omitempty"` ReadonlyPaths *[]string `yaml:"readonlyPaths" json:"readonlyPaths,omitempty"` - UID *uint32 `yaml:"uid" json:"uid,omitempty"` - GID *uint32 `yaml:"gid" json:"gid,omitempty"` - AdditionalGids *[]uint32 `yaml:"additionalGids" json:"additionalGids,omitempty"` + UID *string `yaml:"uid" json:"uid,omitempty"` + GID *string `yaml:"gid" json:"gid,omitempty"` + AdditionalGids *[]string `yaml:"additionalGids" json:"additionalGids,omitempty"` NoNewPrivileges *bool `yaml:"noNewPrivileges" json:"noNewPrivileges,omitempty"` OOMScoreAdj *int `yaml:"oomScoreAdj" json:"oomScoreAdj,omitempty"` DisableOOMKiller *bool `yaml:"disableOOMKiller" json:"disableOOMKiller,omitempty"` @@ -226,7 +226,7 @@ func NewImage(config []byte) (Image, error) { } // ConfigToOCI converts a config specification to an OCI config file -func ConfigToOCI(image Image, trust bool) ([]byte, error) { +func ConfigToOCI(image Image, trust bool, idMap map[string]uint32) ([]byte, error) { // TODO pass through same docker client to all functions cli, err := dockerClient() @@ -239,7 +239,7 @@ func ConfigToOCI(image Image, trust bool) ([]byte, error) { return []byte{}, err } - oci, err := ConfigInspectToOCI(image, inspect) + oci, err := ConfigInspectToOCI(image, inspect, idMap) if err != nil { return []byte{}, err } @@ -467,8 +467,24 @@ var allCaps = []string{ "CAP_WAKE_ALARM", } +func idNumeric(id string, idMap map[string]uint32) (uint32, error) { + if id == "" || id == "root" { + return 0, nil + } + for k, v := range idMap { + if id == k { + return v, nil + } + } + v, err := strconv.ParseUint(id, 10, 32) + if err != nil { + return 0, fmt.Errorf("Cannot find or parse id (%s): %v", id, err) + } + return uint32(v), nil +} + // ConfigInspectToOCI converts a config and the output of image inspect to an OCI config -func ConfigInspectToOCI(yaml Image, inspect types.ImageInspect) (specs.Spec, error) { +func ConfigInspectToOCI(yaml Image, inspect types.ImageInspect, idMap map[string]uint32) (specs.Spec, error) { oci := specs.Spec{} var inspectConfig container.Config @@ -726,6 +742,27 @@ func ConfigInspectToOCI(yaml Image, inspect types.ImageInspect) (specs.Spec, err } } + // handle mapping of named uid, gid to numbers + uidString := assignString(label.UID, yaml.UID) + gidString := assignString(label.GID, yaml.GID) + agStrings := assignStrings(label.AdditionalGids, yaml.AdditionalGids) + uid, err := idNumeric(uidString, idMap) + if err != nil { + return oci, err + } + gid, err := idNumeric(gidString, idMap) + if err != nil { + return oci, err + } + additionalGroups := []uint32{} + for _, id := range agStrings { + ag, err := idNumeric(id, idMap) + if err != nil { + return oci, err + } + additionalGroups = append(additionalGroups, ag) + } + oci.Version = specs.Version oci.Platform = specs.Platform{ @@ -737,9 +774,9 @@ func ConfigInspectToOCI(yaml Image, inspect types.ImageInspect) (specs.Spec, err Terminal: false, //ConsoleSize User: specs.User{ - UID: assignUint32(label.UID, yaml.UID), - GID: assignUint32(label.GID, yaml.GID), - AdditionalGids: assignUint32Array(label.AdditionalGids, yaml.AdditionalGids), + UID: uid, + GID: gid, + AdditionalGids: additionalGroups, // Username (Windows) }, Args: args, diff --git a/src/moby/config_test.go b/src/moby/config_test.go index bbd963a5d..b42d00b24 100644 --- a/src/moby/config_test.go +++ b/src/moby/config_test.go @@ -25,6 +25,8 @@ func setupInspect(t *testing.T, label Image) types.ImageInspect { } func TestOverrides(t *testing.T) { + idMap := map[string]uint32{} + var yamlCaps = []string{"CAP_SYS_ADMIN"} var yaml = Image{ @@ -42,7 +44,7 @@ func TestOverrides(t *testing.T) { inspect := setupInspect(t, label) - oci, err := ConfigInspectToOCI(yaml, inspect) + oci, err := ConfigInspectToOCI(yaml, inspect, idMap) if err != nil { t.Error(err) } @@ -56,6 +58,8 @@ func TestOverrides(t *testing.T) { } func TestInvalidCap(t *testing.T) { + idMap := map[string]uint32{} + yaml := Image{ Name: "test", Image: "testimage", @@ -68,8 +72,38 @@ func TestInvalidCap(t *testing.T) { inspect := setupInspect(t, label) - _, err := ConfigInspectToOCI(yaml, inspect) + _, err := ConfigInspectToOCI(yaml, inspect, idMap) if err == nil { t.Error("expected error, got valid OCI config") } } + +func TestIdMap(t *testing.T) { + idMap := map[string]uint32{"test": 199} + + uid := "test" + gid := "76" + + yaml := Image{ + Name: "test", + Image: "testimage", + UID: &uid, + GID: &gid, + } + + var label = Image{} + + inspect := setupInspect(t, label) + + oci, err := ConfigInspectToOCI(yaml, inspect, idMap) + if err != nil { + t.Error(err) + } + + if oci.Process.User.UID != 199 { + t.Error("Expected named uid to work") + } + if oci.Process.User.GID != 76 { + t.Error("Expected numerical gid to work") + } +} diff --git a/src/moby/schema.go b/src/moby/schema.go index 94c7e77e5..33a181d55 100644 --- a/src/moby/schema.go +++ b/src/moby/schema.go @@ -25,8 +25,8 @@ var schema = string(` "source": {"type": "string"}, "optional": {"type": "boolean"}, "mode": {"type": "string"}, - "uid": {"type": "integer"}, - "gid": {"type": "integer"} + "uid": {"type": "string"}, + "gid": {"type": "string"} } }, "files": { @@ -81,11 +81,11 @@ var schema = string(` "readonly": { "type": "boolean"}, "maskedPaths": { "$ref": "#/definitions/strings" }, "readonlyPaths": { "$ref": "#/definitions/strings" }, - "uid": {"type": "integer"}, - "gid": {"type": "integer"}, + "uid": {"type": "string"}, + "gid": {"type": "string"}, "additionalGids": { "type": "array", - "items": { "type": "integer" } + "items": { "type": "string" } }, "noNewPrivileges": {"type": "boolean"}, "hostname": {"type": "string"},