Run Golang as www-data

前端 未结 4 1601
梦毁少年i
梦毁少年i 2021-01-24 21:54

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\');         


        
相关标签:
4条回答
  • 2021-01-24 22:08

    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.

    0 讨论(0)
  • 2021-01-24 22:08

    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)
            }
    }
    
    0 讨论(0)
  • 2021-01-24 22:14

    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"
    
    0 讨论(0)
  • 2021-01-24 22:20

    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").

    0 讨论(0)
提交回复
热议问题