跳至內容

Bubblewrap/示例

出自 Arch Linux 中文维基

dhcpcd

[編輯 | 編輯原始碼]

創建一個簡單的 dhcpcd 沙箱:

  • 確定可用的內核命名空間(kernel namespaces):
$ ls /proc/self/ns 
cgroup  ipc  mnt  net  pid  uts
注意:缺少 user 意味著內核以 CONFIG_USER_NS=n 參數構建或是被限制了用戶命名空間(user namespace)。
  • 將宿主的整個 / 目錄以讀寫模式綁定到沙箱中的 /
  • 將一個新的 devtmpfs 文件系統掛載到沙箱中的 /dev
  • 創建一個新的 IPC(進程間通信)cgroups(控制組) 命名空間。
  • 創建一個新的 UTS 命名空間並將 dhcpcd 設置為主機名(hostname)。
# /usr/bin/bwrap --bind / / --dev /dev --unshare-ipc --unshare-cgroup --unshare-uts --hostname dhcpcd /usr/bin/dhcpcd -q -b

Unbound

[編輯 | 編輯原始碼]

創建一個稍顆粒化和複雜的 Unbound 沙箱:

  • 將宿主系統的 /usr 目錄以只讀模式綁定到沙箱中的 /usr
  • 將宿主系統的 /usr/lib 目錄軟連結到沙箱中的 /lib64
  • 將宿主系統的 /etc 目錄以只讀模式綁定到沙箱中的 /etc
  • 在沙箱中創建空的 /var/run 目錄。
  • 將一個新的 devtmpfs 文件系統掛載到沙箱中的 /dev
  • 創建一個新的 IPC(進程間通信)和 PID(進程標識符) 以及控制組命名空間(control group namespaces)。
  • 創建一個新的 UTS 命名空間並將 unbound 設置為主機名(hostname)。
# /usr/bin/bwrap --ro-bind /usr /usr --symlink usr/lib /lib64 --ro-bind /etc /etc --dir /var --dir /run --dev /dev --unshare-ipc --unshare-pid --unshare-cgroup --unshare-uts --hostname unbound /usr/bin/unbound -d
提示:參見 systemd#修改現存單元文件以了解如何啟用 systemd 單元文件的 bubblewrapping,包括 unbound.service

本文或本章節的事實準確性存在爭議。

原因: 轉發 X11 socket 可能導致沙箱逃逸。(在 Talk:Bubblewrap/示例 中討論)


在 shell 包裝器(wrapper)中創建環境時,能體現出 bwrap 的強大和靈活性:

  • 將宿主系統的 /usr/bin 目錄以只讀模式綁定到沙箱中的 /usr/bin
  • 將宿主系統的 /usr/lib 目錄以只讀模式綁定到沙箱中的 /usr/lib
  • 將宿主系統的 /usr/lib 目錄軟連結到沙箱中的 /lib64
  • 在沙箱中創建一個 tmpfs 文件系統,覆蓋 /usr/lib/gcc
    • 這麼做可以有效地阻止 /usr/lib/gcc 中的內容出現在沙箱中。
  • 在沙箱中創建一個新的 tmpfs 文件系統,作為 $HOME 目錄。
  • .Xauthority 文件和「Documents」目錄以只讀模式綁定到沙箱。
    • 這麼做可以有效且遞歸地允許訪問 .Xauthority 文件和「Documents」目錄。
  • 在沙箱中創建一個新的 tmpfs 文件系統,作為 /tmp 目錄。
  • X11 socket 以只讀模式綁定到沙箱中以允許訪問。
  • 複製並創建用於容納宿主內核所支持的所有命名空間的私有容器(private containers)。
    • 如果內核不支持非特權的(non-privileged)用戶命名空間,跳過創建過程並繼續操作。
  • 勿將網絡組件放置到私有命名空間內。
    • 這麼做可以允許聯網訪問 URI 超連結。
#!/bin/sh
#~/bwrap/mupdf.sh
(exec bwrap \
--ro-bind /usr/bin /usr/bin \
--ro-bind /usr/lib /usr/lib \
--symlink usr/lib /lib64 \
--tmpfs /usr/lib/gcc \
--tmpfs $HOME \
--ro-bind $HOME/.Xauthority $HOME/.Xauthority \
--ro-bind $HOME/Documents $HOME/Documents \
--tmpfs /tmp \
--ro-bind /tmp/.X11-unix/X0 /tmp/.X11-unix/X0 \
 --unshare-all \
--share-net \
/usr/bin/mupdf "$@")
提示:使用 /usr/bin/sh 執行作為已存在的可執行項目的替代的命令包裝器以調試和驗證沙箱中的內容和文件系統結構。
$ bwrap \
--ro-bind /usr/bin /usr/bin \
--ro-bind /usr/lib /usr/lib \
--symlink usr/lib /lib64 \
--tmpfs /usr/lib/gcc \
--tmpfs $HOME \
--ro-bind $HOME/.Xauthority $HOME/.Xauthority \
--ro-bind $HOME/Desktop $HOME/Desktop \
--tmpfs /tmp \
--ro-bind /tmp/.X11-unix/X0 /tmp/.X11-unix/X0 \
--unshare-all \
--share-net \
 /usr/bin/sh
bash-4.4$ ls -AF
.Xauthority  Documents/

或許在構建一個被 bubblewrap 的文件系統時需考慮到的最重要的規則是:「命令應該按出現的順序依次執行」。 根據上面的 MuPDF 示例:

  • 首先創建了一個 tmpfs 文件系統,其次是綁定並掛載 .Xauthority 文件和「Documents」目錄:
--tmpfs $HOME \
--ro-bind $HOME/.Xauthority $HOME/.Xauthority \
--ro-bind $HOME/Documents $HOME/Documents \
bash-4.4$ ls -a
.  ..  .Xauthority  Desktop
  • 如果在綁定掛載 .Xauthority 後再創建 tmpfs 文件系統,結果將覆蓋先前文件,因此沙箱中只能夠找到「Documents」這個目錄:
--ro-bind $HOME/.Xauthority $HOME/.Xauthority \
--tmpfs $HOME \
--ro-bind $HOME/Desktop $HOME/Desktop \
bash-4.4$ ls -a
.  ..  Desktop

還未對已知漏洞 進行修補的應用程式是 bubblewrap 的主要候選對象:

  • 將宿主系統中的 /usr/bin/7za 可執行文件路徑以只讀模式綁定到沙箱中。
  • 將宿主系統的 /usr/lib 目錄軟連結到沙箱中的 /lib64
  • 用 tmpfs 文件系統覆蓋以禁用沙箱中 /usr/lib/modules/usr/lib/systemd 的內容。
  • 將一個新的 devtmpfs 文件系統掛載到沙箱中的 /dev
  • 將宿主系統的 /sandbox 目錄以讀寫模式綁定到沙箱中的 /sandbox
    • 7za 僅會在宿主的 /sandbox 目錄中運行,或在被 shell 包裝器調用時,在其子文件夾中運行。
  • 為應用程式及其進程創建新的 cgroup、IPC、network、PID 和 UTS 命名空間。
    • 如果內核不支持非特權的用戶命名空間,跳過創建過程並繼續操作。
    • 新創建的 network 命名空間將阻止沙箱獲取網絡訪問權限。
  • 為沙箱設置計算機名,例如 p7zip
  • 取消設置 XAUTHORITY 環境變量以隱藏 X11 conection cookie 的位置。
    • 7za 正常運行時不需要連接到 X11 顯示服務。
  • 啟動一個新的終端會話,防止鍵盤輸入從沙箱中逃逸。
#!/bin/sh
#~/bwrap/pz7ip.sh
(exec bwrap \
--ro-bind /usr/bin/7za /usr/bin/7za \
--symlink usr/lib /lib64 \
--tmpfs /usr/lib/modules \
--tmpfs /usr/lib/systemd \
--dev /dev \
--bind /sandbox /sandbox \
--unshare-all \
--hostname p7zip \
--unsetenv XAUTHORITY \
--new-session \
/usr/bin/7za "$@")
注意:/usr/bin/sh/usr/bin/ls 必須在可執行文件路徑中才能遍歷和驗證沙箱的文件系統。
bwrap \
--ro-bind /usr/bin/7za /usr/bin/7za \
--ro-bind /usr/bin/ls /usr/bin/ls \
--ro-bind /usr/bin/sh /usr/bin/sh \
--symlink usr/lib /lib64 \
--tmpfs /usr/lib/modules \
--tmpfs /usr/lib/systemd \
--dev /dev \
--bind /sandbox /sandbox \
--unshare-all \
--hostname p7zip \
--unsetenv XAUTHORITY \
--new-session \
/usr/bin/sh
bash: no job control in this shell
bash-4.4$ ls -AF         
dev/  lib64@  usr/
bash-4.4$ ls -l /usr/lib/modules 
total 0
bash-4.4$ ls -l /usr/lib/systemd
total 0
bash-4.4$ ls -AF /dev
console  full  null  ptmx@  pts/  random  shm/  stderr@  stdin@  stdout@  tty  urandom  zero
bash-4.4$ ls -A /usr/bin
7za  ls  sh

Firefox

[編輯 | 編輯原始碼]

面向網絡且擁有巨大的潛在被攻擊面的應用程式同樣是 bubblewrap 的理想候選對象:

  • Transmission 包含在沙箱中以響應磁力連結和種子連結並啟動。
  • 示例的 wrap 在GNOMEWayland)下支持音頻(PulseAudio)和列印服務(CUPS/Avahi)。
    • ~/.config/transmission/settings.json 中的 PATH 應反映 --setenv HOME 變量。
  • 環境中的按鍵綁定不支持變量擴展,因此使用了完整路徑。
  • 同時還支持 WebRenderer 和硬體加速混成。
 bwrap \
--symlink usr/lib /lib \
--symlink usr/lib64 /lib64 \
--symlink usr/bin /bin \
--symlink usr/bin /sbin \
--ro-bind /usr/lib /usr/lib \
--ro-bind /usr/lib64 /usr/lib64 \
--ro-bind /usr/bin /usr/bin \
--ro-bind /usr/lib/firefox /usr/lib/firefox \
--ro-bind /usr/share/applications /usr/share/applications \
--ro-bind /usr/share/gtk-3.0 /usr/share/gtk-3.0 \
--ro-bind /usr/share/fontconfig /usr/share/fontconfig \
--ro-bind /usr/share/icu /usr/share/icu \
--ro-bind /usr/share/drirc.d /usr/share/drirc.d \
--ro-bind /usr/share/fonts /usr/share/fonts \
--ro-bind /usr/share/glib-2.0 /usr/share/glib-2.0 \
--ro-bind /usr/share/glvnd /usr/share/glvnd \
--ro-bind /usr/share/icons /usr/share/icons \
--ro-bind /usr/share/libdrm /usr/share/libdrm \
--ro-bind /usr/share/mime /usr/share/mime \
--ro-bind /usr/share/X11/xkb /usr/share/X11/xkb \
--ro-bind /usr/share/icons /usr/share/icons \
--ro-bind /usr/share/mime /usr/share/mime \
--ro-bind /etc/fonts /etc/fonts \
--ro-bind /etc/resolv.conf /etc/resolv.conf \
--ro-bind /usr/share/ca-certificates /usr/share/ca-certificates \
--ro-bind /etc/ssl /etc/ssl \
--ro-bind /etc/ca-certificates /etc/ca-certificates \
--dir "$XDG_RUNTIME_DIR" \
--ro-bind "$XDG_RUNTIME_DIR/pulse" "$XDG_RUNTIME_DIR/pulse" \
--ro-bind "$XDG_RUNTIME_DIR/wayland-1" "$XDG_RUNTIME_DIR/wayland-1" \
--dev /dev \
--dev-bind /dev/dri /dev/dri \
--ro-bind /sys/dev/char /sys/dev/char \
--ro-bind /sys/devices/pci0000:00 /sys/devices/pci0000:00 \
--proc /proc \
--tmpfs /tmp \
--bind /home/example/.mozilla /home/example/.mozilla \
--bind /home/example/.config/transmission /home/example/.config/transmission \
--bind /home/example/Downloads /home/example/Downloads \
--setenv HOME /home/example \
--setenv GTK_THEME Adwaita:dark \
--setenv MOZ_ENABLE_WAYLAND 1 \
--setenv PATH /usr/bin \
--hostname RESTRICTED \
--unshare-all \
--share-net \
--die-with-parent \
--new-session \
/usr/bin/firefox

增強隱私保護

[編輯 | 編輯原始碼]
  • 移除特定條目可以增加訪問限制。
    • 移除下面的條目可以取消音頻支持:
--ro-bind "$XDG_RUNTIME_DIR/pulse" "$XDG_RUNTIME_DIR/pulse" \
  • /sandbox 作為用戶所定義的目錄,並沒有特殊含義,僅用於存放所需的個人資料信息。
$ cp -pR ~/.mozilla /sandbox/

文件位置可以來自網絡共享、USB 掛載設備,或是來自本地文件系統,甚至是 ramfs 或 tmpfs

  • 設置 /home/r 以隱藏真實的 /home/example
  • 設置新的用戶標識符和組標識符。
注意:確保新的用戶標識符(UID)和組標識符(GID)不會與 /etc/passwd/etc/groups 中已存在的值相衝突。
bwrap \
....
--bind /sandbox/.mozilla /home/r/.mozilla \
--bind /sandbox/Downloads /home/r/Downloads \
...
--setenv HOME /home/r \
...
--uid 200 --gid 400 \
...
/usr/bin/firefox --no-remote --private-window

Chromium

[編輯 | 編輯原始碼]

下面是 Wayland 下運行 Chromium 沙箱的簡單示例:

bwrap \
    --symlink usr/lib /lib \
    --symlink usr/lib64 /lib64 \
    --symlink usr/bin /bin \
    --symlink usr/bin /sbin \
    --ro-bind /usr/lib /usr/lib \
    --ro-bind /usr/lib64 /usr/lib64 \
    --ro-bind /usr/bin /usr/bin \
    --ro-bind /etc /etc \
    --ro-bind /usr/lib/chromium /usr/lib/chromium \
    --ro-bind /usr/share /usr/share \
    --dev /dev \
    --dev-bind /dev/dri /dev/dri \
    --proc /proc \
    --ro-bind /sys/dev/char /sys/dev/char \
    --ro-bind /sys/devices /sys/devices \
    --ro-bind /run/dbus /run/dbus \
    --dir "$XDG_RUNTIME_DIR" \
    --ro-bind "$XDG_RUNTIME_DIR/wayland-1" "$XDG_RUNTIME_DIR/wayland-1" \
    --ro-bind "$XDG_RUNTIME_DIR/pipewire-0" "$XDG_RUNTIME_DIR/pipewire-0" \
    --ro-bind "$XDG_RUNTIME_DIR/pulse" "$XDG_RUNTIME_DIR/pulse" \
    --tmpfs /tmp \
    --dir $HOME/.cache \
    --bind $HOME/.config/chromium $HOME/.config/chromium \
    --bind $HOME/Downloads $HOME/Downloads \
    /usr/bin/chromium --enable-features=UseOzonePlatform --ozone-platform=wayland
警告:如果用戶正在使用 linux-hardened 內核,由於 kernel.unprivileged_userns_clone sysctl 已被設置為 0,會無法實現沙箱運行 chromium。用戶可以自行設置為 1,但不建議這麼做(FS#36969)。

一種解決方法是讓 chromium 使用 bubblewrap 創建的命名空間。可以用 zypakAUR 實現,這個方法同樣被 flatpak 用於在一個額外命名空間中運行基於 electron 的應用程式。這個連結是有關如何使用 zypak 運行 chromium/electron 的示例代碼。

  • PipeWire--ro-bind "$XDG_RUNTIME_DIR/pipewire-0" "$XDG_RUNTIME_DIR/pipewire-0" \
    • 不使用 pipewire 的用戶可移除此行內容。
  • --bind $HOME/.config/chromium $HOME/.config/chromium \ 將用戶的 chromium 配置目錄以讀寫模式掛載到沙箱中。
  • --bind $HOME/Downloads $HOME/Downloads \ 將用戶的「~/Downloads」目錄以讀寫模式掛載到沙箱中。
  • 通過增加更多隔離處理可優化本例。

本文或本章節的事實準確性存在爭議。

原因: 轉發 X11 可能導致沙箱逃逸。(在 Talk:Bubblewrap/示例 中討論)


Steam 正常運行時需要訪問 D-Bus。

下面的示例使用 xdg-dbus-proxy 將有限的宿主系統 bus 訪問權限暴露給沙箱。一旦 bwrap 退出,此 proxy 將被一同終止。

steam.py
#!/usr/bin/env python3
from glob import glob
import os
from pathlib import Path
import subprocess

HOME = Path.home()
XDG_RUNTIME_DIR = Path(os.getenv("XDG_RUNTIME_DIR", "/tmp"))
XDG_CACHE_HOME = Path(os.getenv("XDG_CACHE_HOME", HOME / ".cache"))
XDG_DATA_HOME = Path(os.getenv("XDG_DATA_HOME", HOME / ".local/share"))
DBUS_PROXY_PATH = XDG_RUNTIME_DIR / "bus-proxy"
DBUS_PROXY_PATH.mkdir(exist_ok=True, parents=True)

# 
STEAM_HOME = XDG_DATA_HOME / "steam_home"
STEAM_HOME.mkdir(exist_ok=True, parents=True)

# xdg-dbus-proxy
r, w = os.pipe()
session_bus_proxy = DBUS_PROXY_PATH / str(os.getpid())
system_bus_proxy = DBUS_PROXY_PATH / f"{os.getpid()}-system"
subprocess.Popen([
    "/usr/bin/xdg-dbus-proxy",
    f"--fd={w}",
    # session bus
    os.environ["DBUS_SESSION_BUS_ADDRESS"],
    str(session_bus_proxy),
    "--filter",
    "--own=com.steampowered.*",
    "--talk=org.freedesktop.portal.*",
    "--talk=org.gnome.SettingsDaemon.MediaKeys",
    "--talk=org.kde.StatusNotifierWatcher",
    "--talk=org.freedesktop.ScreenSaver",
    "--talk=org.freedesktop.PowerManagement",
    "--talk=org.freedesktop.Notifications",
    # systrem bus
    "unix:path=/run/dbus/system_bus_socket",
    str(system_bus_proxy),
    "--filter",
    "--talk=org.freedesktop.UPower",
    "--talk=org.freedesktop.UDisks2",  # used by wine
], pass_fds=[w])
# wait xdg-dbus-proxy to start
os.read(r, 1)
os.set_inheritable(r, True)

# bwrap
argv: list[str] = [
    "bwrap",
    "--bind", str(STEAM_HOME), str(HOME),
    "--proc", "/proc",
    "--dev", "/dev",
    "--dir", "/tmp",
    "--unshare-cgroup-try",
    "--unshare-pid",
    "--unshare-user-try",
    "--unshare-uts",
    "--die-with-parent",
    "--sync-fd", str(r), # ensures dbus proxy stops when the bwrap bwrap quits
    "--bind", str(system_bus_proxy), "/run/dbus/system_bus_socket",
    "--bind", str(session_bus_proxy), str(XDG_RUNTIME_DIR / "bus"),
]
def rw(*paths): argv.extend([i for path in paths for i in ("--bind-try", str(path), str(path))])
def ro(*paths): argv.extend([i for path in paths for i in ("--ro-bind-try", str(path), str(path))])
def dev(*paths): argv.extend([i for path in paths for i in ("--dev-bind-try", str(path), str(path))])
def link(*links): argv.extend([i for link in links for i in ("--symlink", str(link[0]), str(link[1]))])


rw(
    "/usr",
    "/etc",
    "/opt",
    "/run/systemd/resolve/",
    # WARNING: Forwarding the X11 is insecure and might lead to sandbox escapes
    "/tmp/.X11-unix",
    "/tmp/.ICE-unix",
    *glob(str(XDG_RUNTIME_DIR / "wayland*")),
    # audio
    "/var/lib/alsa/",
    *glob(str(XDG_RUNTIME_DIR / "pulse*")),
    *glob(str(XDG_RUNTIME_DIR / "pipewire*")),
    # shader cache
    XDG_CACHE_HOME / "mesa_shader_cache",
    XDG_CACHE_HOME / "mesa_shader_cache_db",
    XDG_CACHE_HOME / "nv",
    XDG_CACHE_HOME / "nvidia",
    XDG_CACHE_HOME / "radv_builtin_shaders",
    XDG_CACHE_HOME / "radv_builtin_shaders64",
    # steam
    XDG_DATA_HOME / "Steam",
)
link(
    ("/usr/bin", "/bin"),
    ("/usr/bin", "/sbin"),
    ("/usr/lib", "/lib"),
    ("/usr/lib64", "/lib64"),
    ("/run", "/var/run"),
)
dev(
    "/dev/dri",
    "/dev/input",
    "/dev/hugepages",
    *glob("/dev/nvidia*"),
    "/dev/snd",
    "/dev/fuse",
    "/sys/block/",
    "/sys/bus/",
    "/sys/class/",
    "/sys/dev/",
    "/sys/devices/",
    "/sys/module/",
)
os.execvp("bwrap", argv + ["--sync-fd", str(r), "/usr/lib/steam/steam"])
  • 本例中將 $XDG_DATA_HOME/steam_home 掛載為 $HOME。用戶可以自行將 STEAM_HOME 修改為其他位置。
  • Use the rw() or ro() functions to grant read/write or read-only access, respectively, to a host path. For example, rw("HOME/.config/unity3d").
  • 使用 rw()ro() 以授予宿主 path 的讀寫或只讀權限。例如 rw("HOME/.config/unity3d")

NPM, Node Version Manager (NVM), Maven Java

[編輯 | 編輯原始碼]

為能在項目的工作目錄中使用 bubblewrap 運行「npm」,可以參考下面的命令示例。

Bubblewrap 能夠與 Angular、Cypress 和 Maven Java 共同使用並工作良好。X11 和 Wayland 需要被包含在最開始,因為 Cypress 會啟動一個基於 Electron 的圖形界面。

假設用戶在項目工作目錄下執行 npm install,同時「npm」需要讀寫 node_modulespackage.json 等文件,這種方式允許在程序啟動目錄中擁有完整的文件訪問權限。同時「npm」和「nvm」(npm -g install ...)也可以訪問的全局安裝目錄。此外 Cypress 也可以運行在 X11 或 Wayland 下。

注意:綁定到 zsh、maven 和其他的目錄可能與實際情況有所不同。
bwrap_arguments=(
    # 避免成為殭屍進程
    --die-with-parent

    # 依賴項需要網絡訪問
    --unshare-all
    --share-net

    # 創建正確的運行環境
    --tmpfs /
    --tmpfs /run
    --dir /tmp
    --dev /dev
    --proc /proc
    --ro-bind /bin /bin
    --ro-bind /sbin /sbin
    --ro-bind /usr /usr
    --ro-bind /etc /etc
    --ro-bind /lib /lib
    --ro-bind /lib64 /lib64
    --ro-bind /sys /sys
    --ro-bind /var /var

    # systemd-resolve for dns
    --ro-bind /run/systemd/resolve /run/systemd/resolve

    # npm 初始化倉庫時會使用 git,確保已配置好 email 和 username 項
    --ro-bind $XDG_CONFIG_HOME/git/config $XDG_CONFIG_HOME/git/config

    # zsh has to look everywhere cool
    --ro-bind $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshrc
    --ro-bind $XDG_CONFIG_HOME/zsh/.zshenv $XDG_CONFIG_HOME/zsh/.zshenv
    --ro-bind $HOME/.zshenv $HOME/.zshenv

    # Maven
    --ro-bind /opt/maven /opt/maven
    --ro-bind $HOME/.m2 $HOME/.m2

    # NPM
    --bind "$XDG_DATA_HOME/npm" "$XDG_DATA_HOME/npm"

    # npm、cypress、nvm、maven 等程序需要使用 cache
    --bind "$XDG_CACHE_HOME" "$XDG_CACHE_HOME"

    # x11, needed for cypress
    --ro-bind "$XAUTHORITY" "$XAUTHORITY"

    # wayland, might be useful
    --ro-bind "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY"

    # 假定運行時所在的目錄是項目工作目錄,並且擁有完全的訪問權限
    --bind "$(pwd)" "$(pwd)"
)

# 以上方指定的參數運行 bwrap 處理的用戶命令:
$ bwrap "${bwrap_arguments[@]}" "$@"