Golang放弃特权(v1.7)

huangapple go评论89阅读模式
英文:

Golang dropping privileges (v1.7)

问题

我想通过golang创建一个自定义的Web服务器。
它需要以root权限绑定到端口80。
然而,我希望尽快放弃root权限。
根据ticket #1435syscall.SetUid()返回"Not supported"。

我可以通过iptables将端口80重定向到其他端口,但这会使任何非root进程都可以冒充我的Web服务器,我不希望这种情况发生。

如何为我的应用程序降低权限(或者以其他方式解决这个问题)?

英文:

I want to make a custom webserver via golang.
It needs root to bind to port 80.
However I want to drop root as soon as possible.
syscall.SetUid() returns "Not supported" as per ticket #1435.

I could always reroute port 80 to something else via iptables, however this opens up any non-root process to pose as my webserver - which I'd prefer not to be possible.

How do I drop privileges for my application (or alternatively solve this cleanly).

答案1

得分: 8

我最近解决了这个问题,Go语言提供了你所需的所有组件。在这个示例中,我进一步实现了SSL。基本上,你打开端口,检测UID,如果是0,则查找所需的用户,获取UID,然后使用glibc调用来设置进程的UID和GID。我要强调的是,在绑定端口后立即调用setuid代码是最好的做法。降低权限时最大的区别在于你不能使用http.ListenAndServe(TLS)这个帮助函数,你必须手动设置net.Listener,并在端口绑定后但在调用http.Serve之前调用setuid。

这种做法很好,因为你可以在“开发”模式下以UID != 0的身份在高端口上运行,而无需进一步考虑。请记住,这只是一个样例代码 - 我建议在配置文件中设置地址、端口、用户、组和TLS文件名。

package main

import (
	"crypto/tls"
	"log"
	"net/http"
	"os/user"
	"strconv"
	"syscall"
)

import (
	//#include <unistd.h>
	//#include <errno.h>
	"C"
)

func main() {
	cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
	if err != nil {
		log.Fatalln("无法加载证书!", err)
	}
	var tlsconf tls.Config
	tlsconf.Certificates = make([]tls.Certificate, 1)
	tlsconf.Certificates[0] = cert
	listener, err := tls.Listen("tcp4", "127.0.0.1:445", &tlsconf)
	if err != nil {
		log.Fatalln("打开端口时出错:", err)
	}
	if syscall.Getuid() == 0 {
		log.Println("以root身份运行,降级为用户www-data")
		user, err := user.Lookup("www-data")
		if err != nil {
			log.Fatalln("找不到用户或其他错误:", err)
		}
		// TODO: 为从字符串解析的整数编写错误处理
		uid, _ := strconv.ParseInt(user.Uid, 10, 32)
		gid, _ := strconv.ParseInt(user.Gid, 10, 32)
		cerr, errno := C.setgid(C.__gid_t(gid))
		if cerr != 0 {
			log.Fatalln("无法设置GID,错误信息:", errno)
		}
		cerr, errno = C.setuid(C.__uid_t(uid))
		if cerr != 0 {
			log.Fatalln("无法设置UID,错误信息:", errno)
		}
	}
	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
		w.Write([]byte("Hello, world!"))
	})
	err = http.Serve(listener, nil)
	log.Fatalln(err)
}

希望对你有帮助!

英文:

I recently worked through this problem, and Go has all of the pieces you need. In this sample, I went one step further and implemented SSL. Essentially, you open the port, detect the UID, and if it's 0, look for the desired user, get the UID, and then use glibc calls to set the UID and GID of the process. I might emphasize that it's best to call your setuid code right after binding the port. The biggest difference when dropping privileges is that you can't use the http.ListenAndServe(TLS)? helper function - you have to manually set up your net.Listener separately, and then call setuid after the port is bound, but before calling http.Serve.

This way of doing it works well because you can, for example, run as UID != 0 in "development" mode on a high port without further considerations. Remember that this is just a stub - I recommend setting address, port, user, group, and TLS file names in a config file.

package main
import (
&quot;crypto/tls&quot;
&quot;log&quot;
&quot;net/http&quot;
&quot;os/user&quot;
&quot;strconv&quot;
&quot;syscall&quot;
)
import (
//#include &lt;unistd.h&gt;
//#include &lt;errno.h&gt;
&quot;C&quot;
)
func main() {
cert, err := tls.LoadX509KeyPair(&quot;cert.pem&quot;, &quot;key.pem&quot;)
if err != nil {
log.Fatalln(&quot;Can&#39;t load certificates!&quot;, err)
}
var tlsconf tls.Config
tlsconf.Certificates = make([]tls.Certificate, 1)
tlsconf.Certificates[0] = cert
listener, err := tls.Listen(&quot;tcp4&quot;, &quot;127.0.0.1:445&quot;, &amp;tlsconf)
if err != nil {
log.Fatalln(&quot;Error opening port:&quot;, err)
}
if syscall.Getuid() == 0 {
log.Println(&quot;Running as root, downgrading to user www-data&quot;)
user, err := user.Lookup(&quot;www-data&quot;)
if err != nil {
log.Fatalln(&quot;User not found or other error:&quot;, err)
}
// TODO: Write error handling for int from string parsing
uid, _ := strconv.ParseInt(user.Uid, 10, 32)
gid, _ := strconv.ParseInt(user.Gid, 10, 32)
cerr, errno := C.setgid(C.__gid_t(gid))
if cerr != 0 {
log.Fatalln(&quot;Unable to set GID due to error:&quot;, errno)
}
cerr, errno = C.setuid(C.__uid_t(uid))
if cerr != 0 {
log.Fatalln(&quot;Unable to set UID due to error:&quot;, errno)
}
}
http.HandleFunc(&quot;/&quot;, func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte(&quot;Hello, world!&quot;))
})
err = http.Serve(listener, nil)
log.Fatalln(err)
}

答案2

得分: 4

我会按照@JimB的建议进行操作:

首选方法是使用Linux的能力,只允许您的程序绑定到正确的端口,而无需完全具备root权限。

另一方面,在Linux上还有另一个技巧:您可以使用os/exec.Command()执行/proc/self/exe,同时告诉它在生成的os/exec.Cmd实例的SysProcAttr.Credential字段中使用替代凭据。

请参阅go doc os/exec.Cmdgo doc syscall.SysProcAttrgo doc syscall.Credential

确保在重新执行程序时,您需要确保生成的程序的标准输入/输出流连接到其父进程的流,并且所有必要的打开文件也被继承。


另一个值得一提的替代方法是根本不尝试绑定到端口80,并且在那里有一个适当的Web服务器,然后将基于主机名的虚拟主机或特定的URL路径前缀(或前缀)反向代理到您的Go进程,该进程侦听任何TCP或Unix套接字。Apache(至少2.4版本)和Nginx都可以轻松实现这一点。

英文:

I'd do what @JimB suggested:

> The preferred method is to use linux capabilities to only allow your program to bind to the correct port without having full root capabilities.

On the other hand, on Linux there's another trick: you can use os/exec.Command() to execute /proc/self/exe while telling it to use alternative credentials in the SysProcAttr.Credential field of the os/exec.Cmd instance it generates.

See go doc os/exec.Cmd, go doc syscall.SysProcAttr and go doc syscall.Credential.

Make sure that when you make your program re-execute itself, you need to make sure the spawned one has its standard I/O streams connected to those of its parent, and all the necessary opened files are inherited as well.


Another alternatve worth mentioning is to not attempt to bind to port 80 at all and have a proper web server hanging there, and then reverse-proxy either a hostname-based virtual host or a particular URL path prefix (or prefixes) to your Go process listening on any TCP or Unix socket. Both Apache (2.4 at least) and Nginx can do that easily.

答案3

得分: 2

自从Go 1.16版本以来,syscall.Setuid()和相关函数已经正常工作,所以你不再需要像Jon Jenkins的回答中那样使用C代码。你可以像下面这样做:

package main

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

func dropPrivileges(userToSwitchTo string) {

	// 查找要切换到的用户的用户和组ID。
	userInfo, err := user.Lookup(userToSwitchTo)
	if err != nil {
		log.Fatal(err)
	}
	// 将组ID和用户ID从字符串转换为整数。
	gid, err := strconv.Atoi(userInfo.Gid)
	if err != nil {
		log.Fatal(err)
	}
	uid, err := strconv.Atoi(userInfo.Uid)
	if err != nil {
		log.Fatal(err)
	}
	// 取消设置附加组ID。
	err = syscall.Setgroups([]int{})
	if err != nil {
		log.Fatal("Failed to unset supplementary group IDs: " + err.Error())
	}
	// 设置组ID(实际和有效)。
	err = syscall.Setgid(gid)
	if err != nil {
		log.Fatal("Failed to set group ID: " + err.Error())
	}
	// 设置用户ID(实际和有效)。
	err = syscall.Setuid(uid)
	if err != nil {
		log.Fatal("Failed to set user ID: " + err.Error())
	}

}

func main() {

	// 在特权端口上监听连接。
	listener, err := net.Listen("tcp", ":80")
	if err != nil {
		log.Fatal(err)
	}
	defer listener.Close()

	// 放弃root权限;从此刻开始以"user-data"用户身份运行。
	dropPrivileges("user-data")

	// 定义HTTP请求处理程序。
	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(w, "All good, root privileges have been dropped.")
	})

	// 启动HTTP服务器。
	log.Fatal(http.Serve(listener, nil))
}

如果你只想在以root权限运行程序时更改用户,请记住不仅要检查UID,还要检查EUID、GID、EGID和附加的GID。

英文:

Since Go 1.16, syscall.Setuid() and related functions work properly, so you don't need C code anymore, as in Jon Jenkins' answer. You could do something like:

package main
import (
&quot;fmt&quot;
&quot;log&quot;
&quot;net&quot;
&quot;net/http&quot;
&quot;os/user&quot;
&quot;strconv&quot;
&quot;syscall&quot;
)
func dropPrivileges(userToSwitchTo string) {
// Lookup user and group IDs for the user we want to switch to.
userInfo, err := user.Lookup(userToSwitchTo)
if err != nil {
log.Fatal(err)
}
// Convert group ID and user ID from string to int.
gid, err := strconv.Atoi(userInfo.Gid)
if err != nil {
log.Fatal(err)
}
uid, err := strconv.Atoi(userInfo.Uid)
if err != nil {
log.Fatal(err)
}
// Unset supplementary group IDs.
err = syscall.Setgroups([]int{})
if err != nil {
log.Fatal(&quot;Failed to unset supplementary group IDs: &quot; + err.Error())
}
// Set group ID (real and effective).
err = syscall.Setgid(gid)
if err != nil {
log.Fatal(&quot;Failed to set group ID: &quot; + err.Error())
}
// Set user ID (real and effective).
err = syscall.Setuid(uid)
if err != nil {
log.Fatal(&quot;Failed to set user ID: &quot; + err.Error())
}
}
func main() {
// Listen for connections on a privileged port.
listener, err := net.Listen(&quot;tcp&quot;, &quot;:80&quot;)
if err != nil {
log.Fatal(err)
}
defer listener.Close()
// Drop root privileges; run as user &quot;www-data&quot; from this point on.
dropPrivileges(&quot;www-data&quot;)
// Define HTTP request handler.
http.HandleFunc(&quot;/&quot;, func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, &quot;All good, root privileges have been dropped.&quot;)
})
// Start HTTP server.
log.Fatal(http.Serve(listener, nil))
}

If you only want to change the user when the program is run with root privileges, remember to check not just the UID, but also the EUID, GID, EGID and supplementary GIDs.

huangapple
  • 本文由 发表于 2016年12月21日 02:20:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/41248866.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定