Seccomp for Golang Programs 2/n
Introduction
I’m not the first to write about how to use seccomp in your golang program. The golang interface has great documentation, Chris Leroy did a great job, Omri Cohen did a good two parter, Jochen Breuer covered it well. I’m sure many others have too.
So why am I beating this dead horse? Well, good ideas bear repeating. But also I’m compiling all the techniques in this series into a project template for people to use next time they start a project in go https://github.com/joelschopp/hello-secure-golang , so it’s easy to write a secure go program.
You can see Part 1 of this series at:
Quick Background to Seccomp
Since this has been covered exhaustively elsewhere I will be brief. Seccomp limits the Linux kernel system calls that your application can make. Depending on how you set it up when you call a system call not on the allow list the syscall might fail with an error code, log the suspicious syscall, or kill your program.
This can be helpful to just know when your program does something unexpected. It can help you find bugs in your program. But mostly the goal with seccomp is that if your program gets compromised by hackers you would like to know and you would like to limit what damage they can do to the rest of your environment. You don’t want your program to be a jumping off point to compromise the kernel or other programs.
After blocking and logging unauthorized syscalls with seccomp most developers have done their jobs. But since in smaller companies developers also do operations it’s worth mentioning what happens from an ops side.
For the ops person, seccomp irregularities will show up in a modern Extened Detection and Response (XDR) product like Confluera. A good XDR will give seccomp irregularities context and interpretation like being able to determine that a failed syscall was part of an attempted Docker container escape. For the more old fashioned seccomp information also shows up a log from auditd that can point a security researcher in the right direction. Since Linux distros all have auditd packages that’s what we will use below.
Diving Right In
I always like to start seccomp in logging mode and with no system calls on the allow list. Then I’ll run my program and look at the logged syscalls and add them one at a time. Some people will use strace for this, but I find it’s better to use the same logs you will use in production with your program. For a lot of people that will be auditd. After I have done enough testing I’ll change the action in my code from logging to something ActKill, which kills the program when it calls the wrong syscall.
In the hello-secure-golang project file seccomp.go:
syscalls := []string{
}
running the resulting hello binary you would then see this in /var/log/audit/audit.log
type=SECCOMP msg=audit(1601588423.786:17822): auid=1000 uid=1000 gid=1000 ses=647 pid=31498 comm="hello" exe="/home/ubuntu/go/src/github.com/joelschopp/hello-secure-golang/hello" sig=0 arch=c000003e syscall=1 compat=0 ip=0x5647a265a6bb code=0x7ffc0000
type=SECCOMP msg=audit(1601588423.786:17823): auid=1000 uid=1000 gid=1000 ses=647 pid=31498 comm="hello" exe="/home/ubuntu/go/src/github.com/joelschopp/hello-secure-golang/hello" sig=0 arch=c000003e syscall=35 compat=0 ip=0x5647a263157d code=0x7ffc0000
type=SECCOMP msg=audit(1601588423.786:17825): auid=1000 uid=1000 gid=1000 ses=647 pid=31498 comm="hello" exe="/home/ubuntu/go/src/github.com/joelschopp/hello-secure-golang/hello" sig=0 arch=c000003e syscall=35 compat=0 ip=0x5647a263157d code=0x7ffc0000
type=SECCOMP msg=audit(1601588423.786:17824): auid=1000 uid=1000 gid=1000 ses=647 pid=31498 comm="hello" exe="/home/ubuntu/go/src/github.com/joelschopp/hello-secure-golang/hello" sig=0 arch=c000003e syscall=231 compat=0 ip=0x5647a263142b code=0x7ffc0000
We can look those numbers up to get their name. This will make our code more readable. And since it only has to run once at the program startup we aren’t too worried about it taking a few nanoseconds longer.
Now seccomp.go contains:
syscalls := []string{
"write", // 1
"nanosleep", // 35
"exit_group", // 231
}
After updating our code with these new syscalls, audit.log doesn’t get any new messages when we run the hello binary.
At this point we could either leave
filter, err := libseccomp.NewFilter(libseccomp.ActLog)
or comment it out and uncomment
//filter, err := libseccomp.NewFilter(libseccomp.ActKill)
Thre are currently 313 syscalls in Linux, we just limited our hello world program to 3. That’s a 99% reduction in attack surface from our program to the kernel. It also means if our program calls other commands and those commands need some of those syscalls that they won’t run either. Our system just got a lot safer.
That’s it. Pretty easy. Who says security has to be hard.