Skip to content

Commit

Permalink
Explore Go port of libcap with a simple web server.
Browse files Browse the repository at this point in the history
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:

  golang/go#1435

Signed-off-by: Andrew G. Morgan <[email protected]>
  • Loading branch information
AndrewGMorgan committed May 27, 2019
1 parent ba5ade5 commit 9392ef0
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 1 deletion.
6 changes: 5 additions & 1 deletion go/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ include ../Make.Rules

all:
$(MAKE) compare-cap
$(MAKE) web
./compare-cap

src/libcap/cap:
Expand All @@ -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
142 changes: 142 additions & 0 deletions go/web.go
Original file line number Diff line number Diff line change
@@ -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 <list of pids>" 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)
}
}

0 comments on commit 9392ef0

Please sign in to comment.