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