The go-landlock
module provides a Go library for interfacing with the Linux kernel
landlock feature. Landlock is a mechanism for minimizing filesystem access to a
Linux process. Using go-landlock
does not require root
or any escalated capabilities.
Requires Linux 5.13+ with Landlock enabled. There is a no-op implementation provided for non-Linux platforms for convenience, which provide no isolation.
Most recent Linux distributions should be supported.
Verified distros
- Ubuntu 22.04 LTS
- Ubuntu 20.04 LTS
- Fedora 36
The minimum Go version is go1.19
.
Use go get
to grab the latest version of go-landlock
.
go get -u github.com/shoenig/go-landlock@latest
This library is made possible after studying several sources, including but not limited to
- landlock.io official documentation
- LWN's Landlock finally Sails
- pledge.com by Justine Tunney
- sandboxer.c kernel reference implementation
Full documentation is on pkg.go.dev.
The go-landlock
package aims to provide a simple abstraction over the Kernel landlock
implementation details. Simply create a Locker
with the Path
's to expose, and then
call .Lock()
to isolate the process. The process will only be able to access the files
and directories, at the file modes specified. Attempts to access any other filesystem
paths will result in errors returned from the kernel system calls (like open
).
Groups of commonly used paths are pre-defined for convenience.
Shared()
: for executing dynamically linked binariesStdio()
: for using standard I/O operationsTTY()
: for using terminal operationsTmp()
: for accessing system tmp directoryVMInfo()
: for reading system informationDNS()
: for reading DNS related informationCerts()
: for reading system SSL/TLS certificate files
Custom paths can be specified using File()
or Dir()
. Each takes 2 arguments - the actual
filepath (absolute or relative), and a mode
string. A mode string describes what level
of file mode permissions to allow. Must be a subset of "rwxc"
.
r
: enable read permissionsw
: enable write permissionsx
: enable execute permissionsc
: enable create permissions
Once a Locker
is configured, isolation starts on the call to Lock()
. The level
of safety is configured by passing either Mandatory
or Try
.
Mandatory
: return an error is Landlock is unsupported or activation causes an errorTry
: continue without error regardless if landlock is supported or workingOnlySupported
: likeMandatory
, but returns no error if the operating system does not support landlock
Once a process has been locked, it cannot be unlocked. Any descendent processes of the locked process will also be locked, and cannot be unlocked. A child process can further restrict itself via additional uses of landlock.
This is a complete example of a small program which is able to read from
/etc/os-release
, and is unable to access any other part of the filesystem
package main
import (
"fmt"
"os"
"github.com/shoenig/go-landlock"
)
func main() {
l := landlock.New(
landlock.File("/etc/os-release", "r"),
)
err := l.Lock(landlock.Mandatory)
if err != nil {
panic(err)
}
_, err = os.ReadFile("/etc/os-release")
fmt.Println("reading /etc/os-release", err)
_, err = os.ReadFile("/etc/passwd")
fmt.Println("reading /etc/passwd", err)
}
➜ go run main.go
reading /etc/os-release <nil>
reading /etc/passwd open /etc/passwd: permission denied
Programs that exec other processes may need to un-restrict a set of
shared object libraries. go-landlock
provides the Shared()
path
to simplify this configuration.
l := landlock.New(
landlock.Shared(), // common shared object files
landlock.File("/usr/bin/echo", "rx"),
)
// e.g. execute echo in a sub-process
Programs that make use of the internet can use the DNS()
and Certs()
helper paths to unlock necessary files for DNS resolution and reading
system SSL/TLS certificates.
l := landlock.New(
landlock.DNS(),
landlock.Certs(),
)
// e.g.
// _, err = http.Get("https://example.com")
Open source under the MPL