diff --git a/config.go b/config.go index 1bf14eb..c9c82b6 100644 --- a/config.go +++ b/config.go @@ -21,6 +21,7 @@ type Config struct { AllowRootShell bool `json:"allow_root_shell" desc:"Allow entering a sandbox shell as root"` LogXpra bool `json:"log_xpra" desc:"Log output of Xpra"` EnvironmentVars []string `json:"environment_vars" desc:"Default environment variables passed to sandboxes"` + DefaultGroups []string `json:"default_groups" desc:"List of default group names that can be used inside the sandbox"` } const OzVersion = "0.0.1" @@ -28,20 +29,23 @@ const DefaultConfigPath = "/etc/oz/oz.conf" func NewDefaultConfig() *Config { return &Config{ - ProfileDir: "/var/lib/oz/cells.d", - ShellPath: "/bin/bash", - PrefixPath: "/usr/local", - SandboxPath: "/srv/oz", - NMIgnoreFile: "/etc/NetworkManager/conf.d/oz.conf", - BridgeMACAddr: "6A:A8:2E:56:E8:9C", - DivertSuffix: "unsafe", - UseFullDev: false, - AllowRootShell: false, - LogXpra: false, + ProfileDir: "/var/lib/oz/cells.d", + ShellPath: "/bin/bash", + PrefixPath: "/usr/local", + SandboxPath: "/srv/oz", + NMIgnoreFile: "/etc/NetworkManager/conf.d/oz.conf", + BridgeMACAddr: "6A:A8:2E:56:E8:9C", + DivertSuffix: "unsafe", + UseFullDev: false, + AllowRootShell: false, + LogXpra: false, EnvironmentVars: []string{ "USER", "USERNAME", "LOGNAME", "LANG", "LANGUAGE", "_", }, + DefaultGroups: []string{ + "audio", "video", + }, } } diff --git a/oz-daemon/client.go b/oz-daemon/client.go index c029918..977c6b9 100644 --- a/oz-daemon/client.go +++ b/oz-daemon/client.go @@ -53,20 +53,28 @@ func ListSandboxes() ([]SandboxInfo, error) { return body.Sandboxes, nil } -func Launch(arg, cpath string, args, env []string, noexec bool) error { +func Launch(arg, cpath string, args []string, noexec bool) error { idx, name, err := parseProfileArg(arg) if err != nil { return err } pwd, _ := os.Getwd() - + groups, _ := os.Getgroups() + gg := []uint32{} + if len(groups) > 0 { + gg = make([]uint32, len(groups)) + for i, v := range groups { + gg[i] = uint32(v) + } + } resp, err := clientSend(&LaunchMsg{ Index: idx, Name: name, Path: cpath, Pwd: pwd, + Gids: gg, Args: args, - Env: env, + Env: os.Environ(), Noexec: noexec, }) if err != nil { diff --git a/oz-daemon/daemon.go b/oz-daemon/daemon.go index 19542fd..a5b00b8 100644 --- a/oz-daemon/daemon.go +++ b/oz-daemon/daemon.go @@ -1,11 +1,13 @@ package daemon import ( + "bufio" "encoding/json" "fmt" "os" "os/signal" "path" + "strconv" "strings" "syscall" @@ -16,16 +18,23 @@ import ( "github.com/op/go-logging" ) +type groupEntry struct { + Name string + Gid uint32 + Members []string +} + type daemonState struct { - log *logging.Logger - config *oz.Config - profiles oz.Profiles - sandboxes []*Sandbox - nextSboxId int - nextDisplay int - memBackend *logging.ChannelMemoryBackend - backends []logging.Backend - network *network.HostNetwork + log *logging.Logger + config *oz.Config + profiles oz.Profiles + sandboxes []*Sandbox + nextSboxId int + nextDisplay int + memBackend *logging.ChannelMemoryBackend + backends []logging.Backend + network *network.HostNetwork + systemGroups map[string]groupEntry } func Main() { @@ -66,6 +75,9 @@ func initialize() *daemonState { os.Exit(1) } d.profiles = ps + if err := d.cacheSystemGroups(); err != nil { + d.log.Fatalf("Unable to cache list of system groups: %v", err) + } oz.ReapChildProcs(d.log, d.handleChildExit) d.nextSboxId = 1 d.nextDisplay = 100 @@ -80,9 +92,7 @@ func initialize() *daemonState { } d.network = htn - //network.NetPrint(d.log) - break } } @@ -140,6 +150,39 @@ func (d *daemonState) processSignals(c <-chan os.Signal) { } } +func (d *daemonState) cacheSystemGroups() error { + fg, err := os.Open("/etc/group") + if err != nil { + return err + } + defer fg.Close() + + sg := bufio.NewScanner(fg) + newGroups := make(map[string]groupEntry) + for sg.Scan() { + gd := strings.Split(sg.Text(), ":") + if len(gd) < 4 { + continue + } + gid, err := strconv.ParseUint(gd[2], 10, 32) + if err != nil { + continue + } + newGroups[gd[0]] = groupEntry{ + Name: gd[0], + Gid: uint32(gid), + Members: strings.Split(gd[3], ","), + } + } + + if err := sg.Err(); err != nil { + return err + } + d.systemGroups = newGroups + return nil +} + + func (d *daemonState) handleChildExit(pid int, wstatus syscall.WaitStatus) { d.Debug("Child process pid=%d exited with status %d", pid, wstatus.ExitStatus()) diff --git a/oz-daemon/launch.go b/oz-daemon/launch.go index 742d5ad..7b5c08e 100644 --- a/oz-daemon/launch.go +++ b/oz-daemon/launch.go @@ -2,8 +2,10 @@ package daemon import ( "bufio" + "bytes" "crypto/rand" "encoding/hex" + "encoding/json" "fmt" "io" "os" @@ -52,7 +54,7 @@ func createSocketPath(base string) (string, error) { return path.Join(base, fmt.Sprintf("oz-init-control-%s", hex.EncodeToString(bs))), nil } -func createInitCommand(initPath, name string, socketPath string, env []string, uid uint32, display int, stn *network.SandboxNetwork) *exec.Cmd { +func createInitCommand(initPath string, cloneNet bool) *exec.Cmd { cmd := exec.Command(initPath) cmd.Dir = "/" @@ -61,7 +63,7 @@ func createInitCommand(initPath, name string, socketPath string, env []string, u cloneFlags |= syscall.CLONE_NEWPID cloneFlags |= syscall.CLONE_NEWUTS - if stn.Nettype != network.TYPE_HOST { + if cloneNet { cloneFlags |= syscall.CLONE_NEWNET } @@ -69,25 +71,7 @@ func createInitCommand(initPath, name string, socketPath string, env []string, u //Chroot: chroot, Cloneflags: cloneFlags, } - cmd.Env = []string{ - "INIT_PROFILE=" + name, - "INIT_SOCKET=" + socketPath, - fmt.Sprintf("INIT_UID=%d", uid), - } - - if stn.Ip != "" { - cmd.Env = append(cmd.Env, "INIT_ADDR="+stn.Ip) - cmd.Env = append(cmd.Env, "INIT_VHOST="+stn.VethHost) - cmd.Env = append(cmd.Env, "INIT_VGUEST="+stn.VethGuest) - cmd.Env = append(cmd.Env, "INIT_GATEWAY="+stn.Gateway.String()+"/"+stn.Class) - } - - cmd.Env = append(cmd.Env, fmt.Sprintf("INIT_DISPLAY=%d", display)) - - for _, e := range env { - cmd.Env = append(cmd.Env, ozinit.EnvPrefix+e) - } - + return cmd } @@ -107,8 +91,11 @@ func (d *daemonState) launch(p *oz.Profile, msg *LaunchMsg, uid, gid uint32, log */ u, err := user.LookupId(strconv.FormatUint(uint64(uid), 10)) if err != nil { - log.Error("Failed to look up user with uid=%ld: %v", uid, err) - os.Exit(1) + return nil, fmt.Errorf("Failed to look up user with uid=%ld: %v", uid, err) + } + groups, err := d.sanitizeGroups(p, u.Username, msg.Gids) + if err != nil { + return nil, fmt.Errorf("Unable to sanitize user groups: %v", err) } display := 0 @@ -131,20 +118,40 @@ func (d *daemonState) launch(p *oz.Profile, msg *LaunchMsg, uid, gid uint32, log return nil, fmt.Errorf("Failed to create random socket path: %v", err) } initPath := path.Join(d.config.PrefixPath, "bin", "oz-init") - cmd := createInitCommand(initPath, p.Name, socketPath, msg.Env, uid, display, stn) - log.Debug("Command environment: %+v", cmd.Env) + cmd := createInitCommand(initPath, (stn.Nettype != network.TYPE_HOST)) pp, err := cmd.StderrPipe() if err != nil { //fs.Cleanup() return nil, fmt.Errorf("error creating stderr pipe for init process: %v", err) - } + pi, err := cmd.StdinPipe() + if err != nil { + //fs.Cleanup() + return nil, fmt.Errorf("error creating stdin pipe for init process: %v", err) + } + + jdata, err := json.Marshal(ozinit.InitData{ + Display: display, + User: *u, + Uid: uid, + Gid: gid, + Gids: groups, + Network: *stn, + Profile: *p, + Config: *d.config, + Sockaddr: socketPath, + LaunchEnv: msg.Env, + }) + if err != nil { + return nil, fmt.Errorf("Unable to marshal init state: %+v", err) + } + io.Copy(pi, bytes.NewBuffer(jdata)) + pi.Close() if err := cmd.Start(); err != nil { //fs.Cleanup() return nil, fmt.Errorf("Unable to start process: %+v", err) } - //rootfs := path.Join(d.config.SandboxPath, "rootfs") sbox := &Sandbox{ daemon: d, @@ -152,7 +159,7 @@ func (d *daemonState) launch(p *oz.Profile, msg *LaunchMsg, uid, gid uint32, log display: display, profile: p, init: cmd, - cred: &syscall.Credential{Uid: uid, Gid: gid}, + cred: &syscall.Credential{Uid: uid, Gid: gid, Groups: msg.Gids}, user: u, fs: fs.NewFilesystem(d.config, log), //addr: path.Join(rootfs, ozinit.SocketAddress), @@ -205,6 +212,38 @@ func (d *daemonState) launch(p *oz.Profile, msg *LaunchMsg, uid, gid uint32, log return sbox, nil } +func (d *daemonState) sanitizeGroups(p *oz.Profile, username string, gids []uint32) (map[string]uint32, error) { + allowedGroups := d.config.DefaultGroups + allowedGroups = append(allowedGroups, p.AllowedGroups...) + if len(d.systemGroups) == 0 { + if err := d.cacheSystemGroups(); err != nil { + return nil, err + } + } + groups := map[string]uint32{} + for _, sg := range d.systemGroups { + for _, gg := range allowedGroups { + if sg.Name == gg { + found := false + for _, uname := range sg.Members { + if uname == username { + found = true + break + } + } + if !found { + continue + } + d.log.Debug("Allowing user: %s (%d)", gg, sg.Gid) + groups[sg.Name] = sg.Gid + break + } + } + } + + return groups, nil +} + func (sbox *Sandbox) launchProgram(binpath, cpath, pwd string, args []string, log *logging.Logger) { if sbox.profile.AllowFiles { sbox.whitelistArgumentFiles(binpath, pwd, args, log) diff --git a/oz-daemon/protocol.go b/oz-daemon/protocol.go index c60f505..85e6d45 100644 --- a/oz-daemon/protocol.go +++ b/oz-daemon/protocol.go @@ -39,6 +39,7 @@ type LaunchMsg struct { Path string Name string Pwd string + Gids []uint32 Args []string Env []string Noexec bool diff --git a/oz-init/init.go b/oz-init/init.go index d3b14a2..5aeac63 100644 --- a/oz-init/init.go +++ b/oz-init/init.go @@ -2,10 +2,10 @@ package ozinit import ( "bufio" + "encoding/json" "fmt" "io" "io/ioutil" - "net" "os" "os/exec" "os/signal" @@ -26,8 +26,6 @@ import ( "github.com/op/go-logging" ) -const EnvPrefix = "INIT_ENV_" - type initState struct { log *logging.Logger profile *oz.Profile @@ -36,8 +34,9 @@ type initState struct { launchEnv []string lock sync.Mutex children map[int]*exec.Cmd - uid int - gid int + uid uint32 + gid uint32 + gids map[string]uint32 user *user.User display int fs *fs.Filesystem @@ -47,6 +46,19 @@ type initState struct { network *network.SandboxNetwork } +type InitData struct { + Profile oz.Profile + Config oz.Config + Sockaddr string + LaunchEnv []string + Uid uint32 + Gid uint32 + Gids map[string]uint32 + User user.User + Network network.SandboxNetwork + Display int +} + // By convention oz-init writes log messages to stderr with a single character // prefix indicating the logging level. These messages are read one line at a time // over a pipe by oz-daemon and translated into appropriate log events. @@ -76,108 +88,40 @@ func parseArgs() *initState { os.Exit(1) } - getvar := func(name string) string { - val := os.Getenv(name) - if val == "" { - log.Error("Error: missing required '%s' argument", name) - os.Exit(1) - } - return val - } - pname := getvar("INIT_PROFILE") - sockaddr := getvar("INIT_SOCKET") - uidval := getvar("INIT_UID") - dispval := os.Getenv("INIT_DISPLAY") - - stnip := os.Getenv("INIT_ADDR") - stnvhost := os.Getenv("INIT_VHOST") - stnvguest := os.Getenv("INIT_VGUEST") - stngateway := os.Getenv("INIT_GATEWAY") - - var config *oz.Config - config, err := oz.LoadConfig(oz.DefaultConfigPath) - if err != nil { - if os.IsNotExist(err) { - log.Info("Configuration file (%s) is missing, using defaults.", oz.DefaultConfigPath) - config = oz.NewDefaultConfig() - } else { - log.Error("Could not load configuration: %s", oz.DefaultConfigPath, err) - os.Exit(1) - } - } - - p, err := loadProfile(config.ProfileDir, pname) - if err != nil { - log.Error("Could not load profile %s: %v", pname, err) - os.Exit(1) - } - uid, err := strconv.Atoi(uidval) - if err != nil { - log.Error("Could not parse INIT_UID argument (%s) into an integer: %v", uidval, err) - os.Exit(1) - } - u, err := user.LookupId(uidval) - if err != nil { - log.Error("Failed to look up user with uid=%s: %v", uidval, err) - os.Exit(1) - } - gid, err := strconv.Atoi(u.Gid) - if err != nil { - log.Error("Failed to parse gid value (%s) from user struct: %v", u.Gid, err) + initData := new(InitData) + if err := json.NewDecoder(os.Stdin).Decode(&initData); err != nil { + log.Error("unable to decode init data: %v", err) os.Exit(1) } - display := 0 - if dispval != "" { - d, err := strconv.Atoi(dispval) - if err != nil { - log.Error("Unable to parse display (%s) into an integer: %v", dispval, err) - os.Exit(1) - } - display = d - } - - stn := new(network.SandboxNetwork) - if stnip != "" { - gateway, _, err := net.ParseCIDR(stngateway) - if err != nil { - log.Error("Unable to parse network configuration gateway (%s): %v", stngateway, err) - os.Exit(1) - } + log.Debug("Init state: %+v", initData) - stn.Ip = stnip - stn.VethHost = stnvhost - stn.VethGuest = stnvguest - stn.Gateway = gateway + if (initData.User.Uid != strconv.Itoa(int(initData.Uid))) || (initData.Uid == 0) { + log.Error("invalid uid or user passed to init.") + os.Exit(1) } env := []string{} - for _, e := range os.Environ() { - if strings.HasPrefix(e, EnvPrefix) { - e = e[len(EnvPrefix):] - log.Debug("Adding (%s) to launch environment", e) - env = append(env, e) - } - } - + env = append(env, initData.LaunchEnv...) env = append(env, "PATH=/usr/bin:/bin") - if p.XServer.Enabled { - env = append(env, "DISPLAY=:"+strconv.Itoa(display)) + if initData.Profile.XServer.Enabled { + env = append(env, "DISPLAY=:"+strconv.Itoa(initData.Display)) } return &initState{ log: log, - config: config, - sockaddr: sockaddr, + config: &initData.Config, + sockaddr: initData.Sockaddr, launchEnv: env, - profile: p, + profile: &initData.Profile, children: make(map[int]*exec.Cmd), - uid: uid, - gid: gid, - user: u, - display: display, - fs: fs.NewFilesystem(config, log), - network: stn, + uid: initData.Uid, + gid: initData.Gid, + gids: initData.Gids, + user: &initData.User, + display: initData.Display, + fs: fs.NewFilesystem(&initData.Config, log), + network: &initData.Network, } } @@ -196,7 +140,7 @@ func (st *initState) runInit() { os.Exit(1) } - if err := os.Chown(st.sockaddr, st.uid, st.gid); err != nil { + if err := os.Chown(st.sockaddr, int(st.uid), int(st.gid)); err != nil { st.log.Warning("Failed to chown oz-init control socket: %v", err) } @@ -238,6 +182,7 @@ func (st *initState) runInit() { fsbx := path.Join("/tmp", "oz-sandbox") err = ioutil.WriteFile(fsbx, []byte(st.profile.Name), 0644) + // Signal the daemon we are ready os.Stderr.WriteString("OK\n") go st.processSignals(sigs, s) @@ -267,10 +212,22 @@ func (st *initState) startXpraServer() { xpra.Process.Env = []string{ "HOME=" + st.user.HomeDir, } + + groups := append([]uint32{}, st.gid) + if gid, gexists := st.gids["video"]; gexists { + groups = append(groups, gid) + } + if st.profile.XServer.AudioMode != "" && st.profile.XServer.AudioMode != oz.PROFILE_AUDIO_NONE { + if gid, gexists := st.gids["audio"]; gexists { + groups = append(groups, gid) + } + } + xpra.Process.SysProcAttr = &syscall.SysProcAttr{} xpra.Process.SysProcAttr.Credential = &syscall.Credential{ - Uid: uint32(st.uid), - Gid: uint32(st.gid), + Uid: st.uid, + Gid: st.gid, + Groups: groups, } st.log.Info("Starting xpra server") if err := xpra.Process.Start(); err != nil { @@ -325,10 +282,15 @@ func (st *initState) launchApplication(cpath, pwd string, cmdArgs []string) (*ex st.log.Warning("Failed to create stderr pipe: %v", err) return nil, err } + groups := append([]uint32{}, st.gid) + for _, gid := range st.gids { + groups = append(groups, gid) + } cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr.Credential = &syscall.Credential{ - Uid: uint32(st.uid), - Gid: uint32(st.gid), + Uid: st.uid, + Gid: st.gid, + Groups: groups, } cmd.Env = append(cmd.Env, st.launchEnv...) @@ -338,6 +300,9 @@ func (st *initState) launchApplication(cpath, pwd string, cmdArgs []string) (*ex cmd.Args = append(cmd.Args, cmdArgs...) + if pwd == "" { + pwd = st.user.HomeDir + } if _, err := os.Stat(pwd); err == nil { cmd.Dir = pwd } @@ -398,12 +363,19 @@ func (st *initState) handleRunShell(rs *RunShellMsg, msg *ipc.Message) error { if (msg.Ucred.Uid == 0 || msg.Ucred.Gid == 0) && st.config.AllowRootShell != true { return msg.Respond(&ErrorMsg{"Cannot open shell because allowRootShell is disabled"}) } + groups := append([]uint32{}, st.gid) + if (msg.Ucred.Uid != 0 && msg.Ucred.Gid != 0) { + for _, gid := range st.gids { + groups = append(groups, gid) + } + } st.log.Info("Starting shell with uid = %d, gid = %d", msg.Ucred.Uid, msg.Ucred.Gid) cmd := exec.Command(st.config.ShellPath, "-i") cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr.Credential = &syscall.Credential{ - Uid: msg.Ucred.Uid, - Gid: msg.Ucred.Gid, + Uid: msg.Ucred.Uid, + Gid: msg.Ucred.Gid, + Groups: groups, } cmd.Env = append(cmd.Env, st.launchEnv...) if rs.Term != "" { diff --git a/oz/main.go b/oz/main.go index 0c75ecf..21ed788 100644 --- a/oz/main.go +++ b/oz/main.go @@ -49,7 +49,7 @@ func runSandboxed() { os.Exit(1) } } - if err := daemon.Launch("0", apath, os.Args[1:], os.Environ(), false); err != nil { + if err := daemon.Launch("0", apath, os.Args[1:], false); err != nil { fmt.Fprintf(os.Stderr, "launch command failed: %v.\n", err) os.Exit(1) } @@ -128,7 +128,7 @@ func handleLaunch(c *cli.Context) { fmt.Println("Argument needed to launch command") os.Exit(1) } - err := daemon.Launch(c.Args()[0], "", c.Args()[1:], os.Environ(), noexec) + err := daemon.Launch(c.Args()[0], "", c.Args()[1:], noexec) if err != nil { fmt.Printf("launch command failed: %v\n", err) os.Exit(1) @@ -177,7 +177,7 @@ func handleShell(c *cli.Context) { fmt.Printf("start shell command failed: %v\n", err) os.Exit(1) } - fmt.Println("Entering interactive shell?\n") + fmt.Printf("Entering interactive shell in `%s`\n\n", sb.Profile) st, err := SetRawTerminal(0) HandleResize(fd) f := os.NewFile(uintptr(fd), "") diff --git a/profile.go b/profile.go index 41b82a7..e991591 100644 --- a/profile.go +++ b/profile.go @@ -33,6 +33,7 @@ type Profile struct { NoDefaults bool // Allow bind mounting of files passed as arguments inside the sandbox AllowFiles bool `json:"allow_files"` + AllowedGroups []string `json:"allowed_groups"` // List of paths to bind mount inside jail Whitelist []WhitelistItem // List of paths to blacklist inside jail @@ -118,6 +119,7 @@ func NewDefaultProfile() *Profile { return &Profile{ Multi: false, AllowFiles: false, + AllowedGroups: []string{}, XServer: XServerConf{ Enabled: true, EnableTray: false, diff --git a/profiles/torbrowser-launcher.json b/profiles/torbrowser-launcher.json index c4e3269..65d1550 100644 --- a/profiles/torbrowser-launcher.json +++ b/profiles/torbrowser-launcher.json @@ -1,6 +1,7 @@ { "path": "/usr/bin/torbrowser-launcher" , "watchdog": "start-tor-browser" +, "allowed_groups": ["debian-tor"] , "xserver": { "enabled": true , "enable_tray": true @@ -21,8 +22,12 @@ , "blacklist": [ ] , "environment": [ - {"name":"TOR_SKIP_LAUNCH"} - , {"name":"TOR_SOCKS_HOST"} - , {"name":"TOR_SOCKS_PORT"} + {"name":"TOR_SKIP_LAUNCH"} + , {"name":"TOR_SOCKS_HOST"} + , {"name":"TOR_SOCKS_PORT"} + , {"name":"TOR_CONTROL_PORT"} + , {"name":"TOR_CONTROL_PASSWD"} + , {"name":"TOR_CONTROL_AUTHENTICATE"} + , {"name":"TOR_CONTROL_COOKIE_AUTH_FILE"} ] }