<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zoe</title>
    <description>The latest articles on DEV Community by Zoe (@thes1lv3r).</description>
    <link>https://dev.to/thes1lv3r</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F509562%2F91ba5c8b-2570-4c87-9213-c41d1c7558f2.png</url>
      <title>DEV Community: Zoe</title>
      <link>https://dev.to/thes1lv3r</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thes1lv3r"/>
    <language>en</language>
    <item>
      <title>Installing Arch Linux on BTRFS with LUKS and automatic TPM2 unlocking</title>
      <dc:creator>Zoe</dc:creator>
      <pubDate>Sat, 27 Dec 2025 04:21:16 +0000</pubDate>
      <link>https://dev.to/thes1lv3r/installing-arch-linux-on-btrfs-with-luks-and-automatic-tpm2-unlocking-3oio</link>
      <guid>https://dev.to/thes1lv3r/installing-arch-linux-on-btrfs-with-luks-and-automatic-tpm2-unlocking-3oio</guid>
      <description>&lt;p&gt;I recently got a new laptop, and figured I'd install Arch on it, just to have a functional system asap. Now that I have more time to tinker however, I have decided to re-install to get full disk encryption&lt;sup id="fnref1"&gt;1&lt;/sup&gt;. That's what this blog post is!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[NOTE]&lt;br&gt;
This isn't a &lt;em&gt;guide&lt;/em&gt; per say. This is simply my own experience with the setup, and it's the first time I've ever done this so there are likely to be issues. Regardless: Maybe you'll learn something!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Usually I wouldn't go for full encryption, however I chose encryption here for a few reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;As a challenge to myself, and to learn new technologies&lt;/li&gt;
&lt;li&gt;The changing political climates and my status as a minority in more ways than one unfortunately result in me being more likely to be targeted by various actors, state or otherwise, and I figure I need to protect my privacy better.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  BTRFS configuration
&lt;/h2&gt;

&lt;p&gt;Before setting up the system I'll pre-plan the BTRFS subvolumes I'll be using and the options I'll be applying to each of these, to simplify later setup. This is taken a lot from &lt;a href="https://www.jwillikers.com/btrfs-layout" rel="noopener noreferrer"&gt;Jordan Williams' post on Btrfs subvolumes&lt;/a&gt;, so I recommend going there if you want to replicate this yourself.&lt;/p&gt;

&lt;p&gt;All volumes are mounted with the options &lt;code&gt;defaults,noatime,autodefrag,ssd,compress=lzo,commit=30&lt;/code&gt; unless otherwise specified.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Subvol name&lt;/th&gt;
&lt;th&gt;Mount path&lt;/th&gt;
&lt;th&gt;Flags&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;root&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;snapshots&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/.snapshots&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Having snapshots separated is highly recommended to avoid a snapshot-within-snapshot situation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;home&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Some people also recommend having a separate subvol for each user, however for my laptop there's only one user: me. So I stick to only having &lt;code&gt;/home&lt;/code&gt; be its own subvolume.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/opt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;A lot of third-party apps are installed here, and we don't want those to be uninstalled in case of a rootfs rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;srv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/srv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Similar reason to &lt;code&gt;opt&lt;/code&gt;, as well as this being a mountpoint for other drives. Don't wanna take snapshots of everything here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;swap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/swap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove &lt;code&gt;compress&lt;/code&gt; option&lt;/td&gt;
&lt;td&gt;Swapfile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;usr_local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/usr/local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Similar reason to &lt;code&gt;opt&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;podman&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/var/lib/containers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nodatacow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Podman images are stored here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/var/lib/docker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nodatacow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Docker images are stored here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;libvirt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/var/lib/libvirt/images&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nodatacow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Libvirt (qemu, virt-manager) stores data here&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Using &lt;code&gt;lzo&lt;/code&gt; encryption won't save me a &lt;em&gt;lot&lt;/em&gt; of storage space, however it does have the highest transfer speeds out of the three available (ZLIB, LZO, ZSTD) according to &lt;a href="https://thelinuxcode.com/enable-btrfs-filesystem-compression/" rel="noopener noreferrer"&gt;a test by TheLinuxCode&lt;/a&gt;. With me having a 2TB drive, sacrificing some compression in favor of speed is therefore acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secure boot keys
&lt;/h2&gt;

&lt;p&gt;As I already had a configured system with UKI and secure boot-signed images, I made sure to make a copy of the existing secureboot private key and certificates from &lt;code&gt;/etc/kernel/secure-boot-private-key.pem&lt;/code&gt; and &lt;code&gt;secure-boot-certificate.pem&lt;/code&gt;. These were previously generated with &lt;code&gt;ukify genkey&lt;/code&gt; following the guide for &lt;a href="https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot#Assisted_process_with_systemd" rel="noopener noreferrer"&gt;secure boot with systemd&lt;/a&gt; on the Arch Wiki. Later on, when setting up image signing, these will need to be put back in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Before starting the install itself, I boot into my regular arch install to shrink the existing BTRFS partition down to roughly 500G, giving me ~1.5T to install the encrypted OS on: &lt;code&gt;btrfs filesystem resize 500G /&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Following that, installation starts off as usual. I download the latest &lt;a href="https://archlinux.org/downloads" rel="noopener noreferrer"&gt;Arch ISO&lt;/a&gt;, boot it, configure the keyboard, network, etc. All the usual stuff, including opening a &lt;code&gt;tmux&lt;/code&gt; session (because not having scrollback is annoying).&lt;/p&gt;

&lt;h3&gt;
  
  
  Partitions
&lt;/h3&gt;

&lt;p&gt;First things first, I opened &lt;code&gt;/dev/nvme0n1&lt;/code&gt; with &lt;code&gt;fdisk&lt;/code&gt;. Since the BTRFS data has been shrunk to 500G already, I can shrink the partition to match and create a new partition following it for the new installation. Once this has been configured as a linux root partition, I write and close &lt;code&gt;fdisk&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After setting up the partition, I configure it with &lt;code&gt;crypttab&lt;/code&gt;. I pre-generated a passphrase to use through Bitwarden's &lt;a href="https://bitwarden.com/passphrase-generator/#passphrase-generator" rel="noopener noreferrer"&gt;passphrase generator&lt;/a&gt;, and input that when prompted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cryptsetup luksFormat &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; luks2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cipher&lt;/span&gt; aes-xts-plain64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hash&lt;/span&gt; sha256 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--iter-time&lt;/span&gt; 2000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-size&lt;/span&gt; 256 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pbkdf&lt;/span&gt; argon2id &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--use-urandom&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--verify-passphrase&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /dev/nvme0n1p3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this sets up encryption on the partition, requiring it to be opened with &lt;code&gt;cryptsetup&lt;/code&gt; before any further configuration can be made. This will also require the password generated earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cryptsetup open /dev/nvme0n1p3 root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create a device mapper on &lt;code&gt;/dev/mapper/root&lt;/code&gt;, allowing the decrypted partition to be interacted with like any other. This gets followed up with creating a fresh BTRFS filesystem, which I mount at /mnt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkfs.btrfs /dev/mapper/root
mount /dev/mapper/root /mnt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can then create the subvolumes defined earlier, by running the following command with each of the volumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;btrfs subvolume create /mnt/@&lt;span class="nv"&gt;$NAME&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the root-volume gets unmounted, and I mount each of the subvolumes to their correct locations, putting in or removing the appropriate options as required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;umount /mnt
mount &lt;span class="nt"&gt;--mkdir&lt;/span&gt; /dev/mapper/root /mnt/&lt;span class="nv"&gt;$MOUNTPOINT&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; defaults,noatime,autodefrag,ssd,compress&lt;span class="o"&gt;=&lt;/span&gt;lzo,commit&lt;span class="o"&gt;=&lt;/span&gt;30,subvol&lt;span class="o"&gt;=&lt;/span&gt;@&lt;span class="nv"&gt;$NAME&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Meaning of each option&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;defaults&lt;/code&gt;: Default mount options from the mount.btrfs command? Honestly a bit unsure&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noatime&lt;/code&gt;: Disables access time recording, see writeups on &lt;a href="https://www.reddit.com/r/linux/comments/imgler/" rel="noopener noreferrer"&gt;reddit&lt;/a&gt; and &lt;a href="https://lwn.net/Articles/499293/" rel="noopener noreferrer"&gt;LWM&lt;/a&gt; (lwm)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;autodefrag&lt;/code&gt;: Small writes (64kb) are automatically queued for defrag? Again, not completely sure about the full effect from this, it is used in Jordan William's post linked to earlier&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ssd&lt;/code&gt;: Enables some smaller optimizations (&lt;a href="https://unix.stackexchange.com/questions/752748/what-optimizations-are-turned-on-with-the-mount-option-ssd" rel="noopener noreferrer"&gt;StackExchange&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;compress&lt;/code&gt; = Sets compression algorithm to use&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;commit&lt;/code&gt; = Sets how often periodic flushing to permanent storage should be performed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;subvol&lt;/code&gt;: Tells btrfs what subvol to mount&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is followed up with creating and mounting a swapfile using BTRFS' &lt;a href="https://wiki.archlinux.org/title/Btrfs#Swap_file" rel="noopener noreferrer"&gt;&lt;code&gt;filesystem mkswapfile&lt;/code&gt;&lt;/a&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;btrfs filesystem mkswapfile /mnt/swap/swapfile &lt;span class="nt"&gt;--size&lt;/span&gt; 40G &lt;span class="nt"&gt;--uuid&lt;/span&gt; clear
swapon /mnt/swap/swapfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And mounting the EFI partition of course:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mount /dev/nvme0n1p1 /mnt/boot &lt;span class="nt"&gt;--mkdir&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Base setup
&lt;/h3&gt;

&lt;p&gt;After setting up all the partitions, I simply set up the system like any other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate new mirrorlist&lt;/span&gt;
reflector &lt;span class="nt"&gt;--save&lt;/span&gt; /etc/pacman.d/mirrorlist &lt;span class="nt"&gt;--protocol&lt;/span&gt; http,https &lt;span class="nt"&gt;--country&lt;/span&gt; Norway,Sweden,France,Germany,Finland,Iceland,US &lt;span class="nt"&gt;--latest&lt;/span&gt; 250 &lt;span class="nt"&gt;--sort&lt;/span&gt; score &lt;span class="nt"&gt;--ipv4&lt;/span&gt; &lt;span class="nt"&gt;--threads&lt;/span&gt; 4 &lt;span class="nt"&gt;--fastest&lt;/span&gt; 50 &lt;span class="nt"&gt;--age&lt;/span&gt; 6


&lt;span class="c"&gt;# Install base packages&lt;/span&gt;
&lt;span class="c"&gt;# Further hyprland addons (like hyprshot, hyprpicker, various xdg-desktop-portals, etc are installed in the post-install section)&lt;/span&gt;
pacstrap &lt;span class="nt"&gt;-K&lt;/span&gt; /mnt base linux linux-firmware-amdgpu linux-firmware-mediatek systemd-ukify uwsm tmux kate zsh git &lt;span class="nb"&gt;sudo &lt;/span&gt;vim amd-ucode networkmanager btrfs-progs hyprland rofi dolphin man-db greetd-tuigreet fprintd efibootmgr alacritty base-devel

&lt;span class="c"&gt;# Generate an initial fstab&lt;/span&gt;
genfstab &lt;span class="nt"&gt;-U&lt;/span&gt; /mnt &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /mnt/etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing the base packages I &lt;code&gt;chroot&lt;/code&gt;ed into the system with &lt;code&gt;arch-chroot /mnt&lt;/code&gt;. Any commands after here are in the chroot unless otherwise specified.&lt;/p&gt;

&lt;p&gt;In the chroot, I do some other post-config:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup locales: &lt;code&gt;vim /etc/locale.gen&lt;/code&gt; + &lt;code&gt;locale-gen&lt;/code&gt; + &lt;code&gt;/etc/locale.conf&lt;/code&gt; + &lt;code&gt;localectl set-locale&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Setup keymap: &lt;code&gt;/etc/vconsole.conf&lt;/code&gt; + &lt;code&gt;localectl set-keymap&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Setup hostname: &lt;code&gt;/etc/hostname&lt;/code&gt; + &lt;code&gt;hostnamectl hostname&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Setup hosts file: &lt;code&gt;/etc/hosts&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Mkinitcpio
&lt;/h3&gt;

&lt;p&gt;Arch uses &lt;code&gt;mkinitcpio&lt;/code&gt; to generate the kernel and initramfs images by default, so I'll just keep using it for simplicity's sake. I could've used an alternative like &lt;a href="https://wiki.archlinux.org/title/Dracut" rel="noopener noreferrer"&gt;&lt;code&gt;dracut&lt;/code&gt;&lt;/a&gt;, but meh. &lt;code&gt;Mkinitcpio&lt;/code&gt; works.&lt;/p&gt;

&lt;p&gt;There are a few options I need to configure for mkinitcpio to&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Have the required modules to boot the encrypted-signed image&lt;/li&gt;
&lt;li&gt;Generate a &lt;a href="https://wiki.archlinux.org/title/Unified_kernel_image" rel="noopener noreferrer"&gt;UKI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sign the generated image for secure boot&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;First task is configuring &lt;code&gt;mkinitcpio&lt;/code&gt; to have the required modules for my desired setup. Editing the configuration file, I make it look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MODULES&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;btrfs tpm_crb&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;BINARIES&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;/usr/bin/btrfs&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;FILES&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;
&lt;span class="nv"&gt;HOOKS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;systemd autodetect microcode modconf kms keyboard sd-vconsole sd-encrypt block filesystems&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;COMPRESSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"lz4"&lt;/span&gt;
&lt;span class="nv"&gt;COMPRESSION_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="nt"&gt;-9&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This enables btrfs on root, sets up unlocking encryption through systemd, and increases compression ratio to around 2.5 while preserving the fastest decryption speeds (&lt;a href="https://wiki.archlinux.org/title/Mkinitcpio#COMPRESSION" rel="noopener noreferrer"&gt;Mkinitcpio#COMPRESSION on the Arch Wiki&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Second is the UKI. For this, I installed &lt;code&gt;systemd-ukify&lt;/code&gt; during pacstrap, which includes the &lt;code&gt;ukify&lt;/code&gt; binary used by &lt;code&gt;mkinitcpio&lt;/code&gt;. In the &lt;code&gt;/etc/mkinitcpio.d/linux.preset&lt;/code&gt; file, I uncomment the &lt;code&gt;default_uki&lt;/code&gt;, &lt;code&gt;default_options&lt;/code&gt;, &lt;code&gt;fallback_uki&lt;/code&gt;, and &lt;code&gt;fallback_options&lt;/code&gt; lines, and comment out the &lt;code&gt;default_image&lt;/code&gt; and &lt;code&gt;fallback_image&lt;/code&gt; options. I then put in the correct paths in the &lt;code&gt;default_uki&lt;/code&gt; and &lt;code&gt;fallback_uki&lt;/code&gt; lines (in my case &lt;code&gt;/boot/EFI/Linux/arch-linux.efi&lt;/code&gt; and &lt;code&gt;arch-linux-fallback.efi&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Uncommenting these options tells &lt;code&gt;mkinitcpio&lt;/code&gt; to generate a unified image instead of a separate kernel and initramfs. It can also be configured to automatically sign the generated image for secure boot, leading me into the final task of actually signing the images.&lt;/p&gt;

&lt;p&gt;As mentioned in Secure boot keys, I took a copy of the secure boot keys. I'll copy these over into the new system, making sure to set up &lt;code&gt;/etc/kernel/uki.conf&lt;/code&gt; in the process. I'll need to set the &lt;code&gt;SecureBootSigningTool&lt;/code&gt; to &lt;code&gt;systemd-sbsign&lt;/code&gt;, and &lt;code&gt;SecureBootPrivateKey&lt;/code&gt; + &lt;code&gt;SecureBootCertificate&lt;/code&gt; to their appropriate files. &lt;code&gt;SignKernel&lt;/code&gt; must also be set to true. Once this is done, &lt;code&gt;ukify&lt;/code&gt; signs its generated images.&lt;/p&gt;

&lt;h4&gt;
  
  
  cmdline
&lt;/h4&gt;

&lt;p&gt;Finally, I need to set up the cmdline. When using signed UKIs, the system cannot load boot options through the regular &lt;code&gt;/boot/loader/entries&lt;/code&gt; files, as these can be tampered with. Instead the options are built in to the image itself, ensuring they are secure from tampering.&lt;/p&gt;

&lt;p&gt;For my cmdline files I had a few requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The LUKS partition must be set as the root partition, and discovered for decryption&lt;/li&gt;
&lt;li&gt;The decrypted root partition must be properly mounted as BTRFS partitions from /etc/fstab&lt;/li&gt;
&lt;li&gt;Minor tuning must be done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the first and second ones, the Arch wiki is a good resource. By following &lt;a href="https://wiki.archlinux.org/title/Dm-crypt/System_configuration" rel="noopener noreferrer"&gt;Dm-crypt/System configuration&lt;/a&gt;, I set up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;rd&lt;/span&gt;.&lt;span class="n"&gt;luks&lt;/span&gt;.&lt;span class="n"&gt;name&lt;/span&gt;=&lt;span class="n"&gt;UUID&lt;/span&gt;=&lt;span class="n"&gt;root&lt;/span&gt;
&lt;span class="c"&gt;# For later TPM2 unlocking
&lt;/span&gt;&lt;span class="n"&gt;rd&lt;/span&gt;.&lt;span class="n"&gt;luks&lt;/span&gt;.&lt;span class="n"&gt;options&lt;/span&gt;=&lt;span class="n"&gt;UUID&lt;/span&gt;=&lt;span class="n"&gt;tpm2&lt;/span&gt;-&lt;span class="n"&gt;device&lt;/span&gt;=&lt;span class="n"&gt;auto&lt;/span&gt;

&lt;span class="n"&gt;root&lt;/span&gt;=/&lt;span class="n"&gt;dev&lt;/span&gt;/&lt;span class="n"&gt;mapper&lt;/span&gt;/&lt;span class="n"&gt;root&lt;/span&gt;
&lt;span class="n"&gt;rootfstype&lt;/span&gt;=&lt;span class="n"&gt;btrfs&lt;/span&gt;
&lt;span class="n"&gt;rootflags&lt;/span&gt;=&lt;span class="n"&gt;subvol&lt;/span&gt;=@
&lt;span class="n"&gt;rw&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tuning I've gone into in &lt;a href="https://dev.to/blog/tuning-and-performance-optimizations-in-arch-linux"&gt;another post&lt;/a&gt;, so I'll avoid putting it here :3&lt;/p&gt;

&lt;h3&gt;
  
  
  Signing the bootloader
&lt;/h3&gt;

&lt;p&gt;So far I've only set up signing of the images themselves (which can &lt;em&gt;technically&lt;/em&gt; be booted directly), however if I want to ever use a bootloader for multiple OSes or kernels I'll have to sign it too (less I disable secure boot every time, which isn't particularly favorable).&lt;/p&gt;

&lt;p&gt;For pacman, this is luckily decently simple. I'll need to install two hooks to &lt;code&gt;/etc/pacman.d/hooks/&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="///uploaded/80-sign-systemd-boot.hook"&gt;&lt;code&gt;80-sign-systemd-boot.hook&lt;/code&gt;&lt;/a&gt; - As the name implies, this hook signs the systemd-boot efi binary.&lt;/li&gt;
&lt;li&gt;
&lt;a href="///uploaded/95-update-systemd-boot.hook"&gt;&lt;code&gt;95-update-systemd-boot.hook&lt;/code&gt;&lt;/a&gt; - This restarts the systemd-boot updater, ensuring the new version of the binary is put into place immediately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For simplicity's sake I've just uploaded the full files for download instead of putting them into the post itself.&lt;/p&gt;

&lt;p&gt;I'll also make sure to reinstall systemd to make both hooks run once, so the binary gets signed and put into place: &lt;code&gt;sudo pacman -S systemd&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Misc last touches
&lt;/h3&gt;

&lt;p&gt;Before having a usable system, I'll need to configure a few smaller things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Greeter&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable and create cache dir for remembering the last used session
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;greetd
&lt;span class="nb"&gt;mkdir&lt;/span&gt; /var/cache/tuigreet
&lt;span class="nb"&gt;chown &lt;/span&gt;greeter:greeter /var/cache/tuigreet
&lt;span class="nb"&gt;chmod &lt;/span&gt;0755 /var/cache/tuigreet
&lt;/code&gt;&lt;/pre&gt;



&lt;ul&gt;
&lt;li&gt;Select the greeter to use by editing &lt;code&gt;/etc/greetd/config.toml&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tuigreet --time --remember --remember-user-session --user-menu --user-menu-min-uid 1000 --asterisks --cmd 'uwsm start hyprland-uwsm.desktop'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Once this is all completed: Reboot time!&lt;/p&gt;

&lt;h2&gt;
  
  
  Post-install configuration
&lt;/h2&gt;

&lt;p&gt;Now, this is where the actual TPM2 unlocking part comes into place. I'll skip all the boring "copy old home dir and struggle for 2 hours to configure dotfiles and install programs" stuff, and only focus on LUKS and TPM2 here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keys
&lt;/h3&gt;

&lt;p&gt;Once booted into the system I changed the boot order using &lt;code&gt;efibootmgr&lt;/code&gt; to ensure sd-boot was first. Then I went into &lt;code&gt;/boot/loader/loader.conf&lt;/code&gt;, and set &lt;code&gt;secure-boot-enroll force&lt;/code&gt;. This makes sd-boot enroll the keys I copied over earlier, which I in all honesty don't know how got there (see &lt;code&gt;/boot/loader/keys/auto&lt;/code&gt;). They kinda just appeared and I rolled with it.&lt;/p&gt;

&lt;p&gt;Then I reboot and ensure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The old keys are removed, leaving no secure boot keys at all&lt;/li&gt;
&lt;li&gt;Secure boot enforcement is disabled&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When sd-boot then loads it gives a message about keys being enrolled, which I don't interrupt. The system then reboots again, and I can re-activate secure boot. With the signed kernel I should then be put to the password prompt correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  TPM2
&lt;/h3&gt;

&lt;p&gt;First things first: Creating a recovery key using &lt;code&gt;systemd-cryptenroll&lt;/code&gt;. This is essentially a long, easy to type, securely generated password. The command outputs a long string which should be written down someplace safe in case everything else fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-cryptenroll /dev/nvme0n1p3 &lt;span class="nt"&gt;--recovery-key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I enrolled the TPM2 itself in the LUKS volume, again with &lt;code&gt;systemd-cryptenroll&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-cryptenroll /dev/nvme0n1p3 &lt;span class="nt"&gt;--tpm2-device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="nt"&gt;--tpm2-pcrs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7+15:sha256&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;64-zeroes&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why 64 zeroes? Can't explain it myself, the &lt;a href="https://wiki.archlinux.org/title/Systemd-cryptenroll" rel="noopener noreferrer"&gt;Arch Wiki&lt;/a&gt; does a much better job. TLDR: Something about ensuring a TPM measurement is empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;p&gt;While setting up my own system I leaned heavily on a few other blogs, namely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.jwillikers.com/btrfs-layout" rel="noopener noreferrer"&gt;Btrfs Layout - Jordan Williams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gist.github.com/mihirchanduka/a9ba1c6edbfa068d2fbc2acb614c80e8" rel="noopener noreferrer"&gt;Arch Linux Installation Guide - mihirchanduka&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also an almost 100% chance that this won't work properly. I have gone back and forth a bunch to set up my own laptop, a lot of which I unfortunately managed to forget to write down. Hopefully it helps somewhat at least!&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Technically I'm not using &lt;em&gt;full&lt;/em&gt; encryption here, as I am only encrypting the main data partition of the device and not the boot partition, however because I am signing the kernel and creating a unified image I deemed this an acceptable risk. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>linux</category>
      <category>btrfs</category>
      <category>luks</category>
      <category>tpm2</category>
    </item>
    <item>
      <title>My experience installing Nix OS</title>
      <dc:creator>Zoe</dc:creator>
      <pubDate>Sat, 27 Dec 2025 04:18:32 +0000</pubDate>
      <link>https://dev.to/thes1lv3r/my-experience-installing-nix-os-da6</link>
      <guid>https://dev.to/thes1lv3r/my-experience-installing-nix-os-da6</guid>
      <description>&lt;p&gt;After some friends of mine started using and recommending &lt;a href="https://nixos.org" rel="noopener noreferrer"&gt;NixOS&lt;/a&gt; to me I eventually got tempted enough by the sweet, sweet reproducibility and git-managed system that I decided to give it a try.&lt;/p&gt;

&lt;p&gt;At first, I wasn't a massive fan. Configurations were archaic, the nix configuration language had some... quirks (?) that I weren't a big fan of (more on that at #Nix language quirks, and configuring flakes for git-managed configurations wasn't exactly intuitive.&lt;/p&gt;

&lt;p&gt;Later though, I did start getting a bit used to the system. A lot of that was thanks to my friend &lt;a href="https://github.com/soni801" rel="noopener noreferrer"&gt;Soni&lt;/a&gt; who helped me for hours on end with setting up the system, and answered every question I annoyed him with in our DMs. Thanks a lot :3&lt;/p&gt;

&lt;p&gt;To put the TLDR at the start: I am likely not gonna be switching to Nix full time. It's good, just not quite for me. More on that at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation process
&lt;/h2&gt;

&lt;p&gt;First of all, there's the installation process. Now, this was likely mostly an issue with my machine, but for some unknown reason WiFi was absolutely &lt;em&gt;refusing&lt;/em&gt; to connect, whether I was using the Minimal or Graphical installer, and regardless of environment in the Graphical installer. This caused me about an hour of pain with trying, restarting, retrying, and repeat. In the end I just plugged the machine in with a cable, and that solved my issues, but not a "good" first experience at least.&lt;/p&gt;

&lt;p&gt;Next, the installer itself was being kinda annoying. There was no ability to shrink my existing LUKS partition to make space for installing Nix itself, so I had to use GParted for this. That itself is fine, as shrinking a LUKS partition is decently niche. But when I then did shrink the partition to make space, the installer never refreshed the partition selection, forcing me to restart the installer completely. Again: Niche is, but a small annoyance either way.&lt;/p&gt;

&lt;p&gt;Finally, and I noticed this only after the installation itself, it overwrote my &lt;code&gt;systemd-boot&lt;/code&gt; config completely and changed the EFI parameters to ensure Nix was booted first instead of using the existing &lt;code&gt;sd-boot&lt;/code&gt; entry and config. As I had customized my &lt;code&gt;loader.conf&lt;/code&gt; a decent amount, this was kinda annoying. Nothing I couldn't fix, but another small annoyance. As I was quick to notice, there were a lot of those.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nix language quirks
&lt;/h2&gt;

&lt;p&gt;The Nix language itself is very good, I like it quite a lot. However, some small things kinda got to me.&lt;/p&gt;

&lt;p&gt;First: The fact that there is a semicolon between &lt;code&gt;with pkgs&lt;/code&gt; and an array. To me, coming from other languages (and even from the nix language itself), this seems incorrect. To me, a semicolon indicates "end of this statement", so in Nix' case "end of this variable or definition". It was not logical to me that in &lt;em&gt;this&lt;/em&gt; case only, the statement continued after the semicolon. Once I got used to this it was no issue at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="c"&gt;# How it is:&lt;/span&gt;
&lt;span class="nv"&gt;packages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kn"&gt;with&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c"&gt;# How it makes intuitive sense for me:&lt;/span&gt;
&lt;span class="nv"&gt;packages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kn"&gt;with&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, and this is more a nitpick than anything: Indents and bracketing. Why does &lt;code&gt;nixfmt&lt;/code&gt; insist on this pattern (pseudocode):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="nv"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;openssh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="nv"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;openssh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again: Preference, doesn't at all matter for the operation of the system, but just tiny nitpicks. I don't &lt;em&gt;have&lt;/em&gt; to use &lt;code&gt;nixfmt&lt;/code&gt; at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;Now for a change of pace: Some good things and things I liked with Nix!&lt;/p&gt;

&lt;p&gt;First: I absolutely LOVE the declarative configuration. I am absolutely a fan of being able to define "I want foo, bar, and baz" and have only that be installed, and if I remove anything from that list, it gets removed automatically. I am ABSOLUTELY a fan of that, not a question about it.&lt;/p&gt;

&lt;p&gt;Further: I love the ability to, with a tiny bit of config, have different setups for different machines while being able to share base configurations. Being able to add &lt;code&gt;nixosConfigurations&lt;/code&gt; and run &lt;code&gt;nixos-rebuild switch --flake .#hostname&lt;/code&gt; to have my desired configuration for my exact system: Absolutely love it. That is exactly what other dotfile managers and config managers should strive to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bad
&lt;/h2&gt;

&lt;p&gt;Now: Back to some more complaining (unfortunately). NixOS' lack of following the &lt;a href="https://refspecs.linuxfoundation.org/FHS_3.0/fhs-3.0.html" rel="noopener noreferrer"&gt;FHS&lt;/a&gt; absolutely made the transition to Nix more difficult for me. Trying to find config files for programs and services (or really any files at all) was made incredibly difficult. I essentially needed to throw away everything I knew and start completely anew (which I guess is kinda the whole point of immutable systems?)&lt;/p&gt;

&lt;p&gt;This leads me into my second main problem: The documentation is... something. Configuration documentation is seemingly spread across 10 different websites, all of which having something the others don't have. The &lt;a href="https://wiki.nixos.org/" rel="noopener noreferrer"&gt;nix wiki&lt;/a&gt; is a good starting point and a good resource for basic configuration, but the second I needed anything else I found myself digging down GitHub issues, Nix search sites, and my friends' DMs.&lt;/p&gt;

&lt;p&gt;Finally, and again, Nitpick (I do that a lot, huh): Running VSCode through the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh" rel="noopener noreferrer"&gt;Remote - SSH&lt;/a&gt; extension was... not easy. Turns out the node binary that ships with that extension and the remote server is dynamically linked, which NixOS doesn't support without additional configuration. That in and of itself isn't necessarily bad, as there are workarounds.&lt;/p&gt;

&lt;p&gt;The issue I encountered after that though, that was a bit annoying. Turns out if I choose the &lt;a href="https://github.com/K900/vscode-remote-workaround" rel="noopener noreferrer"&gt;patched binary workaround&lt;/a&gt;, the remote server gets ran in &lt;a href="https://github.com/containers/bubblewrap" rel="noopener noreferrer"&gt;bubblewrap&lt;/a&gt;, making &lt;code&gt;sudo&lt;/code&gt; unavailable, meaning I had to have an open terminal on the laptop itself for rebuilding the system. Now I admit, my use case is... different than a lot of others'. But: Slight annoyance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The confusing
&lt;/h2&gt;

&lt;p&gt;Then come the parts that are just kinda... confusing? They're not inherently bad, just not intuitive for a new user.&lt;/p&gt;

&lt;p&gt;First: The (at least) 5 different way to install packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;systemPackages&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;programs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;home-manager &lt;code&gt;packages&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wayland.windowManager&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After using and working with Nix a bit more I do understand &lt;em&gt;why&lt;/em&gt; it is this way. All of them serve a different purpose, and configure a slightly different part of the system. But knowing which to use when was maybe one of the most confusing things for me as a new user.&lt;/p&gt;

&lt;p&gt;Next: User services. Ohhh did I ever struggle with user services. I was thinking I was gonna install and use &lt;a href="https://github.com/ErikReider/SwayNotificationCenter" rel="noopener noreferrer"&gt;SwayNotificationCenter&lt;/a&gt; for my notifications in Hyprland. So first I need to figure out what package to use. I try what seems like the most logical option, the &lt;a href="https://mynixos.com/nixpkgs/package/swaynotificationcenter" rel="noopener noreferrer"&gt;&lt;code&gt;pkgs.swaynotificationcenter&lt;/code&gt;&lt;/a&gt; package. That one did work if launched manually or through the hypr &lt;code&gt;exec-once&lt;/code&gt; config, but as I was using &lt;a href="https://github.com/Vladimir-csp/uwsm" rel="noopener noreferrer"&gt;uwsm&lt;/a&gt;, I was thinking I could enable the user service. Only issue: There was none!&lt;/p&gt;

&lt;p&gt;So I started digging. Was it in /etc...? No, right, Nix doesn't use FHS. /usr/lib/systemd..? Nope, nothing there either. I dug for about half an hour until I realized: The package itself doesn't include a service! So I started researching other methods, and saw that home-manager includes a &lt;code&gt;services&lt;/code&gt; key. However it &lt;em&gt;clearly&lt;/em&gt; (very sarcastic) uses a different format to regular flakes and nix config, and keys used in &lt;code&gt;nix&lt;/code&gt; aren't available in &lt;code&gt;home-manager&lt;/code&gt; despite using the same file format.&lt;/p&gt;

&lt;p&gt;So, after another half hour of debugging, I did figure out how to get it running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="nv"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;swaync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Easy enough, I guess, just not intuitive at all. I also have no idea where this has installed the binaries or programs themselves /shrug.&lt;/p&gt;

&lt;p&gt;Finally, what seems like a packaging bug to me? The &lt;a href="https://search.nixos.org/packages?channel=unstable&amp;amp;query=krunner" rel="noopener noreferrer"&gt;&lt;code&gt;krunner&lt;/code&gt;&lt;/a&gt; package just... didn't install &lt;code&gt;krunner&lt;/code&gt;? In fact, it doesn't install &lt;em&gt;anything&lt;/em&gt;! I believe this is probably just a bug, but an annoying one nonetheless. Only way to get krunner was by installing the &lt;code&gt;kdePackages.plasma-workspace&lt;/code&gt; package, but this also installed a lot of junk I didn't want at all. I dug into only including the &lt;code&gt;bin/krunner&lt;/code&gt; part of the package, but that got me into &lt;a href="https://nix.dev/manual/nix/2.22/language/derivations" rel="noopener noreferrer"&gt;derivations&lt;/a&gt; which I was not nearly ready for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overall
&lt;/h2&gt;

&lt;p&gt;So far it might've seemed like I've mostly complained, however: I do like Nix. It's confusing for sure, and not necessarily very intuitive even for seasoned Linux users like myself, but the ideas and concepts it brings to the table are ones I very much enjoy. I won't be switching to Nix full-time, but perhaps I'll be more open to dabbling around in it in the future? For now though, I'll be sticking to Arch, btw :3&lt;/p&gt;

</description>
      <category>nixos</category>
      <category>linux</category>
    </item>
    <item>
      <title>[Minipost] Fixing krunner not showing applications in Hyprland</title>
      <dc:creator>Zoe</dc:creator>
      <pubDate>Sat, 27 Dec 2025 04:17:18 +0000</pubDate>
      <link>https://dev.to/thes1lv3r/minipost-fixing-krunner-not-showing-applications-in-hyprland-3emd</link>
      <guid>https://dev.to/thes1lv3r/minipost-fixing-krunner-not-showing-applications-in-hyprland-3emd</guid>
      <description>&lt;p&gt;Quick one: If on hyprland (or likely any non-plasma WM/DE), krunner is likely to not be showing installed desktop applications. This is caused by a missing symlink in the &lt;code&gt;/etc/xdg/menus&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;Easy copy-paste fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/xdg/menus/plasma-applications.menu /etc/xdg/menus/applications.menu
&lt;span class="nb"&gt;sudo &lt;/span&gt;update-desktop-database &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rebuilds the cache and allows krunner to see the missing applications :3&lt;/p&gt;

</description>
      <category>hyprland</category>
      <category>xdg</category>
      <category>linux</category>
    </item>
    <item>
      <title>Automatic LetsEncrypt certificates with Tailscale and Traefik in Docker</title>
      <dc:creator>Zoe</dc:creator>
      <pubDate>Sat, 27 Dec 2025 03:11:45 +0000</pubDate>
      <link>https://dev.to/thes1lv3r/automatic-letsencrypt-certificates-with-tailscale-and-traefik-in-docker-333i</link>
      <guid>https://dev.to/thes1lv3r/automatic-letsencrypt-certificates-with-tailscale-and-traefik-in-docker-333i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Just want it to work? See #The Solution. Want to know &lt;em&gt;why&lt;/em&gt; it works, along with some backstory? Read on :)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Recently (not really, 2022), Traefik released support for &lt;a href="https://traefik.io/blog/exploring-the-tailscale-traefik-proxy-integration" rel="noopener noreferrer"&gt;obtaining LetsEncrypt certs for Tailscale hosts&lt;/a&gt;. With this, Traefik gained the ability to automatically (if configured correctly) communicate with a locally running Tailscale daemon to request a certificate through the &lt;a href="https://tailscale.com/kb/1153/enabling-https" rel="noopener noreferrer"&gt;Tailscale certificate cervices&lt;/a&gt; for the locally running machine.&lt;/p&gt;

&lt;p&gt;Doing some research, this seemed to perfectly align with my goals, so I gave it a try. Reading up on documentation a bit, I figured it would be enough to just configure Traefik to be on the same network as Tailscale, so I tried the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tailscale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# redacated for example&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service:tailscale&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, when trying this out I kept getting a strange error... Traefik was unable to communicate with the Tailscale daemon even though it was running on the same network!&lt;/p&gt;

&lt;p&gt;Looking into the logs and source, I discovered that Traefik is trying to access Tailscale through the local socket on &lt;code&gt;/var/run/tailscale/tailscaled.sock&lt;/code&gt;. This obviously isn't passed through through the above &lt;code&gt;network:&lt;/code&gt; option, so I add some more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tailscale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;var_run_tailscale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;/var/run/tailscale&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;var_run_tailscale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;/var/run/tailscale&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;var_run_tailscale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But, this also didn't work! At this point I got stumped for a solid while. It should've worked, no? Traefik has access to the Tailscale socket? But no! Digging into the containers themselves and checking the status of the socket, I see this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;traefik sh
/ &lt;span class="c"&gt;# stat /var/run/tailscale/tailscaled.sock&lt;/span&gt;
  File: &lt;span class="s1"&gt;'/var/run/tailscale/tailscaled.sock'&lt;/span&gt; -&amp;gt; &lt;span class="s1"&gt;'/tmp/tailscaled.sock'&lt;/span&gt;
&lt;span class="c"&gt;#...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aha! The socket, for whatever reason, is actually a symlink to &lt;code&gt;/tmp&lt;/code&gt;?? This really confused me, as on my non-docker install of tailscale the socket is simply a regular socket.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Talking with a friend, I was told that sockets are usually put in &lt;code&gt;/tmp&lt;/code&gt; on rootless containers, however Tailscale seems to run as rootfull? Running &lt;code&gt;ps aux&lt;/code&gt; in the container reveals &lt;code&gt;tailscaled&lt;/code&gt; running as root, so I don't think that's the explanation in this case.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, more source code digging is required. Looking into the container, I see it starts a binary called &lt;code&gt;containerboot&lt;/code&gt;. I checked the tailscale repo, and found the matching file: &lt;a href="https://github.com/tailscale/tailscale/blob/b21cba0921dfd4c8ac9cf4fa7210879d0ea7cf34/cmd/containerboot/tailscaled.go" rel="noopener noreferrer"&gt;&lt;code&gt;cmd/containerboot/tailscaled.go&lt;/code&gt;&lt;/a&gt; Nestled in there, on lines 78-82, there is the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StateDir&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--statedir="&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StateDir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--state=mem:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--statedir=/tmp"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And where does &lt;code&gt;cfg.StateDir&lt;/code&gt; come from? Well, &lt;code&gt;cfg&lt;/code&gt; is initialized in &lt;a href="https://github.com/tailscale/tailscale/blob/d451cd54a70152a95ad708592a981cb5e37395a8/cmd/containerboot/main.go#L154" rel="noopener noreferrer"&gt;&lt;code&gt;cmd/containerboot/main.go&lt;/code&gt;&lt;/a&gt; by calling &lt;code&gt;configFromEnv()&lt;/code&gt;, and that is defined in &lt;a href="https://github.com/tailscale/tailscale/blob/d451cd54a70152a95ad708592a981cb5e37395a8/cmd/containerboot/settings.go#L105" rel="noopener noreferrer"&gt;&lt;code&gt;cmd/containerboot/settings.go&lt;/code&gt;&lt;/a&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;configFromEnv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
    &lt;span class="n"&gt;Socket&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;defaultEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TS_SOCKET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/tmp/tailscaled.sock"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aha! It defaults to putting the socket in &lt;code&gt;/tmp&lt;/code&gt; (for whatever reason)! Why this was introduced I have NO idea, I tried to dig around a bit and got nowhere&lt;sup id="fnref1"&gt;1&lt;/sup&gt;, however this does tell me what to change to get it to work properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;So with these in mind, there are a few required properties for the compose file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Traefik has to use Tailscale's networking to allow using the tailnet&lt;/li&gt;
&lt;li&gt;Traefik needs R/W access to &lt;code&gt;tailscale.sock&lt;/code&gt; to be able to request a certificate&lt;/li&gt;
&lt;li&gt;Tailscale needs to put the socket in the correct location&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives me the following file in the end (only including keys required for this specific config):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service:tailscale&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--certificatesResolvers.tailscale.tailscale=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--entrypoints.websecure.address=:443&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--entrypoints.websecure.http.tls.certResolver=tailscale&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;var_run_tailscale:/var/run/tailscale:rw,Z&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;tailscale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

  &lt;span class="na"&gt;tailscale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;var_run_tailscale:/var/run/tailscale:rw,Z&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;TS_SOCKET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/run/tailscale/tailscale.sock&lt;/span&gt;
      &lt;span class="na"&gt;TS_USERSPACE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;# Traffic goes through traefik, not directly through tailscale itself&lt;/span&gt;
      &lt;span class="na"&gt;TS_HOSTNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-app.tailscale-domain.ts.net&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those options will expose Traefik to the tailnet, and allow it to get certificates for itself. Note that Traefik can only ever get a single cert, namely whatever matches the &lt;code&gt;TS_HOSTNAME&lt;/code&gt; env var. This is a limitation of Tailscale, and while there is an open issue to get this changed (&lt;a href="https://github.com/tailscale/tailscale/issues/7081" rel="noopener noreferrer"&gt;#7081&lt;/a&gt;), it has not seen activity in almost two years as of this writing, so I am not hopeful for it being implemented anytime soon.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Well actually that's not &lt;em&gt;completely&lt;/em&gt; true. I did find a few issues on it, most notably &lt;a href="https://github.com/tailscale/tailscale/issues/6849" rel="noopener noreferrer"&gt;#6849 - Change default socket path in containers&lt;/a&gt; as well as the commit where this was introduced (&lt;a href="https://github.com/tailscale/tailscale/commit/2c403cbb313c89741068c7d9d700169c1bbf3ad5" rel="noopener noreferrer"&gt;&lt;code&gt;2c403cb&lt;/code&gt;&lt;/a&gt;), however neither of these explain &lt;em&gt;why&lt;/em&gt; it was done the way it was. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>tailscale</category>
      <category>docker</category>
      <category>tls</category>
      <category>letsencrypt</category>
    </item>
  </channel>
</rss>
