- 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 ```