blog

git clone https://git.ce9e.org/blog.git

commit
13c8939f981724e554fa3cd47775b59d6e20d585
parent
d560e68d628f2c30c16904b8c391495bb8f308cc
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2025-10-11 16:56
add post on terminal sandbox

Diffstat

A _content/posts/2025-10-11-terminal-sandbox/index.md 215 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

1 files changed, 215 insertions, 0 deletions


diff --git a/_content/posts/2025-10-11-terminal-sandbox/index.md b/_content/posts/2025-10-11-terminal-sandbox/index.md

@@ -0,0 +1,215 @@
   -1     1 ---
   -1     2 title: How to Sandbox a Terminal
   -1     3 date: 2025-10-11
   -1     4 tags: [linux, security]
   -1     5 description: "I often pull code from remote repositories and execute it for testing. This is an area where sandboxing would be extremely beneficial."
   -1     6 ---
   -1     7 
   -1     8 I do most of my work in terminals, using a multitude of small, composable
   -1     9 command line tools. During development, I often pull code from remote
   -1    10 repositories and execute it for testing. This is an area where sandboxing would
   -1    11 be extremely beneficial.
   -1    12 
   -1    13 The best option would probably be to explicitly use a sandboxing wrapper every
   -1    14 time I execute untrusted code. But that is also tedious. In this post, I want
   -1    15 to explore what we can restrict on the terminal as a whole.
   -1    16 
   -1    17 Restricting the terminal is tricky because it is a usually thought of as a
   -1    18 representation of the user. It should be able to do anything that the user can
   -1    19 do. On the other hand, we are already used to the idea that we cannot install
   -1    20 packages without using `sudo`, so maybe we can expand that concept.
   -1    21 
   -1    22 ## Tools at our Disposal
   -1    23 
   -1    24 The main mechanism I want to use for this are mount namespaces. They allow us
   -1    25 to run the terminal with a completely different file tree. However, I mostly
   -1    26 want to keep the existing tree and just hide some files, or make them
   -1    27 read-only. The tool that I am using for that is
   -1    28 [`bwrap`](https://github.com/containers/bubblewrap).
   -1    29 
   -1    30 Additionally, many services on a modern Linux desktop use DBus, which means
   -1    31 that they all share the same socket. So we also need
   -1    32 [`xdg-dbus-proxy`](https://github.com/flatpak/xdg-dbus-proxy) to control access
   -1    33 to individual services.
   -1    34 
   -1    35 To simplify the usage of both of these tools, I’ve written a wrapper called
   -1    36 [xiwrap](https://github.com/xi/xiwrap).
   -1    37 
   -1    38 ## Privilege Escalation
   -1    39 
   -1    40 We need to prevent processes from escaping the sandbox. There are two DBus
   -1    41 services on the session bus that allow unchecked privilege escalations:
   -1    42 `org.freedesktop.systemd1` (used by `systemd-run`) and
   -1    43 `org.freedesktop.portal.Flatpak` (used by `flatpak-spawn`). They should
   -1    44 definitely be blocked.
   -1    45 
   -1    46 On the other hand, we do want to have an escape hatch to allow users to do
   -1    47 things that would normally be prevented by the sandbox. Just like `sudo` allows
   -1    48 us to escalate our privileges by entering a password.
   -1    49 
   -1    50 `org.freedesktop.systemd1` on the system bus asks for a password for any
   -1    51 privileged action, so it is safe to expose. It even provides the
   -1    52 [`run0`](https://mastodon.social/@pid_eins/112353324518585654) command as a
   -1    53 drop-in for `sudo`. We can also use `run0 --user $USER` to escape the sandbox
   -1    54 without becoming root.
   -1    55 
   -1    56 I must admit that I didn't understand `run0` when it was first announced. But
   -1    57 now I really see the appeal of a privilege escalation with user interaction
   -1    58 that does not depend on a configuration file in the current mount namespace.[^2]
   -1    59 
   -1    60 Of course, a more low-tech option is to add just launch an unsandboxed
   -1    61 terminal.
   -1    62 
   -1    63 [^2]: Another, simpler implementation of this concept can be found in
   -1    64 	[s6](https://skarnet.org/software/s6/s6-sudo.html).
   -1    65 
   -1    66 ## Portals
   -1    67 
   -1    68 The DBus services `org.freedesktop.portal.Desktop` and
   -1    69 `org.freedesktop.portal.Documents`, collectively known as portals, are
   -1    70 specifically designed for sandboxing and are generally safe to use. Most
   -1    71 features they expose are not particularly relevant for terminals though. The
   -1    72 only interface I actually use is `org.freedesktop.portal.OpenURI` to open files
   -1    73 or links in the appropriate applications.
   -1    74 
   -1    75 Unfortunately, we are not at a point where portals are used by default. For
   -1    76 example, `xdg-open` will only use the portal if `$XDG_RUNTIME_DIR/flatpak-info`
   -1    77 exists. GTK applications behave differently depending on the contents of
   -1    78 `/.flatpak-info` and whether `GIO_USE_PORTALS` is set. I am still searching for
   -1    79 a setup that works correctly.
   -1    80 
   -1    81 This is mostly an issue for the terminal GUI itself though, because, as I said,
   -1    82 I rarely use portals from inside the terminal.
   -1    83 
   -1    84 ## Wayland, AT-SPI, Pulse
   -1    85 
   -1    86 The system's input and output rely on three main protocols:
   -1    87 
   -1    88 -	Wayland for video, typically via the socket `$XDG_RUNTIME_DIR/wayland-0`
   -1    89 -	Pipewire for audio, typically via the sockets `$XDG_RUNTIME_DIR/pipewire-0`
   -1    90 -	AT-SPI for accessibility, typically via the sockets in `$XDG_RUNTIME_DIR/at-spi`
   -1    91 
   -1    92 Unfortunately, these protocols do not clearly separate between clients
   -1    93 that provide data, and clients that consume data. For example, any client with
   -1    94 access to the respective sockets can access the accessibility trees of all
   -1    95 other clients, monitor the audio output of other clients, or access the
   -1    96 microphone.
   -1    97 
   -1    98 Wayland provides some isolation, but is not without flaws either. See [my
   -1    99 previous post](https://blog.ce9e.org/posts/2025-10-03-wayland-security/) for
   -1   100 details.[^1]
   -1   101 
   -1   102 There is not much we can do about this until the protocols are changed with
   -1   103 security in mind. Until then, I would recommend to not expose the AT-SPI socket
   -1   104 in the sandbox unless you need it.
   -1   105 
   -1   106 [^1]: Pipewire seems to have a [security
   -1   107 	contexts](https://docs.pipewire.org/structpw__security__context__methods.html#a78e54bfd81f8e41605152161f29ad166)
   -1   108 	extension that is closely modelled after that of Wayland.
   -1   109 
   -1   110 ## Read-only Path and Config
   -1   111 
   -1   112 Allowing untrusted code to install binaries or change configuration is
   -1   113 potentially dangerous. At the same time, this is rarely necessary during
   -1   114 normal operations. So mounting `~/.config/` and `~/.local/bin` read-only is a
   -1   115 quick win.
   -1   116 
   -1   117 ## Fixing SSH
   -1   118 
   -1   119 Due to the way user namespaces work, root is not mapped inside of the sandbox.
   -1   120 This breaks ssh, because it checks that its configuration files are owned by
   -1   121 root:
   -1   122 
   -1   123 ```
   -1   124 Bad owner or permissions on /etc/ssh/ssh_config.d/20-systemd-ssh-proxy.conf
   -1   125 ```
   -1   126 
   -1   127 As far as I can tell, `/etc/ssh/ssh_config.d/` does not contain anything
   -1   128 relevant on my system, so my workaround was simply to bind a tmpfs over it.
   -1   129 
   -1   130 ## Nesting
   -1   131 
   -1   132 The setup I am describing here is about applying restrictions on multiple
   -1   133 levels:
   -1   134 
   -1   135 -	The terminal as a whole is restricted
   -1   136 -	Within the terminal, I may use a sandboxing wrapper to execute untrusted code
   -1   137 -	That code may in turn apply restrictions to its child processes
   -1   138 
   -1   139 The kernel models namespaces as a tree structure, which works well with
   -1   140 nesting. Unfortunately, some user space protocols, notably
   -1   141 [DBus](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/281)
   -1   142 and [Wayland](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/281),
   -1   143 use a simpler model where only the app ID of the outermost sandbox is
   -1   144 considered. Portals also rely on app IDs to manage permissions.
   -1   145 
   -1   146 This model is fundamentally incompatible with the kind of nesting I have in
   -1   147 mind. But the community seems to be set on its current approach, so I have
   -1   148 little hope of improvement in this area.
   -1   149 
   -1   150 ## Conclusion
   -1   151 
   -1   152 Sandboxing on Linux is still incredibly bumpy. Many tools and protocols still
   -1   153 need to be adapted. And those that have been adapted often rely on
   -1   154 implementation details of Flatpak.
   -1   155 
   -1   156 Still, it is possible to cobble together a decent sandbox for the terminal as a
   -1   157 whole. I hope this post gave you some inspiration to experiment with your own
   -1   158 configuration!
   -1   159 
   -1   160 ## Annex: xiwrap config
   -1   161 
   -1   162 At the time of writing, this is the xiwrap config I am using:
   -1   163 
   -1   164 ```
   -1   165 setenv USER
   -1   166 setenv HOME
   -1   167 setenv XDG_RUNTIME_DIR
   -1   168 setenv PATH $HOME/.local/bin:/usr/games:/usr/bin
   -1   169 setenv SHELL
   -1   170 include locale
   -1   171 
   -1   172 ro-bind /usr
   -1   173 ro-bind /etc
   -1   174 ro-bind /var
   -1   175 
   -1   176 # usr-merge symlinks
   -1   177 ro-bind-try /bin
   -1   178 ro-bind-try /lib
   -1   179 ro-bind-try /lib32
   -1   180 ro-bind-try /lib64
   -1   181 
   -1   182 dev /dev
   -1   183 proc /proc
   -1   184 tmpfs /tmp
   -1   185 tmpfs $XDG_RUNTIME_DIR
   -1   186 
   -1   187 bind $HOME
   -1   188 ro-bind-try $HOME/.config
   -1   189 ro-bind-try $HOME/.dotfiles
   -1   190 ro-bind-try $HOME/.local/bin
   -1   191 
   -1   192 share-net
   -1   193 
   -1   194 ro-bind-try $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY
   -1   195 setenv WAYLAND_DISPLAY
   -1   196 setenv XDG_CURRENT_DESKTOP
   -1   197 setenv XDG_SEAT
   -1   198 setenv XDG_SESSION_CLASS
   -1   199 setenv XDG_SESSION_ID=1
   -1   200 setenv XDG_SESSION_TYPE
   -1   201 
   -1   202 ro-bind-try $XDG_RUNTIME_DIR/pipewire-0
   -1   203 
   -1   204 setenv DBUS_SESSION_BUS_ADDRESS
   -1   205 dbus-talk org.freedesktop.portal.Desktop
   -1   206 dbus-talk org.freedesktop.portal.Documents
   -1   207 
   -1   208 # allow to use run0 and systemctl
   -1   209 dbus-system-talk org.freedesktop.systemd1
   -1   210 dbus-system-talk org.freedesktop.login1
   -1   211 ro-bind /run/systemd
   -1   212 
   -1   213 # ignore ssh config with wrongly mapped owner
   -1   214 tmpfs /etc/ssh/ssh_config.d
   -1   215 ```