mirror of
				https://github.com/linuxkit/linuxkit.git
				synced 2025-10-31 11:13:22 +00:00 
			
		
		
		
	Merge pull request #98 from justincormack/named-uids
Assign each container a uid and gid it can use
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`. | 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. | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -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"}, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user