| // Progam web provides an example of a webserver using capabilities to |
| // bind to a privileged port, and then drop all capabilities before |
| // handling the first web request. |
| // |
| // This program cannot work reliably as a pure Go application without |
| // the equivalent of the Go runtime patch that adds a POSIX semantics |
| // wrapper around the system calls that change per-thread security |
| // state. A patch for the pure Go compiler/runtime to add this support |
| // is available here [2019-12-14]: |
| // |
| // https://go-review.googlesource.com/c/go/+/210639/ |
| // |
| // Until that patch, or something like it, is absorbed into the Go |
| // runtime the only way to get capabilities to work reliably on the Go |
| // runtime is to use something like libpsx via CGo to do capability |
| // setting syscalls in C with POSIX semantics. As of this build of the |
| // Go "kernel.org/pub/linux/libs/security/libcap/cap" package, |
| // courtesy of the "kernel.org/pub/linux/libs/security/libcap/psx" |
| // package, this is how things work. |
| // |
| // To set this up, compile and empower this binary as follows (read |
| // over the detail in the psx package description if this doesn't |
| // 'just' work): |
| // |
| // go build web.go |
| // sudo setcap cap_setpcap,cap_net_bind_service=p web |
| // ./web --port=80 |
| // |
| // Make requests using wget and observe the log of web: |
| // |
| // wget -o/dev/null -O/dev/stdout localhost:80 |
| package main |
| |
| import ( |
| "flag" |
| "fmt" |
| "log" |
| "net" |
| "net/http" |
| "runtime" |
| "syscall" |
| |
| "kernel.org/pub/linux/libs/security/libcap/cap" |
| ) |
| |
| var ( |
| port = flag.Int("port", 0, "port to listen on") |
| 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, |
| // or being invoked by root. That is, the preparer isn't setting up |
| // the program correctly. |
| func ensureNotEUID() { |
| euid := syscall.Geteuid() |
| uid := syscall.Getuid() |
| egid := syscall.Getegid() |
| gid := syscall.Getgid() |
| if uid != euid || gid != egid { |
| log.Fatalf("go runtime is setuid uids:(%d vs %d), gids(%d vs %d)", uid, euid, gid, egid) |
| } |
| if uid == 0 { |
| log.Fatalf("go runtime is running as root - cheating") |
| } |
| } |
| |
| // listen creates a listener by raising effective privilege only to |
| // bind to address and then lowering that effective privilege. |
| func listen(network, address string) (net.Listener, error) { |
| if *skipPriv { |
| return net.Listen(network, address) |
| } |
| |
| orig := cap.GetProc() |
| defer orig.SetProc() // restore original caps on exit. |
| |
| c, err := orig.Dup() |
| if err != nil { |
| return nil, fmt.Errorf("failed to dup caps: %v", 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 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) { |
| runtime.LockOSThread() |
| // Get some numbers consistent to the current execution, so |
| // the returned web page demonstrates that the code execution |
| // is bouncing around on different kernel thread ids. |
| p := syscall.Getpid() |
| t := syscall.Gettid() |
| c := cap.GetProc() |
| runtime.UnlockOSThread() |
| |
| log.Printf("Saying hello from proc: %d->%d, caps=%q", p, t, c) |
| fmt.Fprintf(w, "Hello from proc: %d->%d, caps=%q\n", p, t, c) |
| } |
| |
| func main() { |
| flag.Parse() |
| |
| if *port == 0 { |
| log.Fatal("please supply --port value") |
| } |
| |
| ensureNotEUID() |
| |
| ls, err := listen("tcp", fmt.Sprintf(":%d", *port)) |
| if err != nil { |
| log.Fatalf("aborting: %v", err) |
| } |
| defer ls.Close() |
| |
| if !*skipPriv { |
| if err := cap.ModeNoPriv.Set(); err != nil { |
| log.Fatalf("unable to drop all privilege: %v", err) |
| } |
| } |
| |
| if err := http.Serve(ls, &Handler{}); err != nil { |
| log.Fatalf("server failed: %v", err) |
| } |
| } |