Building Go programs that manipulate capabilities

Since libcap-2.28 the build tree includes Go support for manipulating capabilities. The support stabilized by libcap-2.46 to be reliably usable. What started out as what I thought would be a trivial re-coding effort, turned into a year plus saga. I'll give some flavor of my journey at some point, but this page is a simple HOWTO for writing a Go program that can manipulate process capabilities using the cap package.

Building your first capable program using Go

Download some code and build it

$ mkdir foo

$ cd foo

$ wget https://git.kernel.org/pub/scm/libs/libcap/libcap.git/plain/goapps/web/web.go

$ go mod init web

$ go get kernel.org/pub/linux/libs/security/libcap/cap

$ go build web.go

These instructions assume you are using Go 1.15 (or later) to build the program. If you are using an earlier version of the Go compiler, then the last two lines will fail. The fix is this (ie., bash command for setting an environment variable):

$ export CGO_LDFLAGS_ALLOW="-Wl,-?-wrap[=,][^-.@][^,]*"

$ go build web.go

  • Note to explain this work around: under the covers (for all Go tool chains prior to go1.16) to get capability manipulation to work, we need some things not natively supported by the Go runtime. We have to appeal to C support via CGo linkage and the needed linkage is quite unusual. I didn't realize I needed this workaround until Go 1.14 was already released. So, go1.15beta1 is the first revision of Go to permit the needed functionality by default.

Having built the binary, we can take a look at it with the library tool, ldd:

$ ldd web

linux-vdso.so.1 (0x00007ffec0b4b000)

libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f74897d7000)

libc.so.6 => /lib64/libc.so.6 (0x00007f748960d000)

/lib64/ld-linux-x86-64.so.2 (0x00007f748981c000)

What that last command is showing is what system libraries (C code) the Go program is linked against. In this case, the code was compiled on an x86-64 architecture, and it links against -lpthread -lc. The executable looks like a pretty normal executable. Importantly, you don't see -lcap here because this code does not simply CGo link to libcap.so.

In general, Go programs, if they are pure-Go, link statically and don't link to any C-code or shared libraries, but that wasn't supported before go1.16 for privilege setting. With Go toolchains 1.16+ you can also build a non-CGo version of the program like this:

$ CGO_ENABLED=0 go build web.go

$ ldd web

not a dynamic executable

Given all that, CGo or not, we have a compiled program and we want to execute it!

First attempt:

$ ./web

2020/07/03 20:38:39 please supply --port value

$ ./web --help

Usage of ./web:

-port int

port to listen on

-skip

skip raising the effective capability - will fail for low ports

$

The program web is a micro-webserver with a very limited set of features. It won't launch if we don't tell it what port to listen to. Let's try again with an unprivileged port:

Serve some web traffic

$ ./web --port=8080 --skip

[... seems to hang, which means it is working ...]

Fire up another terminal on the same computer, and run this command:

Client making queries

$ wget -o/dev/null -O/dev/stdout localhost:8080

Hello from proc: 94850->94854, caps="="

$ wget -o/dev/null -O/dev/stdout localhost:8080

Hello from proc: 94850->94852, caps="="

$ wget -o/dev/null -O/dev/stdout localhost:8080

Hello from proc: 94850->94854, caps="="

$

That makes three web requests of our little server. The server replies with the Hello messages. Indeed, if we look back to the terminal running the ./web binary it now reads something like this:

How the server saw it.

$ ./web --port 8080 --skip

2020/07/03 20:44:45 Saying hello from proc: 94850->94854, caps="="

2020/07/03 20:44:51 Saying hello from proc: 94850->94852, caps="="

2020/07/03 20:44:52 Saying hello from proc: 94850->94854, caps="="

[ ... hanging here, so press Ctrl-C to kill it ...]

$

You can see that each client request made with wget has a matching line in the log generated by the web server. The log explains that the web program has process ID (pid) = 94850 (in this example, but your code will generate a different number each time you invoke it). However, the second number (after the ->) is the thread ID (tid) of the POSIX thread (pthread) executing the server side Go code to handle that request. Note, the pthread that is executing is pretty random - indeed, the Go code has no idea which one will execute it. What is also displayed in both terminals is the observation that caps="=". This is libcap speak for the program has no privilege. This is as expected.

So far, we've not executed code with any interesting privilege. Indeed, if we try to bind to what is called a privileged port (a port number less than 1024) the program is initially unable to do that. One, legacy way of giving a binary privilege is to run it "as root". The program has some defenses against that, since the program is intended to be used in a capabilities only manner, and fails when we try to invoke it that way. That is as follows:

Try setuid-root

$ ./web --port 80

2020/07/03 20:56:37 aborting: insufficient privilege to bind to low ports - want "cap_net_bind_service", have "="

$ sudo ./web --port 80

2020/07/03 20:57:01 go runtime is running as root - cheating

$ sudo chown root.root ./web

$ sudo chmod +s ./web

$ ls -l ./web

-rwsrwsr-x. 1 root root 7536432 Jul 3 20:57 ./web

$ ./web --port=80

2020/07/03 20:57:13 go runtime is setuid uids:(1000 vs 0), gids(1000 vs 0)

$ echo $?

1

$ rm -f ./web

The last line above, cleans up from the failed experiment. So, in the next try, we'll rebuild the binary and try to give it privilege the right (fully capable) way:

Granting just enough capabilities to the program (see note above about the needed workaround for Go releases prior to 1.15):

$ go build web.go

$ ./web --port=80

2020/07/03 20:58:37 aborting: insufficient privilege to bind to low ports - want "cap_net_bind_service", have "="

$ sudo /sbin/setcap cap_net_bind_service,cap_setpcap=p ./web

$ ls -l ./web

-rwxrwxr-x. 1 luser luser 7536432 Jul 3 20:58 ./web

$ /sbin/getcap ./web

./web cap_setpcap,cap_net_bind_service=p

$ ./web --port=80

[... hangs again implying it is waiting for a query. Eventually, we'll see ...]

2020/07/03 21:30:34 Saying hello from proc: 97073->97073, caps="="

2020/07/03 21:30:35 Saying hello from proc: 97073->97075, caps="="

2020/07/03 21:30:37 Saying hello from proc: 97073->97075, caps="="

<Ctrl-C>

$

The (generally) colored listing for ./web indicates that the binary has some privilege, but note that it is not owned by root. Indeed, its capabilities are limited, and once invoked it is only permitted to raise some privilege but is not forced to have it. In other places around the internet, you will see binaries are encouraged to gain capabilities via a command like this: sudo /sbin/setcap cap_net_bind_service,cap_setpcap=ep ./web . That differs from what we did above by the addition of an 'e' - the so-called effective bit. What that does is force the kernel to both grant the potential (permitted) bits and force them to be active (effecitive) before the first instruction of the program is performed. In our case, we have just given the program the potential (permission) to have the privilege, but we leave it up to the program to decide when to raise that privilege. This is a secure programming technique known as privilege bracketing. In short it makes it easier for a programmer/reviewer to recognize when an exploit is easiest, and it prevents code from accidentally acting with privilege; think Captain Hook rubbing his eyes...

A feature of libcap, and its Go package equivalent "kernel.org/pub/linux/libs/security/libcap/cap", is the ability to raise and lower potential privilege to make it effective only when needed. This is what the ./web program does, it raises cap.NET_BIND_SERVICE into the effective state only to bind to port 80. It then raises cap.SETPCAP to perform a libcap-mode transition to a state of no privilege - a state so restrictive that neither the program itself nor any child of the process can ever gain privilege again - all without ever changing user ID! The fact that the running program has no capabilities by the time web requests are served is witnessed by the caps="=" in the logged lines.

For completeness, here is how we generate queries to this server:

Requesting from port 80 this time

$ wget -o/dev/null -O/dev/stdout localhost:80

Hello from proc: 97073->97073, caps="="

$ wget -o/dev/null -O/dev/stdout localhost:80

Hello from proc: 97073->97075, caps="="

$ wget -o/dev/null -O/dev/stdout localhost:80

Hello from proc: 97073->97075, caps="="

$

Finally, now you know what the program is supposed to do, you've demonstrated you can build it, you might like to look at the source code to see how it does it. (If this was all just a curiosity, and you feel daunted at the thought that this is the first Go code you've looked at, you might like to take the Go Tour instead.) You started this exercise by downloading the code, so you have a copy you can fire up in your favorite editor... But, if you would rather view it on the web, you can view the source code for a full example, from Git here: web .