landdown
Simple Sandboxing for shell scripts.
git clone git://mccd.space/landdown
| Log | Files | Refs | README | LICENSE |
commit 6288e3a538b0fe79984b37832c3e6a94217e2d97 Author: Marc <marc@coquand.email> Date: Tue, 31 Mar 2026 12:50:50 +0200 Initial commit Diffstat:
| A | README.md | | | 61 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | go.mod | | | 9 | +++++++++ |
| A | go.sum | | | 6 | ++++++ |
| A | main.go | | | 147 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 223 insertions(+), 0 deletions(-)
diff --git a/README.md b/README.md
@@ -0,0 +1,61 @@
+# Landdown - Easy shell script sandbox
+
+A minimal Linux utility tool for locking down a shell script's access using [landlock](https://landlock.io).
+
+The aim is to allow developers to easily lock down scripts. I personally use it for my CGI scripts to have per-endpoint sandboxing.
+
+## Supported directives
+
+```
+- rof <file>
+- rwf <file>
+- ro <dir>
+- rw <dir>
+- bind <port>
+- connect <port>
+```
+
+## Examples
+
+### Hello world
+
+```sh
+#!/usr/local/bin/landdown
+ro /bin
+ro /lib
+#!/bin/sh
+echo "Hello world"
+```
+
+Try removing `/bin` or `/lib` and it should fail.
+
+### Edit a file
+
+```sh
+#!/usr/local/bin/landdown
+ro /bin
+ro /lib
+rwf /tmp/some-file.txt
+#!/bin/sh
+echo Edit > /tmp/some-file.txt
+```
+
+Try removing `rwf /tmp/some-file.txt` and the script should fail.
+
+Note: the file need to exist in order to landlock it.
+
+### Curl google
+
+```sh
+#!/usr/local/bin/landdown
+ro /bin
+ro /lib
+ro /etc/ssl
+rof /etc/resolv.conf
+connect 443
+#!/bin/sh
+curl https://www.google.com
+```
+
+Try removing `ro /etc/ssl`, `rof /etc/resolv.conf`, or `connect 443` and it should fail.
+
diff --git a/go.mod b/go.mod
@@ -0,0 +1,9 @@
+module landdown
+
+go 1.26.1
+
+require (
+ github.com/landlock-lsm/go-landlock v0.7.0 // indirect
+ golang.org/x/sys v0.40.0 // indirect
+ kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect
+)
diff --git a/go.sum b/go.sum
@@ -0,0 +1,6 @@
+github.com/landlock-lsm/go-landlock v0.7.0 h1:gXz0+Phg3vddZjpPzXL4pQy/MgsTMHZBs+9zgUIyu/0=
+github.com/landlock-lsm/go-landlock v0.7.0/go.mod h1:mn5GSi81Jf7yMs5WSi+SUi4sUeNLUGVdbT4Id6wXNQw=
+golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
+golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 h1:Z06sMOzc0GNCwp6efaVrIrz4ywGJ1v+DP0pjVkOfDuA=
+kernel.org/pub/linux/libs/security/libcap/psx v1.2.77/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
diff --git a/main.go b/main.go
@@ -0,0 +1,147 @@
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "strconv"
+ "strings"
+ "syscall"
+
+ "github.com/landlock-lsm/go-landlock/landlock"
+)
+
+func main() {
+ if len(os.Args) < 2 {
+ fmt.Fprintf(os.Stderr, "Usage: landdown <script> [args...]\n")
+ os.Exit(1)
+ }
+
+ scriptPath := os.Args[1]
+ extraArgs := os.Args[2:]
+
+ data, err := os.ReadFile(scriptPath)
+ if err != nil {
+ log.Fatalf("failed to read script: %v", err)
+ }
+
+ var roPaths, roFilePaths, rwFilePaths, rwPaths []string
+ var netRules []landlock.Rule = []landlock.Rule{}
+ var execCmd []string
+ var stdinData []byte
+
+ scanner := bufio.NewScanner(bytes.NewReader(data))
+ lineNum := 0
+ for scanner.Scan() {
+ lineNum++
+ line := strings.TrimSpace(scanner.Text())
+
+ // Skip the first shebang
+ if lineNum == 1 && strings.HasPrefix(line, "#!") {
+ continue
+ }
+
+ if strings.HasPrefix(line, "#!") {
+ execCmd = strings.Fields(line[2:])
+ // Everything remaining is stdin
+ var rest bytes.Buffer
+ for scanner.Scan() {
+ rest.Write(scanner.Bytes())
+ rest.WriteByte('\n')
+ }
+ stdinData = rest.Bytes()
+ break
+ }
+
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+
+ switch {
+ case strings.HasPrefix(line, "ro "):
+ roPaths = append(roPaths, strings.TrimSpace(line[3:]))
+ case strings.HasPrefix(line, "rof "):
+ roFilePaths = append(roFilePaths, strings.TrimSpace(line[4:]))
+ case strings.HasPrefix(line, "rw "):
+ rwPaths = append(rwPaths, strings.TrimSpace(line[3:]))
+ case strings.HasPrefix(line, "rwf "):
+ rwFilePaths = append(rwFilePaths, strings.TrimSpace(line[4:]))
+ case strings.HasPrefix(line, "bind "):
+ s := strings.TrimSpace(line[5:])
+ port, err := strconv.ParseUint(s, 10, 16)
+ if err != nil {
+ panic("Invalid bind rule. Must be an integer. Ex: bind 8080")
+ }
+ netRules = append(netRules, landlock.BindTCP(uint16(port)))
+ case strings.HasPrefix(line, "connect "):
+ s := strings.TrimSpace(line[8:])
+ port, err := strconv.ParseUint(s, 10, 16)
+ if err != nil {
+ panic("Invalid connect rule. Must be an integer. Ex: connect 443")
+ }
+ netRules = append(netRules, landlock.ConnectTCP(uint16(port)))
+ default:
+ log.Fatalf("line %d: unknown directive: %s", lineNum, line)
+ }
+ }
+
+ if len(execCmd) == 0 {
+ log.Fatal("no exec target found (second #! line)")
+ }
+
+ // Apply landlock
+ var rules []landlock.Rule
+ if len(roPaths) > 0 {
+ rules = append(rules, landlock.RODirs(roPaths...))
+ }
+ if len(roFilePaths) > 0 {
+ rules = append(rules, landlock.ROFiles(roFilePaths...))
+ }
+ if len(rwPaths) > 0 {
+ rules = append(rules, landlock.RWDirs(rwPaths...))
+ }
+ if len(rwFilePaths) > 0 {
+ rules = append(rules, landlock.RWFiles(rwFilePaths...))
+ }
+ if len(netRules) > 0 {
+ rules = append(rules, netRules...)
+ }
+
+ if len(rules) > 0 {
+ err = landlock.V5.BestEffort().Restrict(rules...)
+ if err != nil {
+ log.Fatalf("failed to apply landlock: %v", err)
+ }
+ }
+
+ fullPath, err := exec.LookPath(execCmd[0])
+ if err != nil {
+ log.Fatalf("command not found: %v", err)
+ }
+
+ argv := append(execCmd, extraArgs...)
+
+ env := os.Environ()
+
+ // Replace file descriptor 0 with data after second shebang,
+ // so the exec'd process gets the script content instead
+ if len(stdinData) > 0 {
+ r, w, err := os.Pipe()
+ if err != nil {
+ log.Fatalf("failed to create pipe: %v", err)
+ }
+ go func() {
+ w.Write(stdinData)
+ w.Close()
+ }()
+ syscall.Dup2(int(r.Fd()), 0)
+ r.Close()
+ }
+
+ if err := syscall.Exec(fullPath, argv, env); err != nil {
+ log.Fatalf("failed to exec: %v", err)
+ }
+}