Skip to content

Switch user and drop privileges #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}