Wren Linux

wren-linux / extras / firefox-sandbox.sh

#!/bin/bash
# Firefox as its own locked-down user, for everyday browsing. The firefox
# command opens it as a normal window on your sway, but it runs as a different
# user ("firefox") that has no password, no login and none of your files, so a
# hacked page reaches only that user's own profile, not your documents, ssh keys
# or passwords.
#  - Your logins, history and bookmarks are kept, in that user's own home
#    (/var/lib/wren-firefox).
#  - The clipboard is shared while its window has focus; ~/Downloads is shared
#    both ways (treat what lands there as untrusted).
#  - Links from other programs open in the Firefox that is already running.
#  - Sound goes through a pipe into a player of yours; it never gets your audio
#    server, so it cannot hear your microphone.
#  - A firewall rule on that user keeps it off your router, printers and this
#    computer's own services (data/firefox-firewall.nft); how to lift it for a
#    while is in that file. DNS stays open.
#  - Your user may run exactly one thing as firefox (a rule in /etc/sudoers.d).
#  - It asks six things (remembered, in /etc/wren/firefox.conf, which only
#    root can write; asked again when you run this again in a terminal):
#    fast JavaScript (the JIT; default no: it is where many browser attacks
#    happen), sound (default yes), whether it is your default browser, so
#    links from other programs open in it (default yes), uBlock Origin's
#    mode: medium (default: other sites' scripts blocked, ublock-allow lets a
#    site through) or standard (ads, trackers and malware only), and, for
#    when secure DNS is switched off, Firefox's own encrypted DNS
#    to Quad9: strict (default) or with fallback to the network's; and
#    whether cookies, site data and the cache are deleted when it closes
#    (default delete; sites you allow under Manage Exceptions keep theirs). firefox-esr is the
#    same Firefox run as you, with everything you can reach, and stays
#    installed. No video calls: it has no microphone or camera, so WebRTC is off.
#  - Firefox profiles made with firefox-profiles/new-profile.sh each get a user
#    of their own like this one (fx-NAME).
#  - It sets up Firefox ESR the Wren Linux way for every profile (video codecs,
#    uBlock Origin, HTTPS-Only Mode, the JavaScript JIT off, a restart notice:
#    parts/firefox-hardening.sh), and sets up incognito too, a throwaway Firefox
#    in a locked-down user of its own with nothing kept (parts/incognito.sh).
# There is no KeePassXC browser add-on (Wren Linux's KeePassXC is the minimal
# build without it): copy and paste a password; the clipboard is shared.
# Usage: bash firefox-sandbox.sh [--remove | --manual]
# summary: Firefox as its own locked-down user
# security: most
# category: web
# status: file:/usr/local/bin/firefox
# files: bin/no-abstract-sockets bin/wren-sandbox bin/wayland-restrict data/firefox-firewall.nft data/firefox-user.js data/wren-firefox.desktop data/firefox-completion.bash lib/firefox-sandbox.sh bin/sandbox-net-check
. "$(dirname -- "${BASH_SOURCE[0]}")/../lib.sh"
. "$WREN_DIR/lib/firefox-sandbox.sh"
# The hardening and incognito it sets up first, then the sandbox itself
if [ "${1:-}" = --manual ]; then print_manual firefox; print_manual; exit; fi
need_user

NAME=firefox
HOME_DIR=/var/lib/wren-firefox
CMD=/usr/local/bin/firefox
SESSION=/usr/local/libexec/firefox-session
SHARE=/usr/local/share/wren
PREFS=$SHARE/firefox-user.js
LIMITS=/etc/security/limits.d/50-wren-firefox.conf
FIREWALL=/etc/wren/firefox.nft
UNIT=/etc/systemd/system/wren-firefox-firewall.service
DESKTOP="$HOME/.local/share/applications/wren-firefox.desktop"
# firefox's Tab completion (its options and the profiles), where bash-completion looks
COMPLETION="${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions/firefox"
# Firefox run as you, by name: firefox-unsafe while this is installed (Debian's
# /usr/bin/firefox-esr moved there with dpkg-divert, so its updates land there
# too); firefox-esr itself, and Debian's /usr/bin/firefox and x-www-browser
# that run it, open the sandboxed Firefox instead. The menu entry Debian's
# package gives Firefox (it names the program itself) is hidden for you.
UNSAFE=/usr/bin/firefox-unsafe
ESR=/usr/bin/firefox-esr
ESR_HIDE="${XDG_DATA_HOME:-$HOME/.local/share}/applications/firefox-esr.desktop"

if [ "${1:-}" = --remove ]; then
    fx_sandbox_unused
    # incognito first: whether it stays decides what else may go
    keep_incognito=false
    if [ -x /usr/local/bin/incognito ]; then
        if ask "Remove incognito (the throwaway Firefox) too?" y; then
            bash "$WREN_DIR/parts/incognito.sh" --remove
        else
            keep_incognito=true
        fi
    fi
    sudo systemctl disable --now wren-firefox-firewall.service >/dev/null 2>&1 || true
    sudo nft delete table inet wren_firefox 2>/dev/null || true
    fx_disallow_session "$NAME"
    sudo rm -f /etc/wren/firefox.conf
    sudo rm -f "$LIMITS" "$CMD" "$SESSION" "$PREFS" "$FIREWALL" "$UNIT"
    remove_wayland_restrict
    remove_no_abstract
    remove_sandbox_lib
    remove_net_check
    sudo systemctl daemon-reload >/dev/null 2>&1 || true
    sudo rmdir "$SHARE" /usr/local/libexec /etc/wren 2>/dev/null || true
    sudo find /dev/shm -maxdepth 1 \( -name firefox-run -o -name 'firefox-run.*' \) -exec rm -rf {} + 2>/dev/null || true
    # links open in firefox-esr again, and firefox-esr is Firefox as you again
    unset_default_app wren-firefox.desktop
    rm -f "$DESKTOP" "$COMPLETION"
    if [ "$(dpkg-divert --truename "$ESR" 2>/dev/null)" = "$UNSAFE" ]; then
        sudo rm -f "$ESR"
        sudo dpkg-divert --quiet --local --rename --remove "$ESR"
        note "firefox-esr runs Firefox as you again (firefox-unsafe is gone)."
    fi
    if grep -qs '^# Written by Wren Linux extras/firefox-sandbox.sh' "$ESR_HIDE"; then rm -f "$ESR_HIDE"; fi
    if id "$NAME" >/dev/null 2>&1; then
        sudo pkill -KILL -u "$NAME" 2>/dev/null || true
        sudo userdel "$NAME"
        note "Removed the user $NAME."
    fi
    # The group the firewall rule matched; no user is left in it now
    if getent group "$FX_GROUP" >/dev/null; then sudo groupdel "$FX_GROUP" 2>/dev/null || true; fi
    if [ -d "$HOME_DIR" ]; then
        note "Its profile (logins, history, bookmarks) is still in $HOME_DIR."
        if ask "Delete that profile too?" n; then sudo rm -rf "$HOME_DIR"; fi
    fi
    # Firefox itself and its hardening, unless incognito still uses them
    if $keep_incognito; then
        note "incognito stays, and with it Firefox ESR and its hardening."
    else
        bash "$WREN_DIR/parts/firefox-hardening.sh" --remove
    fi
    apt_purge
    note "Your own Firefox profiles (firefox-esr's bookmarks, logins) stay in ~/.mozilla."
    exit
fi

say "Firefox and what it needs"
# acl: gives the firefox user the socket and ~/Downloads, and takes them away again
# pipewire-bin, wireplumber, pipewire-pulse: its own audio server; pw-cat (in
# pipewire-bin) is the player that reads its pipe. dbus-daemon: the private bus
# that carries links to the running Firefox. nftables: the firewall rule.
# (the PipeWire ones don't start a sound server for you: only the sound extra does, apt_install
# sees to it)
apt_install firefox-esr libavcodec61 webext-ublock-origin-firefox acl nftables python3 dbus-daemon \
    pipewire-bin pipewire-pulse wireplumber
bash "$WREN_DIR/parts/firefox-hardening.sh"
# Wren Linux once had a separate extra for Firefox run as you (extras/firefox.sh);
# this one installs it now. Its marker package goes: what it held, this
# script's marker holds now, so nothing is removed.
if is_installed wren-firefox; then
    WREN_OWNER=firefox apt_purge >/dev/null
    note "Took over Firefox ESR from Wren Linux's old Firefox extra."
fi

say "Your choices (asked again whenever you run this in a terminal)"
CHOICES=/etc/wren/firefox.conf
chosen() { sed -n "s/^$1=//p" "$CHOICES" 2>/dev/null | head -1; }
# last time's answers are the defaults (with no terminal to ask on: kept)
fast=n sound=y links=y ublock=medium dns=strict cookies=delete
[ "$(chosen fast_js)" != yes ] || fast=y
[ "$(chosen ublock)" != standard ] || ublock=standard
[ "$(chosen dns)" != fallback ] || dns=fallback
[ "$(chosen cookies)" != keep ] || cookies=keep
[ "$(chosen sound)" != no ] || sound=n
[ "$(chosen default_browser)" != no ] || links=n
note "Fast JavaScript makes heavy sites quicker, but the part that does it (the JIT)"
note "is where many browser attacks happen; off, sites still work, just slower."
if ask "Turn on fast JavaScript?" "$fast"; then fast=yes; else fast=no; fi
# Its sound comes through yours: without the sound extra there is none, so nothing to ask
if ! user_audio_wanted; then
    sound=no
    note "No sound in Firefox: that needs the sound extra (wrenctl install pipewire-audio), then run this again."
elif ask "Sound in Firefox (videos, music)? It can't hear your microphone either way." "$sound"; then sound=yes; else sound=no; fi
if ask "Open links from other programs in it (your default browser)?" "$links"; then links=yes; else links=no; fi
note "uBlock Origin blocks ads in every Firefox here. It can work two ways:"
note "  medium    blocks ads, trackers and known malware sites, and also the"
note "            scripts and frames a site loads from other sites, which is where"
note "            most malicious ads come from. Safer, but many sites break until"
note "            you allow them, one by one: ublock-allow example.com (then"
note "            restart Firefox). Wren Linux's default."
note "  standard  blocks ads, trackers and known malware sites; the rest of a site"
note "            runs, so sites work as you expect. Easier for people who'd"
note "            rather not allow sites."
choose ublock "Which way should uBlock Origin work?" "$ublock" medium standard
note "While secure DNS is on, Firefox uses it (every program's lookups, encrypted)."
note "While it is switched off (the Wren menu, for a Wi-Fi sign-in page), Firefox"
note "encrypts its own, to Quad9, so your network can't see which sites you look up:"
note "  strict    always; a Wi-Fi sign-in page (cafés, hotels) then won't load in"
note "            Firefox even with secure DNS off, and on a network that blocks"
note "            encrypted DNS nothing does. Wren Linux's default."
note "  fallback  if encrypted DNS fails, the network's own, unencrypted: with secure"
note "            DNS switched off, sign-in pages work, but such a network can see"
note "            those lookups."
choose dns "Firefox's own encrypted DNS?" "$dns" strict fallback
note "Websites keep cookies and other data in Firefox, which lets them, and the ad"
note "and tracker companies behind them, recognise you from one visit to the next."
note "  delete  cookies, site data and the cache go when Firefox closes. You are"
note "          logged out of every site then (saved passwords still fill in, on a"
note "          click). To stay logged in to a few: Firefox's Settings > Privacy &"
note "          Security > Cookies and Site Data > Manage Exceptions > Allow."
note "          Wren Linux's default."
note "  keep    they stay, as in a normal Firefox: you stay logged in everywhere."
note "History, bookmarks and saved passwords stay either way: they are yours, and no"
note "website can read them."
choose cookies "When Firefox closes, its cookies and site data:" "$cookies" delete keep
sudo install -d -o root -g root -m 0755 "${CHOICES%/*}"
printf '%s\n' '# Written by Wren Linux extras/firefox-sandbox.sh: run it again to change these' \
    "fast_js=$fast" "sound=$sound" "default_browser=$links" "ublock=$ublock" "dns=$dns" "cookies=$cookies" | sudo tee "$CHOICES" >/dev/null
sudo chmod 0644 "$CHOICES"
note "Fast JavaScript: $fast. Sound: $sound. Default browser: $links. uBlock: $ublock mode."
note "DNS: $dns. Cookies and site data when it closes: $cookies."
# uBlock's mode and Firefox's DNS, for every Firefox profile (ublock-allow came
# with the hardening above; it reads the DNS choice from $CHOICES)
sudo /usr/local/bin/ublock-allow --mode "$ublock" >/dev/null || warn "Couldn't set uBlock's $ublock mode: ublock-allow --mode $ublock"

say "The $NAME user"
# No password to guess, no login shell, a home only it can enter; in the group
# wren-fx that the firewall rule matches (profiles with a user of their own are too)
fx_create_user "$NAME" "$HOME_DIR" "Wren Linux sandboxed Firefox"
note "$NAME: no password, no login, group $FX_GROUP (and render); home $HOME_DIR."

say "The firefox command"
sudo install -d -o root -g root -m 0755 "$SHARE" /usr/local/libexec
install_bin wayland-restrict
install_no_abstract
install_sandbox_lib
install_net_check
sudo install -o root -g root -m 0755 "$WREN_DIR/bin/firefox" "$CMD"
sudo install -o root -g root -m 0755 "$WREN_DIR/bin/firefox-session" "$SESSION"
sudo install -o root -g root -m 0644 "$WREN_DIR/data/firefox-user.js" "$PREFS"

say "What your user may run as $NAME"
fx_allow_session "$NAME"
note "Wrote /etc/sudoers.d/wren-$NAME"
# (profiles made before this: their settings name the site they are for)
fx_secure_profiles

# A limit on processes keeps a runaway page from filling the machine; for every
# user in the group (a profile with a user of its own too)
printf '# Written by Wren Linux extras/firefox-sandbox.sh\n@%s hard nproc 4096\n@%s hard core 0\n' "$FX_GROUP" "$FX_GROUP" |
    sudo tee "$LIMITS" >/dev/null

say "Keeping $NAME off your network"
sudo install -d -o root -g root -m 0755 "${FIREWALL%/*}"
sudo install -o root -g root -m 0644 "$WREN_DIR/data/firefox-firewall.nft" "$FIREWALL"
# Loaded before the network comes up, at every boot
printf '%s\n' '# Written by Wren Linux extras/firefox-sandbox.sh' '[Unit]' \
    'Description=Keep the firefox user off the local network (Wren Linux)' \
    'Before=network-pre.target' 'Wants=network-pre.target' '' '[Service]' \
    'Type=oneshot' 'RemainAfterExit=yes' \
    "ExecStart=/usr/sbin/nft -f $FIREWALL" \
    'ExecStop=/usr/sbin/nft delete table inet wren_firefox' '' '[Install]' \
    'WantedBy=multi-user.target' | sudo tee "$UNIT" >/dev/null
sudo systemctl daemon-reload
# Restart, not just enable --now: a changed rule file is only loaded by a restart
sudo systemctl enable wren-firefox-firewall.service >/dev/null &&
    sudo systemctl restart wren-firefox-firewall.service ||
    warn "Couldn't load the firewall rule; check: systemctl status wren-firefox-firewall"

say "Default browser"
# Links from other programs go to xdg-open, which asks this list. Only your user's
# settings change; firefox-esr stays installed and can still be started by name.
install -D -m 0644 "$WREN_DIR/data/wren-firefox.desktop" "$DESKTOP"
install -D -m 0644 "$WREN_DIR/data/firefox-completion.bash" "$COMPLETION"
if [ "$links" = yes ]; then
    set_default_app wren-firefox.desktop x-scheme-handler/http x-scheme-handler/https
    note "Links from other programs now open in the sandboxed Firefox."
else
    unset_default_app wren-firefox.desktop
    note "Links from other programs open as before (not in the sandboxed Firefox)."
fi

say "firefox-esr opens the sandboxed Firefox"
# (a program that wants Firefox as you by name: firefox-unsafe)
if [ "$(dpkg-divert --truename "$ESR")" != "$UNSAFE" ]; then
    sudo dpkg-divert --quiet --local --rename --divert "$UNSAFE" --add "$ESR"
fi
printf '%s\n' '#!/bin/sh' '# Written by Wren Linux extras/firefox-sandbox.sh: firefox-esr, and what runs it' \
    '# by name (/usr/bin/firefox, x-www-browser), opens the sandboxed Firefox.' \
    '# Firefox run as you, with all your files: firefox-unsafe' \
    'exec /usr/local/bin/firefox "$@"' | sudo tee "$ESR.wren-new" >/dev/null
sudo chmod 0755 "$ESR.wren-new"
sudo mv "$ESR.wren-new" "$ESR"
mkdir -p "${ESR_HIDE%/*}"
printf '%s\n' '# Written by Wren Linux extras/firefox-sandbox.sh: Debian'"'"'s menu entry for' \
    '# Firefox run as you, hidden while the sandboxed one is here (firefox-unsafe runs it)' \
    '[Desktop Entry]' 'Type=Application' 'Name=Firefox ESR' 'Hidden=true' >"$ESR_HIDE"
note "firefox-esr (and firefox, x-www-browser) open the sandboxed Firefox. Firefox as you, with"
note "all your files: firefox-unsafe."

say "incognito"
note "incognito opens a throwaway Firefox as a separate, locked-down user, with"
note "its own clipboard, no shared folders, sound and video, and nothing kept."
bash "$WREN_DIR/parts/incognito.sh" ||
    warn "incognito didn't finish setting up; run it again with: bash $WREN_DIR/parts/incognito.sh"

note "Run it with: firefox   (or Super+d). firefox-unsafe runs Firefox as you instead."
note "Your own profiles, a Firefox of its own for one site: firefox --new-profile"
note "It cannot reach your router or local services; to allow that for a while:"
note "  sudo systemctl stop wren-firefox-firewall"
note "Downloads go to ~/Downloads, shared with you. Copy and paste works while its window has focus."