From 9392ef0d09ca8821d159bf4bb8d9b153b1da8a33 Mon Sep 17 00:00:00 2001 From: "Andrew G. Morgan" Date: Mon, 27 May 2019 07:48:54 -0700 Subject: [PATCH] Explore Go port of libcap with a simple web server. The program web.go uses "libcap/cap" to raise and lower capabilities in order to bind to a privileged port. Writing this code, I now realize that Go's runtime is not really suited to minimal privilege guarantees. The code does raise and lower the effective capability Value needed, but to be fully robust, we're going to have to wait for the following issue with the Go runtime to find a resolution: https://github.com/golang/go/issues/1435 Signed-off-by: Andrew G. Morgan --- go/Makefile | 6 ++- go/web.go | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 go/web.go diff --git a/go/Makefile b/go/Makefile index 059e0c12..1381f3d9 100644 --- a/go/Makefile +++ b/go/Makefile @@ -6,6 +6,7 @@ include ../Make.Rules all: $(MAKE) compare-cap + $(MAKE) web ./compare-cap src/libcap/cap: @@ -32,7 +33,10 @@ $(GOPACKAGE): src/libcap/cap/names.go src/libcap/cap/cap.go src/libcap/cap/text. compare-cap: compare-cap.go $(GOPACKAGE) GOPATH=$(realpath .) go build $< +web: web.go $(GOPACKAGE) + GOPATH=$(realpath .) go build $< + clean: GOPATH=$(realpath .) go clean -x -i libcap/cap || exit 0 - rm -f *.o *.so mknames compare-cap *~ ../cap/*~ ../cap/names.go + rm -f *.o *.so mknames web compare-cap *~ ../cap/*~ ../cap/names.go rm -fr pkg src diff --git a/go/web.go b/go/web.go new file mode 100644 index 00000000..82b363c9 --- /dev/null +++ b/go/web.go @@ -0,0 +1,142 @@ +// Progam web provides an example of a webserver using capabilities to +// bind to a privileged port. +// +// While this program serves as a demonstration of how to use +// libcap/cap to achieve this, it currently reveals how problematic +// the Go runtime is for actually dropping all privilege. For now, the +// runtime can only raise and lower effective capabilities in critical +// sections with any reliability: it cannot drop privilege. +package main + +import ( + "flag" + "fmt" + "libcap/cap" + "log" + "net" + "net/http" + "runtime" + "syscall" +) + +var ( + port = flag.Int("port", 0, "port to listen on") + debug = flag.Bool("debug", false, "enable to observe the go runtime os thread state confusion") + skipPriv = flag.Bool("skip", false, "skip raising the effective capability - will fail for low ports") +) + +// ensureNotEUID aborts the program if it is running setuid something, +// since it can't be forced to get euid to match uid etc. Go's +// runtime model is fragile with respect fully dropping capabilities, +// or other forms of privilege, so we need to collapse the runtime to +// a single os process. Until such time as Go supports some sort of +// "serialize execution and run this on all hardware threads before +// resuming" functionality, dropping capabilities and euid vs uid +// kinds of discrepencies cannot be secured for all hardware threads +// of the running program. +// +// Read more about this here: +// +// https://github.com/golang/go/issues/1435 . +func ensureNotEUID() { + euid := syscall.Geteuid() + uid := syscall.Getuid() + egid := syscall.Getegid() + gid := syscall.Getgid() + if uid != euid || gid != egid { + log.Fatalf("go runtime unable to resolve differing uids:(%d vs %d), gids(%d vs %d)", uid, euid, gid, egid) + } +} + +// listen creates a listener by raising effective privilege only to +// bind to address and then lowering that effective privilege. To set +// this up, compile and empower this binary as follows (package +// libcap/cap should be installed): +// +// go build web.go +// sudo setcap cap_net_bind_service=p web +// ./web --port=80 +// +// Make requests using wget and observe the log of web (try --debug as +// a web command line flag too): +// +// wget -o/dev/null -O/dev/stdout localhost:80 +func listen(network, address string) (net.Listener, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // The intention of the following code is as follows. + // Collapse down the number of hardware threads to one so we + // can drop privilege and only then up them again. (This does + // not seem to do that by killing the surplas threads. You can + // run --debug and try "pstree -p ; getpcap " to + // get a sense of what is going on.) + count := runtime.GOMAXPROCS(1) + defer runtime.GOMAXPROCS(count) + log.Printf("max proc count = %d", count) + + ensureNotEUID() + + c := cap.GetProc() + orig, err := c.Dup() + if err != nil { + return nil, fmt.Errorf("failed to dup cap.Set: %v", err) + } + if *debug { + defer func() { + if err := cap.NewSet().SetProc(); err != nil { + panic(fmt.Errorf("unable to drop all privilege: %v", err)) + } + return + }() + } else { + defer func() { + if err := orig.SetProc(); err != nil { + panic(fmt.Errorf("unable to lower privilege (%q): %v", orig, err)) + } + }() + } + + if on, _ := c.GetFlag(cap.Permitted, cap.NET_BIND_SERVICE); !on { + return nil, fmt.Errorf("insufficient privilege to bind to low ports - want %q, have %q", cap.NET_BIND_SERVICE, c) + } + if !*skipPriv { + if err := c.SetFlag(cap.Effective, true, cap.NET_BIND_SERVICE); err != nil { + return nil, fmt.Errorf("unable to set capability: %v", err) + } + } + if err := c.SetProc(); err != nil { + return nil, fmt.Errorf("unable to raise capabilities %q: %v", c, err) + } + + return net.Listen(network, address) +} + +// Handler is used to abstract the ServeHTTP function. +type Handler struct{} + +// ServeHTTP says hello from a single Go hardware thread and reveals its capabilities. +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + p := syscall.Getpid() + c := cap.GetProc() + log.Printf("Saying hello from proc: %d, caps=%q", p, c) + fmt.Fprintf(w, "Hello from proc: %d, caps=%q\n", p, c) +} + +func main() { + flag.Parse() + + if *port == 0 { + log.Fatal("please supply --port value") + } + + ls, err := listen("tcp", fmt.Sprintf(":%d", *port)) + if err != nil { + log.Fatalf("aborting: %v", err) + } + defer ls.Close() + + if err := http.Serve(ls, &Handler{}); err != nil { + log.Fatalf("server failed: %v", err) + } +}