Ephemeral Firefox Profiles on NixOS

Date:

I run an ephemeral NixOS system (previous post), based on the “Erase your darlings” idea of erasing your system on every boot, and selectively persisting state. My Firefox profile (~/.mozilla) was a prime candidate for this treatment: I’ll have it wiped on every boot. This post details my two-year journey of managing customised ephemeral Firefox profiles.

Firefox offers enterprise/distribution customisation through policy.json (documented here). We can use this for personal configuration. Nix provides excellent tools for generating this JSON.

Settings

Let’s set our preferred about:config settings.

programs.firefox = {
  enable = true;
  preferences = {

    # Reduce visual noise
    "browser.tabs.firefox-view" = false;
    "dom.webnotifications.enabled" = false;
    
    # Improve unlinkability
    "privacy.firstparty.isolate" = true;
    "privacy.resistFingerprinting" = true;
    
    # Disable DOH, since I prefer my local resolver.
    # https://wiki.mozilla.org/Trusted_Recursive_Resolver
    "network.trr.mode" = 0;
  };
};

Bookmarks

Bookmarks are managed through the Bookmarks field in policy.json. I store my bookmarks in bookmarks.json:

[
  {"title": "Groovesalad", "uri": "https://ice4.somafm.com/groovesalad-128-aac"},
  {"title": "Raspberry Pi Pinout", "uri": "https://pinout.xyz/"}
]

And then have Nix read that and dump it into the Bookmarks field.

programs.firefox = {
  policies = {
    Bookmarks = map (b: {
      Folder = "Managed Bookmarks";
      Title = b.title;
      URL = b.uri;
    }) (builtins.fromJSON (builtins.readFile ./bookmarks.json));
  };
};

History/caches

For many years before I had ephemeral Firefox profiles, I had “Delete history and cache” on quit, so losing this on Firefox is fine.

I preserve this behaviour with:

  programs.firefox = {
    policies = {
      SanitizeOnShutdown = {
        Cache = true;
        Cookies = true;
        Downloads = true;
        FormData = true;
        History = true;
        Sessions = true;
        SiteSettings = true;
        OfflineApps = true;
      };
    };
  };

While I’m here, let’s disable a bunch of others that I don’t use:

  policies = {
    AppAutoUpdate = false;
    CaptivePortal = false;
    DisableFirefoxAccounts = true;
    DisableFirefoxStudies = true;
    DisablePocket = true;
    DisableTelemetry = true;
    EnableTrackingProtection.Value = true;
    ExtensionUpdate = false;
    NetworkPrediction = false;
    NoDefaultBookmarks = true;
    OfferToSaveLogins = false;
    PasswordManagerEnabled = false;
    SanitizeOnShutdown = {
      Cache = true;
      Cookies = true;
      Downloads = true;
      FormData = true;
      History = true;
      Sessions = true;
      SiteSettings = true;
      OfflineApps = true;
    };
    SearchSuggestEnabled = false;
  };

Extensions

Extensions are managed via the ExtensionSettings field:

let
  # Set of extension IDs to rycee package names: https://gitlab.com/rycee/nur-expressions/-/blob/master/pkgs/firefox-addons/generated-firefox-addons.nix
  ryceeExtensions = {
    # https://addons.mozilla.org/en-US/firefox/addon/boring-rss/
    "{45d4d1a3-4faa-42b7-9747-bcf2153310cd}" = {
      name = "boring-rss";
      permissions = [];
    };

    # https://addons.mozilla.org/en-US/firefox/addon/disable-facebook-news-feed/
    "{85cd2b5d-b3bd-4037-8335-ced996a95092}" = {
      name = "disable-facebook-news-feed";
      permissions = [
        "*://*.facebook.com/*"
      ];
    };

    # https://addons.mozilla.org/en-US/firefox/addon/old-reddit-redirect/
    "{9063c2e9-e07c-4c2c-9646-cfe7ca8d0498}" = {
      name = "old-reddit-redirect";
      permissions = [
        "webRequest"
        "webRequestBlocking"
        "*://reddit.com/*"
        "*://www.reddit.com/*"
        "*://np.reddit.com/*"
        "*://amp.reddit.com/*"
        "*://i.reddit.com/*"
        "*://i.redd.it/*"
        "*://preview.redd.it/*"
        "*://old.reddit.com/*"
      ];
    };
in {
  config = {
    programs.firefox = {
      policies = {
        ExtensionSettings =
          (builtins.mapAttrs (k: v: {
            "installation_mode" = "normal_installed";
            # https://gitlab.com/rycee/nur-expressions/-/blob/5dce5ca68c1fcd569b8edd8b64033a9e62aa57c5/pkgs/firefox-addons/default.nix#L19
            "install_url" = "file://${config.nur.repos.rycee.firefox-addons.${v.name}}/share/mozilla/extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}/${k}.xpi";
          }) ryceeExtensions);
      };
    };
  };
}

This relies on having the great work rycee has done in making Firefox extensions available at nix build time.

Extension permissions

I use permission allowlists (as described here) to control extension permissions and prevent unwanted permissions on upgrades.

The result

Each boot provides a fresh Firefox instance, configured according to my preferences. Changes are managed through Nix, benefiting from version control, upgrades, rollbacks, and safe experimentation.

Limitations

Ephemeral Firefox profiles have drawbacks:

FAQ