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`.
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
@ -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.
- `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.

View File

@ -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 {

View File

@ -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,

View File

@ -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")
}
}

View File

@ -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"},