Wren Linux

wren-linux / lib / firefox-sandbox.sh

# shellcheck shell=bash
# lib/firefox-sandbox.sh: the locked-down users the sandboxed Firefox runs as.
# Sourced after lib.sh by extras/firefox-sandbox.sh (the user "firefox") and
# extras/firefox-profiles/new-profile.sh (one user "fx-NAME" per profile).
# All of them share one system group, so one firewall rule covers every one of
# them and a new profile never has to touch the firewall.

FX_GROUP=wren-fx
FX_HOME_ROOT=/var/lib/wren-fx
# (these three paths can be pointed elsewhere by a test)
FX_PROFILE_DIR=${WREN_FX_PROFILE_DIR:-/etc/wren/firefox-profiles}
FX_SESSION=${WREN_FX_SESSION:-/usr/local/libexec/firefox-session}
FX_CLIENT=${WREN_FX_CLIENT:-/usr/local/bin/firefox}

# fx_profile_name_ok NAME: a name that makes a valid user, fx-NAME (at most 31
# characters), and a command, firefox-NAME (when NAME itself is taken:
# lib/firefox-profiles.sh), that isn't one already: firefox-esr
# is Firefox itself, and Wren Linux has firefox-restart-check and firefox-session
fx_profile_name_ok() {
    [[ $1 =~ ^[a-z][a-z0-9-]{0,27}$ ]] || return 1
    case $1 in esr | restart-check | session) return 1 ;; esac
}

fx_ensure_group() {
    getent group "$FX_GROUP" >/dev/null || sudo groupadd --system "$FX_GROUP"
}

# fx_create_user USER HOME COMMENT: a system user in the shared group with no
# password, no login shell and a home only it can enter. Run again, it fixes
# the group and the permissions of an existing one.
fx_create_user() {
    local user=$1 home=$2 comment=$3
    fx_ensure_group
    if id "$user" >/dev/null 2>&1; then
        [ "$(id -u "$user")" -lt 1000 ] ||
            die "A regular user account called $user already exists; this needs a system user. Rename or remove it first."
        sudo usermod -g "$FX_GROUP" "$user"
    else
        sudo mkdir -p "${home%/*}"
        sudo useradd --system --gid "$FX_GROUP" --create-home --home-dir "$home" \
            --shell /usr/sbin/nologin --comment "$comment" "$user"
    fi
    sudo usermod --lock --shell /usr/sbin/nologin "$user"
    sudo chmod 0700 "$home"
    # Firefox opens the GPU's render node (software rendering is forced; this
    # is only so it can start). render, not video: no modesetting.
    if getent group render >/dev/null; then sudo usermod -aG render "$user"; fi
}

# fx_allow_session USER: your user may run the session script as USER and
# nothing else. The sudoers file is checked with visudo first: a broken one
# would lock you out of sudo.
fx_allow_session() {
    local user=$1 rule
    rule=$(mktemp)
    printf '# Written by Wren Linux (sandboxed Firefox). %s may run the firefox session script,\n# and nothing else, as the user %s.\n%s ALL=(%s) NOPASSWD: %s\n' \
        "$(id -un)" "$user" "$(id -un)" "$user" "$FX_SESSION" >"$rule"
    if ! sudo visudo -cf "$rule" >/dev/null; then
        rm -f "$rule"
        die "The sudoers rule didn't pass visudo's check, so it wasn't installed."
    fi
    sudo install -o root -g root -m 0440 "$rule" "/etc/sudoers.d/wren-$user"
    rm -f "$rule"
}

fx_disallow_session() { sudo rm -f "/etc/sudoers.d/wren-$1"; }

# fx_conf_private USER CONF: a profile's settings name the site it is for, so only root, you and
# that profile's own user can read them (not another profile's user, which shares the group), and
# the folder can't be listed by a sandbox user: it passes through to its own file and no more.
# Your own access is by name too: you wrote nothing here, root did.
fx_conf_private() {
    local user=$1 conf=$2 me
    me=$(id -un)
    sudo install -d -o root -g root -m 0750 "$FX_PROFILE_DIR"
    sudo setfacl -m "u:$me:rx" "$FX_PROFILE_DIR"
    sudo setfacl -m "u:$user:x" "$FX_PROFILE_DIR"
    sudo chmod 0640 "$conf"
    sudo setfacl -m "u:$user:r,u:$me:r" "$conf"
}

# fx_secure_profiles: fx_conf_private for every profile there is, those made by an older version too
fx_secure_profiles() {
    local n
    while IFS= read -r n; do
        [ -n "$n" ] || continue
        fx_conf_private "fx-$n" "$FX_PROFILE_DIR/$n.conf"
    done < <(fx_profile_names)
}

# fx_sandbox_unused: stops (die) while profiles with a user of their own exist.
# They run through the sandbox (its firefox command, session script, firewall
# rule and limits), so removing it would leave them unable to start.
fx_sandbox_unused() {
    local profiles=()
    mapfile -t profiles < <(fx_profile_names)
    [ ${#profiles[@]} -eq 0 ] ||
        die "These Firefox profiles still use the sandbox: ${profiles[*]}. Remove each first with: bash extras/firefox-profiles/new-profile.sh --remove NAME. Nothing was changed."
}

# fx_profile_names: the profiles that have a user, one per line
fx_profile_names() {
    local f
    for f in "$FX_PROFILE_DIR"/*.conf; do
        [ -f "$f" ] || continue
        f=${f##*/}
        echo "${f%.conf}"
    done
}