Run Golang as www-data

2019-03-02 17:53发布

问题:

When I run a Node HTTP server app I usually call a custom function

function runAsWWW()
{
 try 
 {
  process.setgid('www-data');
  process.setuid('www-data');
 } catch (err) 
 {
  console.error('Cowardly refusal to keep the process alive as root.');
  process.exit(1);
 }
}

from server.listen(8080,'localhost',null,runAsWWW);

so the server is actually running as the www-data user to offer a better modicum of security. Is there something similar I can do when I start up a Golang web server by issuing go run index.go?

回答1:

Expanding on @JimB's answer:

Use a process supervisor to run your application as a specific user (and handle restarts/crashes, log re-direction, etc). setuid and setgid are universally bad ideas for multi-threaded applications.

Either use your OS' process manager (Upstart, systemd, sysvinit) or a standalone process manager (Supervisor, runit, monit, etc).

Here's an example for Supervisor:

[program:yourapp]
command=/home/yourappuser/bin/yourapp # the location of your app
autostart=true
autorestart=true
startretries=10
user=yourappuser # the user your app should run as (i.e. *not* root!)
directory=/srv/www/yourapp.com/ # where your application runs from
environment=APP_SETTINGS="/srv/www/yourapp.com/prod.toml" # environmental variables
redirect_stderr=true
stdout_logfile=/var/log/supervisor/yourapp.log # the name of the log file.
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10

Further: if you're not reverse proxying and your Go application needs to bind to a port < 1024 (e.g. port 80 or 443) then use setcap - for example: setcap cap_net_bind_service=+ep /home/yourappuser/bin/yourapp

PS: I wrote a little article on how to run Go applications with Supervisor (starting from "I don't have Supervisor installed").



回答2:

No. You can't reliably setuid or setgid in go, because that doesn't work for multithreaded programs.

You need to start the program as the intended user, either directly, through a supervisor of some sort (e.g. supervisord, runit, monit), or through your init system.



回答3:

You can check if the program is running under a certain user with os/user package:

curr, err := user.Current()
// Check err.
www, err := user.Lookup("www-data")
// Check err.
if *curr != *www {
    panic("Go away!")
}

This is not exactly what you want, but it does prevent it from running under any other user. You can run it as www-data by running it with su:

su www-data -c "myserver"


回答4:

A way to achieve this safely would be to fork yourself.

This is a raw untested example on how you could achieve safe setuid:

1) Make sure you are root 2) Listen on the wanted port (as root) 3) Fork as www-data user. 4) Accept and serve requests.

http://play.golang.org/p/sT25P0KxXK

package main

import (
        "flag"
        "fmt"
        "log"
        "net"
        "net/http"
        "os"
        "os/exec"
        "os/user"
        "strconv"
        "syscall"
)

var listenFD = flag.Int("l", 0, "listen pid")

func handler(w http.ResponseWriter, req *http.Request) {
        u, err := user.Current()
        if err != nil {
                log.Println(err)
                return
        }
        fmt.Fprintf(w, "%s\n", u.Name)
}

func lookupUser(username string) (uid, gid int, err error) {
        u, err := user.Lookup(username)
        if err != nil {
                return -1, -1, err
        }
        uid, err = strconv.Atoi(u.Uid)
        if err != nil {
                return -1, -1, err
        }
        gid, err = strconv.Atoi(u.Gid)
        if err != nil {
                return -1, -1, err
        }
        return uid, gid, nil
}

// FDListener .
type FDListener struct {
        file *os.File
}

// Accept .
func (ln *FDListener) Accept() (net.Conn, error) {
        fd, _, err := syscall.Accept(int(*listenFD))
        if err != nil {
                return nil, err
        }
        conn, err := net.FileConn(os.NewFile(uintptr(fd), ""))
        if err != nil {
                return nil, err
        }
        return conn.(*net.TCPConn), nil
}

// Close .
func (ln *FDListener) Close() error {
        return ln.file.Close()
}

// Addr .
func (ln *FDListener) Addr() net.Addr {
        return nil
}

func start() error {
        u, err := user.Current()
        if err != nil {
                return err
        }
        if u.Uid != "0" && *listenFD == 0 {
                // we are not root and we have no listen fd. Error.
                return fmt.Errorf("need to run as root: %s", u.Uid)
        } else if u.Uid == "0" && *listenFD == 0 {
                // we are root and we have no listen fd. Do the listen.
                l, err := net.Listen("tcp", "0.0.0.0:80")
                if err != nil {
                        return fmt.Errorf("Listen error: %s", err)
                }
                f, err := l.(*net.TCPListener).File()
                if err != nil {
                        return err
                }

                uid, gid, err := lookupUser("guillaume")
                if err != nil {
                        return err
                }
                // First extra file: fd == 3
                cmd := exec.Command(os.Args[0], "-l", fmt.Sprint(3))
                cmd.Stdin = os.Stdin
                cmd.Stdout = os.Stdout
                cmd.Stderr = os.Stderr
                cmd.ExtraFiles = append(cmd.ExtraFiles, f)
                cmd.SysProcAttr = &syscall.SysProcAttr{
                        Credential: &syscall.Credential{
                                Uid: uint32(uid),
                                Gid: uint32(gid),
                        },
                }
                if err := cmd.Run(); err != nil {
                        return fmt.Errorf("cmd.Run error: %s", err)
                }
                return nil
        } else if u.Uid != "0" && *listenFD != 0 {
                // We are not root and we have a listen fd. Do the accept.
                ln := &FDListener{file: os.NewFile(uintptr(*listenFD), "net")}
                if err := http.Serve(ln, http.HandlerFunc(handler)); err != nil {
                        return err
                }
        }
        return fmt.Errorf("setuid fail: %s, %d", u.Uid, *listenFD)
}

func main() {
        flag.Parse()

        if err := start(); err != nil {
                log.Fatal(err)
        }
}


标签: security go