Ich verwende Firefox als Webbrowser und schon eh und je ist er eine besondere Anwendung, bei der ich zum Beispiel früher am OOM-Killer-Wert gedreht habe, damit Firefox vor anderen wichtigen Programmen beendet wird. Aber auch heute noch hat Firefox einiges Verhalten, weshalb ich ihn besonders behandele.

Verwaltet von Systemd

Mit systemd-run kann man Prozesse von Systemd überwachen lassen. Mit der Option --user wird dafür der Benutzerdienst und nicht der Systemdient genutzt. Es ist ähnlich wie man mit Cron Prozesse zu einer bestimmten Zeit ausführen kann, die dann von Cron überwacht werden.

Der Vorteil an der Überwachung mit Systemd sind die vielen Funktionen, die Systemd bietet. Unter anderem werden die Ausgaben der Prozesse ins Journal geschrieben und verschwinden nicht im Nirgendwo oder landen ohne Zuordnung in ~/.xsession-errors.

srun

Als Hilfe habe ich mir ein Skript srun in ~/bin angelegt, um die Optionen besser kontrollieren und das Skript auch für andere Programme einsetzen zu können. Das gute an systemd-run ist, dass es die Prozesse in einer bereinigten Prozessumgebung startet. Daher werden bei mir jedoch auch einige wichtige Variablen nicht übernommen, die ich mithilfe der Option übergebe:

#!/bin/sh

exec systemd-run --user --quiet --collect \
  --setenv=PATH=$HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
  --setenv=GDK_SCALE=$GDK_SCALE --setenv=GDK_DPI_SCALE=$GDK_DPI_SCALE \
  --setenv=QT_AUTO_SCREEN_SCALE_FACTOR=$QT_AUTO_SCREEN_SCALE_FACTOR \
  ${DESKTOP_STARTUP_ID:+--setenv=DESKTOP_STARTUP_ID=$DESKTOP_STARTUP_ID} \
  "$@"

Die Variablen mit GDK und QT sind bei mir für den HiRes-Display notwendig. Die DESKTOP_STARTUP_ID ist notwendig, damit der Fenster-Manager AwesomeWM die Fenster einem bestimmten Prozess zuordnen kann. Durch --collect bleiben fehlerhaft beendete Prozesse nicht als Eintrag bei Systemd hängen und würden zukünftige Aufrufe blockieren.

ff

Zum Starten von Firefox verwende ich bereits das Skript ff, um besser meine Browser-Profile verwalten zu können. Dieses Skript habe ich jetzt um den Aufruf von srun erweitert. Um besser im Journal die Firefox-Instanzen unterscheiden zu können, habe ich den Units, die automatisch von systemd-run erzeugt werden, entsprechende Namen gegeben. Die normale Firefox-Instanz heißt ff, die für die Bank ff-bank u. s. w.

Da eine Unit nur ein Mal gestartet werden darf und ein weiterer Aufruf von Firefox nur den Hauptprozess kontaktiert und eine Seite öffnen lässt, soll nur der erste Aufruf tatsächlich mit srun geschehen:

if systemctl --user is-active $unit >/dev/null
then
    exec firefox "$@"
else
    exec srun --unit=$unit --property=SyslogIdentifier=$unit firefox "$@"
fi

Möglichkeiten mit AppArmor begrenzen

Systemd bietet die Einstellung, für eine Unit ein AppArmor-Profil festzulegen. Da ich unterschiedliche Browser-Profile nutze, um Online-Bank, Matrix, Entwicklung und Nightly vom normalen Surfen zu trennen, benötige ich auch unterschiedliche AppArmor-Regeln.

Die Entwickler- und die Nightly-Version habe ich in eigenen Verzeichnissen abgelegt, die in den Profilen variieren. Jede Instanz soll dabei nur auf ihr eigenen Firefox-Profilverzeichnis zugreifen dürfen. Dafür habe ich die zwei Variablen @{FF_PROFILE} und @{FF_DIR} verwendet, die ich in jedem Profil entsprechend setze.

Unter /etc/apparmor.d/abstractions/firefox habe ich alle Regeln abgelegt, die allen Profilen gemeinsam sind.

  include <abstractions/base>
  include <abstractions/dbus-session>
  include <abstractions/dconf>
  include <abstractions/gnome>
  include <abstractions/mesa>
  include <abstractions/nameservice>

  @{FF_DIR}/** r,
  @{FF_DIR}/*.so m,
  @{FF_DIR}/firefox{,-bin} ix,
  /usr/share/firefox/** r,
  /usr/share/xul-ext/** r,
  /usr/share/hunspell/ r,
  /usr/share/hunspell/* r,

  /usr/bin/lsb_release Ux,

  owner @{PROC}/@{pid}/fd/ r,
  owner @{PROC}/@{pid}/mountinfo r,
  @{PROC}/@{pid}/net/arp r,
  owner @{PROC}/@{pid}/stat r,
  owner @{PROC}/@{pid}/statm r,
  owner @{PROC}/@{pid}/smaps r,
  owner @{PROC}/@{pid}/task/@{tid}/stat r,

  /etc/timezone r,
  /etc/mailcap r,
  /etc/mime.types r,
  owner @{HOME}/.mailcap r,
  owner @{HOME}/.mime.types r,
  owner @{HOME}/.cache/thumbnails/*/*.png r,

  # DRI
  # VGA device (`lspci |grep VGA`)
  /sys/devices/pci0000:00/0000:00:02.0/* r,

  # firefox specific
  /etc/firefox/ r,
  /etc/firefox/** r,
  /etc/xul-ext/** r,
  /usr/share/mozilla/extensions/** r,
  /usr/share/webext/** r,
  /sys/devices/system/cpu/present r,
  /sys/devices/system/cpu/*/cache/*/size r,
  /sys/devices/system/cpu/cpufreq/*/cpuinfo_max_freq r,
  /sys/devices/system/node/ r,
  /sys/devices/system/node/*/meminfo r,
  owner /dev/shm/org.mozilla.ipc.* rwk,
  owner /dev/shm/org.chromium.* rw,

  # per-user firefox configuration
  owner @{HOME}/.cache/mozilla/firefox/@{FF_PROFILE}/ r,
  owner @{HOME}/.cache/mozilla/firefox/@{FF_PROFILE}/** rw,
  owner @{HOME}/.cache/mozilla/firefox/@{FF_PROFILE}/**/*.{db,parentlock,sqlite}* k,
  owner @{HOME}/.mozilla/firefox/profiles.ini r,
  owner @{HOME}/.mozilla/firefox/@{FF_PROFILE}/ r,
  owner @{HOME}/.mozilla/firefox/@{FF_PROFILE}/** rw,
  owner @{HOME}/.mozilla/firefox/@{FF_PROFILE}/**.{db,parentlock,sqlite}* k,
  owner @{HOME}/.mozilla/**/plugins/** rm,

  # suppress warnings about some violations Firefox does
  # execve("/bin/sh", ["/bin/sh", "-c", "test \"$DISPLAY\" != \"\""],
  deny /bin/dash x,
  deny /dev/ r,
  deny /etc/fstab r,
  deny "@{HOME}/.mozilla/firefox/Crash Reports/**" rw,
  deny "@{HOME}/.mozilla/firefox/Pending Pings/" r,
  deny "@{HOME}/.mozilla/firefox/Pending Pings/*" r,
  deny @{FF_DIR}/fonts/.uuid.* w,

Die Profile habe ich in /etc/apparmor.d unter den Namen ff (Standardprofil), ff-bank, ff-dev u. s. w. abgelegt, so dass sie mit den Namen der Units beim Aufruf von srun korrespondieren. Beim Aufruf von srun musste ich nur noch die Option -p AppArmorProfile=$unit ergänzen und die Profile mit apparmor_parser -a /etc/apparmor.d/ff* aktivieren. Wenn man zusätzlich die Option -C angibt, werden die Regeln nicht erzwungen, aber Meldungen bei Verstößen ausgegeben – ein Testmodus.

Das Standardprofil sieht dann am Ende so aus:

include <tunables/global>

@{FF_PROFILE}=pjtzli8s.default
@{FF_DIR} = /usr/lib/firefox

profile ff {
  include <abstractions/audio>
  include <abstractions/cups-client>
  include <abstractions/firefox>

  # For KeePassXC's remote access
  owner @{HOME}/.mozilla/native-messaging-hosts/* r,
  /home/joerg/git/keepassxc/build/src/proxy/keepassxc-proxy Ux,

  # Make browsing directories work
  / r,
  /**/ r,

  owner @{HOME}/Downloads/ rw,
  owner @{HOME}/Downloads/** rw,

  owner @{HOME}/[^.]** r,
  owner @{HOME}/.keysnail.js r,
  # don't report Firefox tries to access hidden files, it happens in File Open dialogue
  deny @{HOME}/.* r,
  deny @{HOME}/.*/ r,

  deny @{FF_DIR}/update.test{,/} w,
}

Da ich in meinem Online-Bank-Profil weder Musik hören, noch Drucken will, habe ich diese include-Anweisungen weggelassen und nur noch den Zugriff auf das Verzeichnis ~/Bank erlaubt, damit ich dort Dateien speichern kann.

include <tunables/global>

@{FF_PROFILE} = f66a55ci.bank
@{FF_DIR} = /usr/lib/firefox

profile ff-bank {
  include <abstractions/firefox>

  owner @{HOME}/Bank/ rw,
  owner @{HOME}/Bank/** rw,
}

Für die Entwicklerversion und die Nightly-Version sehen die Profile ähnlich aus. Zu der Entwicklerversion ist noch zu sagen, dass Firefox immer versucht ein zusätzliches Profil anzulegen, was man aber durch die leere Datei ~/.mozilla/firefox/ignore-dev-edition-profile unterbinden kann.

include <tunables/global>

@{FF_PROFILE} = aelzkv52.dev
@{FF_DIR} = /home/joerg/kein_Backup/firefox-dev

profile ff-dev {
  include <abstractions/audio>
  include <abstractions/firefox>
  include <abstractions/firefox-updater+pinger>

  # For KeePassXC's remote access
  owner @{HOME}/.mozilla/native-messaging-hosts/* r,
  /home/joerg/git/keepassxc/build/src/proxy/keepassxc-proxy Ux,

  # Make browsing directories work
  / r,
  /**/ r,

  owner @{HOME}/Downloads/ rw,
  owner @{HOME}/Downloads/** rw,

  owner @{HOME}/[^.]** r,
  # don't report Firefox tries to access hidden files, it happens in File Open dialogue
  deny @{HOME}/.* r,
  deny @{HOME}/.*/ r,
}

Da die beiden Versionen von Firefox sich selbst aktualisieren und ich auch die Berichterstattung an Mozilla erlaubt habe, habe ich in der Datei /etc/apparmor.d/abstractions/firefox-updater+pinger die Regeln hierfür abgelegt, die ich in den Profilen einbinde.

  # Pingsender (Mozilla call-back)
  @{FF_DIR}/pingsender cx -> pingsender,

  profile pingsender {
    include <abstractions/base>
    include <abstractions/nameservice>

    @{FF_DIR}/pingsender r,
    owner @{HOME}/.mozilla/firefox/Pending Pings/** rw,

    owner @{HOME}/.mozilla/firefox/@{FF_PROFILE}/saved-telemetry-pings/* r,
  }

  # Updater
  @{FF_DIR}/active-update.xml{,.tmp} w,
  @{FF_DIR}/updates.xml{,.tmp} w,
  @{FF_DIR}/update.test{,/} w,
  @{FF_DIR}/updates/** w,
  @{FF_DIR}/updates/*/updater cx -> updater,

  profile updater {
    include <abstractions/base>
    include <abstractions/gnome>

    @{FF_DIR}/** rw,
    @{FF_DIR}/*.so m,

    @{FF_DIR}/firefox-bin px -> @{profile_name},
  }

Fazit

Mit systemd-run und AppArmor lässt sich Firefox etwas besser einhegen, um im Falle eines Fehlverhaltens – das im Web nicht abwegig ist – den Schaden zu begrenzen. Unter Umständen kann man in speziellen Profilen die Zugriffsrechte noch weiter einschränken.