Skip to content

Commit bad82d8

Browse files
committed
ENH: Add sitecustomize with seccomp filter; respect SANDBOX_SECCOMP_ALLOW= env var
1 parent 3e5fc8d commit bad82d8

File tree

7 files changed

+187
-14
lines changed

7 files changed

+187
-14
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ jobs:
2323
# Alternative:
2424
# sudo sysctl -w kernel.apparmor_restrict_unprivileged_unconfined=1
2525
# sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=1
26-
- run: sudo apt-get install bubblewrap
26+
- run: sudo apt-get install bubblewrap libseccomp
2727
- run: time bash tests/smoke-test.sh
2828
- run: time bash tests/test-bwrap-opts.sh
29+
- run: time bash tests/test-seccomp.sh
2930

3031
workflow-keepalive:
3132
if: github.event_name == 'schedule'

README.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,14 @@ Installation
5757
There are **no dependencies other than a POSIX shell** with
5858
[its standard set of utilities](https://en.wikipedia.org/wiki/List_of_POSIX_commands)
5959
**and `bubblewrap`**.
60-
The installation instructions, as well as the script runtime,
61-
should work similarly on all relevant compute platforms,
60+
The installation process, as well as the script runtime,
61+
should behave similarly on all relevant compute platforms,
6262
including GNU/Linux and even
6363
[Windos/WSL](https://learn.microsoft.com/en-us/windows/wsl/install). 🤞
6464

6565
```shell
66-
# Install required dependencies, e.g.
67-
sudo apt install binutils bubblewrap python3
66+
# Install the few, unlikely to be missing dependencies, e.g.
67+
sudo apt install coreutils binutils bubblewrap libseccomp python3
6868

6969
# Download the script and put it somewhere on PATH
7070
curl -vL 'https://bit.ly/sandbox-venv' | sudo tee /usr/local/bin/sandbox-venv
@@ -126,6 +126,21 @@ Anything else not explicitly mounted by an extra CLI switch
126126
is lost upon container termination.
127127

128128

129+
#### Linux Seccomp
130+
131+
Linux kernel `seccomp` facility for restricting syscalls is
132+
**automatically enabled when the appropriate package is
133+
available**`apt install seccomp` (requires virtualenv with `--system-site-packages`)
134+
or `pip install pyseccomp`.
135+
The initialiying module `sitecustomize.py` installs a filter
136+
that thereafter only allows syscalls listed in the environment variable
137+
`SANDBOX_SECCOMP_ALLOW=`
138+
(by default, some 200 syscalls that should cover most non-special cases).
139+
You can populate the variable with custom stricter syscalls list
140+
or set it to blank
141+
(i.e. `export SANDBOX_SECCOMP_ALLOW=`) to disable seccomp completely.
142+
143+
129144
#### Runtime monitoring
130145

131146
If **environment variable `VERBOSE=`** is set to a non-empty value,
@@ -159,7 +174,13 @@ Viable alternatives
159174
[<del>Red Hat</del><ins>IBM</ins> has a reasonable position on](https://github.com/containers/bubblewrap?tab=readme-ov-file#related-project-comparison-firejail))
160175
that sets up its own sandbox. I guess it's a matter of trust.
161176
Similarly to AppArmor, requires writing a custom profile.
162-
4. On macOS, [`sandbox-exec`](https://igorstechnoclub.com/sandbox-exec/)
177+
4. A custom
178+
[`seccomp` initialization script](https://healeycodes.com/running-untrusted-python-code),
179+
executed at interpreter startup using
180+
[~~`PYTHONSTARTUP=`~~](https://docs.python.org/3/using/cmdline.html#envvar-PYTHONSTARTUP)
181+
[`sitecustomize`](https://docs.python.org/3/library/site.html#module-sitecustomize)
182+
startup hook.
183+
5. On macOS, [`sandbox-exec`](https://igorstechnoclub.com/sandbox-exec/)
163184
or Apple Containerization®.
164185

165186
In comparison to the above, `sandbox-venv` is like `chroot` on steroids.

_wrapper_exe.sh

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ executables="
3636
/bin/env
3737
/bin/ls
3838
/bin/sh
39+
3940
/bin/uname
40-
"
41+
/sbin/ldconfig
42+
"
4143

4244
case $- in *x*) xtrace=-x ;; *) xtrace=+x ;; esac; set +x
4345

@@ -67,7 +69,12 @@ py_libs="
6769
/usr/include/python3*
6870
/usr/lib/python3*
6971
/usr/lib64/python3*
70-
/usr/local/lib/python3*"
72+
/usr/local/lib/python3*
73+
74+
/usr/lib/python3*/*/seccomp.*.so
75+
/usr/lib/*/libseccomp.so*
76+
/usr/lib64/libseccomp.so*
77+
"
7178
git_libs="
7279
/usr/lib*/git-core
7380
"
@@ -84,7 +91,7 @@ ro_bind_extra="
8491
/etc/pki
8592
/etc/ssl
8693
/usr/share/pki*
87-
"
94+
"
8895

8996
collect="
9097
$collect
@@ -182,4 +189,5 @@ exec bwrap \
182189
--setenv HOME "$home" \
183190
--setenv USER "user" \
184191
--setenv VIRTUAL_ENV "$venv" \
192+
--setenv PYTHONPATH "$venv/sandbox:${PYTHONPATH-}" \
185193
"$@"

build.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ script_file="sandbox-venv.sh"
66
out="build/${script_file%.sh}"
77
{
88
cat "$script_file"
9-
appendix_cnt=1
10-
for appendix in _wrapper_pip _wrapper_exe; do
9+
i=1
10+
for appendix in _wrapper_pip.sh _wrapper_exe.sh sitecustomize.py; do
1111
printf '\n\n'
12-
echo "# CUT HERE ------------------- Appendix $appendix_cnt: sandbox-venv $appendix.sh script"
13-
cat "$appendix.sh"
14-
appendix_cnt=$((appendix_cnt + 1))
12+
echo "# CUT HERE ------------------- Appendix $i: sandbox-venv $appendix script"
13+
cat "$appendix"
14+
i=$((i + 1))
1515
done
1616
} > "$out"
1717
chmod +x "$out"

sandbox-venv.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,15 @@ wrap_all () (
8989
echo "$file"
9090
done
9191

92+
# Install $venv/bin/shell
9293
file="$(realpath "$bin/shell")"
9394
add_bin_shell "$file"
9495
chmod +x "$file"
9596
echo "$file"
97+
98+
# Install PYTHONPATH=$venv/sandbox with sitecustomize.py
99+
mkdir -p "$bin/../sandbox"
100+
extract_segment 3 "$@" > "$bin/../sandbox/sitecustomize.py"
96101
)
97102

98103
wrap_all "$@"

sitecustomize.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#!/usr/bin/python3
2+
"""
3+
Importing this module inits seccomp/pyseccomp, restricting allowed syscalls to those
4+
listed in SANDBOX_SECCOMP_ALLOW environment variable (or, by default, the list below).
5+
The module is imported automatically upon startup when on PYTHONPATH.
6+
7+
https://docs.python.org/3/library/site.html#module-sitecustomize
8+
"""
9+
import os
10+
import re
11+
import sys
12+
13+
# XXX: You can debug this list (e.g. update missing items) by
14+
# looking for EPERM in `strace -f` output.
15+
ALLOW_SYSCALLS = [
16+
# Process & signals
17+
"clone", "clone3", "fork", "vfork", "execve", "exit", "exit_group",
18+
"kill", "tgkill", "tkill", "wait4", "getpid", "getppid", "gettid",
19+
"prctl", "arch_prctl", "getpgrp",
20+
"pidfd_open", "pidfd_send_signal",
21+
"sigaction", "sigreturn",
22+
"rt_sigaction", "rt_sigreturn",
23+
"rt_sigprocmask", "rt_sigpending", "rt_sigsuspend",
24+
"sigaltstack", "rseq", "process_madvise",
25+
26+
# File I/O
27+
"open", "openat", "close", "close_range", "read", "write",
28+
"name_to_handle_at",
29+
"pread64", "pwrite64", "preadv", "pwritev", "preadv2", "pwritev2",
30+
"lseek",
31+
"stat", "statx", "fstat", "lstat", "fstatat64", "newfstatat",
32+
"stat64", "lstat64", "fstat64", "fadvise64",
33+
"faccessat2", "getdents", "getdents64", "access",
34+
"unlink", "unlinkat", "mkdir", "mkdirat", "rmdir",
35+
"rename", "renameat", "renameat2",
36+
"readlink", "readlinkat", "symlink", "symlinkat", "link", "linkat",
37+
"truncate", "ftruncate", "utime", "utimes", "futimesat", "utimensat",
38+
"chown", "fchown", "lchown", "fchmod", "fchmodat", "chmod", "mknod", "mknodat",
39+
"getxattr", "setxattr", "listxattr", "removexattr",
40+
41+
# fd ops
42+
"dup", "dup2", "dup3", "pipe", "pipe2",
43+
"fcntl", "flock", "fsync", "fdatasync",
44+
"readahead",
45+
"splice", "tee", "vmsplice",
46+
47+
# mmap / mem / threads
48+
"brk", "mmap", "mmap2", "munmap", "mremap", "mprotect", "madvise", "mincore",
49+
"futex", "futex_time64", "futex_waitv", "set_tid_address", "set_robust_list", "get_robust_list",
50+
"sched_yield", "sched_getaffinity",
51+
"mlock", "munlock", "mlockall", "munlockall",
52+
"get_mempolicy", "set_mempolicy", "mbind", "migrate_pages", "move_pages",
53+
"membarrier",
54+
55+
# Time / clocks
56+
"nanosleep", "clock_gettime", "clock_getres", "gettimeofday", "time",
57+
"clock_nanosleep", "clock_gettime64",
58+
"timer_create", "timer_settime", "timer_gettime", "timer_delete",
59+
"setitimer", "getitimer", "times",
60+
"timerfd_create", "timerfd_settime", "timerfd_gettime",
61+
62+
# Networking
63+
"socket", "socketpair", "socketcall", "bind", "listen",
64+
"accept", "accept4", "connect",
65+
"getsockname", "getpeername",
66+
"sendto", "recvfrom", "sendmsg", "recvmsg", "shutdown",
67+
"setsockopt", "getsockopt",
68+
"recvmmsg", "sendmmsg",
69+
"sendfile", "sendfile64",
70+
71+
# epoll/poll/select
72+
"epoll_create", "epoll_create1", "epoll_ctl", "epoll_wait",
73+
"epoll_pwait", "epoll_pwait2",
74+
"poll", "ppoll", "ppoll_time64", "select", "pselect6",
75+
"eventfd", "eventfd2",
76+
77+
# Descriptors / metadata
78+
"ioctl", "readv", "writev",
79+
"getcwd", "chdir", "fchdir",
80+
"statfs", "fstatfs", "statfs64", "fstatfs64",
81+
82+
# UIDs/GIDs (read + drop privileges)
83+
"getuid", "geteuid", "getgid", "getegid", "getgroups",
84+
"getresuid", "getresgid", "setresuid", "setresgid", "setreuid", "setregid",
85+
"setuid", "setgid",
86+
87+
# Limits / info
88+
"getrlimit", "setrlimit", "prlimit64", "uname", "sysinfo", "getrusage", "umask",
89+
90+
# Random
91+
"getrandom",
92+
93+
# Misc
94+
"seccomp", "capget",
95+
"shmget", "shmat", "shmdt", "shmctl",
96+
]
97+
98+
99+
try:
100+
allow_syscalls = re.findall(r'\w+', os.environ['SANDBOX_SECCOMP_ALLOW'])
101+
except KeyError:
102+
allow_syscalls = ALLOW_SYSCALLS
103+
104+
if allow_syscalls:
105+
try:
106+
import seccomp
107+
except ImportError:
108+
try:
109+
import pyseccomp as seccomp
110+
except ImportError:
111+
seccomp = None
112+
if sys.platform.startswith('linux'):
113+
print("sandbox-venv/seccomp: Python package 'seccomp' (or 'pyseccomp') "
114+
"not available. If you want seccomp support, apt install python3-seccomp "
115+
"(requires venv created with --system-site-packages) "
116+
"or pip install pyseccomp.", file=sys.stderr)
117+
if seccomp:
118+
print(f'sandbox-venv/seccomp: allowing {len(allow_syscalls)} syscalls', file=sys.stderr)
119+
default_action = seccomp.ERRNO(seccomp.errno.EPERM) # EPERM, Operation not permitted
120+
filter = seccomp.SyscallFilter(default_action)
121+
for syscall in allow_syscalls:
122+
# print(syscall, file=sys.stderr)
123+
filter.add_rule(seccomp.ALLOW, syscall)
124+
filter.load()

tests/test-seccomp.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
. "${0%/*}/_init.sh"
5+
6+
sandbox-venv .venv
7+
8+
pip install pyseccomp
9+
10+
assert_have_seccomp () { "$@" 2>&1 | grep -q 'sandbox-venv/seccomp: allowing'; }
11+
12+
strace -f python -vvv -c 'import os'
13+
14+
printf '\n\n\n ALL OK ✅\n\n\n'

0 commit comments

Comments
 (0)