Skip to content

Commit 6fbdf2e

Browse files
authored
Switch user and drop privileges (#13)
1 parent 5a4c741 commit 6fbdf2e

File tree

3 files changed

+163
-3
lines changed

3 files changed

+163
-3
lines changed

cmd/localstack/awsutil.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ type Sandbox interface {
121121
Invoke(responseWriter http.ResponseWriter, invoke *interop.Invoke) error
122122
}
123123

124+
// GetenvWithDefault returns the value of the environment variable key or the defaultValue if key is not set
124125
func GetenvWithDefault(key string, defaultValue string) string {
125-
envValue := os.Getenv(key)
126-
127-
if envValue == "" {
126+
envValue, ok := os.LookupEnv(key)
127+
if !ok {
128128
return defaultValue
129129
}
130130

cmd/localstack/main.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type LsOpts struct {
1919
RuntimeEndpoint string
2020
RuntimeId string
2121
InitTracingPort string
22+
User string
2223
CodeArchives string
2324
HotReloadingPaths []string
2425
EnableDnsServer string
@@ -40,6 +41,7 @@ func InitLsOpts() *LsOpts {
4041
// optional with default
4142
InteropPort: GetenvWithDefault("LOCALSTACK_INTEROP_PORT", "9563"),
4243
InitTracingPort: GetenvWithDefault("LOCALSTACK_RUNTIME_TRACING_PORT", "9564"),
44+
User: GetenvWithDefault("LOCALSTACK_USER", "sbx_user1051"),
4345
// optional or empty
4446
CodeArchives: os.Getenv("LOCALSTACK_CODE_ARCHIVES"),
4547
HotReloadingPaths: strings.Split(GetenvWithDefault("LOCALSTACK_HOT_RELOADING_PATHS", ""), ","),
@@ -48,11 +50,36 @@ func InitLsOpts() *LsOpts {
4850
}
4951
}
5052

53+
// UnsetLsEnvs unsets environment variables specific to LocalStack to achieve better runtime parity with AWS
54+
func UnsetLsEnvs() {
55+
unsetList := [...]string{
56+
// LocalStack internal
57+
"LOCALSTACK_RUNTIME_ENDPOINT",
58+
"LOCALSTACK_RUNTIME_ID",
59+
"LOCALSTACK_INTEROP_PORT",
60+
"LOCALSTACK_RUNTIME_TRACING_PORT",
61+
"LOCALSTACK_USER",
62+
"LOCALSTACK_CODE_ARCHIVES",
63+
"LOCALSTACK_HOT_RELOADING_PATHS",
64+
"LOCALSTACK_ENABLE_DNS_SERVER",
65+
// Docker container ID
66+
"HOSTNAME",
67+
// User
68+
"HOME",
69+
}
70+
for _, envKey := range unsetList {
71+
if err := os.Unsetenv(envKey); err != nil {
72+
log.Warnln("Could not unset environment variable:", envKey, err)
73+
}
74+
}
75+
}
76+
5177
func main() {
5278
// we're setting this to the same value as in the official RIE
5379
debug.SetGCPercent(33)
5480

5581
lsOpts := InitLsOpts()
82+
UnsetLsEnvs()
5683

5784
// set up logging (logrus)
5885
//log.SetFormatter(&log.JSONFormatter{})
@@ -67,6 +94,20 @@ func main() {
6794
// enable dns server
6895
dnsServerContext, stopDnsServer := context.WithCancel(context.Background())
6996
go RunDNSRewriter(lsOpts, dnsServerContext)
97+
98+
// Switch to non-root user and drop root privileges
99+
if IsRootUser() && lsOpts.User != "" {
100+
uid := 993
101+
gid := 990
102+
AddUser(lsOpts.User, uid, gid)
103+
if err := os.Chown("/tmp", uid, gid); err != nil {
104+
log.Warnln("Could not change owner of /tmp:", err)
105+
}
106+
UserLogger().Debugln("Process running as root user.")
107+
DropPrivileges(lsOpts.User)
108+
UserLogger().Debugln("Process running as non-root user.")
109+
}
110+
70111
// parse CLI args
71112
opts, args := getCLIArgs()
72113
bootstrap, handler := getBootstrap(args, opts)

cmd/localstack/user.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// User utilities to create UNIX users and drop root privileges
2+
package main
3+
4+
import (
5+
"fmt"
6+
log "github.com/sirupsen/logrus"
7+
"os"
8+
"os/user"
9+
"strconv"
10+
"strings"
11+
"syscall"
12+
)
13+
14+
// AddUser adds a UNIX user (e.g., sbx_user1051) to the passwd and shadow files if not already present
15+
// The actual default values are based on inspecting the AWS Lambda runtime in us-east-1
16+
// /etc/group is empty and /etc/gshadow is not accessible in AWS
17+
// The home directory does not exist in AWS Lambda
18+
func AddUser(user string, uid int, gid int) {
19+
// passwd file format: https://www.cyberciti.biz/faq/understanding-etcpasswd-file-format/
20+
passwdFile := "/etc/passwd"
21+
passwdEntry := fmt.Sprintf("%[1]s:x:%[2]v:%[3]v::/home/%[1]s:/sbin/nologin", user, uid, gid)
22+
if !doesFileContainEntry(passwdFile, passwdEntry) {
23+
addEntry(passwdFile, passwdEntry)
24+
}
25+
// shadow file format: https://www.cyberciti.biz/faq/understanding-etcshadow-file/
26+
shadowFile := "/etc/shadow"
27+
shadowEntry := fmt.Sprintf("%s:*:18313:0:99999:7:::", user)
28+
if !doesFileContainEntry(shadowFile, shadowEntry) {
29+
addEntry(shadowFile, shadowEntry)
30+
}
31+
}
32+
33+
// doesFileContainEntry returns true if the entry string exists in the given file
34+
func doesFileContainEntry(file string, entry string) bool {
35+
data, err := os.ReadFile(file)
36+
if err != nil {
37+
log.Warnln("Could not read file:", file, err)
38+
return false
39+
}
40+
text := string(data)
41+
return strings.Contains(text, entry)
42+
}
43+
44+
// addEntry appends an entry string to the given file
45+
func addEntry(file string, entry string) error {
46+
f, err := os.OpenFile(file,
47+
os.O_APPEND|os.O_WRONLY, 0644)
48+
if err != nil {
49+
log.Errorln("Error opening file:", file, err)
50+
return err
51+
}
52+
defer f.Close()
53+
if _, err := f.WriteString(entry); err != nil {
54+
log.Errorln("Error appending entry to file:", file, err)
55+
return err
56+
}
57+
return nil
58+
}
59+
60+
// IsRootUser returns true if the current process is root and false otherwise.
61+
func IsRootUser() bool {
62+
return os.Getuid() == 0
63+
}
64+
65+
// UserLogger returns a context logger with user fields.
66+
func UserLogger() *log.Entry {
67+
// Skip user lookup at debug level
68+
if !log.IsLevelEnabled(log.DebugLevel) {
69+
return log.WithFields(log.Fields{})
70+
}
71+
uid := os.Getuid()
72+
uidString := strconv.Itoa(uid)
73+
user, err := user.LookupId(uidString)
74+
if err != nil {
75+
log.Warnln("Could not look up user by uid:", uid, err)
76+
}
77+
return log.WithFields(log.Fields{
78+
"username": user.Username,
79+
"uid": uid,
80+
"euid": os.Geteuid(),
81+
"gid": os.Getgid(),
82+
})
83+
}
84+
85+
// DropPrivileges switches to another UNIX user by dropping root privileges
86+
// Initially based on https://stackoverflow.com/a/75545491/6875981
87+
func DropPrivileges(userToSwitchTo string) error {
88+
// Lookup user and group IDs for the user we want to switch to.
89+
userInfo, err := user.Lookup(userToSwitchTo)
90+
if err != nil {
91+
log.Errorln("Error looking up user:", userToSwitchTo, err)
92+
return err
93+
}
94+
// Convert group ID and user ID from string to int.
95+
gid, err := strconv.Atoi(userInfo.Gid)
96+
if err != nil {
97+
log.Errorln("Error converting gid:", userInfo.Gid, err)
98+
return err
99+
}
100+
uid, err := strconv.Atoi(userInfo.Uid)
101+
if err != nil {
102+
log.Errorln("Error converting uid:", userInfo.Uid, err)
103+
return err
104+
}
105+
106+
// Limitation: Debugger gets stuck when stepping over these syscalls!
107+
// No breakpoints beyond this point are hit.
108+
// Set group ID (real and effective).
109+
if err = syscall.Setgid(gid); err != nil {
110+
log.Errorln("Failed to set group ID:", err)
111+
return err
112+
}
113+
// Set user ID (real and effective).
114+
if err = syscall.Setuid(uid); err != nil {
115+
log.Errorln("Failed to set user ID:", err)
116+
return err
117+
}
118+
return nil
119+
}

0 commit comments

Comments
 (0)