Change user/group creation to use flags to adduser/useradd to enforce it
being a system user. Use system user defaults that auto-create a
matching group. These changes allow us to remove all group creation
code, and in doing so we also removed the code that finds available uid,
gid integers and use post-creation query to gather the system-generated
uid and gid.
The only added complexity is that today distros don't auto-create
subordinate ID ranges for a new ID if it is a system ID, so we now need
to handle finding a free range and then calling the `usermod` tool to
add the ranges for that ID. Note that this requires the distro supports
the `-v` and `-w` flags on `usermod` for subordinate ID range additions.
Docker-DCO-1.1-Signed-off-by: Phil Estes <estesp@linux.vnet.ibm.com> (github: estesp)
| ... | ... |
@@ -155,6 +155,9 @@ func parseSubgid(username string) (ranges, error) {
|
| 155 | 155 |
return parseSubidFile(subgidFileName, username) |
| 156 | 156 |
} |
| 157 | 157 |
|
| 158 |
+// parseSubidFile will read the appropriate file (/etc/subuid or /etc/subgid) |
|
| 159 |
+// and return all found ranges for a specified username. If the special value |
|
| 160 |
+// "ALL" is supplied for username, then all ranges in the file will be returned |
|
| 158 | 161 |
func parseSubidFile(path, username string) (ranges, error) {
|
| 159 | 162 |
var rangeList ranges |
| 160 | 163 |
|
| ... | ... |
@@ -178,8 +181,7 @@ func parseSubidFile(path, username string) (ranges, error) {
|
| 178 | 178 |
if len(parts) != 3 {
|
| 179 | 179 |
return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path)
|
| 180 | 180 |
} |
| 181 |
- if parts[0] == username {
|
|
| 182 |
- // return the first entry for a user; ignores potential for multiple ranges per user |
|
| 181 |
+ if parts[0] == username || username == "ALL" {
|
|
| 183 | 182 |
startid, err := strconv.Atoi(parts[1]) |
| 184 | 183 |
if err != nil {
|
| 185 | 184 |
return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)
|
| ... | ... |
@@ -4,31 +4,31 @@ import ( |
| 4 | 4 |
"fmt" |
| 5 | 5 |
"os/exec" |
| 6 | 6 |
"path/filepath" |
| 7 |
+ "regexp" |
|
| 8 |
+ "sort" |
|
| 9 |
+ "strconv" |
|
| 7 | 10 |
"strings" |
| 8 |
- "syscall" |
|
| 9 | 11 |
) |
| 10 | 12 |
|
| 11 | 13 |
// add a user and/or group to Linux /etc/passwd, /etc/group using standard |
| 12 | 14 |
// Linux distribution commands: |
| 13 |
-// adduser --uid <id> --shell /bin/login --no-create-home --disabled-login --ingroup <groupname> <username> |
|
| 14 |
-// useradd -M -u <id> -s /bin/nologin -N -g <groupname> <username> |
|
| 15 |
-// addgroup --gid <id> <groupname> |
|
| 16 |
-// groupadd -g <id> <groupname> |
|
| 17 |
- |
|
| 18 |
-const baseUID int = 10000 |
|
| 19 |
-const baseGID int = 10000 |
|
| 20 |
-const idMAX int = 65534 |
|
| 15 |
+// adduser --system --shell /bin/false --disabled-login --disabled-password --no-create-home --group <username> |
|
| 16 |
+// useradd -r -s /bin/false <username> |
|
| 21 | 17 |
|
| 22 | 18 |
var ( |
| 23 |
- userCommand string |
|
| 24 |
- groupCommand string |
|
| 19 |
+ userCommand string |
|
| 25 | 20 |
|
| 26 | 21 |
cmdTemplates = map[string]string{
|
| 27 |
- "adduser": "--uid %d --shell /bin/false --no-create-home --disabled-login --ingroup %s %s", |
|
| 28 |
- "useradd": "-M -u %d -s /bin/false -N -g %s %s", |
|
| 29 |
- "addgroup": "--gid %d %s", |
|
| 30 |
- "groupadd": "-g %d %s", |
|
| 22 |
+ "adduser": "--system --shell /bin/false --no-create-home --disabled-login --disabled-password --group %s", |
|
| 23 |
+ "useradd": "-r -s /bin/false %s", |
|
| 24 |
+ "usermod": "-%s %d-%d %s", |
|
| 31 | 25 |
} |
| 26 |
+ |
|
| 27 |
+ idOutRegexp = regexp.MustCompile(`uid=([0-9]+).*gid=([0-9]+)`) |
|
| 28 |
+ // default length for a UID/GID subordinate range |
|
| 29 |
+ defaultRangeLen = 65536 |
|
| 30 |
+ defaultRangeStart = 100000 |
|
| 31 |
+ userMod = "usermod" |
|
| 32 | 32 |
) |
| 33 | 33 |
|
| 34 | 34 |
func init() {
|
| ... | ... |
@@ -38,11 +38,6 @@ func init() {
|
| 38 | 38 |
} else if _, err := resolveBinary("useradd"); err == nil {
|
| 39 | 39 |
userCommand = "useradd" |
| 40 | 40 |
} |
| 41 |
- if _, err := resolveBinary("addgroup"); err == nil {
|
|
| 42 |
- groupCommand = "addgroup" |
|
| 43 |
- } else if _, err := resolveBinary("groupadd"); err == nil {
|
|
| 44 |
- groupCommand = "groupadd" |
|
| 45 |
- } |
|
| 46 | 41 |
} |
| 47 | 42 |
|
| 48 | 43 |
func resolveBinary(binname string) (string, error) {
|
| ... | ... |
@@ -62,94 +57,132 @@ func resolveBinary(binname string) (string, error) {
|
| 62 | 62 |
return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath)
|
| 63 | 63 |
} |
| 64 | 64 |
|
| 65 |
-// AddNamespaceRangesUser takes a name and finds an unused uid, gid pair |
|
| 66 |
-// and calls the appropriate helper function to add the group and then |
|
| 67 |
-// the user to the group in /etc/group and /etc/passwd respectively. |
|
| 68 |
-// This new user's /etc/sub{uid,gid} ranges will be used for user namespace
|
|
| 65 |
+// AddNamespaceRangesUser takes a username and uses the standard system |
|
| 66 |
+// utility to create a system user/group pair used to hold the |
|
| 67 |
+// /etc/sub{uid,gid} ranges which will be used for user namespace
|
|
| 69 | 68 |
// mapping ranges in containers. |
| 70 | 69 |
func AddNamespaceRangesUser(name string) (int, int, error) {
|
| 71 |
- // Find unused uid, gid pair |
|
| 72 |
- uid, err := findUnusedUID(baseUID) |
|
| 70 |
+ if err := addUser(name); err != nil {
|
|
| 71 |
+ return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err)
|
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+ // Query the system for the created uid and gid pair |
|
| 75 |
+ out, err := execCmd("id", name)
|
|
| 73 | 76 |
if err != nil {
|
| 74 |
- return -1, -1, fmt.Errorf("Unable to find unused UID: %v", err)
|
|
| 77 |
+ return -1, -1, fmt.Errorf("Error trying to find uid/gid for new user %q: %v", name, err)
|
|
| 78 |
+ } |
|
| 79 |
+ matches := idOutRegexp.FindStringSubmatch(strings.TrimSpace(string(out))) |
|
| 80 |
+ if len(matches) != 3 {
|
|
| 81 |
+ return -1, -1, fmt.Errorf("Can't find uid, gid from `id` output: %q", string(out))
|
|
| 75 | 82 |
} |
| 76 |
- gid, err := findUnusedGID(baseGID) |
|
| 83 |
+ uid, err := strconv.Atoi(matches[1]) |
|
| 77 | 84 |
if err != nil {
|
| 78 |
- return -1, -1, fmt.Errorf("Unable to find unused GID: %v", err)
|
|
| 85 |
+ return -1, -1, fmt.Errorf("Can't convert found uid (%s) to int: %v", matches[1], err)
|
|
| 79 | 86 |
} |
| 80 |
- |
|
| 81 |
- // First add the group that we will use |
|
| 82 |
- if err := addGroup(name, gid); err != nil {
|
|
| 83 |
- return -1, -1, fmt.Errorf("Error adding group %q: %v", name, err)
|
|
| 87 |
+ gid, err := strconv.Atoi(matches[2]) |
|
| 88 |
+ if err != nil {
|
|
| 89 |
+ return -1, -1, fmt.Errorf("Can't convert found gid (%s) to int: %v", matches[2], err)
|
|
| 84 | 90 |
} |
| 85 |
- // Add the user as a member of the group |
|
| 86 |
- if err := addUser(name, uid, name); err != nil {
|
|
| 87 |
- return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err)
|
|
| 91 |
+ |
|
| 92 |
+ // Now we need to create the subuid/subgid ranges for our new user/group (system users |
|
| 93 |
+ // do not get auto-created ranges in subuid/subgid) |
|
| 94 |
+ |
|
| 95 |
+ if err := createSubordinateRanges(name); err != nil {
|
|
| 96 |
+ return -1, -1, fmt.Errorf("Couldn't create subordinate ID ranges: %v", err)
|
|
| 88 | 97 |
} |
| 89 | 98 |
return uid, gid, nil |
| 90 | 99 |
} |
| 91 | 100 |
|
| 92 |
-func addUser(userName string, uid int, groupName string) error {
|
|
| 101 |
+func addUser(userName string) error {
|
|
| 93 | 102 |
|
| 94 | 103 |
if userCommand == "" {
|
| 95 | 104 |
return fmt.Errorf("Cannot add user; no useradd/adduser binary found")
|
| 96 | 105 |
} |
| 97 |
- args := fmt.Sprintf(cmdTemplates[userCommand], uid, groupName, userName) |
|
| 98 |
- return execAddCmd(userCommand, args) |
|
| 106 |
+ args := fmt.Sprintf(cmdTemplates[userCommand], userName) |
|
| 107 |
+ out, err := execCmd(userCommand, args) |
|
| 108 |
+ if err != nil {
|
|
| 109 |
+ return fmt.Errorf("Failed to add user with error: %v; output: %q", err, string(out))
|
|
| 110 |
+ } |
|
| 111 |
+ return nil |
|
| 99 | 112 |
} |
| 100 | 113 |
|
| 101 |
-func addGroup(groupName string, gid int) error {
|
|
| 114 |
+func createSubordinateRanges(name string) error {
|
|
| 102 | 115 |
|
| 103 |
- if groupCommand == "" {
|
|
| 104 |
- return fmt.Errorf("Cannot add group; no groupadd/addgroup binary found")
|
|
| 116 |
+ // first, we should verify that ranges weren't automatically created |
|
| 117 |
+ // by the distro tooling |
|
| 118 |
+ ranges, err := parseSubuid(name) |
|
| 119 |
+ if err != nil {
|
|
| 120 |
+ return fmt.Errorf("Error while looking for subuid ranges for user %q: %v", name, err)
|
|
| 105 | 121 |
} |
| 106 |
- args := fmt.Sprintf(cmdTemplates[groupCommand], gid, groupName) |
|
| 107 |
- // only error out if the error isn't that the group already exists |
|
| 108 |
- // if the group exists then our needs are already met |
|
| 109 |
- if err := execAddCmd(groupCommand, args); err != nil && !strings.Contains(err.Error(), "already exists") {
|
|
| 110 |
- return err |
|
| 122 |
+ if len(ranges) == 0 {
|
|
| 123 |
+ // no UID ranges; let's create one |
|
| 124 |
+ startID, err := findNextUIDRange() |
|
| 125 |
+ if err != nil {
|
|
| 126 |
+ return fmt.Errorf("Can't find available subuid range: %v", err)
|
|
| 127 |
+ } |
|
| 128 |
+ out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "v", startID, startID+defaultRangeLen-1, name)) |
|
| 129 |
+ if err != nil {
|
|
| 130 |
+ return fmt.Errorf("Unable to add subuid range to user: %q; output: %s, err: %v", name, out, err)
|
|
| 131 |
+ } |
|
| 111 | 132 |
} |
| 112 |
- return nil |
|
| 113 |
-} |
|
| 114 | 133 |
|
| 115 |
-func execAddCmd(cmd, args string) error {
|
|
| 116 |
- execCmd := exec.Command(cmd, strings.Split(args, " ")...) |
|
| 117 |
- out, err := execCmd.CombinedOutput() |
|
| 134 |
+ ranges, err = parseSubgid(name) |
|
| 118 | 135 |
if err != nil {
|
| 119 |
- return fmt.Errorf("Failed to add user/group with error: %v; output: %q", err, string(out))
|
|
| 136 |
+ return fmt.Errorf("Error while looking for subgid ranges for user %q: %v", name, err)
|
|
| 137 |
+ } |
|
| 138 |
+ if len(ranges) == 0 {
|
|
| 139 |
+ // no GID ranges; let's create one |
|
| 140 |
+ startID, err := findNextGIDRange() |
|
| 141 |
+ if err != nil {
|
|
| 142 |
+ return fmt.Errorf("Can't find available subgid range: %v", err)
|
|
| 143 |
+ } |
|
| 144 |
+ out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "w", startID, startID+defaultRangeLen-1, name)) |
|
| 145 |
+ if err != nil {
|
|
| 146 |
+ return fmt.Errorf("Unable to add subgid range to user: %q; output: %s, err: %v", name, out, err)
|
|
| 147 |
+ } |
|
| 120 | 148 |
} |
| 121 | 149 |
return nil |
| 122 | 150 |
} |
| 123 | 151 |
|
| 124 |
-func findUnusedUID(startUID int) (int, error) {
|
|
| 125 |
- return findUnused("passwd", startUID)
|
|
| 152 |
+func findNextUIDRange() (int, error) {
|
|
| 153 |
+ ranges, err := parseSubuid("ALL")
|
|
| 154 |
+ if err != nil {
|
|
| 155 |
+ return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subuid file: %v", err)
|
|
| 156 |
+ } |
|
| 157 |
+ sort.Sort(ranges) |
|
| 158 |
+ return findNextRangeStart(ranges) |
|
| 126 | 159 |
} |
| 127 | 160 |
|
| 128 |
-func findUnusedGID(startGID int) (int, error) {
|
|
| 129 |
- return findUnused("group", startGID)
|
|
| 161 |
+func findNextGIDRange() (int, error) {
|
|
| 162 |
+ ranges, err := parseSubgid("ALL")
|
|
| 163 |
+ if err != nil {
|
|
| 164 |
+ return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subgid file: %v", err)
|
|
| 165 |
+ } |
|
| 166 |
+ sort.Sort(ranges) |
|
| 167 |
+ return findNextRangeStart(ranges) |
|
| 130 | 168 |
} |
| 131 | 169 |
|
| 132 |
-func findUnused(file string, id int) (int, error) {
|
|
| 133 |
- for {
|
|
| 134 |
- cmdStr := fmt.Sprintf("cat /etc/%s | cut -d: -f3 | grep '^%d$'", file, id)
|
|
| 135 |
- cmd := exec.Command("sh", "-c", cmdStr)
|
|
| 136 |
- if err := cmd.Run(); err != nil {
|
|
| 137 |
- // if a non-zero return code occurs, then we know the ID was not found |
|
| 138 |
- // and is usable |
|
| 139 |
- if exiterr, ok := err.(*exec.ExitError); ok {
|
|
| 140 |
- // The program has exited with an exit code != 0 |
|
| 141 |
- if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
|
| 142 |
- if status.ExitStatus() == 1 {
|
|
| 143 |
- //no match, we can use this ID |
|
| 144 |
- return id, nil |
|
| 145 |
- } |
|
| 146 |
- } |
|
| 147 |
- } |
|
| 148 |
- return -1, fmt.Errorf("Error looking in /etc/%s for unused ID: %v", file, err)
|
|
| 149 |
- } |
|
| 150 |
- id++ |
|
| 151 |
- if id > idMAX {
|
|
| 152 |
- return -1, fmt.Errorf("Maximum id in %q reached with finding unused numeric ID", file)
|
|
| 170 |
+func findNextRangeStart(rangeList ranges) (int, error) {
|
|
| 171 |
+ startID := defaultRangeStart |
|
| 172 |
+ for _, arange := range rangeList {
|
|
| 173 |
+ if wouldOverlap(arange, startID) {
|
|
| 174 |
+ startID = arange.Start + arange.Length |
|
| 153 | 175 |
} |
| 154 | 176 |
} |
| 177 |
+ return startID, nil |
|
| 178 |
+} |
|
| 179 |
+ |
|
| 180 |
+func wouldOverlap(arange subIDRange, ID int) bool {
|
|
| 181 |
+ low := ID |
|
| 182 |
+ high := ID + defaultRangeLen |
|
| 183 |
+ if (low >= arange.Start && low <= arange.Start+arange.Length) || |
|
| 184 |
+ (high <= arange.Start+arange.Length && high >= arange.Start) {
|
|
| 185 |
+ return true |
|
| 186 |
+ } |
|
| 187 |
+ return false |
|
| 188 |
+} |
|
| 189 |
+ |
|
| 190 |
+func execCmd(cmd, args string) ([]byte, error) {
|
|
| 191 |
+ execCmd := exec.Command(cmd, strings.Split(args, " ")...) |
|
| 192 |
+ return execCmd.CombinedOutput() |
|
| 155 | 193 |
} |