英文:
Run Golang as www-data
问题
当我运行一个Node HTTP服务器应用时,我通常会调用一个自定义函数:
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);
}
}
从 server.listen(8080,'localhost',null,runAsWWW);
所以服务器实际上是以 www-data
用户身份运行,以提供更好的安全性。当我通过 go run index.go
启动一个Golang Web服务器时,是否有类似的操作?
英文:
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
得分: 7
不。在Go语言中,无法可靠地使用setuid或setgid,因为这对于多线程程序是不起作用的。
您需要以预期的用户身份启动程序,可以直接启动,也可以通过某种监控程序(例如supervisord、runit、monit)或通过您的初始化系统启动。
英文:
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.
答案2
得分: 4
扩展@JimB的答案:
使用进程监管器以特定用户身份运行应用程序(并处理重启/崩溃、日志重定向等)。对于多线程应用程序,setuid
和setgid
是普遍不好的想法。
可以使用操作系统的进程管理器(Upstart、systemd、sysvinit)或独立的进程管理器(Supervisor、runit、monit等)。
以下是Supervisor的示例:
[program:yourapp]
command=/home/yourappuser/bin/yourapp # 应用程序的位置
autostart=true
autorestart=true
startretries=10
user=yourappuser # 应用程序应以此用户身份运行(即不是root!)
directory=/srv/www/yourapp.com/ # 应用程序运行的目录
environment=APP_SETTINGS="/srv/www/yourapp.com/prod.toml" # 环境变量
redirect_stderr=true
stdout_logfile=/var/log/supervisor/yourapp.log # 日志文件的名称
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
另外:如果您没有使用反向代理,并且您的Go应用程序需要绑定到小于1024的端口(例如端口80或443),则可以使用setcap - 例如:setcap cap_net_bind_service=+ep /home/yourappuser/bin/yourapp
附注:我写了一篇关于如何使用Supervisor运行Go应用程序的小文章(从“我没有安装Supervisor”开始)。
英文:
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").
答案3
得分: 3
你可以使用os/user
包来检查程序是否在特定用户下运行:
curr, err := user.Current()
// 检查错误。
www, err := user.Lookup("www-data")
// 检查错误。
if *curr != *www {
panic("Go away!")
}
这并不完全符合你的要求,但它确实可以防止程序在其他用户下运行。你可以通过使用su
命令以www-data
用户身份运行它:
su www-data -c "myserver"
英文:
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
得分: 1
实现这个安全的方法是通过分叉(fork)自己。
以下是一个未经测试的示例,展示了如何安全地实现 setuid:
1)确保你是 root 用户。
2)在所需的端口上进行监听(作为 root 用户)。
3)以 www-data
用户身份进行分叉。
4)接受并处理请求。
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)
}
}
希望对你有帮助!
英文:
A way to achieve this safely would be to fork yourself.
This is a raw untested example on how you could achieve safe setuid:
- Make sure you are root
- Listen on the wanted port (as root)
- Fork as
www-data
user. - 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)
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论