SYDTUTORIAL(7) Miscellaneous Information Manual SYDTUTORIAL(7)

sydtutorial - A tutorial introduction to Syd

syd *

This tutorial explains how to sandbox applications using Syd, write sandbox profiles, and configure Syd at runtime from within the sandbox. If you are instead primarily interested in using Syd as a package build sandbox, like we do at Exherbo Linux, you may prefer to start with syd(2) and the "paludis" profile whose rules you may list using "syd-cat -p paludis".

Syd is secure by default and highly configurable for your application's usecase. As we go towards the steps you are going to learn how to restrict an application in various ways and at the same time keep the sandbox flexible for cases where restriction is not possible and/or needed. To make the most out of this tutorial, you are recommended to pick an application whose systemic functionality is known to you and try and sandbox this application similar to the instructions in the respective chapter. This functionality, above all, includes the system calls the process calls to interact with the Linux kernel and which parts of the filesystem/network the application needs to access to fulfill its functionality correctly. bpftrace(1) and strace(1) are your friends. In a further chapter we'll also get to know pandora(1) which is a tool to generate Syd profiles automatically for a given application, stay tuned!

In its simplest sense, you can think Syd as a proxy between the Linux kernel and the sandbox process: Syd checks system call arguments for access and if access is granted Syd will execute the system call on behalf of the sandbox process and return the result to the sandbox process. Going forward this is important to keep in mind: from the point of view of the Linux kernel, it's one of Syd's syscall handler threads that's running the syscall not the sandbox process. This is necessary to achieve a Time-of-check-to-Time-of-use free sandbox. Syd does their best to reduce the side-effects, e.g. with ls /proc/self, the sandbox process will still see their own process ID, not Syd's.

1.
Learn by doing: Trace your applications, learn the ins-and-outs!
2.
Experiment: Tweak Syd in various different ways and observe the effects!
3.
Make it a game: Try and break the own sandbox profile you configured, then make it stricter and retry!

You have the following alternatives:

1.
Use the latest release binary located @ https://distfiles.exherbolinux.org/#sydbox/
2.
cave resolve sys-apps/sydbox:3 # if Exherbo (unmask with testing keyword)
3.
emerge sys-apps/syd # if Gentoo
4.
cargo install syd # You will not get the manual pages, check: https://man.exherbolinux.org
5.
Take the time to package Syd for your Linux distribution and spread the love!

Note, releases are signed with this PGP key https://keybase.io/alip/pgp_keys.asc, so take the time to verify the tarball you downloaded. If using cargo to install, you need to install the "libseccomp" library manually. This is a relatively common library and it's packaged by almost all Linux distributions these days. Two things to keep in mind:

1.
Install libseccomp development headers (usually included or comes with e.g. the package libseccomp-devel).
2.
Install libseccomp static libraries if you want to link Syd statically (usually included or comes with e.g. the package libseccomp-static).

One final note, at the time of writing with libseccomp version 2.5.5, a patched libseccomp is required to make interrupts work correctly under Syd (libseccomp.git has support for the new Linux kernel flag already, we also add a patch to set it by default). The binary release is built with a patched libseccomp and Exherbo source build patches the libseccomp package during preparation phase. Note, in our experience, this bug is mostly noticable when you sandbox applications written in the Go language. Otherwise, you'll rarely notice it with the latest libseccomp release version. For reference, the patchset resides here: https://gitlab.exherbo.org/exherbo/arbor/-/tree/master/packages/sys-libs/libseccomp/files

First, if you run Syd without arguments, you'll silently drop into a new shell. This is because Syd is designed to act as a login shell and in this case it uses the "user" sandbox profile. We'll get to profiles at a later chapter but if you're curious do "syd-cat -p user | less" and read through the rules.

Second, Syd is secure by default and allows you to construct a sandbox to your applications' needs. Here is how the state of the sandbox looks before we pass any options to Syd:

$ syd -mstat
syd:
Process ID: 0
Lock: None
Capabilities: Read, Stat, Write, Execute, Connect, Bind
Options:
Memory Max: 134217728
Virtual Memory Max: 4294967296
Pid Max: 128
SegvGuard Max Crashes: 5
SegvGuard Expiry: 120 seconds
SegvGuard Suspension: 600 seconds
Allowed UID Transitions: (total: 0, source -> target)
Allowed GID Transitions: (total: 0, source -> target)
Cidr Rules: (total 0, highest precedence first)
Glob Rules: (total 0, highest precedence first)
Mask Rules: (total 1)
1. Pattern: /proc/cmdline
Force Rules: (total 0, default action: Kill)
$

For now let's just take into attention the "Capabilities" line. These are the sandboxing types that are enabled at startup by default.

Initially, we'll do the bare minimum and try to execute a statically linked binary under Syd. busybox(1) is a handy tool for our experiment:

$ file $(which busybox)
/usr/host/bin/busybox: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
$ syd busybox true
syd: exec error: No such file or directory
$ echo $?
2
$ syd-err 2
2       ENOENT  No such file or directory
$

We get an error that the path does not exist. This is because "Stat Sandboxing" is on by default and the path to the busybox(1) binary is hidden. We can see that Syd makes clear by its exit value which error caused the execution to fail. We use the utility syd-err(1), one of the many utilities that come with syd(1), to look up the error definition by the exit code.

Let's try to allow and retry:

$ syd -m'allow/stat+/usr/host/bin/busybox' busybox true
syd: exec error: No such file or directory
$

No luck, we get the same error. This is because the path we specified to "allow/stat" is not a canonicalised path. A canonicalised path is a path which begins with "/" and has neither "." nor ".." nor repeating slashes nor any symbolic links in any of its path components. Let's find out the canonicalised path to our busybox(1) binary and retry with it.

$ readlink -f /usr/host/bin/busybox
/usr/x86_64-pc-linux-musl/bin/busybox
$ syd -m'allow/stat+/usr/x86_64-pc-linux-musl/bin/busybox' busybox true
{"act":"Deny","cap":"x","ctx":"access","id":"nostalgic_black","l":2,"path":"/usr/x86_64-pc-linux-musl/bin/busybox","pid":2602591,"sys":"execve","uid":1000,...}
syd: exec error: Permission denied
$ echo $?
13
$ syd-err 13
13      EACCES  Permission denied
$

We get an error again, but this time we have context. Since Stat Sandboxing is about hiding paths, reporting access violations about it on standard error would beat its purpose so Syd was quiet. However, this time we see "Exec Sandboxing" at play and Syd gives us details about the access violation. The format is JSON lines. It may be hard to read at first but the fact that it's easily parseable allows you to easily search for Syd access violation logs in your system log and filter using tools such as jq(1). Note, every access violation, and in general every log entry with a "l"evel 1 (= error), and 2 (= warn) go to syslog(3) too. For systems with journalctl(1) the helper syd-log(1) is provided. As an exercise, you are recommended to play with "syd-log | jq <args>" and get a feel for the format.

Back to the task, for now let's briefly observe that this was an access violation ("ctx":"access") about the execve(2) system call ("sys":"execve"). The access violation is of category Exec ("cap":"x") and the target path is "/usr/x86_64-pc-linux-musl/bin/busybox". The decision was to deny the system call ("act":"Deny"). We also have useful metadata such as the process ID ("pid") and the user ID ("uid") executing the offending system call. The "id" field is a human-readable name generated from the "pid" field to make logs easier to follow. There are more information in the omitted fields, it's recommended that you take a look at a complete access violation log entry on your own and make note of the fields that are of value to you. Let's this time allow our busybox(1) binary for exec and retry:

$ syd -m'allow/exec,stat+/usr/x86_64-pc-linux-musl/bin/busybox' busybox true
$ echo $?
$ 0

Task accomplished! Note, how we used the short notation "allow/exec,stat+/path" which is a convenient way to pass -m "allow/exec+/path" -m "allow/stat+/path" as a single rule.

Now let's try again with a dynamically linked executable and figure out what we have to add to make it work. This time we will use the gtrue(1) utility from the GNU coreutils project which is dynamically linked on this system:

$ file $(which gtrue)
/usr/host/bin/gtrue: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /usr/x86_64-pc-linux-musl/lib/ld-musl-x86_64.so.1, stripped
$ lddtree $(which gtrue)
/usr/x86_64-pc-linux-musl/lib/ld-musl-x86_64.so.1 => /usr/x86_64-pc-linux-musl/lib/libc.so
libc.so => /usr/x86_64-pc-linux-musl/lib/libc.so
$ readlink -f $(which gtrue)
/usr/x86_64-pc-linux-musl/bin/gtrue
$ syd -m'allow/exec,stat+/usr/x86_64-pc-linux-musl/bin/gtrue' gtrue
{"act":"Kill","cap":"x","ctx":"access","id":"compassionate_spence","l":2,"path":"/usr/x86_64-pc-linux-musl/lib/libc.so","pid":2601331,"sys":"exec","uid":1000,...}
$ echo $?
137
$

Observing the offending path of the new access violation, we understand libc.so is denied execution access. We can also observe, this time Syd has terminated the process ("act":"Kill") rather than denying access to the system call ("act":"Deny"). This is also evident from the exit code which is 137 = 128 + 9 where 9 is the value of the signal "SIGKILL". The deny/kill distinction stems from Syd internals and is not significant for us at this point. Suffice it to say in both cases the execution has been stopped before any code of the target binary had a chance to run.

During access check for Exec Sandboxing, Syd treats dynamically linked executables and their tree of dynamic library dependencies as a single unit. In that sense "allow/read+/path/to/libc.so" and "allow/exec+/path/to/libc.so" serves two different purposes: the former allows you to literally read the contents of the file whilst the latter allows you to load the file into memory as part of an executable.

Having clarified that, let's allow libc.so and retry:

$ syd -m'allow/exec,stat+/usr/x86_64-pc-linux-musl/bin/gtrue' -m 'allow/exec+/usr/x86_64-pc-linux-musl/lib/libc.so' gtrue
$ echo $?
0
$

Task accomplished! Curious reader will recognise we did not have to add an "allow/stat" clause for "libc.so". This is because the concepts of Stat Sandboxing and Path Hiding pertain specifically to direct access to file paths. Loading libraries into memory is part of the execution process and is therefore only subject to Exec Sandboxing (and Force Sandboxing, aka Binary Verification, which we'll talk more about later).

Now at the third step, let's generalise our small sandbox such that it will allow whichever version of the true(1) binary we execute, moreover it will also allow the execution of any other coreutils utility prefixed with "g*". We also do not want to worry if "libc.so" has a version suffix and want to allow all libraries under the common library paths without having to list them one by one. To achieve all this we're going to use glob(3) patterns:

$ eclectic coreutils list
Available providers for coreutils:
[1]   gnu
[2]   busybox *
$ readlink -f /bin/true
/usr/x86_64-pc-linux-musl/bin/busybox
$ syd -m'allow/stat,exec+/usr/**/bin/{busybox,g*}' -m 'allow/exec+/usr/**/lib*/*.so*' true
$ echo $?
0
$ doas eclectic coreutils set -1
$ readlink -f /bin/true
/usr/x86_64-pc-linux-musl/bin/gtrue
$ syd -m'allow/stat,exec+/usr/**/bin/{busybox,g*}' -m 'allow/exec+/usr/**/lib*/*.so*' true
$ echo $?
0
$

We have seen how glob(3) patterns make life easy for us in configuring our sandbox. We have seen using "**" is possible to match recursively and alternates of the form "{foo,bar}" are supported. Syd also supports empty alternates of the form "foo/{bar/,}baz" and the triple star extension, ie "foo/***" is equivalent to the combination of the two patterns "foo" and "foo/**". Finally we can see we managed to allow a lot more using the same number of rules. Syd has many more powerful features that makes rule editing simple and efficient such as:

  • You may specify denylisted paths with "deny/" in addition to "allow/".
  • You may specify filtered paths with "filter/", similar to "deny/" and "allow/" to quiet access violations but still deny access.
  • If more than one rule matches the target path, the last matching rule wins.
  • Many rules may be assembled into a configuration file and passed to Syd with -P<path>.
  • Files having common rulesets can be included from other configuration files using the "include <path>" clause.
  • Relative paths in "include" clauses are canonicalised based on the parent directory of the current configuration file (not the current working directory!).
  • Environment variables are expanded in configuration files. Unset environment variables will cause an error.
  • Configuration can be locked at any point with the "lock:on" clause preventing further edits to the sandbox.

At this point you're highly recommended to experiment with configuring Syd. Do not be afraid to add as many rules as you like. Internally, Syd keeps glob(3) patterns as globsets and compiles them into a single regular expression for efficient matching. This offers acceptable performance up to roughly 10k rules on my system, your mileage may vary.

We have taken a sneak peek at how to configure Syd path allowlists. This is similar for other sandboxing types. Let's leave those for later and explore another way of configuring Syd. This time we'll do it at runtime, from within the sandbox. It may come as a shock from a security perspective to allow access to the sandbox policy from within the sandbox but Syd has a fair set of restrictions to provide this usecase securely and as we'll see later this gives the chance to restrict the sandbox process even further. Another alternative is to make Syd load a dynamic library at startup rather than running a command which is another advanced topic for later. The idea of runtime configuration depends on the Sandbox Lock and the lock can have three states: "on", "off", and "exec". The first two are self-explanatory while "exec", allows access to the sandbox policy only for the initial sandbox process. Once the sandbox lock is set to "on", there is no turning back so subsequent edits to the sandbox will no longer be possible. Now let's execute a shell under Syd. This time we will not submit any configuration at startup and run Syd without arguments. This is going to put Syd into login mode when Syd will use the builtin, dynamic "user" profile and spawn a shell. We will not delve into details of the user profile for now, check out "syd-cat -p user" if you're curious. Suffice it to say it provides a relatively safe set of access rules to system paths and read+write access to your HOME directory and user "/run"time paths. In addition, Syd comes with a shell library, called "esyd", that makes Syd interaction easier:

TODO

Maintained by Ali Polatel. Up-to-date sources can be found at https://gitlab.exherbo.org/sydbox/sydbox.git and bugs/patches can be submitted to https://gitlab.exherbo.org/groups/sydbox/-/issues. Discuss in #sydbox on Libera Chat.

2025-02-14