Skip to content

Commit

Permalink
recompute groups on first-run (#37)
Browse files Browse the repository at this point in the history
* recompute groups on first-run

Signed-off-by: Caleb Lloyd <[email protected]>

* set supplemental groups every run

Signed-off-by: Caleb Lloyd <[email protected]>

* compute groups based off runtimeUID

Signed-off-by: Caleb Lloyd <[email protected]>

---------

Signed-off-by: Caleb Lloyd <[email protected]>
  • Loading branch information
caleblloyd authored Aug 17, 2023
1 parent 2da266a commit 2952c72
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 45 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ jobs:
steps:

- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3

- name: Setup Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4

- name: Print Go Version
run: go version
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/fixuid
/fixuid-*.tar.gz

/.idea
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ paths:

## Run in Startup Script instead of Entrypoint

You can run `fixuid` as part of your container's startup script. `fixuid` will `export HOME=/path/to/home` if $HOME is the default value of `/`, so be sure to evaluate the output of `fixuid` when running as a script.
You can run `fixuid` as part of your container's startup script. `fixuid` will `export HOME=/path/to/home` if $HOME is the default value of `/`, so be sure to evaluate the output of `fixuid` when running as a script. Supplementary groups will not be set in this mode.

```
#!/bin/sh
Expand Down
15 changes: 13 additions & 2 deletions docker/fs-stage/usr/local/bin/fixuid-test.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#!/bin/sh

expected_user=$1
expected_group=$2
expected_user="$1"
expected_group="$2"
expected_groups="$3"
if [ -z "$expected_groups" ]; then
expected_groups="$expected_group"
fi

if [ ! -f /var/run/fixuid.ran ]
then
Expand All @@ -27,6 +31,13 @@ then
rc=1
fi

groups=$(groups)
if [ "$groups" != "$expected_groups" ]
then
>&2 echo "expected groups: [$expected_groups], actual groups: [$groups]"
rc=1
fi

OLD_IFS="$IFS"
IFS="|"
files="/tmp/test-dir|/tmp/test-dir/test-file|/tmp/test-file|/home/docker|/home/docker/aaa|/home/docker/zzz|/tmp/space dir|/tmp/space dir/space file|/tmp/space file"
Expand Down
151 changes: 133 additions & 18 deletions fixuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"errors"
"flag"
"fmt"
"io/ioutil"
"golang.org/x/exp/slices"
"log"
"os"
"os/exec"
Expand Down Expand Up @@ -42,7 +42,7 @@ func main() {
// only run once on the system
if _, err := os.Stat(ranFile); !os.IsNotExist(err) {
logInfo("already ran on this system; will not attempt to change UID/GID")
exitOrExec(runtimeUIDInt, runtimeGIDInt, argsWithoutProg)
exitOrExec(runtimeUID, runtimeUIDInt, runtimeGIDInt, -1, argsWithoutProg)
}

// check that script is running as root
Expand Down Expand Up @@ -78,6 +78,7 @@ func main() {
if containerUser == "" {
logger.Fatalln("cannot find key 'user' in configuration file " + filePath)
}

containerUID, containerUIDError := findUID(containerUser)
if containerUIDError != nil {
logger.Fatalln(containerUIDError)
Expand Down Expand Up @@ -146,7 +147,7 @@ func main() {
}
}

// deicide if need to change GIDs
// decide if need to change GIDs
existingGroup, existingGroupError := findGroup(runtimeGID)
if existingGroupError != nil {
logger.Fatalln(existingGroupError)
Expand Down Expand Up @@ -186,7 +187,7 @@ func main() {
// search entire filesystem and chown containerUID:containerGID to runtimeUID:runtimeGID
if needChown {

// proccess /proc/mounts
// process /proc/mounts
mounts, err := parseProcMounts()
if err != nil {
logger.Fatalln(err)
Expand Down Expand Up @@ -254,7 +255,7 @@ func main() {
}

// mark the script as ran
if err := ioutil.WriteFile(ranFile, []byte{}, 0644); err != nil {
if err := os.WriteFile(ranFile, []byte{}, 0644); err != nil {
logger.Fatalln(err)
}

Expand All @@ -271,8 +272,15 @@ func main() {
}
}

oldGIDInt := -1
if oldGID != "" && oldGID != newGID {
if gid, err := strconv.Atoi(oldGID); err != nil {
oldGIDInt = gid
}
}

// all done
exitOrExec(runtimeUIDInt, runtimeGIDInt, argsWithoutProg)
exitOrExec(runtimeUID, runtimeUIDInt, runtimeGIDInt, oldGIDInt, argsWithoutProg)
}

func logInfo(v ...interface{}) {
Expand All @@ -281,20 +289,73 @@ func logInfo(v ...interface{}) {
}
}

func exitOrExec(runtimeUIDInt int, runtimeGIDInt int, argsWithoutProg []string) {
// oldGIDInt should be -1 if the GID was not changed
func exitOrExec(runtimeUID string, runtimeUIDInt, runtimeGIDInt, oldGIDInt int, argsWithoutProg []string) {
if len(argsWithoutProg) > 0 {
// exec mode - de-escalate privileges and exec new process
binary, err := exec.LookPath(argsWithoutProg[0])
if err != nil {
logger.Fatalln(err)
}

// de-escalate the user back to the original
if err := syscall.Setreuid(runtimeUIDInt, runtimeUIDInt); err != nil {
// get real user
user, err := findUser(runtimeUID)
if err != nil {
logger.Fatalln(err)
}

// set groups
if user != "" {
// get all existing group IDs
existingGIDs, err := syscall.Getgroups()
if err != nil {
logger.Fatalln(err)
}

// get primary GID from /etc/passwd
primaryGID, err := findPrimaryGID(runtimeUID)
if err != nil {
logger.Fatalln(err)
}

// get supplementary GIDs from /etc/group
supplementaryGIDs, err := findUserSupplementaryGIDs(user)
if err != nil {
logger.Fatalln(err)
}

// add all GIDs to a map
allGIDs := append(existingGIDs, primaryGID)
allGIDs = append(allGIDs, supplementaryGIDs...)
gidMap := make(map[int]struct{})
for _, gid := range allGIDs {
gidMap[gid] = struct{}{}
}

// remove the old GID if it was changed
if oldGIDInt >= 0 {
delete(gidMap, oldGIDInt)
}

groups := make([]int, 0, len(gidMap))
for gid := range gidMap {
groups = append(groups, gid)
}

// set groups
err = syscall.Setgroups(groups)
if err != nil {
logger.Fatalln(err)
}
}

// de-escalate the group back to the original
if err := syscall.Setregid(runtimeGIDInt, runtimeGIDInt); err != nil {
if err := syscall.Setegid(runtimeGIDInt); err != nil {
logger.Fatalln(err)
}

// de-escalate the user back to the original
if err := syscall.Seteuid(runtimeUIDInt); err != nil {
logger.Fatalln(err)
}

Expand All @@ -309,7 +370,7 @@ func exitOrExec(runtimeUIDInt int, runtimeGIDInt int, argsWithoutProg []string)
os.Exit(0)
}

func searchColonDelimetedFile(filePath string, search string, searchOffset int, returnOffset int) (string, error) {
func searchColonDelimitedFile(filePath string, search string, searchOffset int, returnOffset int) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
Expand All @@ -319,6 +380,9 @@ func searchColonDelimetedFile(filePath string, search string, searchOffset int,
scanner := bufio.NewScanner(file)
for scanner.Scan() {
cols := strings.Split(scanner.Text(), ":")
if len(cols) < (searchOffset+1) || len(cols) < (returnOffset+1) {
continue
}
if cols[searchOffset] == search {
return cols[returnOffset], nil
}
Expand All @@ -327,23 +391,68 @@ func searchColonDelimetedFile(filePath string, search string, searchOffset int,
}

func findUID(user string) (string, error) {
return searchColonDelimetedFile("/etc/passwd", user, 0, 2)
return searchColonDelimitedFile("/etc/passwd", user, 0, 2)
}

func findUser(uid string) (string, error) {
return searchColonDelimetedFile("/etc/passwd", uid, 2, 0)
return searchColonDelimitedFile("/etc/passwd", uid, 2, 0)
}

// returns -1 if not found
func findPrimaryGID(uid string) (int, error) {
gid, err := searchColonDelimitedFile("/etc/passwd", uid, 2, 3)
if err != nil {
return -1, err
}
if gid == "" {
return -1, nil
}
return strconv.Atoi(gid)
}

func findHomeDir(uid string) (string, error) {
return searchColonDelimetedFile("/etc/passwd", uid, 2, 5)
return searchColonDelimitedFile("/etc/passwd", uid, 2, 5)
}

func findGID(group string) (string, error) {
return searchColonDelimetedFile("/etc/group", group, 0, 2)
return searchColonDelimitedFile("/etc/group", group, 0, 2)
}

func findGroup(gid string) (string, error) {
return searchColonDelimetedFile("/etc/group", gid, 2, 0)
return searchColonDelimitedFile("/etc/group", gid, 2, 0)
}

func findUserSupplementaryGIDs(user string) ([]int, error) {
// group:pass:gid:users
file, err := os.Open("/etc/group")
if err != nil {
return nil, err
}

var gids []int
scanner := bufio.NewScanner(file)
for scanner.Scan() {
cols := strings.Split(scanner.Text(), ":")
if len(cols) < 4 {
continue
}
users := strings.Split(cols[3], ",")
if !slices.Contains(users, user) {
continue
}
gid, err := strconv.Atoi(cols[2])
if err != nil {
continue
}
gids = append(gids, gid)
}
file.Close()

if err := scanner.Err(); err != nil {
return nil, err
}

return gids, nil
}

func updateEtcPasswd(user string, oldUID string, newUID string, oldGID string, newGID string) error {
Expand All @@ -357,6 +466,9 @@ func updateEtcPasswd(user string, oldUID string, newUID string, oldGID string, n
scanner := bufio.NewScanner(file)
for scanner.Scan() {
cols := strings.Split(scanner.Text(), ":")
if len(cols) < 4 {
continue
}
if oldUID != "" && newUID != "" && cols[0] == user && cols[2] == oldUID {
cols[2] = newUID
}
Expand All @@ -371,7 +483,7 @@ func updateEtcPasswd(user string, oldUID string, newUID string, oldGID string, n
return err
}

if err := ioutil.WriteFile("/etc/passwd", []byte(newLines), 0644); err != nil {
if err := os.WriteFile("/etc/passwd", []byte(newLines), 0644); err != nil {
return err
}

Expand All @@ -389,6 +501,9 @@ func updateEtcGroup(group string, oldGID string, newGID string) error {
scanner := bufio.NewScanner(file)
for scanner.Scan() {
cols := strings.Split(scanner.Text(), ":")
if len(cols) < 3 {
continue
}
if oldGID != "" && newGID != "" && cols[0] == group && cols[2] == oldGID {
cols[2] = newGID
}
Expand All @@ -400,7 +515,7 @@ func updateEtcGroup(group string, oldGID string, newGID string) error {
return err
}

if err := ioutil.WriteFile("/etc/group", []byte(newLines), 0644); err != nil {
if err := os.WriteFile("/etc/group", []byte(newLines), 0644); err != nil {
return err
}

Expand Down
10 changes: 7 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
module github.com/boxboat/fixuid

go 1.15
go 1.20

require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/go-ozzo/ozzo-config v0.0.0-20160627170238-0ff174cf5aa6
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb
)

require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/hnakamur/jsonpreprocess v0.0.0-20171017030034-a4e954386171 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/go-ozzo/ozzo-config v0.0.0-20160627170238-0ff174cf5aa6 h1:T2JpXPk0mDD6uTT6vAwmd6pmaPqiHsBvP9Ggjr3UpE4=
github.com/go-ozzo/ozzo-config v0.0.0-20160627170238-0ff174cf5aa6/go.mod h1:2RI3/USV7S8KzKNwmZtofbkg/BsCIAmeqJ5sJBWQ6T4=
github.com/hnakamur/jsonpreprocess v0.0.0-20171017030034-a4e954386171 h1:G9nrYr376hLdDulCFOSmRiEa6X5vV6E/ANh+lQWmN4I=
github.com/hnakamur/jsonpreprocess v0.0.0-20171017030034-a4e954386171/go.mod h1:ZSbf3Rg8HEW2bz6oeZBK8FbwS+g/s/KSrpZOx7CQSmw=
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA=
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
Loading

0 comments on commit 2952c72

Please sign in to comment.