Merge pull request #21266 from estesp/dockremap-system-user

Change subordinate range-owning user to be a system user
This commit is contained in:
Alexander Morozov 2016-03-17 11:42:15 -07:00
commit 9527d789e7
2 changed files with 125 additions and 90 deletions

View file

@ -155,6 +155,9 @@ func parseSubgid(username string) (ranges, error) {
return parseSubidFile(subgidFileName, username) return parseSubidFile(subgidFileName, username)
} }
// parseSubidFile will read the appropriate file (/etc/subuid or /etc/subgid)
// and return all found ranges for a specified username. If the special value
// "ALL" is supplied for username, then all ranges in the file will be returned
func parseSubidFile(path, username string) (ranges, error) { func parseSubidFile(path, username string) (ranges, error) {
var rangeList ranges var rangeList ranges
@ -178,8 +181,7 @@ func parseSubidFile(path, username string) (ranges, error) {
if len(parts) != 3 { if len(parts) != 3 {
return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path) return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path)
} }
if parts[0] == username { if parts[0] == username || username == "ALL" {
// return the first entry for a user; ignores potential for multiple ranges per user
startid, err := strconv.Atoi(parts[1]) startid, err := strconv.Atoi(parts[1])
if err != nil { if err != nil {
return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err) return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)

View file

@ -4,31 +4,31 @@ import (
"fmt" "fmt"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp"
"sort"
"strconv"
"strings" "strings"
"syscall"
) )
// add a user and/or group to Linux /etc/passwd, /etc/group using standard // add a user and/or group to Linux /etc/passwd, /etc/group using standard
// Linux distribution commands: // Linux distribution commands:
// adduser --uid <id> --shell /bin/login --no-create-home --disabled-login --ingroup <groupname> <username> // adduser --system --shell /bin/false --disabled-login --disabled-password --no-create-home --group <username>
// useradd -M -u <id> -s /bin/nologin -N -g <groupname> <username> // useradd -r -s /bin/false <username>
// addgroup --gid <id> <groupname>
// groupadd -g <id> <groupname>
const baseUID int = 10000
const baseGID int = 10000
const idMAX int = 65534
var ( var (
userCommand string userCommand string
groupCommand string
cmdTemplates = map[string]string{ cmdTemplates = map[string]string{
"adduser": "--uid %d --shell /bin/false --no-create-home --disabled-login --ingroup %s %s", "adduser": "--system --shell /bin/false --no-create-home --disabled-login --disabled-password --group %s",
"useradd": "-M -u %d -s /bin/false -N -g %s %s", "useradd": "-r -s /bin/false %s",
"addgroup": "--gid %d %s", "usermod": "-%s %d-%d %s",
"groupadd": "-g %d %s",
} }
idOutRegexp = regexp.MustCompile(`uid=([0-9]+).*gid=([0-9]+)`)
// default length for a UID/GID subordinate range
defaultRangeLen = 65536
defaultRangeStart = 100000
userMod = "usermod"
) )
func init() { func init() {
@ -38,11 +38,6 @@ func init() {
} else if _, err := resolveBinary("useradd"); err == nil { } else if _, err := resolveBinary("useradd"); err == nil {
userCommand = "useradd" userCommand = "useradd"
} }
if _, err := resolveBinary("addgroup"); err == nil {
groupCommand = "addgroup"
} else if _, err := resolveBinary("groupadd"); err == nil {
groupCommand = "groupadd"
}
} }
func resolveBinary(binname string) (string, error) { func resolveBinary(binname string) (string, error) {
@ -62,94 +57,132 @@ func resolveBinary(binname string) (string, error) {
return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath) return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath)
} }
// AddNamespaceRangesUser takes a name and finds an unused uid, gid pair // AddNamespaceRangesUser takes a username and uses the standard system
// and calls the appropriate helper function to add the group and then // utility to create a system user/group pair used to hold the
// the user to the group in /etc/group and /etc/passwd respectively. // /etc/sub{uid,gid} ranges which will be used for user namespace
// This new user's /etc/sub{uid,gid} ranges will be used for user namespace
// mapping ranges in containers. // mapping ranges in containers.
func AddNamespaceRangesUser(name string) (int, int, error) { func AddNamespaceRangesUser(name string) (int, int, error) {
// Find unused uid, gid pair if err := addUser(name); err != nil {
uid, err := findUnusedUID(baseUID) return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err)
if err != nil {
return -1, -1, fmt.Errorf("Unable to find unused UID: %v", err)
}
gid, err := findUnusedGID(baseGID)
if err != nil {
return -1, -1, fmt.Errorf("Unable to find unused GID: %v", err)
} }
// First add the group that we will use // Query the system for the created uid and gid pair
if err := addGroup(name, gid); err != nil { out, err := execCmd("id", name)
return -1, -1, fmt.Errorf("Error adding group %q: %v", name, err) if err != nil {
return -1, -1, fmt.Errorf("Error trying to find uid/gid for new user %q: %v", name, err)
} }
// Add the user as a member of the group matches := idOutRegexp.FindStringSubmatch(strings.TrimSpace(string(out)))
if err := addUser(name, uid, name); err != nil { if len(matches) != 3 {
return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err) return -1, -1, fmt.Errorf("Can't find uid, gid from `id` output: %q", string(out))
}
uid, err := strconv.Atoi(matches[1])
if err != nil {
return -1, -1, fmt.Errorf("Can't convert found uid (%s) to int: %v", matches[1], err)
}
gid, err := strconv.Atoi(matches[2])
if err != nil {
return -1, -1, fmt.Errorf("Can't convert found gid (%s) to int: %v", matches[2], err)
}
// Now we need to create the subuid/subgid ranges for our new user/group (system users
// do not get auto-created ranges in subuid/subgid)
if err := createSubordinateRanges(name); err != nil {
return -1, -1, fmt.Errorf("Couldn't create subordinate ID ranges: %v", err)
} }
return uid, gid, nil return uid, gid, nil
} }
func addUser(userName string, uid int, groupName string) error { func addUser(userName string) error {
if userCommand == "" { if userCommand == "" {
return fmt.Errorf("Cannot add user; no useradd/adduser binary found") return fmt.Errorf("Cannot add user; no useradd/adduser binary found")
} }
args := fmt.Sprintf(cmdTemplates[userCommand], uid, groupName, userName) args := fmt.Sprintf(cmdTemplates[userCommand], userName)
return execAddCmd(userCommand, args) out, err := execCmd(userCommand, args)
}
func addGroup(groupName string, gid int) error {
if groupCommand == "" {
return fmt.Errorf("Cannot add group; no groupadd/addgroup binary found")
}
args := fmt.Sprintf(cmdTemplates[groupCommand], gid, groupName)
// only error out if the error isn't that the group already exists
// if the group exists then our needs are already met
if err := execAddCmd(groupCommand, args); err != nil && !strings.Contains(err.Error(), "already exists") {
return err
}
return nil
}
func execAddCmd(cmd, args string) error {
execCmd := exec.Command(cmd, strings.Split(args, " ")...)
out, err := execCmd.CombinedOutput()
if err != nil { if err != nil {
return fmt.Errorf("Failed to add user/group with error: %v; output: %q", err, string(out)) return fmt.Errorf("Failed to add user with error: %v; output: %q", err, string(out))
} }
return nil return nil
} }
func findUnusedUID(startUID int) (int, error) { func createSubordinateRanges(name string) error {
return findUnused("passwd", startUID)
// first, we should verify that ranges weren't automatically created
// by the distro tooling
ranges, err := parseSubuid(name)
if err != nil {
return fmt.Errorf("Error while looking for subuid ranges for user %q: %v", name, err)
}
if len(ranges) == 0 {
// no UID ranges; let's create one
startID, err := findNextUIDRange()
if err != nil {
return fmt.Errorf("Can't find available subuid range: %v", err)
}
out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "v", startID, startID+defaultRangeLen-1, name))
if err != nil {
return fmt.Errorf("Unable to add subuid range to user: %q; output: %s, err: %v", name, out, err)
}
} }
func findUnusedGID(startGID int) (int, error) { ranges, err = parseSubgid(name)
return findUnused("group", startGID) if err != nil {
return fmt.Errorf("Error while looking for subgid ranges for user %q: %v", name, err)
}
if len(ranges) == 0 {
// no GID ranges; let's create one
startID, err := findNextGIDRange()
if err != nil {
return fmt.Errorf("Can't find available subgid range: %v", err)
}
out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "w", startID, startID+defaultRangeLen-1, name))
if err != nil {
return fmt.Errorf("Unable to add subgid range to user: %q; output: %s, err: %v", name, out, err)
}
}
return nil
} }
func findUnused(file string, id int) (int, error) { func findNextUIDRange() (int, error) {
for { ranges, err := parseSubuid("ALL")
cmdStr := fmt.Sprintf("cat /etc/%s | cut -d: -f3 | grep '^%d$'", file, id) if err != nil {
cmd := exec.Command("sh", "-c", cmdStr) return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subuid file: %v", err)
if err := cmd.Run(); err != nil { }
// if a non-zero return code occurs, then we know the ID was not found sort.Sort(ranges)
// and is usable return findNextRangeStart(ranges)
if exiterr, ok := err.(*exec.ExitError); ok { }
// The program has exited with an exit code != 0
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { func findNextGIDRange() (int, error) {
if status.ExitStatus() == 1 { ranges, err := parseSubgid("ALL")
//no match, we can use this ID if err != nil {
return id, nil return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subgid file: %v", err)
}
sort.Sort(ranges)
return findNextRangeStart(ranges)
}
func findNextRangeStart(rangeList ranges) (int, error) {
startID := defaultRangeStart
for _, arange := range rangeList {
if wouldOverlap(arange, startID) {
startID = arange.Start + arange.Length
} }
} }
return startID, nil
} }
return -1, fmt.Errorf("Error looking in /etc/%s for unused ID: %v", file, err)
} func wouldOverlap(arange subIDRange, ID int) bool {
id++ low := ID
if id > idMAX { high := ID + defaultRangeLen
return -1, fmt.Errorf("Maximum id in %q reached with finding unused numeric ID", file) if (low >= arange.Start && low <= arange.Start+arange.Length) ||
(high <= arange.Start+arange.Length && high >= arange.Start) {
return true
} }
return false
} }
func execCmd(cmd, args string) ([]byte, error) {
execCmd := exec.Command(cmd, strings.Split(args, " ")...)
return execCmd.CombinedOutput()
} }