mirror of
				https://github.com/linuxkit/linuxkit.git
				synced 2025-11-04 16:04:46 +00:00 
			
		
		
		
	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 <justin.cormack@docker.com>
This commit is contained in:
		
							
								
								
									
										29
									
								
								docs/yaml.md
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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"},
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user