1// Program web provides an example of a webserver using capabilities to 2// bind to a privileged port, and then drop all capabilities before 3// handling the first web request. 4// 5// This program can be compiled CGO_ENABLED=0 with the go1.16+ 6// toolchain. 7// 8// Go versions prior to 1.16 use some cgo support provided by the 9// "kernel.org/pub/linux/libs/security/libcap/psx" package. 10// 11// To set this up, compile and empower this binary as follows (the 12// README contains a pointer to a full writeup for building this 13// package - go versions prior to 1.15 need some environment variable 14// workarounds): 15// 16// go mod init web 17// go mod tidy 18// go build web.go 19// sudo setcap cap_setpcap,cap_net_bind_service=p web 20// ./web --port=80 21// 22// Make requests using wget and observe the log of web: 23// 24// wget -o/dev/null -O/dev/stdout localhost:80 25package main 26 27import ( 28 "flag" 29 "fmt" 30 "log" 31 "net" 32 "net/http" 33 "runtime" 34 "syscall" 35 36 "kernel.org/pub/linux/libs/security/libcap/cap" 37) 38 39var ( 40 port = flag.Int("port", 0, "port to listen on") 41 skipPriv = flag.Bool("skip", false, "skip raising the effective capability - will fail for low ports") 42) 43 44// ensureNotEUID aborts the program if it is running setuid something, 45// or being invoked by root. That is, the preparer isn't setting up 46// the program correctly. 47func ensureNotEUID() { 48 euid := syscall.Geteuid() 49 uid := syscall.Getuid() 50 egid := syscall.Getegid() 51 gid := syscall.Getgid() 52 if uid != euid || gid != egid { 53 log.Fatalf("go runtime is setuid uids:(%d vs %d), gids(%d vs %d)", uid, euid, gid, egid) 54 } 55 if uid == 0 { 56 log.Fatalf("go runtime is running as root - cheating") 57 } 58} 59 60// listen creates a listener by raising effective privilege only to 61// bind to address and then lowering that effective privilege. 62func listen(network, address string) (net.Listener, error) { 63 if *skipPriv { 64 return net.Listen(network, address) 65 } 66 67 orig := cap.GetProc() 68 defer orig.SetProc() // restore original caps on exit. 69 70 c, err := orig.Dup() 71 if err != nil { 72 return nil, fmt.Errorf("failed to dup caps: %v", err) 73 } 74 75 if on, _ := c.GetFlag(cap.Permitted, cap.NET_BIND_SERVICE); !on { 76 return nil, fmt.Errorf("insufficient privilege to bind to low ports - want %q, have %q", cap.NET_BIND_SERVICE, c) 77 } 78 79 if err := c.SetFlag(cap.Effective, true, cap.NET_BIND_SERVICE); err != nil { 80 return nil, fmt.Errorf("unable to set capability: %v", err) 81 } 82 83 if err := c.SetProc(); err != nil { 84 return nil, fmt.Errorf("unable to raise capabilities %q: %v", c, err) 85 } 86 return net.Listen(network, address) 87} 88 89// Handler is used to abstract the ServeHTTP function. 90type Handler struct{} 91 92// ServeHTTP says hello from a single Go hardware thread and reveals 93// its capabilities. 94func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 95 runtime.LockOSThread() 96 // Get some numbers consistent to the current execution, so 97 // the returned web page demonstrates that the code execution 98 // is bouncing around on different kernel thread ids. 99 p := syscall.Getpid() 100 t := syscall.Gettid() 101 c := cap.GetProc() 102 runtime.UnlockOSThread() 103 104 log.Printf("Saying hello from proc: %d->%d, caps=%q", p, t, c) 105 fmt.Fprintf(w, "Hello from proc: %d->%d, caps=%q\n", p, t, c) 106} 107 108func main() { 109 flag.Parse() 110 111 if *port == 0 { 112 log.Fatal("please supply --port value") 113 } 114 115 ensureNotEUID() 116 117 ls, err := listen("tcp", fmt.Sprintf(":%d", *port)) 118 if err != nil { 119 log.Fatalf("aborting: %v", err) 120 } 121 defer ls.Close() 122 123 if !*skipPriv { 124 if err := cap.ModeNoPriv.Set(); err != nil { 125 log.Fatalf("unable to drop all privilege: %v", err) 126 } 127 } 128 129 if err := http.Serve(ls, &Handler{}); err != nil { 130 log.Fatalf("server failed: %v", err) 131 } 132} 133