How to secure a Go application with seccomp
Spoiler alert: This will only work on Linux, since seccomp is a feature of the Linux kernel. With seccomp you can limit the kernel syscalls a program can use and you can do that from the program itself. The good news is that it’s dead simple to do this from Go!
The ingredients
- For Go we need libseccomp-golang.
- Then we also need the C libs. There is a Github repo, but most distributions should ship a package named
libseccomp-dev
orlibseccomp-devel
.
And that’s it.
Whitelisting syscalls
You only whitelist the syscalls that your program needs. In order to see which syscalls are actually used, you can use strace
.
An example for the cli tool date
would look like this
$ strace -qcf date
Di 19. Nov 22:05:18 CET 2019
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
37.28 0.000085 85 1 munmap
12.72 0.000029 7 4 openat
11.84 0.000027 5 6 close
11.40 0.000026 4 6 fstat
7.02 0.000016 16 1 write
6.14 0.000014 5 3 read
6.14 0.000014 5 3 brk
4.82 0.000011 2 6 mmap
2.63 0.000006 6 1 lseek
0.00 0.000000 0 4 mprotect
0.00 0.000000 0 3 3 access
0.00 0.000000 0 1 execve
0.00 0.000000 0 1 arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00 0.000228 40 3 total
On the right side you can see all of the syscalls date
needs. Now date
is a very simple application. If you have something more complex, you might need to trigger everything your application can do so that the syscalls can actually show up here.
The Go code
import (
"fmt"
"syscall"
libseccomp "github.com/seccomp/libseccomp-golang"
)
func applySyscallRestrictions() {
// syscalls go here
var syscalls = []string{"read", "write", "close", "mmap", "munmap",
"rt_sigaction", "rt_sigprocmask", "clone", "execve", "sigaltstack",
"arch_prctl", "gettid", "futex", "sched_getaffinity", "epoll_ctl",
"openat", "newfstatat", "readlinkat", "pselect6", "epoll_pwait",
"epoll_create1", "exit_group"}
whiteList(syscalls)
}
// Load the seccomp whitelist.
func whiteList(syscalls []string) {
filter, err := libseccomp.NewFilter(
libseccomp.ActErrno.SetReturnCode(int16(syscall.EPERM)))
if err != nil {
fmt.Printf("Error creating filter: %s\n", err)
}
for _, element := range syscalls {
// fmt.Printf("[+] Whitelisting: %s\n", element)
syscallID, err := libseccomp.GetSyscallFromName(element)
if err != nil {
panic(err)
}
filter.AddRule(syscallID, libseccomp.ActAllow)
}
filter.Load()
}
Now if you invoke applySyscallRestrictions()
from your main
function, this gets send to the kernel and from now on only those syscalls are available to your application. If an attacker is now able to hijack your application, through errors in a parser or network connections for example the damage your application can do is limited to this. That means the attack surface is more limited, but an attacker might still be able to write to an file if this capability is whitelisted.
What about Windows and *BSD?
Since this is only available for Linux, you should exclude every other OS.
In the main.go
I initialize the syscall restrictions in the init()
function:
func init() {
applySyscallRestrictions()
}
Then there are two files:
syscall-restrictions-linux.go
andsyscall-restrictions-not-implemented.go
syscall-restrictions-linux.go
implements the functionality like described above and has an conditional compilation target for linux: // +build linux
In syscall-restrictions-not-implemented.go
the function is empty and the compilation target excludes Linux and therefore applies to every other OS:
// +build !linux
package main
// We only have seccomp for linux right now.
func appylSyscallRestrictions() {
}
To see all of this in one pull request take a look at bscdiff.
If you’ve got any suggestions or question you can drop me a note via email, twitter or mastodon.
Have fun!