From 5ffad4a9febfa09da8dd5bb9753f1bff0a5a1ba9 Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Mon, 6 Mar 2023 21:34:49 +0100 Subject: [PATCH 1/8] Switch to sbx user and drop root privileges --- cmd/localstack/main.go | 9 ++++ cmd/localstack/user.go | 111 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 cmd/localstack/user.go diff --git a/cmd/localstack/main.go b/cmd/localstack/main.go index a7cc198..0307867 100644 --- a/cmd/localstack/main.go +++ b/cmd/localstack/main.go @@ -60,6 +60,15 @@ func main() { log.SetLevel(log.DebugLevel) log.SetReportCaller(true) + // Switch to sbx user and drop root privileges + if IsRootUser() { + UserLogger().Debugln("Drop privileges and switch user.") + user := "sbx_user1051" + AddUser(user) + DropPrivileges(user) + UserLogger().Debugln("Process running as sbx user.") + } + // download code archive if env variable is set if err := DownloadCodeArchives(lsOpts.CodeArchives); err != nil { log.Fatal("Failed to download code archives") diff --git a/cmd/localstack/user.go b/cmd/localstack/user.go new file mode 100644 index 0000000..b5db4a8 --- /dev/null +++ b/cmd/localstack/user.go @@ -0,0 +1,111 @@ +// 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) { + // passwd file format: https://www.cyberciti.biz/faq/understanding-etcpasswd-file-format/ + passwdFile := "/etc/passwd" + passwdEntry := fmt.Sprintf("%[1]s:x:993:990::/home/%[1]s:/sbin/nologin", user) + 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 of the entry string is contained in the given file +func doesFileContainEntry(file string, entry string) bool { + data, err := os.ReadFile(file) + if err != nil { + log.Errorln("Error reading file:", file, err) + } + text := string(data) + return strings.Contains(text, entry) +} + +// addEntry appends an entry string to the given file +func addEntry(file string, entry string) { + f, err := os.OpenFile(file, + os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + log.Errorln("Error opening file:", file, err) + } + defer f.Close() + if _, err := f.WriteString(entry); err != nil { + log.Errorln("Error appending entry to file:", file, err) + } +} + +// 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.Errorln("Error looking up user for 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) { + // 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) + } + // 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) + } + uid, err := strconv.Atoi(userInfo.Uid) + if err != nil { + log.Errorln("Error converting uid:", userInfo.Uid, err) + } + + // Limitation: Debugger gets stuck when stepping over these syscalls! + // No breakpoints beyond this point are hit. + // Set group ID (real and effective). + err = syscall.Setgid(gid) + if err != nil { + log.Errorln("Failed to set group ID:", err) + } + // Set user ID (real and effective). + err = syscall.Setuid(uid) + if err != nil { + log.Errorln("Failed to set user ID:", err) + } +} From d3fef42b3c809b3ad69851ce8d37a54c6d346380 Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Tue, 7 Mar 2023 19:12:12 +0100 Subject: [PATCH 2/8] Distinguish between empty and unset environment variables Make `GetenvWithDefault` behave like `os.environ.get` in Python --- cmd/localstack/awsutil.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 } From d6b539c35cdf5a1539f885b8f540824b61db255a Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Tue, 7 Mar 2023 19:14:35 +0100 Subject: [PATCH 3/8] Make user configurable --- cmd/localstack/main.go | 17 ++++++++++------- cmd/localstack/user.go | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/cmd/localstack/main.go b/cmd/localstack/main.go index 0307867..425acc4 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", ""), ","), @@ -60,13 +62,14 @@ func main() { log.SetLevel(log.DebugLevel) log.SetReportCaller(true) - // Switch to sbx user and drop root privileges - if IsRootUser() { - UserLogger().Debugln("Drop privileges and switch user.") - user := "sbx_user1051" - AddUser(user) - DropPrivileges(user) - UserLogger().Debugln("Process running as sbx user.") + // Switch to non-root user and drop root privileges + if IsRootUser() && lsOpts.User != "" { + uid := 993 + gid := 990 + AddUser(lsOpts.User, uid, gid) + UserLogger().Debugln("Process running as root user.") + DropPrivileges(lsOpts.User) + UserLogger().Debugln("Process running as non-root user.") } // download code archive if env variable is set diff --git a/cmd/localstack/user.go b/cmd/localstack/user.go index b5db4a8..601bf9c 100644 --- a/cmd/localstack/user.go +++ b/cmd/localstack/user.go @@ -15,10 +15,10 @@ import ( // 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) { +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:993:990::/home/%[1]s:/sbin/nologin", user) + 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) } From bc79b784b1fd2026dab4307d519bb3469cf24a1b Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Tue, 7 Mar 2023 19:16:01 +0100 Subject: [PATCH 4/8] Change owner of tmp directory --- cmd/localstack/main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/localstack/main.go b/cmd/localstack/main.go index 425acc4..0c671ae 100644 --- a/cmd/localstack/main.go +++ b/cmd/localstack/main.go @@ -67,6 +67,10 @@ func main() { uid := 993 gid := 990 AddUser(lsOpts.User, uid, gid) + err := os.Chown("/tmp", uid, gid) + if err != nil { + log.Errorln("Error changing owner of /tmp:", err) + } UserLogger().Debugln("Process running as root user.") DropPrivileges(lsOpts.User) UserLogger().Debugln("Process running as non-root user.") From 990988164da6d1b2f1b8e5f030d83bc58e2cf288 Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Wed, 8 Mar 2023 09:46:13 +0100 Subject: [PATCH 5/8] Apply review feedback --- cmd/localstack/main.go | 3 +-- cmd/localstack/user.go | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/cmd/localstack/main.go b/cmd/localstack/main.go index 0c671ae..34591f1 100644 --- a/cmd/localstack/main.go +++ b/cmd/localstack/main.go @@ -67,8 +67,7 @@ func main() { uid := 993 gid := 990 AddUser(lsOpts.User, uid, gid) - err := os.Chown("/tmp", uid, gid) - if err != nil { + if err := os.Chown("/tmp", uid, gid); err != nil { log.Errorln("Error changing owner of /tmp:", err) } UserLogger().Debugln("Process running as root user.") diff --git a/cmd/localstack/user.go b/cmd/localstack/user.go index 601bf9c..9be1f66 100644 --- a/cmd/localstack/user.go +++ b/cmd/localstack/user.go @@ -30,7 +30,7 @@ func AddUser(user string, uid int, gid int) { } } -// doesFileContainEntry returns true of the entry string is contained in the given file +// 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 { @@ -41,16 +41,19 @@ func doesFileContainEntry(file string, entry string) bool { } // addEntry appends an entry string to the given file -func addEntry(file string, entry string) { +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. @@ -80,32 +83,36 @@ func UserLogger() *log.Entry { // DropPrivileges switches to another UNIX user by dropping root privileges // Initially based on https://stackoverflow.com/a/75545491/6875981 -func DropPrivileges(userToSwitchTo string) { +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). - err = syscall.Setgid(gid) - if err != nil { + if err = syscall.Setgid(gid); err != nil { log.Errorln("Failed to set group ID:", err) + return err } // Set user ID (real and effective). - err = syscall.Setuid(uid) - if err != nil { + if err = syscall.Setuid(uid); err != nil { log.Errorln("Failed to set user ID:", err) + return err } + return nil } From 95959e21bded8ca0158103f769ea238d7ef6bdc5 Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Wed, 8 Mar 2023 11:16:31 +0100 Subject: [PATCH 6/8] Drop privileges after enabling DNS server As @dfangl pointed out: Binding port 53 might require root permissions for binding port < 1024 depending on the Docker version https://github.com/moby/moby/pull/41030 --- cmd/localstack/main.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cmd/localstack/main.go b/cmd/localstack/main.go index 34591f1..224ea61 100644 --- a/cmd/localstack/main.go +++ b/cmd/localstack/main.go @@ -62,6 +62,14 @@ func main() { log.SetLevel(log.DebugLevel) log.SetReportCaller(true) + // download code archive if env variable is set + if err := DownloadCodeArchives(lsOpts.CodeArchives); err != nil { + log.Fatal("Failed to download code archives") + } + // 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 @@ -75,13 +83,6 @@ func main() { UserLogger().Debugln("Process running as non-root user.") } - // download code archive if env variable is set - if err := DownloadCodeArchives(lsOpts.CodeArchives); err != nil { - log.Fatal("Failed to download code archives") - } - // enable dns server - dnsServerContext, stopDnsServer := context.WithCancel(context.Background()) - go RunDNSRewriter(lsOpts, dnsServerContext) // parse CLI args opts, args := getCLIArgs() bootstrap, handler := getBootstrap(args, opts) From 0548cf7e67f4f48d33b89de3afc9f62a384500a8 Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Wed, 8 Mar 2023 14:01:38 +0100 Subject: [PATCH 7/8] Unset LS-internal env variables --- cmd/localstack/main.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/cmd/localstack/main.go b/cmd/localstack/main.go index 224ea61..f1e808b 100644 --- a/cmd/localstack/main.go +++ b/cmd/localstack/main.go @@ -50,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{}) From d59ae7ee9b599820486e547208310c736ecb4554 Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Wed, 8 Mar 2023 14:02:18 +0100 Subject: [PATCH 8/8] Adjust log level Using warning level for recoverable warnings rather than breaking errors. --- cmd/localstack/main.go | 2 +- cmd/localstack/user.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/localstack/main.go b/cmd/localstack/main.go index f1e808b..f2e0195 100644 --- a/cmd/localstack/main.go +++ b/cmd/localstack/main.go @@ -101,7 +101,7 @@ func main() { gid := 990 AddUser(lsOpts.User, uid, gid) if err := os.Chown("/tmp", uid, gid); err != nil { - log.Errorln("Error changing owner of /tmp:", err) + log.Warnln("Could not change owner of /tmp:", err) } UserLogger().Debugln("Process running as root user.") DropPrivileges(lsOpts.User) diff --git a/cmd/localstack/user.go b/cmd/localstack/user.go index 9be1f66..13c5f5d 100644 --- a/cmd/localstack/user.go +++ b/cmd/localstack/user.go @@ -34,7 +34,8 @@ func AddUser(user string, uid int, gid int) { func doesFileContainEntry(file string, entry string) bool { data, err := os.ReadFile(file) if err != nil { - log.Errorln("Error reading file:", file, err) + log.Warnln("Could not read file:", file, err) + return false } text := string(data) return strings.Contains(text, entry) @@ -71,7 +72,7 @@ func UserLogger() *log.Entry { uidString := strconv.Itoa(uid) user, err := user.LookupId(uidString) if err != nil { - log.Errorln("Error looking up user for uid:", uid, err) + log.Warnln("Could not look up user by uid:", uid, err) } return log.WithFields(log.Fields{ "username": user.Username,