Why use Go-Landlock for sandboxing?

Motivation and use of the Go Landlock library

This is the article version of a talk I presented a the Zurich Gophers Meetup on 2022-10-25, with some less relevant parts shortened.

The slides are also available at https://blog.gnoack.org/talks/go-landlock.

Motivation

I started to get interested in computer security about 20 years ago. In the late 90’s and early 2000’s, buffer overflow exploits were all the rage and started to be more widely understood and researched.

This image depicts a high level overview over a common pattern of a technical attack (“exploit”) on a computer program:

The attack channel can take many forms, such as over the network, by invoking a privileged executable file, or by distributing crafted input files.
The attacker talks to the attacked process through the same channels like any other user would do. But unlike another user's input, the attacker's input is maliciously crafted to trick the attacked process into misinterpreting or wrongly validating it, and thereby doing things on behalf of the attacker that it was not originally designed to do.

This can range from exposing process-local memory (e.g. Heartbleed), to exposing the contents of files that were not intended to be exposed (e.g. directory traversal attacks), to even giving the attacker full control over the attacked process (e.g. a classic buffer overflow exploit).

Now all of this would be less critical, if the attacked process only had access to the resources that it needs for execution, but as it turns out, in the normal UNIX model, access rights are usually determined by the user that a program runs as, and that often means that these programs have access to significantly more things than they need. (Ambient Authority).

For instance, programs that you run on your desktop are going to have access to your bank documents, your (hopefully encrypted) SSH and PGP keys, your cookies, your git repositories, etc.

– so: Let’s limit this ambient access!

Philosophy

I can’t speak for Mickaël Salaün’s vision for sandboxing, but this is mine, and it aligns very well with Landlock’s approach.

First of all, I hold the belief that:

Software authors are generally well-meaning. They want their software to work, and to be secure. If provided with the right tools for securing applications, they will use them.

However, if we take a look at the adoption of unprivileged sandboxing on Linux (namely, seccomp-bpf), it shows that there are only a handful of programs making use of it – but what is the reason for that?

Which leads me to this hypothesis:

It is too difficult for software authors to confine their software to have just the access that the software needs, even though these software authors are in the best position to reason about the required scope of access.

The Landlock approach

There are two main points that I’d like to push for in Go-Landlock, which I think make it more usable for sandboxing than other approaches:

1. Make it really easy to use

The existing confinement approaches on Linux tend to be too difficult to set up or maintain, which means that they don’t get used as much as they should.

2. Make sandboxing enablement part of program initialization

This is the other main idea – it should be up to each process to enable its own sandbox.

The rough approach is:

Note: If programs sandbox themselves, they can drop more permissions, because they can also drop the permissions that were required for their initialization phase.

UNIX enforces permissions when opening files, so an already opened file can continue to get used by a process, even when it does not have the permissions to open the file again.

Other operating systems

These ideas are not new - OpenBSD has demonstrated the feasability of this approach with pledge() and unveil(), which is now used in many of OpenBSD’s userland programs. Various slide decks on the topic can be found at https://www.openbsd.org/events.html.

Capsicum on FreeBSD is another unprivileged sandboxing mechanism, which is used in Chromium and other programs (see its homepage). Capsicum is a flexible capability system, but it also has a larger API surface.

Other Linux sandboxing technologies

A deeper discussion of seccomp-bpf and the various Linux Security Modules would be beyond the scope of this article.

For a discussion of the other unprivileged sandboxing mechanism, see my previous article about it on this blog.

The other Linux sandboxing mechanisms are either only available to privileged users, or they are still very difficult to set up.

How to use Go-Landlock

This diagram shows the overall approach for program initialization when using Landlock (it’s a more detailed view of the program initialization overview diagram from above):

The important parts here are:

The steps required to enforce Landlock are:

Step 1: Make sure your kernel supports Landlock

For development, it’s recommended to double check that Landlock is already working, so that you can try out your policies. Landlock is already enabled on many major distributions today.

On most systems, you can check whether Landlock is working using cat /sys/kernel/security/lsm.

On systems that do not have it yet:

For deployment, you can pick whether your program should insist on running on a Landlock-enabled kernel, or whether it should fall back to using weaker (or no) Landlock policies on older systems:

Step 2: State what file accesses you are going to do!

This is the only function invocation your program needs in order to confine itself in a Landlock policy:

…and that’s it. After this invocation, your program will be unable to work with files other than the ones specified or the ones that have already been opened before.

Link to the full documentation

Examples

This section will list a few practical examples.

Image converter

This is a basic image conversion tool in the spirit of ImageMagick’s “convert”. Multimedia processing libraries are often optimized for performance and can be particularly prone to programming bugs, and we want to protect against attackers providing malicious image files as input.

In this example, the Landlock invocation happens right at the start and is landlock.V2.BestEffort().RestrictPaths():

Link to the full example

Web Server

This simple wiki software only needs access to the directory where the wiki pages are stored.

The Go-Landlock invocation is:

err = landlock.V2.BestEffort().RestrictPaths(
    landlock.RWDirs(*storeDir),
)

This gives the process the right to serve and edit the wiki pages, but removes the rights for all other file paths (within the limits of what is possible on the current system).

(Small side note: The net.Listen() call happens before Landlock enablement – I am using this with a named UNIX domain socket.)

Link to the full example

The Go-Landlock example tool

The Go-landlock library comes with a simple example tool, which lets you play with Landlock from bash, similar to the one in the samples/landlock subdirectory in the kernel source, but written using the Go library:

The command to install the landlock-restrict tool is:

go install github.com/landlock-lsm/go-landlock/cmd/landlock-restrict@latest

The above shell transcript starts bash under a Landlock policy where it only has access to these files:

Link to the landlock-restrict example tool

Current Landlock limitations

Some things that Landlocked processes can never do are:

Also,

What file system operations are restrictable?

Landlock gains features and makes it possible to restrict more file system operations. To make this observable, Landlock’s ABI is versioned. As of November 2022, the current ABI is V2.

The list of restrictable file system operations is documented in the Landlock documentation.

Warning: Not all file system operations can be restricted yet. Currently, notable exceptions are file truncation (as a form of file modification), and various ways to observe presence and metadata of files (but not reading their contents).

In future Landlock ABI versions, new operations will start to be configurable. Currently ongoing patch sets (as of November 2022) are:

Summary

In short, please try it out! The invocation is as simple as:

err := landlock.V2.BestEffort().RestrictPaths(
    landlock.RODirs("/usr", "/bin"),
    landlock.RWDirs("/tmp"),
)

I would love to hear your feedback!

For further reference, here are some additional links:

Comments