Skip to content

Commit

Permalink
Switch user and drop privileges (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
joe4dev authored Mar 8, 2023
1 parent 5a4c741 commit 6fbdf2e
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 3 deletions.
6 changes: 3 additions & 3 deletions cmd/localstack/awsutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ type Sandbox interface {
Invoke(responseWriter http.ResponseWriter, invoke *interop.Invoke) error
}

// GetenvWithDefault returns the value of the environment variable key or the defaultValue if key is not set
func GetenvWithDefault(key string, defaultValue string) string {
envValue := os.Getenv(key)

if envValue == "" {
envValue, ok := os.LookupEnv(key)
if !ok {
return defaultValue
}

Expand Down
41 changes: 41 additions & 0 deletions cmd/localstack/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type LsOpts struct {
RuntimeEndpoint string
RuntimeId string
InitTracingPort string
User string
CodeArchives string
HotReloadingPaths []string
EnableDnsServer string
Expand All @@ -40,6 +41,7 @@ func InitLsOpts() *LsOpts {
// optional with default
InteropPort: GetenvWithDefault("LOCALSTACK_INTEROP_PORT", "9563"),
InitTracingPort: GetenvWithDefault("LOCALSTACK_RUNTIME_TRACING_PORT", "9564"),
User: GetenvWithDefault("LOCALSTACK_USER", "sbx_user1051"),
// optional or empty
CodeArchives: os.Getenv("LOCALSTACK_CODE_ARCHIVES"),
HotReloadingPaths: strings.Split(GetenvWithDefault("LOCALSTACK_HOT_RELOADING_PATHS", ""), ","),
Expand All @@ -48,11 +50,36 @@ func InitLsOpts() *LsOpts {
}
}

// UnsetLsEnvs unsets environment variables specific to LocalStack to achieve better runtime parity with AWS
func UnsetLsEnvs() {
unsetList := [...]string{
// LocalStack internal
"LOCALSTACK_RUNTIME_ENDPOINT",
"LOCALSTACK_RUNTIME_ID",
"LOCALSTACK_INTEROP_PORT",
"LOCALSTACK_RUNTIME_TRACING_PORT",
"LOCALSTACK_USER",
"LOCALSTACK_CODE_ARCHIVES",
"LOCALSTACK_HOT_RELOADING_PATHS",
"LOCALSTACK_ENABLE_DNS_SERVER",
// Docker container ID
"HOSTNAME",
// User
"HOME",
}
for _, envKey := range unsetList {
if err := os.Unsetenv(envKey); err != nil {
log.Warnln("Could not unset environment variable:", envKey, err)
}
}
}

func main() {
// we're setting this to the same value as in the official RIE
debug.SetGCPercent(33)

lsOpts := InitLsOpts()
UnsetLsEnvs()

// set up logging (logrus)
//log.SetFormatter(&log.JSONFormatter{})
Expand All @@ -67,6 +94,20 @@ func main() {
// enable dns server
dnsServerContext, stopDnsServer := context.WithCancel(context.Background())
go RunDNSRewriter(lsOpts, dnsServerContext)

// Switch to non-root user and drop root privileges
if IsRootUser() && lsOpts.User != "" {
uid := 993
gid := 990
AddUser(lsOpts.User, uid, gid)
if err := os.Chown("/tmp", uid, gid); err != nil {
log.Warnln("Could not change owner of /tmp:", err)
}
UserLogger().Debugln("Process running as root user.")
DropPrivileges(lsOpts.User)
UserLogger().Debugln("Process running as non-root user.")
}

// parse CLI args
opts, args := getCLIArgs()
bootstrap, handler := getBootstrap(args, opts)
Expand Down
119 changes: 119 additions & 0 deletions cmd/localstack/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// User utilities to create UNIX users and drop root privileges
package main

import (
"fmt"
log "github.com/sirupsen/logrus"
"os"
"os/user"
"strconv"
"strings"
"syscall"
)

// AddUser adds a UNIX user (e.g., sbx_user1051) to the passwd and shadow files if not already present
// The actual default values are based on inspecting the AWS Lambda runtime in us-east-1
// /etc/group is empty and /etc/gshadow is not accessible in AWS
// The home directory does not exist in AWS Lambda
func AddUser(user string, uid int, gid int) {
// passwd file format: https://www.cyberciti.biz/faq/understanding-etcpasswd-file-format/
passwdFile := "/etc/passwd"
passwdEntry := fmt.Sprintf("%[1]s:x:%[2]v:%[3]v::/home/%[1]s:/sbin/nologin", user, uid, gid)
if !doesFileContainEntry(passwdFile, passwdEntry) {
addEntry(passwdFile, passwdEntry)
}
// shadow file format: https://www.cyberciti.biz/faq/understanding-etcshadow-file/
shadowFile := "/etc/shadow"
shadowEntry := fmt.Sprintf("%s:*:18313:0:99999:7:::", user)
if !doesFileContainEntry(shadowFile, shadowEntry) {
addEntry(shadowFile, shadowEntry)
}
}

// doesFileContainEntry returns true if the entry string exists in the given file
func doesFileContainEntry(file string, entry string) bool {
data, err := os.ReadFile(file)
if err != nil {
log.Warnln("Could not read file:", file, err)
return false
}
text := string(data)
return strings.Contains(text, entry)
}

// addEntry appends an entry string to the given file
func addEntry(file string, entry string) error {
f, err := os.OpenFile(file,
os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
log.Errorln("Error opening file:", file, err)
return err
}
defer f.Close()
if _, err := f.WriteString(entry); err != nil {
log.Errorln("Error appending entry to file:", file, err)
return err
}
return nil
}

// IsRootUser returns true if the current process is root and false otherwise.
func IsRootUser() bool {
return os.Getuid() == 0
}

// UserLogger returns a context logger with user fields.
func UserLogger() *log.Entry {
// Skip user lookup at debug level
if !log.IsLevelEnabled(log.DebugLevel) {
return log.WithFields(log.Fields{})
}
uid := os.Getuid()
uidString := strconv.Itoa(uid)
user, err := user.LookupId(uidString)
if err != nil {
log.Warnln("Could not look up user by uid:", uid, err)
}
return log.WithFields(log.Fields{
"username": user.Username,
"uid": uid,
"euid": os.Geteuid(),
"gid": os.Getgid(),
})
}

// DropPrivileges switches to another UNIX user by dropping root privileges
// Initially based on https://stackoverflow.com/a/75545491/6875981
func DropPrivileges(userToSwitchTo string) error {
// Lookup user and group IDs for the user we want to switch to.
userInfo, err := user.Lookup(userToSwitchTo)
if err != nil {
log.Errorln("Error looking up user:", userToSwitchTo, err)
return err
}
// Convert group ID and user ID from string to int.
gid, err := strconv.Atoi(userInfo.Gid)
if err != nil {
log.Errorln("Error converting gid:", userInfo.Gid, err)
return err
}
uid, err := strconv.Atoi(userInfo.Uid)
if err != nil {
log.Errorln("Error converting uid:", userInfo.Uid, err)
return err
}

// Limitation: Debugger gets stuck when stepping over these syscalls!
// No breakpoints beyond this point are hit.
// Set group ID (real and effective).
if err = syscall.Setgid(gid); err != nil {
log.Errorln("Failed to set group ID:", err)
return err
}
// Set user ID (real and effective).
if err = syscall.Setuid(uid); err != nil {
log.Errorln("Failed to set user ID:", err)
return err
}
return nil
}

0 comments on commit 6fbdf2e

Please sign in to comment.