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

  1. For Go we need libseccomp-golang.
  2. Then we also need the C libs. There is a Github repo, but most distributions should ship a package named libseccomp-dev or libseccomp-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 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!