notes/portmaster-on-nixos.md 4 min read

Running Portmaster on NixOS

Portmaster is now packaged in nixpkgs with a full NixOS module. How to enable it, how declarative application profiles work, and what to watch out for.


What Portmaster is

Portmaster is a free and open-source application firewall by Safing. It monitors and controls network connections per application, so you can see what each program talks to and set rules for it. For a privacy-focused setup it fills a gap that a plain packet filter does not. Rules follow the application, not the port.

Why it took until 2026

A package request for Portmaster had been open in nixpkgs since 2023, and a first packaging attempt started the same year at version 1.6.18. It never made it in, and that was not carelessness. Early Portmaster did not have the underlying support for a system like NixOS. It managed its own binaries through a built-in update system and expected to write into its own install directory. On a system where the store is read-only and Nix owns every installed path, there was nothing stable to package.

Portmaster 2 restructured the project into parts that can be built separately. A Go core, an Angular web UI, and a Tauri desktop client. That made a real package possible. The nixpkgs package builds all three from source. An early revision repackaged the official Debian release, and review moved it to a full source build. I packaged it and wrote the NixOS module, and both were merged into nixpkgs in August 2026 after close to a year of review. The package carries two small patches. One makes Portmaster accept a read-only binary directory. The other disables the binary self-updater, because Nix owns the installed files. Intelligence data updates are not affected and keep working.

The module ships with the upcoming NixOS 26.11 release. On stable releases before that you can get it from the unstable channel.

Enabling it

The minimal configuration is one line.

{
  services.portmaster.enable = true;
}

This starts the firewall service and installs the desktop client in the system environment. The client starts in the background with graphical sessions and authenticates against the package’s immutable binary directory. Set services.portmaster.settings.devmode = true only when you need unrestricted browser or debugging access to http://127.0.0.1:817.

Intelligence data is stored under services.portmaster.stateDir. Changing that path does not move existing state for you.

Declarative global settings

Global settings can live in your NixOS configuration instead of the UI. Later inputs override earlier inputs in this order: settings, then settingsFile, then secretsFile. Use secretsFile for values that must not be copied to the Nix store.

One thing to be aware of: when any of these options is used, the module owns Portmaster’s runtime config.json. Global changes made only through the UI are replaced at the next service start. Put everything you care about in the configuration.

Declarative application profiles

This is the part that took the longest to get right, and it came out of review. Portmaster identifies applications by fingerprints, and by default those are paths. On NixOS the binary path changes with every store hash, so during testing of the pull request every rebuild silently dropped the per-app rules. Reviewers also pointed out that hand-written regex workarounds were both tedious and easy to get wrong. The module’s profile handling grew out of that feedback.

It now generates fingerprints that ignore the store hash and the package version, and that match both the normal executable and Nix-generated .program-wrapped executables. A profile written once keeps working across rebuilds and updates.

Profiles combine packages with manually specified fingerprints, and a prefix makes module managed profiles easy to spot in the UI.

{ pkgs, ... }:
{
  services.portmaster = {
    enable = true;
    profilePrefix = "[NixOS] ";

    profiles = {
      Firefox = {
        packages = [ pkgs.firefox ];
        settings.filter.defaultAction = "permit";
      };

      Vesktop = {
        fingerprints = [
          {
            type = "env";
            key = "CHROME_DESKTOP";
            operation = "equals";
            value = "vesktop.desktop";
          }
        ];
      };
    };
  };
}

Some packages start their real executable outside bin. Those layouts can be spelled out.

{ pkgs, ... }:
{
  services.portmaster.profiles = {
    Brave.packages = [
      {
        package = pkgs.brave;
        directory = "opt/brave.com/brave";
      }
    ];
  };
}

What to watch out for

A few honest caveats, all documented in the module manual as well.

Package name matching is a convenience, not a security boundary against local Nix users. Someone who can add arbitrary store paths can create a derivation with the same name. Use manually chosen fingerprints when that threat matters to you.

Fingerprints are alternatives. A process matches when any fingerprint matches, so a broad regular expression can catch unintended applications.

Removing a profile declaration does not remove a profile that Portmaster already imported. And changing fingerprints changes the derived profile ID. Portmaster 2.2.1 and later can migrate edited profiles, but stable package identities avoid the churn in the first place.

I maintain the package and module in nixpkgs. If something breaks on your setup, open an issue on nixpkgs and tag me, or reach me through the contact section.