Landrun: Sandbox any Linux process using Landlock, no root or containers
github.com
github.com
I built `landrun`, a small CLI tool in Go, to make it practical to sandbox any command with fine-grained filesystem and network access controls. No root. No containers. No SELinux/AppArmor configs.
It's lightweight, auditable, and wraps Landlock v5 features (file access + TCP restrictions).
Demo + usage examples in the README.
Would love feedback from the HN crowd!
Also bubblewrap cannot ask for a decision in runtime: you must set up the rules beforehand.
Since I only see net options specifying ports, does this handle this use case?
I routinely --unshare-net with UDS ro-binds.
You may be using abstract sockets (@/path/uds.sock) and those do require the same netns I think.
Landlock doesn't use namespaces, they are orthogonal.
- sandbox-exec's scheme one https://github.com/BrianSwift/macOSSandboxBuild/blob/main/co...
- AppArmor https://wiki.apparmor.net/ (although I'm cognizant that tries to address way more than just filesystem access)
- Java's permission one https://docs.oracle.com/javase/8/docs/technotes/guides/secur...
Likely tens more
https://github.com/landlock-lsm/landlockconfig
I'm going to write up some Go bindings for this when it becomes relevant.
Awesome! I'm happy to hear that you and others are interested in the configuration language. We should probably coordinate that on the Landlock mailing list when the time comes, so that we don't duplicate that work. We are open to outside contributions :)
Akin to systemd SystemCallFilter directive for no-code application of seccomp filters to the sandboxed process https://www.freedesktop.org/software/systemd/man/latest/syst...
We are working to make it part of the OCI runtime specification too.
Using existing configuration format would not work because Landlock has its own unique properties: unprivileged, nested sandboxes, dedicated Linux syscalls, and a good compatibility story with opt-in and incremental features.
# rm -f /tmp/foo; ./landrun-linux-amd64 --log-level debug --ro /usr/bin --ro /lib --ro /lib64 --rw /tmp touch /tmp/foo
[landrun] 2025/03/22 10:28:02 Sandbox config: {ReadOnlyPaths:[/usr/bin /lib /lib64] ReadWritePaths:[/tmp] AllowExec:false BindTCPPorts:[] ConnectTCPPorts:[] BestEffort:true}
[landrun:debug] 2025/03/22 10:28:02 Adding read-only path: /usr/bin
[landrun:debug] 2025/03/22 10:28:02 Adding read-only path: /lib
[landrun:debug] 2025/03/22 10:28:02 Adding read-only path: /lib64
[landrun:debug] 2025/03/22 10:28:02 Adding read-write path: /tmp
[landrun:debug] 2025/03/22 10:28:02 Applying Landlock restrictions
[landrun] 2025/03/22 10:28:02 Landlock restrictions applied successfully
[landrun] 2025/03/22 10:28:02 Executing: [touch /tmp/foo]
touch: cannot touch '/tmp/foo': Permission denied
Looks very interesting. I'm achieving something somewhat similar by running soeme processes under docker and mounting volumes ro, but could definitely see a usecase for adding landlock to more server processes.landrun --log-level debug --exec --ro /usr/bin --ro /usr/lib --rw /tmp touch /tmp/foo
Personally I don't like that --exec would allow binaries in /tmp to be executed as well...
For added security, I'd create an ephemeral tmpfs disk for each landlocked invocation: obviously the program we're running has no business seeing what other processes may have put to /tmp.
And now you've just invented firejail.
`landrun --ro /usr/bin --ro /lib --ro /lib64 --rw /path/to/dir touch /path/to/dir/newfile`
vs
`landrun --ro /usr/bin --ro /lib --ro /lib64 --exec /usr/bin/bash`
seems to indicate that `--exec` is only required if the command you're executing then uses an `exec`-call internally, which `bash` would need to be able to fork.
So `touch` should not need `--exec`, while `bash` should be able to run anything it can read (including that whitelisted `/tmp`).
It seems that using --ro or --rw at all makes --exec also mandatory.
well when wouldn't it do that? in what scenario could you even use this tool without needing to execute a binary?
$ landrun --ro /usr/bin cat a
[landrun:error] 2025/03/22 23:50:16 permission denied
$ landrun --ro /usr cat /usr/bin/ls | wc -l
400
executing ls (as in actual binary execution) will require --exec
$ landrun --ro /usr ls /usr/bin/
ls: cannot open directory '/usr/bin/': Permission denied
$ landrun --ro /usr --exec ls /usr/bin/
list of billions of files
note that I don't really love the --exec thingy, if it's not "on" by default it's just for sake of being explicit.
Update: there's a bug to limit "file access", which I'll fix asap.
Update2: Adding a --exec-path instead to limit executable, it wasn't the best idea to have a global --exec anyway
Update3: Have a look at V0.1.4, I think it's far cleaner now.
> Read the fine manuals of syd, libsyd, gosyd, plsyd, pysyd, rbsyd, syd.el and watch the asciicasts Memory Sandboxing, PID Sandboxing, Network Sandboxing, and Sandboxing Emacs with syd.
I do agree, though, that the docs could be improved.
if they can polish up the public facing side of the project, it would instill more confidence.
In other documents too. And very repetitive.
I don't need a link to Wikipedia every time "PoC" is used. Or to an online man page every time strace(1) is mentioned.
I get it that a documentation can have more than one "entry point", and hyperlinking all occurrences solves that.
But I think assuming certain audience leads to a document that is more effective. You don't explain addition in university-level textbooks, to make it easier to children from primary school.
This product is simply not for people who hear of strace for the first time.
The bigger issue IMO is that the links seem to be automatically-generated, and the generation is a bit sloppy; for example, the "Syd" links should probably link to the sandboxing technology instead of Pink Floyd's original frontman.
I dare you, check the git history! (if you care anyway)
It's all manually crafted, with love. From the Shine On You Crazy Diamond badge at the top down to the very last link.
Wikipedia needs some notion of "suggested links" that don't become links unless the text is selected or they're toggled globally or some other explicit action. With those, authors could go and link every last word if they like.
Tip: in Firefox, you can hold Alt to drag and select text without triggering links.
Still using Chrome for work stuff, since profile management in FF is still pure hate.
It's been a few years, so I'll give profiles another try I guess. Containers likely won't do it since multiple profiles all use the same domain (console.aws.amazon.com being the obvious one).
https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Link...
either case have a look at latest release, it's a bit cleaner.
// If we have no rules, just return
if len(rules) == 0 {
log.Info("No sandbox rules to apply")
return nil
}
Really cool and well-written project, but I disagree with this choice: No rules should mean no rules (everything denied).I would have suggested support for more fine-grained file/directory permissions—good to see that’s already planned.
OpenBSD really got it right with pledge and unveil.
See the example at the top of the Readme at https://github.com/landlock-lsm/go-landlock
(Full disclosure, I am the author of that library)
FWIW, I do hope that we can motivate people to use Landlock in the same way as people use pledge on OpenBSD, as a lightweight self-sandboxing mechanism that requires fewer architectural changes to your program and results in more constrained sandboxes than Linux namespaces and other mechanisms do.
The Landlock interface must not change the underlying semantic of what is allowed or denied, otherwise it could break apps build for an older or a newer kernel. However, these apps should still use all the available security features. This is challenging.
Landlock provides a way to define fine-grained security policies. I would not say the kernel interface is complex (rather flexible), but what really matter are the user space library interfaces and how they can safely abstract complexity.
About interfaces, another example is that Unveil is configured with path names but Landlock uses file descriptors instead (more flexible).
Also, these OpenBSD primitives only apply to the current executed binary, there is no nested sandboxes because the goal is not to create this kind of secure environment but mainly to secure a trusted binary.
Calling clock_gettime() in libc will call the newer syscall (assuming __TIME_BITS=64 is set). But Linux has kept backwards compat, old programs can still call the old syscall.
If you wrote your seccomp rule for your program before clock_gettime64 existed, it'd break when glibc switched. I guess that implies each language stdlib should have their own seccomp etc wrappers.
For landlock, the equivalent is that glibc reads various files in /etc varying per libc version or system settings, so landlock rules need to account for that.
That arguably shows that seccomp is operating at the wrong abstraction level, or the kernel needs another higher level api. With pledge, you operate on capabilities and as new functionality is added to the kernel it is categorized under existing capabilities (for example, if your program pledges not to use networking you can assume that it should not be able to use new networking syscalls added to the kernel in the future).
With Landlock, the access control is at the right layer, and the semantic is guaranteed to be the same even if the kernel gets new syscalls. Landlock is the closest thing to Pledge/Unveil we can get with the Linux constraints (and it is gaining new features).
Which also points to landlock-make[0] or vice-versa (the original project that made me aware of the kernel functionality (although didn't realize it also isolated network which is great).
Defaults are important.
My operating system design is: programs start with nothing other than the ability to perform deterministic computation and to send/receive messages with the capabilities it receives in the initial message. It is not allowed to know what these capabilities refer to; they may be proxies set up by the user, network resources, or something else, and is not necessarily what it asked for. All I/O including the ability to determine the current date/time or how much time has passed, requires the use of capabilities. (Due to this, a program with no capabilities left can be terminated automatically by the operating system (unless a debugger is attached; it is also necessary that the program cannot notice the debugger attached to it), since it is no longer capable of any I/O.)
I think nsjail uses mount namespaces (CLONE_NEWNS) instead of landlock for filesystem sandboxing, but what would the practical differences be?
I'm trying to run a self-contained webserver executable without any external dependency. It starts but daemon <-> workers communication doesn't seem working (it is done via unix socket)
It works fine with bubblewrap or inside a scratch docker container.
or in the other direction, to truly prevent e.g. xorg socket from being accessed by a bubblejailed application, it should exclude --share-net, regardless if you bind the actual path to the socket (since abstract permeates beyond that)
You're telling me there's another reason, then... Can't guess which one.
Hmmm...
As a very average user, that’s the kind of thing I miss on windows and Linux.
Because I installed Google chrome, it doesn’t mean I want it to be able to scan every single file I have on my computer yet there is no way to prevent it and I feel it’s a big security and privacy issue that no one speak about !
When I used it, it break many things. Some app would have weird behavior, theming would break, app wouldn’t open.
Then you get, for those peasant like me who have very slow internet, a 1 hour to download a app that would otherwise take 30 seconds because flatpak download lots of other stuff.
I get why flatpak is great, it’s like docker or python environment, but as usual with Linux it’s more like a developer thing and a recipe for headache and frustration to the average computer user.
I've used Linux for over a decade now, but there are still many things I haven't learned, so maybe I'm missing something in this regard.
The GitHub page says
- TCP network access control (binding and connecting)
and
- Support for UDP and other network protocol restrictions (when supported by Linux kernel)
so maybe this can be used to firewall processes in an easy way (assuming that it is easy to set up landrun)?
[1] https://firejail.wordpress.com/documentation-2/basic-usage/#... "A network namespace is a new, independent TCP/IP stack attached to the sandbox. The stack has its own routing table, firewall and set of interfaces."
You could also set a special mark on a packet for each container and then filter based on that. The Internet is surprsingly very thin on nft resources. I spent a few weeks learning how to write them. Definitely, not for the average consumer.
There is, with cgroups: https://www.kernel.org/doc/Documentation/cgroup-v1/net_cls.t...
Systemd has `NetworkNamespacePath` directive which can spin up services in new namespaces as well. See `man 5 systemd.exec`
Then you have to explicitly launch applications in a desired network namespace such as physical (eth0, wlan0 etc) or vpn (wg0).
Accidentally launched applications, or something like the desktop environment have no network connectivity.
0. If you set up no additional network namespaces, there is still one present, this is called the "default" or "root" network namespace. It is what you refered to as "main host".
1. Say the default net ns has device eth0 that your server receives traffic on.
2. You create a veth pair in the default net ns, veth0 and veth1.
3. You create a new net ns and move veth1 into new net ns. Only veth0 and eth0 remain in default net ns.
4. You set up routes and nftable rules in default net ns as you would normally. Certain traffic you want to route to your new net is so you have a next hop veth0 (note, you ha e to route through to the IP of veth1, using veth0 as next hop)
5. You set up additional nftable rules and whatever you want in the new net ns and this is isolated from default net ns.
End-to-end flow: packet arrives on eth0, traverses netfilter (nftables/iptables) and route lookup to route to "new network" via veth0. Packet is sent "out" the default net stack via veth0 and arrives on veth1 (since they are a pair) in new net ns network stack. There, the packet traverses an isolated netfilter and routing table and a socket can be listening for your service or whatever. Replies would follow the same in reverse. Sent out veth1 in new net ns, arrive on veth0 in default net ns, and exit that stack via eth0
The fact that local users can simply create namespaces that bypass the host's firewall is extremely dangerous in my opinion.
well, they already have a user namespace per app which they can match on the firewall rule, but a per "main" program pid net namespace would be pretty much the same. i guess this can be a cool patch to this plus a one weekend qt+rust gui to manage the firewall (or a patch to firewalld gui)... only if i ever had a weekend.
[1] Linux Sandboxing with Landlock - Mickaël Salaün, Microsoft [video]:
Could it be possible for the system to use the Landlock api to catch unauthorized net/fs access by an app and display a popup to ask for authorization, like macOS does?
Namespaces can also be used for sandboxing, but they have a series of problems. Most importantly, they require more substantial changes to your program that wants to sandbox itself, and the program has to jump through a series of hoops to get everything into the right state. It is possible, but the resulting program environment is in the end more unusual and the mechanisms for enabling unprivileged namespaces are making it difficult to use it for smaller use cases. (It involves re-execution of the program that wants to sandbox itself, whereas with Landlock, a small program can just install a Landlock policy during an early startup phase and continue with that.)
Controlling the rules through a separate process is not currently possible, but it was proposed earlier this month on the kernel mailing lists:
https://lore.kernel.org/all/cover.1741047969.git.m@maowtm.or...
E.g. if you can cat CAP_NET_ADMIN even within a restricted namespace, you have access to huge amounts of horrbly broken kernel code. It's easy (for people who know how to exploit kernel bugs) to escalate privileges from there.
Distros have their own fixes for this issue so namespaces definitely aren't useless in practice for sandboxing. But the basic mechanism just doesn't that well suited to it.
But a prctl like NO_NEW_PRIVS would be better, since it could avoid an intermediary namespace that is needed for the namespace-aware sysctl.
I think probably we want an inherited mask of what capabilities you can get in child namespaces. I think I heard someone proposed that upstream but I haven't seen the patches.
Sending ICMP Echo in userspace (over UDP) is a thing on Linux. From experience, for public Internet, where possible, it is always better to rely on TLS connects (then TCP or UDP, and then ICMP) to ascertain connectivity (lest some middleware meddle with IP or Transport replies).
Landlock is (only) an access control system, but it's designed to let any process use it, including potentially untrusted ones, which makes it suitable for any apps. It's close and complementary to seccomp.
So yeah, --rox is fine semantically, just ugly. :D
"Any color the customer wants, as long as it's black"
$ landrun --log-level debug --ro /usr/bin,/lib,/lib64 /usr/bin/bash --norc
[landrun] 2025/03/22 17:16:29 Sandbox config: {ReadOnlyPaths:[/usr/bin /lib /lib64] ReadWritePaths:[] AllowExec:false BindTCPPorts:[] ConnectTCPPorts:[] BestEffort:true}
[landrun:debug] 2025/03/22 17:16:29 Adding read-only path: /usr/bin
[landrun:debug] 2025/03/22 17:16:29 Adding read-only path: /lib
[landrun:debug] 2025/03/22 17:16:29 Adding read-only path: /lib64
[landrun:debug] 2025/03/22 17:16:29 Applying Landlock restrictions
[landrun] 2025/03/22 17:16:29 Landlock restrictions applied successfully
[landrun] 2025/03/22 17:16:29 Executing: [/usr/bin/bash --norc]
bash-5.2$
bash-5.2$ /usr/bin/uname -r
6.13.7-200.fc41.aarch64systemd-run --user --scope -p MemoryMax=1G,IOReadIOPSMax=8000,CPUQuota=20%,<...> landrun ...
Enabled by default: this strikes me as a particularly poor design choice
And someone also said, but Firejail supports Landlock, too: https://github.com/netblue30/firejail/pull/6078.
setpriv --landlock-access 'fs:remove-file,remove-dir,write-file,make-reg' touch /tmp/foo # Permission denied
setpriv --landlock-access 'fs:remove-file,remove-dir,write-file,make-reg' --landlock-rule "path-beneath:make-reg:/tmp" touch /tmp/foo # Allowed
Very verbose unlike unshare and really deals with internal details, so I'd find it hard to use setpriv in practice.
Hard to choose! One thing I don’t run anymore is docker.
I'm not sure this is better, but assuming it is by the author into.
Here's a pretty good overview article: https://benjamintoll.com/2022/02/04/on-running-systemd-nspaw...
It is a library, as already linked in the other comment: https://github.com/landlock-lsm/go-landlock
The landrun tool is built on the same library. We also provide an official library for Rust, and obviously you can do it from C as well.
I also collected some libraries for other languages at https://wiki.gnoack.org/SoftwareUsingLandlock (but I can not vouch for their quality in detail)
From the point of view of an app developer, it might not make sense to filters peers but services (ports) instead, and filtering peers without their names would not be ideal (the kernel doesn't know about DNS, only IPs). Anyway, this feature might come one day if someone want to work on it, but we follow well-tested incremental development.
Netfiler is a privileged network feature that allows to do almost anything with the network, which makes it unsuitable for (app/unprivileged) sandboxing.
A rough description of upcoming network restriction features in Landlock and how they map to the BSD socket API is in the talk at https://youtu.be/K2onopkMhuM?start=2025 starting around 33:45
I really hope we can get back to these features soon :) I think these would be very useful.
...of course it is common enough that it would make sense to abstract over the different protocols that have more or less the same concept of ports.