Merge pull request #98 from justincormack/named-uids

Assign each container a uid and gid it can use
This commit is contained in:
Justin Cormack 2017-06-30 19:40:47 +01:00 committed by GitHub
commit c7c4c9ef2a
5 changed files with 145 additions and 29 deletions

View File

@ -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`. 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 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` ## `kernel`
The `kernel` section is only required if booting a VM. The files will be put into the `boot/` 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 - path: dir/name3
contents: "orange" contents: "orange"
mode: "0644" mode: "0644"
uid: 100
gid: 100
``` ```
Specifying the `mode` is optional, and will default to `0600`. Leading directories will be Specifying the `mode` is optional, and will default to `0600`. Leading directories will be
@ -123,9 +146,9 @@ bind mounted into a container.
- `readonly` sets the root filesystem to read only, and changes the other default filesystems to read only. - `readonly` sets the root filesystem to read only, and changes the other default filesystems to read only.
- `maskedPaths` sets paths which should be hidden. - `maskedPaths` sets paths which should be hidden.
- `readonlyPaths` sets paths to read only. - `readonlyPaths` sets paths to read only.
- `uid` sets the user id of the process. Only numbers are accepted. - `uid` sets the user id of the process.
- `gid` sets the group id of the process. Only numbers are accepted. - `gid` sets the group id of the process.
- `additionalGids` sets additional groups for the process. A list of numbers is accepted. - `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. - `noNewPrivileges` is `true` means no additional capabilities can be acquired and `suid` binaries do not work.
- `hostname` sets the hostname inside the image. - `hostname` sets the hostname inside the image.
- `oomScoreAdj` changes the OOM score. - `oomScoreAdj` changes the OOM score.

View File

@ -125,6 +125,18 @@ func Build(m Moby, w io.Writer, pull bool, tp string) error {
// add additions // add additions
addition := additions[tp] 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 != "" { if m.Kernel.Image != "" {
// get kernel and initrd tarball from container // get kernel and initrd tarball from container
log.Infof("Extract kernel image: %s", m.Kernel.Image) 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 { for i, image := range m.Onboot {
log.Infof(" Create OCI config for %s", image.Image) log.Infof(" Create OCI config for %s", image.Image)
useTrust := enforceContentTrust(image.Image, &m.Trust) useTrust := enforceContentTrust(image.Image, &m.Trust)
config, err := ConfigToOCI(image, useTrust) config, err := ConfigToOCI(image, useTrust, idMap)
if err != nil { if err != nil {
return fmt.Errorf("Failed to create config.json for %s: %v", image.Image, err) 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 { for _, image := range m.Services {
log.Infof(" Create OCI config for %s", image.Image) log.Infof(" Create OCI config for %s", image.Image)
useTrust := enforceContentTrust(image.Image, &m.Trust) useTrust := enforceContentTrust(image.Image, &m.Trust)
config, err := ConfigToOCI(image, useTrust) config, err := ConfigToOCI(image, useTrust, idMap)
if err != nil { if err != nil {
return fmt.Errorf("Failed to create config.json for %s: %v", image.Image, err) 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 // add files
err := filesystem(m, iw) err := filesystem(m, iw, idMap)
if err != nil { if err != nil {
return fmt.Errorf("failed to add filesystem parts: %v", err) 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 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 // TODO also include the files added in other parts of the build
var addedFiles = map[string]bool{} var addedFiles = map[string]bool{}
@ -372,6 +384,16 @@ func filesystem(m Moby, tw *tar.Writer) error {
if dirMode&0007 != 0 { if dirMode&0007 != 0 {
dirMode |= 0001 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 var contents []byte
if f.Contents != nil { if f.Contents != nil {
contents = []byte(*f.Contents) contents = []byte(*f.Contents)
@ -414,8 +436,8 @@ func filesystem(m Moby, tw *tar.Writer) error {
Name: root, Name: root,
Typeflag: tar.TypeDir, Typeflag: tar.TypeDir,
Mode: dirMode, Mode: dirMode,
Uid: int(f.UID), Uid: int(uid),
Gid: int(f.GID), Gid: int(gid),
} }
err := tw.WriteHeader(hdr) err := tw.WriteHeader(hdr)
if err != nil { if err != nil {
@ -428,8 +450,8 @@ func filesystem(m Moby, tw *tar.Writer) error {
hdr := &tar.Header{ hdr := &tar.Header{
Name: f.Path, Name: f.Path,
Mode: mode, Mode: mode,
Uid: int(f.UID), Uid: int(uid),
Gid: int(f.GID), Gid: int(gid),
} }
if f.Directory { if f.Directory {
if f.Contents != nil { if f.Contents != nil {

View File

@ -45,8 +45,8 @@ type File struct {
Source string Source string
Optional bool Optional bool
Mode string Mode string
UID uint32 `yaml:"uid" json:"uid"` UID string `yaml:"uid" json:"uid"`
GID uint32 `yaml:"gid" json:"gid"` GID string `yaml:"gid" json:"gid"`
} }
// Image is the type of an image config // Image is the type of an image config
@ -69,9 +69,9 @@ type Image struct {
Readonly *bool `yaml:"readonly" json:"readonly,omitempty"` Readonly *bool `yaml:"readonly" json:"readonly,omitempty"`
MaskedPaths *[]string `yaml:"maskedPaths" json:"maskedPaths,omitempty"` MaskedPaths *[]string `yaml:"maskedPaths" json:"maskedPaths,omitempty"`
ReadonlyPaths *[]string `yaml:"readonlyPaths" json:"readonlyPaths,omitempty"` ReadonlyPaths *[]string `yaml:"readonlyPaths" json:"readonlyPaths,omitempty"`
UID *uint32 `yaml:"uid" json:"uid,omitempty"` UID *string `yaml:"uid" json:"uid,omitempty"`
GID *uint32 `yaml:"gid" json:"gid,omitempty"` GID *string `yaml:"gid" json:"gid,omitempty"`
AdditionalGids *[]uint32 `yaml:"additionalGids" json:"additionalGids,omitempty"` AdditionalGids *[]string `yaml:"additionalGids" json:"additionalGids,omitempty"`
NoNewPrivileges *bool `yaml:"noNewPrivileges" json:"noNewPrivileges,omitempty"` NoNewPrivileges *bool `yaml:"noNewPrivileges" json:"noNewPrivileges,omitempty"`
OOMScoreAdj *int `yaml:"oomScoreAdj" json:"oomScoreAdj,omitempty"` OOMScoreAdj *int `yaml:"oomScoreAdj" json:"oomScoreAdj,omitempty"`
DisableOOMKiller *bool `yaml:"disableOOMKiller" json:"disableOOMKiller,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 // 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 // TODO pass through same docker client to all functions
cli, err := dockerClient() cli, err := dockerClient()
@ -239,7 +239,7 @@ func ConfigToOCI(image Image, trust bool) ([]byte, error) {
return []byte{}, err return []byte{}, err
} }
oci, err := ConfigInspectToOCI(image, inspect) oci, err := ConfigInspectToOCI(image, inspect, idMap)
if err != nil { if err != nil {
return []byte{}, err return []byte{}, err
} }
@ -467,8 +467,24 @@ var allCaps = []string{
"CAP_WAKE_ALARM", "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 // 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{} oci := specs.Spec{}
var inspectConfig container.Config 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.Version = specs.Version
oci.Platform = specs.Platform{ oci.Platform = specs.Platform{
@ -737,9 +774,9 @@ func ConfigInspectToOCI(yaml Image, inspect types.ImageInspect) (specs.Spec, err
Terminal: false, Terminal: false,
//ConsoleSize //ConsoleSize
User: specs.User{ User: specs.User{
UID: assignUint32(label.UID, yaml.UID), UID: uid,
GID: assignUint32(label.GID, yaml.GID), GID: gid,
AdditionalGids: assignUint32Array(label.AdditionalGids, yaml.AdditionalGids), AdditionalGids: additionalGroups,
// Username (Windows) // Username (Windows)
}, },
Args: args, Args: args,

View File

@ -25,6 +25,8 @@ func setupInspect(t *testing.T, label Image) types.ImageInspect {
} }
func TestOverrides(t *testing.T) { func TestOverrides(t *testing.T) {
idMap := map[string]uint32{}
var yamlCaps = []string{"CAP_SYS_ADMIN"} var yamlCaps = []string{"CAP_SYS_ADMIN"}
var yaml = Image{ var yaml = Image{
@ -42,7 +44,7 @@ func TestOverrides(t *testing.T) {
inspect := setupInspect(t, label) inspect := setupInspect(t, label)
oci, err := ConfigInspectToOCI(yaml, inspect) oci, err := ConfigInspectToOCI(yaml, inspect, idMap)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -56,6 +58,8 @@ func TestOverrides(t *testing.T) {
} }
func TestInvalidCap(t *testing.T) { func TestInvalidCap(t *testing.T) {
idMap := map[string]uint32{}
yaml := Image{ yaml := Image{
Name: "test", Name: "test",
Image: "testimage", Image: "testimage",
@ -68,8 +72,38 @@ func TestInvalidCap(t *testing.T) {
inspect := setupInspect(t, label) inspect := setupInspect(t, label)
_, err := ConfigInspectToOCI(yaml, inspect) _, err := ConfigInspectToOCI(yaml, inspect, idMap)
if err == nil { if err == nil {
t.Error("expected error, got valid OCI config") 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")
}
}

View File

@ -25,8 +25,8 @@ var schema = string(`
"source": {"type": "string"}, "source": {"type": "string"},
"optional": {"type": "boolean"}, "optional": {"type": "boolean"},
"mode": {"type": "string"}, "mode": {"type": "string"},
"uid": {"type": "integer"}, "uid": {"type": "string"},
"gid": {"type": "integer"} "gid": {"type": "string"}
} }
}, },
"files": { "files": {
@ -81,11 +81,11 @@ var schema = string(`
"readonly": { "type": "boolean"}, "readonly": { "type": "boolean"},
"maskedPaths": { "$ref": "#/definitions/strings" }, "maskedPaths": { "$ref": "#/definitions/strings" },
"readonlyPaths": { "$ref": "#/definitions/strings" }, "readonlyPaths": { "$ref": "#/definitions/strings" },
"uid": {"type": "integer"}, "uid": {"type": "string"},
"gid": {"type": "integer"}, "gid": {"type": "string"},
"additionalGids": { "additionalGids": {
"type": "array", "type": "array",
"items": { "type": "integer" } "items": { "type": "string" }
}, },
"noNewPrivileges": {"type": "boolean"}, "noNewPrivileges": {"type": "boolean"},
"hostname": {"type": "string"}, "hostname": {"type": "string"},