<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>WitteShadovv · notes</title><description>Technical notes on NixOS, privacy-focused system design, and forensic testing.</description><link>https://witteshadovv.dev/</link><language>en</language><lastBuildDate>Tue, 18 Aug 2026 00:00:00 GMT</lastBuildDate><copyright>WitteShadovv, CC BY 4.0</copyright><docs>https://www.rssboard.org/rss-specification</docs><atom:link href="https://witteshadovv.dev/rss.xml" rel="self" type="application/rss+xml"/><item><title>Running forensic tools against my own hidden NixOS environment</title><link>https://witteshadovv.dev/notes/forensic-residue-hidden-nixos/</link><guid isPermaLink="true">https://witteshadovv.dev/notes/forensic-residue-hidden-nixos/</guid><description>I ran disk forensic tooling against the hidden NixOS environment from my NAILS thesis and wrote down what leaked and what did not.</description><pubDate>Tue, 18 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Why I pointed the tools at my own machine&lt;/h2&gt;
&lt;p&gt;I built NAILS as the artifact for my MSc thesis. It is a dual-environment setup for NixOS. One environment is a visible decoy. The other is a hidden environment that lives on encrypted storage and is layered over the decoy with OverlayFS. The point of the design is to keep hidden activity off the persistent decoy disk. A design goal is easy to state and hard to trust. So I ran forensic tooling against my own system and wrote down what I found.&lt;/p&gt;
&lt;p&gt;This post is a plain summary of that evaluation. The numbers and the labels come from the thesis. Where the thesis hedges, I hedge here too.&lt;/p&gt;
&lt;h2&gt;The dual-environment design in one paragraph&lt;/h2&gt;
&lt;p&gt;The decoy is an ordinary NixOS installation. It uses an impermanent root, which means the root filesystem is a tmpfs and only an explicit set of paths under &lt;code&gt;/persist&lt;/code&gt; survives a reboot. The hidden environment is a full NixOS configuration stored on a mounted hidden root, in the tested case a VeraCrypt container. During activation NAILS mounts OverlayFS layers over selected paths such as &lt;code&gt;/home&lt;/code&gt;, &lt;code&gt;/etc&lt;/code&gt;, &lt;code&gt;/nix&lt;/code&gt;, and &lt;code&gt;/var&lt;/code&gt;. The lower layer is the visible filesystem. The upper and work directories sit on the hidden root. Writes during a hidden session land on the hidden root, not on the decoy. NAILS does not unlock the container. It assumes the hidden root is already mounted.&lt;/p&gt;
&lt;h2&gt;The test setup&lt;/h2&gt;
&lt;p&gt;I ran a VirtualBox VM based on my amnesic reference base, NAILS OS. The VM had a tmpfs root, a persistent &lt;code&gt;/persist&lt;/code&gt;, no swap, and a VeraCrypt container as the hidden backend. I captured four disk states: baseline before any hidden activity, active during a hidden session, post-standard-deactivation, and post-emergency-deactivation.&lt;/p&gt;
&lt;p&gt;The two deactivation paths differ. Standard deactivation returns to the decoy and reboots at the end to wipe everything not explicitly persisted. Emergency deactivation removes the overlays without a reboot, for the case where a normal reboot is not safe or not available. In the forensic run I still rebooted manually before capturing the post-emergency state, which is why both post states are compared after a reboot. I ran the workflow once per path and compared each result against the same baseline.&lt;/p&gt;
&lt;p&gt;The hidden session was a scripted workload meant to approximate a small journalist or activist session. It touched browsers (Brave, Firefox, tor-browser), a messaging app (signal-desktop), a password manager (keepassxc), document and image tools (libreoffice, GIMP), and a wallet marker (electrum). It also ran cowsay through nix-shell as an artificial Nix package canary. The canary is not a real task. It only tests whether temporary Nix package use leaves a visible trace after deactivation.&lt;/p&gt;
&lt;h2&gt;What I checked, and with what&lt;/h2&gt;
&lt;p&gt;The disk analysis used The Sleuth Kit. TSK &lt;code&gt;fls -r&lt;/code&gt; gave recursive file listings, including deleted entries still present in metadata. I took those listings against &lt;code&gt;/persist&lt;/code&gt; and &lt;code&gt;/nix&lt;/code&gt; on the visible NAILS OS installation, not against the hidden VeraCrypt container. On top of that I used baseline diffs, recovered-file extraction, raw disk carving, raw-image string scans, a separate EFI and GPT plaintext scan, and ext4 journal plaintext extraction. The journal was checked on its own because journal residue can outlive ordinary file deletion.&lt;/p&gt;
&lt;p&gt;The targeted artifact checks covered the classes that usually give a hidden volume away: shell history, recently-used file metadata, browser profiles, SSH state, dconf, thumbnails, system logs, service-path canaries, and mount records.&lt;/p&gt;
&lt;p&gt;I fixed the interpretation rules before looking at results, to avoid reading meaning into every keyword hit. An indicator was baseline-equivalent when the same class of entry was already in the baseline or when its count and context did not increase after hidden use. An indicator was newly visible when it appeared only after deactivation or grew in a way I could attribute to the hidden session. An indicator was not identified when the selected checks simply did not recover it. Baseline-equivalent does not mean absent from disk. Not identified does not mean unrecoverable by any tool.&lt;/p&gt;
&lt;h2&gt;What did not show up&lt;/h2&gt;
&lt;p&gt;After both deactivation modes, the targeted user-facing checks did not recover the artifacts I looked for. Shell history was absent. Recently-used metadata was absent. Brave and Firefox state was absent. SSH state was absent. Thumbnail metadata was absent. The visible text logs were clean.&lt;/p&gt;
&lt;p&gt;The package, mount, raw-string, and canary indicators stayed baseline-equivalent rather than becoming newly visible. Hidden-package name hits sat at 3,752 lines and hidden-store paths at 464 across baseline and both post states. The Nix database check reported 20 matches in all three states. Bulk hits for &lt;code&gt;/mnt/hidden&lt;/code&gt; were 0 everywhere. Overlay strings were 1,725 and mount-option hits were 13 across all three states. The signal-desktop and cowsay tokens did appear, but only inside Nix package-definition files that were already on the visible system, not in new installed program files. keepassxc and tor-browser were baseline packages to begin with.&lt;/p&gt;
&lt;p&gt;A few flat counts are worth naming. TSK &lt;code&gt;fls -r&lt;/code&gt; returned 720,370 lines for &lt;code&gt;/persist&lt;/code&gt; and 719,962 lines for &lt;code&gt;/nix&lt;/code&gt; in the baseline and both post states. Recovered-file counts held at 3,821 in all three states. EFI and GPT string hits were 0 throughout. The ext4 journal string hits went from 167 at baseline to 113 in both post states, a decrease rather than a new signal.&lt;/p&gt;
&lt;h2&gt;What did show up&lt;/h2&gt;
&lt;p&gt;One class was not clean, and I report it as such. Both post states had 832 diff lines under &lt;code&gt;/persist&lt;/code&gt;. The rewrites sit under &lt;code&gt;/persist/var/lib/nixos&lt;/code&gt; and cover &lt;code&gt;declarative-groups&lt;/code&gt;, &lt;code&gt;gid-map&lt;/code&gt;, &lt;code&gt;declarative-users&lt;/code&gt;, &lt;code&gt;uid-map&lt;/code&gt;, and &lt;code&gt;auto-subuid-map&lt;/code&gt;. These are the bookkeeping files the current NixOS activation path uses for users, groups, and UID and GID maps. I do not treat this as hidden-session content leakage. I do treat it as an observable effect of running the activation path. Standard and emergency deactivation produced the same diff count and the same rewritten files in this run. That is a result for this workload and this VM, not a general claim that the two paths are equivalent.&lt;/p&gt;
&lt;h2&gt;What this does not prove&lt;/h2&gt;
&lt;p&gt;The honest part. This is scoped qualitative work, not a proof of undetectability.&lt;/p&gt;
&lt;p&gt;I only ran external disk forensics against the VeraCrypt backend. LUKS with ext4 and plain directory roots are exercised by automated tests, which catch regressions but do not establish forensic equivalence across backends. I did not analyze volatile memory at all. Encryption keys, command state, and application data can persist in RAM while the hidden environment is active, and that surface is untouched here. The run was a single VM on a single host, so firmware logs, vendor journaling, and device-mapper edge cases on real hardware may behave differently. It was one user role with one fixed workload. A different application mix or a longer session may surface artifacts this script did not.&lt;/p&gt;
&lt;p&gt;There is also the reading itself. I set the interpretation rules in advance, but I am the single author who classified the output, and no second examiner repeated the work. The selected tools may have missed indicators that were present. A more aggressive forensic procedure might recover things these checks did not.&lt;/p&gt;
&lt;p&gt;And there is a limit the design does not try to hide. The evaluation measures whether hidden-session content leaks. It does not disguise the fact that the machine is an impermanence-based NixOS with encrypted storage and no swap. The &lt;code&gt;/persist/var/lib/nixos&lt;/code&gt; rewrites stay visible on the decoy, and the forensic tooling I used leaves its own presence on the analysis host. The setup reduces specific persistent artifacts. It does not make the machine look like it was never configured for this.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;p&gt;NAILS is research software and still alpha. If you want the code or the full evaluation, both are public.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NAILS repository: &lt;a href=&quot;https://github.com/WitteShadovv/nails&quot;&gt;https://github.com/WitteShadovv/nails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Thesis record: &lt;a href=&quot;https://urn.kb.se/resolve?urn=urn:nbn:se:su:diva-256198&quot;&gt;https://urn.kb.se/resolve?urn=urn:nbn:se:su:diva-256198&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>WitteShadovv</dc:creator></item><item><title>Running Portmaster on NixOS</title><link>https://witteshadovv.dev/notes/portmaster-on-nixos/</link><guid isPermaLink="true">https://witteshadovv.dev/notes/portmaster-on-nixos/</guid><description>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.</description><pubDate>Tue, 18 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What Portmaster is&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://safing.io/portmaster/&quot;&gt;Portmaster&lt;/a&gt; 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.&lt;/p&gt;
&lt;h2&gt;Why it took until 2026&lt;/h2&gt;
&lt;p&gt;A package request for Portmaster had been open in nixpkgs since 2023, and a &lt;a href=&quot;https://github.com/NixOS/nixpkgs/pull/264454&quot;&gt;first packaging attempt&lt;/a&gt; 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.&lt;/p&gt;
&lt;p&gt;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 &lt;a href=&quot;https://github.com/NixOS/nixpkgs/pull/442904&quot;&gt;merged into nixpkgs in August 2026&lt;/a&gt; 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.&lt;/p&gt;
&lt;p&gt;The module ships with the upcoming NixOS 26.11 release. On stable releases before that you can get it from the unstable channel.&lt;/p&gt;
&lt;h2&gt;Enabling it&lt;/h2&gt;
&lt;p&gt;The minimal configuration is one line.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  services.portmaster.enable = true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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&apos;s immutable binary directory. Set &lt;code&gt;services.portmaster.settings.devmode = true&lt;/code&gt; only when you need unrestricted browser or debugging access to &lt;code&gt;http://127.0.0.1:817&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Intelligence data is stored under &lt;code&gt;services.portmaster.stateDir&lt;/code&gt;. Changing that path does not move existing state for you.&lt;/p&gt;
&lt;h2&gt;Declarative global settings&lt;/h2&gt;
&lt;p&gt;Global settings can live in your NixOS configuration instead of the UI. Later inputs override earlier inputs in this order: &lt;code&gt;settings&lt;/code&gt;, then &lt;code&gt;settingsFile&lt;/code&gt;, then &lt;code&gt;secretsFile&lt;/code&gt;. Use &lt;code&gt;secretsFile&lt;/code&gt; for values that must not be copied to the Nix store.&lt;/p&gt;
&lt;p&gt;One thing to be aware of: when any of these options is used, the module owns Portmaster&apos;s runtime &lt;code&gt;config.json&lt;/code&gt;. Global changes made only through the UI are replaced at the next service start. Put everything you care about in the configuration.&lt;/p&gt;
&lt;h2&gt;Declarative application profiles&lt;/h2&gt;
&lt;p&gt;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&apos;s profile handling grew out of that feedback.&lt;/p&gt;
&lt;p&gt;It now generates fingerprints that ignore the store hash and the package version, and that match both the normal executable and Nix-generated &lt;code&gt;.program-wrapped&lt;/code&gt; executables. A profile written once keeps working across rebuilds and updates.&lt;/p&gt;
&lt;p&gt;Profiles combine packages with manually specified fingerprints, and a prefix makes module managed profiles easy to spot in the UI.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ pkgs, ... }:
{
  services.portmaster = {
    enable = true;
    profilePrefix = &quot;[NixOS] &quot;;

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

      Vesktop = {
        fingerprints = [
          {
            type = &quot;env&quot;;
            key = &quot;CHROME_DESKTOP&quot;;
            operation = &quot;equals&quot;;
            value = &quot;vesktop.desktop&quot;;
          }
        ];
      };
    };
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some packages start their real executable outside &lt;code&gt;bin&lt;/code&gt;. Those layouts can be spelled out.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ pkgs, ... }:
{
  services.portmaster.profiles = {
    Brave.packages = [
      {
        package = pkgs.brave;
        directory = &quot;opt/brave.com/brave&quot;;
      }
    ];
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What to watch out for&lt;/h2&gt;
&lt;p&gt;A few honest caveats, all documented in the module manual as well.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Fingerprints are alternatives. A process matches when any fingerprint matches, so a broad regular expression can catch unintended applications.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Module options: search for &lt;code&gt;services.portmaster&lt;/code&gt; on &lt;a href=&quot;https://search.nixos.org/options?query=services.portmaster&quot;&gt;search.nixos.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The nixpkgs pull request: &lt;a href=&quot;https://github.com/NixOS/nixpkgs/pull/442904&quot;&gt;NixOS/nixpkgs#442904&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Portmaster upstream: &lt;a href=&quot;https://safing.io/portmaster/&quot;&gt;safing.io/portmaster&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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 &lt;a href=&quot;https://witteshadovv.dev/#verify&quot;&gt;contact section&lt;/a&gt;.&lt;/p&gt;
</content:encoded><dc:creator>WitteShadovv</dc:creator></item></channel></rss>