Go-Landlock
Günther Noack
blog.gnoack.org
2022-10-05
This slide deck: https://blog.gnoack.org/talks/go-landlock
High level overview of an attack
Attacker
Attacked process
Ambient access to various resources
SSH keys
Cookie files
Bank documents
Love letters
Git repos
gains unauthorized control over
Let’s limit this ambient access!
Show of hands!
Limiting access is too hard with existing solutions!
Cost
Benefit
Idea 1: Make it so simple that everyone can do it
Cost
Benefit
Idea 2: Make it part of program initialization
Initialization phase (flag parsing, open necessary files and sockets)
Restrict own access (drop permissions)
Start processing untrusted input
These ideas are not new
int pledge(const char *promises, const char *execpromises);
int unveil(const char *path, const char *permissions);
Very lightweight to use from C, a lot of OpenBSD programs are “pledged”
Unprivileged sandboxing on Linux
…is otherwise very hard to use
(there are more detailed slides on these at the end, if needed)
How to use Go-Landlock
Architecture
Userspace
Go program
Linux kernel
System calls
Landlock
Linux Security Module
System call impl
Check whether permitted
Go-landlock library
Enable Landlock for the calling thread
Initialization
System call impl
Drop rights
Process untrusted input
Step 1: Make sure your Linux kernel supports Landlock
gnoack:~$ cat /sys/kernel/security/lsm
Capability,landlock,lockdown,yama,bpf
(source)
(source)
Step 2: State what file accesses you are going to do!
err := landlock.V2.BestEffort().RestrictPaths(
landlock.RODirs("/usr", "/bin"),
landlock.RWDirs("/tmp"),
)
Use the best set of Landlock features available on the current kernel
Files we need to read*
Files we need to write*
Use the highest Landlock ABI version you can, increase it opportunistically
* access can be made more granular if required
Example: Image converter
func main() {
if err := landlock.V2.BestEffort().RestrictPaths(); err != nil {
log.Fatal("Could not enable Landlock:", err)
}
imgData, _, err := image.Decode(os.Stdin)
if err != nil {
log.Fatal("Could not read input:", err)
}
if err := png.Encode(os.Stdout, imgData); err != nil {
log.Fatal("Could not write output:", err)
}
}
Drop access rights
Process untrusted input
Example: Wiki software (simplified)
func main() {
flag.Parse()
d := diskv.New(diskv.Options{BasePath: *storeDir})
http.Handle("/", &ukuleleweb.PageHandler{MainPage: *mainPage, D: d})
s := http.Server{}
l, err := net.Listen(*listenNet, *listenAddr)
if err != nil { log.Fatalf("net.Listen: %v", err) }
err = landlock.V2.BestEffort().RestrictPaths(
landlock.RWDirs(*storeDir),
)
if err != nil { log.Fatalf("Landlock: %v", err) }
err = s.Serve(l)
if err != nil { log.Printf("http.ListenAndServe: %v", err) }
}
Program initialization
Drop access rights
Process untrusted input
Unix Domain Socket!
Example: Play with the go-landlock example tool
gnoack:~$ go install github.com/landlock-lsm/go-landlock/cmd/landlock-restrict@latest
gnoack:~$ export HOME=$(mktemp --directory -t tmphome-XXXXXXX)
gnoack:/home/gnoack$ export TMPDIR=$HOME/.localtmp
gnoack:/home/gnoack$ mkdir -p $TMPDIR
gnoack:/home/gnoack$ cd
gnoack:~$ landlock-restrict -ro /usr /lib /etc -rw "${HOME}" /dev -- /bin/bash
[gnoack@nuc ~]$ ls
[gnoack@nuc ~]$ pwd
/tmp/tmphome-zMtxO01
[gnoack@nuc ~]$ id
uid=1000(gnoack) gid=1000(gnoack) groups=1000(gnoack),962(docker)
[gnoack@nuc ~]$ ls ..
ls: cannot open directory '..': Permission denied
[gnoack@nuc ~]$
Current Limitations
Current limitations
Some small things that Landlocked processes can never do:
Current Limitations
What is restrictable? (V1)
What is restrictable? (V2)
What is restrictable? (the future)
+ Networking support?
V3+?
HIGHLY SPECULATIVE
HIGHLY SPECULATIVE
HIGHLY SPECULATIVE
Key Point
Please try it out!
err := landlock.V2.BestEffort().RestrictPaths(
landlock.RODirs("/usr", "/bin"),
landlock.RWDirs("/tmp"),
)
I would ❤️ to hear your feedback
Landlock mailing list:
Or to my own email:
PGP: 7F02 BDCC 6157 6E11 1A87
9BD1 1C62 9E5A F9E8 CDA1
Thank you!
Links
Go-Landlock:
Landlock Linux Security Module:
This talk: https://blog.gnoack.org/talks/go-landlock
Questions
Bonus Slides
Go-Landlock Implementation
Architecture
Userspace
Go program
Linux kernel
System calls
Landlock
Linux Security Module
System call impl
Check whether permitted
Go-landlock library
Enable Landlock for the calling thread
Initialization
System call impl
Drop rights
Process untrusted input
How does Landlock get enabled?
😱
Pop quiz: How many Goroutines are running here?
func main() {
err := landlock.V2.BestEffort().RestrictPaths()
// …
callSomeFunc()
}
… and how many OS threads?
Answer: Too many!
The Go runtime already starts goroutines before main()
😱
syscall.AllThreadsSyscall to the rescue
syscall.AllThreadsSyscall(
SYS_LANDLOCK_RESTRICT_SELF,
uintptr(rulesetFd), uintptr(flags), 0)
A helper exposed by the runtime:
Works for Go! \o/
😱
But not for cgo
Libpsx to the rescue
So…
https://sites.google.com/site/fullycapable/who-ordered-libpsx explains it in detail
The upside: This sounds more horrible than it is
Glibc
Testing learnings…
Other Linux Sandboxing technology
Seccomp-BPF
Mount namespaces
Same goes for most other namespaces (network, pid, ipc, …)
AppArmor, SELinux, SMACK, TOMOYO
Various command line tools, firejail and friends