diff --git a/cmd/localstack/awsutil.go b/cmd/localstack/awsutil.go index dac5a02..1007458 100644 --- a/cmd/localstack/awsutil.go +++ b/cmd/localstack/awsutil.go @@ -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 } diff --git a/cmd/localstack/main.go b/cmd/localstack/main.go index a7cc198..f2e0195 100644 --- a/cmd/localstack/main.go +++ b/cmd/localstack/main.go @@ -19,6 +19,7 @@ type LsOpts struct { RuntimeEndpoint string RuntimeId string InitTracingPort string + User string CodeArchives string HotReloadingPaths []string EnableDnsServer string @@ -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", ""), ","), @@ -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{}) @@ -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) diff --git a/cmd/localstack/user.go b/cmd/localstack/user.go new file mode 100644 index 0000000..13c5f5d --- /dev/null +++ b/cmd/localstack/user.go @@ -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 +}