<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Eric Heinemann</title>
    <subtitle>Systems engineer for NixOS, infrastructure and security. Homelab, open source and a tech blog.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://daskladas.de/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://daskladas.de"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-05-01T00:00:00+00:00</updated>
    <id>https://daskladas.de/atom.xml</id>
    <entry xml:lang="en">
        <title>My NixOS-based Ugreen NAS</title>
        <published>2026-04-01T00:00:00+00:00</published>
        <updated>2026-05-01T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://daskladas.de/blog/nixos-nas-setup/"/>
        <id>https://daskladas.de/blog/nixos-nas-setup/</id>
        
        <content type="html" xml:base="https://daskladas.de/blog/nixos-nas-setup/">&lt;p&gt;I&#39;ve been using NixOS for about two years now. Started in a VM just to play around, and these days I run it everywhere. So when I ordered the NAS, the OS question was settled before the box even arrived. But before I started, I did some digging. I couldn&#39;t find anything about a NixOS-based NAS. Not on the Discourse forum, not on GitHub, nowhere. Which only made me more eager to find out whether it could really work the way I wanted. So I ordered the Ugreen DXP4800 Plus with a few intentions in mind. On the one hand, ending up with a stable, open source, reproducible NAS that lives in Git and runs in production. On the other, getting to know NixOS even better in yet another use case — treating it as a learning and testing project along the way. The pre-installed OS from Ugreen (UGOS) is fine, but I want full control. No proprietary OS, no clicking through web UIs, no mysterious background services. And above all: no forced account creation and no shady telemetry. Just NixOS, where every file, every NFS export, every kernel module lives in config files I can read, version, and rebuild from scratch.&lt;/p&gt;
&lt;p&gt;Here&#39;s some of what came out of that journey. This post covers both the software and the hardware.&lt;/p&gt;
&lt;h2 id=&quot;the-hardware&quot;&gt;The Hardware&lt;/h2&gt;
&lt;p&gt;The DXP4800 Plus is a 4-bay x86 NAS with an Intel Pentium Gold 8505 (5C/6T), 8 GB DDR5, two internal NVMe slots, an extra internal SSD slot, and 2.5GbE plus an unused 10GbE port. I liked this NAS not only for its clean, modern look, but also for the upgrade options — the RAM situation in particular won me over. Upgrading the hardware is genuinely easy. Unlike my experience with the QNAP TS-253E-8G I had before, the Ugreen is a joy to work on. Loosen one screw on the bottom panel and you&#39;re in. RAM and SSDs are right there. Here&#39;s what I have in mine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2× Patriot P300 128GB NVMe → mdadm RAID1, ext4, this is where NixOS lives&lt;/li&gt;
&lt;li&gt;3× Seagate IronWolf 8TB → mdadm RAID5, btrfs+zstd:1, ~14.55 TiB usable on &lt;code&gt;/data&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Bay 4 free for later&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So one of the three HDDs holding the data can fail (RAID5 lets the missing data be reconstructed from the other two), and so can one of the two SSDs holding the OS, since they&#39;re mirrored.
The internal SSD is always shipped with UGOS and is, for the moment, almost completely untouched. More on why in a bit.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;/blog/nixos-nas-setup/down.jpg&quot; alt=&quot;Underside of the Ugreen DXP4800 Plus with the RAM slot and M.2 NVMe slots&quot; width=&quot;1440&quot; height=&quot;957&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
  &lt;figcaption&gt;The underside of the NAS, where the hardware upgrades happen. Photo: techreviewer.de&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;disko-and-the-raid-layout&quot;&gt;Disko and the RAID Layout&lt;/h2&gt;
&lt;p&gt;Disko is a tool that lets you partition the entire system declaratively. Basically: &quot;Write into this file how the partitioning should look, and so it shall be.&quot; For this we can look at the following two files. The mdadm we&#39;ll bump into more often is a Linux tool (multiple device administrator), which we&#39;ll use to create, manage, and monitor our RAIDs.&lt;/p&gt;
&lt;p&gt;The whole partitioning lives in &lt;code&gt;disko-config.nix&lt;/code&gt;. I&#39;ll split it into two parts to make it easier to walk through.
Part 1:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #D3C6AA); background-color: light-dark(#FFFFFF, #2D353B);&quot; &gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;mdadm&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#C2C3C5, #859289);&quot;&gt;  #&lt;/span&gt;&lt;span style=&quot;color: light-dark(#C2C3C5, #859289);&quot;&gt; NVMe RAID1 Mirror → NixOS root (ext4)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  nixos&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    type&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;mdadm&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    level&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; 1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    metadata&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;1.2&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    content&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;      type&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;filesystem&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;      format&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;ext4&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;      mountpoint&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this snippet I&#39;m saying: take the two NVMe partitions (defined further up in disko-config.nix) and build an mdadm RAID1 array called nixos, format it as ext4, mount it at /. Disko creates the array, the filesystem, and the mountpoint completely automatically at install time — no manual mdadm --create, mkfs.ext4, or /etc/fstab fiddling needed.&lt;/p&gt;
&lt;p&gt;Part 2:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #D3C6AA); background-color: light-dark(#FFFFFF, #2D353B);&quot; &gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#C2C3C5, #859289);&quot;&gt;  #&lt;/span&gt;&lt;span style=&quot;color: light-dark(#C2C3C5, #859289);&quot;&gt; HDD RAID5 Array → Data (btrfs with compression)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  data&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    type&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;mdadm&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    level&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; 5&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    metadata&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;1.2&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    content&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;      type&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;filesystem&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;      format&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;btrfs&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;      mountpoint&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;/data&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;      mountOptions&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;defaults&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;noatime&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;compress=zstd:1&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;commit=3600&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Disko-speak it reads like this: three drives, RAID level 5, btrfs as the filesystem, mountpoint /data. The mount options carry most of the tuning — noatime suppresses access-time updates that would otherwise turn every read into an extra metadata write, compress=zstd:1 is the cheapest compression level, and commit=3600 raises the btrfs metadata commit interval from the default 30s to 3600s. Data writes themselves still go through the normal write path and stay consistent thanks to btrfs&#39;s copy-on-write. The trade: in a power loss without sync, up to the last 60 minutes of metadata updates are gone (filename changes, new snapshots, etc.) — the data files themselves stay uncorrupted. On a home NAS without a UPS, a deliberately accepted trade against write load on the HDDs.&lt;/p&gt;
&lt;h2 id=&quot;the-boot-madness&quot;&gt;The Boot Madness&lt;/h2&gt;
&lt;p&gt;The DXP4800 Plus has one downside that cost me some time. The BIOS only treats the factory NVMe slot (where UGOS lives) as bootable. My two Patriot SSDs are invisible in the boot menu. So: the bootloader has to stay on the UGOS SSD&#39;s ESP, even though everything else runs from the Patriots.&lt;/p&gt;
&lt;p&gt;Quick refresher in case you&#39;re not deep in UEFI land: the ESP (EFI System Partition) is the small FAT32 partition where the bootloader lives. At startup, the firmware reads the ESP, finds the systemd-boot entry there, and starts the kernel through it. No reachable ESP, no boot menu, no kernel, nothing. And above all: no NixOS.&lt;/p&gt;
&lt;p&gt;Solution: NixOS&#39; primary ESP sits on Patriot #1, and on every nixos-rebuild /boot gets synced to two more places — the backup ESP on Patriot #2 (so the system still boots if one NVMe dies) and the UGOS SSD&#39;s ESP (which is where the BIOS actually looks). The UGOS one is the only one that ever actually gets booted from — the other two are insurance policies for the case where I eventually pull the UGOS SSD or it dies.&lt;/p&gt;
&lt;p&gt;Now to the code snippet. What this snippet does is automatically write a copy of the bootloader to the two other ESPs after every nixos-rebuild. That&#39;s necessary because NixOS only updates the primary ESP on its own — my two backup ESPs would get more outdated with every update and end up booting an old, broken state if I ever needed them.&lt;/p&gt;
&lt;p&gt;This goes through the NixOS hook boot.loader.systemd-boot.extraInstallCommands, into which you can drop a small shell script. Mine mounts the backup ESP and the UGOS ESP one after the other, copies /boot over with rsync, and unmounts again. For the UGOS part I also briefly have to drop the read-only flag with blockdev --setrw (otherwise nothing can be written), and re-enable it right after with --setro.&lt;/p&gt;
&lt;p&gt;Here&#39;s the code:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #D3C6AA); background-color: light-dark(#FFFFFF, #2D353B);&quot; &gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;boot&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;loader&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;systemd-boot&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;extraInstallCommands&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  # Sync to backup ESP (Patriot P300 #2)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  DEVICE=&amp;quot;/dev/disk/by-id/nvme-Patriot_M.2_P300_128GB_P300LCBA2508221720&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  BACKUP_PART=&amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #A7C080);&quot;&gt;&amp;#39;&amp;#39;$&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;{DEVICE}-part1&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  mkdir -p /boot-backup&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  mount &amp;quot;$BACKUP_PART&amp;quot; /boot-backup || true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  rsync -a --delete /boot/ /boot-backup/ || true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  umount /boot-backup 2&amp;gt;/dev/null || true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  # Sync to UGOS ESP (BIOS boots from here)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  UGOS_DEV=$(readlink -f /dev/disk/by-id/nvme-YSO128GTLCW-...)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  if [ -n &amp;quot;$UGOS_DEV&amp;quot; ]; then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    blockdev --setrw &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #A7C080);&quot;&gt;&amp;#39;&amp;#39;$&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;{UGOS_DEV}&amp;quot; || true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    blockdev --setrw &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #A7C080);&quot;&gt;&amp;#39;&amp;#39;$&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;{UGOS_DEV}p1&amp;quot; || true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    mkdir -p /boot-ugos&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    mount -o rw &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #A7C080);&quot;&gt;&amp;#39;&amp;#39;$&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;{UGOS_DEV}p1&amp;quot; /boot-ugos || true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    rsync -a --delete --exclude=&amp;#39;EFI/debian&amp;#39; --exclude=&amp;#39;boot&amp;#39; /boot/ /boot-ugos/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    umount /boot-ugos&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    blockdev --setro &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #A7C080);&quot;&gt;&amp;#39;&amp;#39;$&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;{UGOS_DEV}p1&amp;quot; || true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    blockdev --setro &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #A7C080);&quot;&gt;&amp;#39;&amp;#39;$&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;{UGOS_DEV}&amp;quot; || true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(I shortened the absolute store paths to keep this readable — the real script has &lt;code&gt;${pkgs.rsync}/bin/rsync&lt;/code&gt; and so on.)&lt;/p&gt;
&lt;p&gt;The two &lt;code&gt;blockdev --setrw&lt;/code&gt; / &lt;code&gt;--setro&lt;/code&gt; rounds around the UGOS mount are necessary because the SSD is otherwise pinned read-only — we&#39;ll get to that.&lt;/p&gt;
&lt;h2 id=&quot;nfs-shares-for-backups-home-theater-and-staging&quot;&gt;NFS — Shares for Backups, Home Theater, and Staging&lt;/h2&gt;
&lt;p&gt;The NAS is supposed to expose three network shares: first, as a backup target for my Proxmox cluster; second, as a media source for Jellyfin (my home theater setup, which runs on a separate server); and third, as a staging area also used by the Jellyfin server.&lt;/p&gt;
&lt;p&gt;These three use cases all have completely different requirements, so each one gets its own mountpoint with its own permissions. As the protocol I&#39;m using NFS — it&#39;s the standard in Linux land, every client can speak it without extra software, and performance over LAN is more than enough. Specifically I&#39;m using NFSv4 instead of v3, because v4 handles everything over a single TCP port (2049). With v3 you had to assign fixed ports to rpc.mountd, rpc.statd, and lockd individually and open all of them in the firewall — more fiddling that v4 saves you.&lt;/p&gt;
&lt;p&gt;In NixOS, the NFS server is set up via services.nfs.server. The exports strings are one-to-one the same content that would go into /etc/exports on a classic distro — NixOS just renders the file from this:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #D3C6AA); background-color: light-dark(#FFFFFF, #2D353B);&quot; &gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;services&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;nfs&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;server&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  enable&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  exports&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    /data/backup   192.168.60.0/24(rw,sync,no_subtree_check,no_root_squash)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    /data/media    192.168.60.0/24(ro,sync,no_subtree_check,root_squash) 192.168.50.0/24(ro,sync,no_subtree_check,root_squash)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    /data/incoming 192.168.60.27(rw,sync,no_subtree_check,no_root_squash)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;  &amp;#39;&amp;#39;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each line follows the pattern &lt;code&gt;&amp;lt;path&amp;gt; &amp;lt;client&amp;gt;(&amp;lt;options&amp;gt;)&lt;/code&gt;. Quick rundown of what the options do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rw / ro — write or read access&lt;/li&gt;
&lt;li&gt;sync — server only acknowledges writes once they&#39;re actually on disk (safer than async, but slower; for backups that&#39;s exactly what I want)&lt;/li&gt;
&lt;li&gt;no_subtree_check — disables an old consistency check that today only causes problems and costs performance&lt;/li&gt;
&lt;li&gt;root_squash / no_root_squash — decides whether a root user on the client is also treated as root on the NAS, or gets mapped to the unprivileged user &quot;nobody&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The split hangs entirely on that last point. Proxmox writes its VM and LXC backups as root, so /data/backup absolutely needs no_root_squash and rw — otherwise the backups end up on the NAS as &quot;nobody&quot; and Proxmox bails out with permission errors on the next run. Restricted to the LAN subnet (192.168.60.0/24).&lt;/p&gt;
&lt;p&gt;/data/media is read-only and shared with two subnets at once — the LAN and the VLAN where the Jellyfin server hangs. root_squash is fine here since it&#39;s read-only anyway; it&#39;s the safe default.&lt;/p&gt;
&lt;p&gt;/data/incoming is the trickiest path because it gets written to with root privileges (no_root_squash + rw). To make sure not just any client on the LAN can do damage, the share is restricted to a single IP: 192.168.60.27, my media server. No other host can mount it in the first place.&lt;/p&gt;
&lt;p&gt;Besides SSH, that means the firewall only has port 2049 (TCP+UDP) open — that&#39;s all NFSv4 needs.&lt;/p&gt;
&lt;h2 id=&quot;smart-scrubs-and-not-losing-it-when-a-drive-dies&quot;&gt;SMART, Scrubs, and Not Losing It When a Drive Dies&lt;/h2&gt;
&lt;p&gt;Hard drives die. The question isn&#39;t if, but when. Usually exactly when you don&#39;t expect it or really can&#39;t deal with it. You can&#39;t prevent that, but you can prepare. For exactly that there are two independent mechanisms I have running: SMART monitoring on the drive level and btrfs scrubs on the filesystem level.&lt;/p&gt;
&lt;p&gt;SMART (Self-Monitoring, Analysis and Reporting Technology) is built into every halfway modern drive. The drive itself counts how many read errors have occurred, how hot it&#39;s getting, how many sectors had to be remapped, how many hours it&#39;s been running. But just having those values isn&#39;t enough — you have to read them regularly, compare them over time, and raise an alarm when something looks off. Basically the standard built-in monitoring for HDDs. And that&#39;s exactly what smartd, the daemon from the smartmontools package, does. In NixOS it&#39;s enabled via services.smartd:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #D3C6AA); background-color: light-dark(#FFFFFF, #2D353B);&quot; &gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;services&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;smartd&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  enable&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  autodetect&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  notifications&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;mail&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;enable&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  defaults&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;monitored&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;-a -o on -S on -n standby,q -s (S/../.././13|L/../../7/13) -W 4,45,55&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;autodetect = true makes sure smartd finds all attached drives automatically, so I don&#39;t have to list them one by one. More interesting is defaults.monitored — that&#39;s the default option set smartd applies to every drive. What the flags do, individually:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-a — monitor everything SMART has to offer (health status, attributes, self-test logs, error logs)&lt;/li&gt;
&lt;li&gt;-o on / -S on — automatic offline data collection and attribute saving enabled; the drive happily collects its own values in the background&lt;/li&gt;
&lt;li&gt;-n standby,q — if a drive is currently sleeping, don&#39;t wake it up just to check on it (q = quiet, so no log spam over skipped checks). That matters because the spindown timer from the previous section would otherwise be pointless and the drives would never go to sleep.&lt;/li&gt;
&lt;li&gt;-s (S/../.././13|L/../../7/13) — schedule for self-tests: a short test every day at 1 PM (S for Short), a long one every Sunday at 1 PM (L for Long). The times are deliberately not at night, because between 02 and 04 AM my Proxmox Backup Server (PBS) writes its VM snapshots to the NAS — a long test running in parallel would double the drive load and push backup duration into uncomfortable territory.&lt;/li&gt;
&lt;li&gt;-W 4,45,55 — temperature monitoring: log every 4°C of change, warn at 45°C, critical at 55°C. IronWolf drives are spec&#39;d up to ~65°C, so 55°C is still in the green zone, but the point at which I want to check on what&#39;s going on&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But that only covers what the drive itself notices. There&#39;s a second class of errors SMART is blind to: silent bitrot. That&#39;s when a single bit flips on an otherwise healthy drive — through magnetic drift over the years, marginal sectors, imprecise write heads, or rarely some electronic interference. (Cosmic rays often get cited in this context, but they&#39;re practically irrelevant for magnetic storage — that argument applies to DRAM, and that&#39;s what ECC RAM is for.) The drive registered no read or write error, SMART says everything&#39;s green, but the file sitting there is corrupt anyway. With 14 TiB of storage that&#39;s not a theoretical worry.&lt;/p&gt;
&lt;p&gt;Exactly against that, btrfs maintains checksums for every block. For that to actually protect anything, the blocks have to be regularly read and verified against their checksum — otherwise nobody notices a thing until you happen to open the broken file. That process is called a scrub, and in NixOS I enable it with three lines:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #D3C6AA); background-color: light-dark(#FFFFFF, #2D353B);&quot; &gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;services&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;btrfs&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;autoScrub&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  enable&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  interval&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;monthly&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  fileSystems&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;/data&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s it — once a month a systemd timer kicks off and reads and verifies every single block on /data. Wherever a checksum doesn&#39;t match, btrfs repairs the block automatically — possible in my case because the underlying mdadm RAID5 has parity data from which the correct version can be reconstructed. At 14 TiB a scrub takes a few hours, runs in the background with low I/O priority, and doesn&#39;t get in the way during normal use.&lt;/p&gt;
&lt;p&gt;The two mechanisms complement each other: SMART warns me when a drive is about to die (slowly rising read error rates, climbing reallocated sector counts, temperature anomalies). The btrfs scrub catches the errors that fly under the SMART radar. Together I&#39;ll know with reasonably high probability before &quot;one drive&#39;s acting up&quot; turns into &quot;one drive is dead and is taking data with it&quot;.&lt;/p&gt;
&lt;h2 id=&quot;the-hdd-spindown-bug-i-had-for-weeks&quot;&gt;The HDD Spindown Bug I Had for Weeks&lt;/h2&gt;
&lt;p&gt;Now for the slightly embarrassing part. I had hdparm spindown configured forever. Only after experimenting back and forth and digging into why my drives were constantly spinning did I get it: I&#39;d set it via &lt;code&gt;powerUpCommands&lt;/code&gt;, and that only runs after resume-from-suspend. Servers don&#39;t suspend. Meaning: the setting was simply never active.&lt;/p&gt;
&lt;p&gt;The fix is a &lt;code&gt;oneshot&lt;/code&gt; systemd service that runs at boot:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #D3C6AA); background-color: light-dark(#FFFFFF, #2D353B);&quot; &gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;systemd&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;services&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;hdd-power-management&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  description&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;Set HDD spindown timer and acoustic management&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  wantedBy&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;multi-user.target&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  after&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;systemd-udev-settle.service&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  serviceConfig&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    Type&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;oneshot&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    RemainAfterExit&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  script&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    hdparm -S 240 /dev/sda /dev/sdb /dev/sdc || true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;  &amp;#39;&amp;#39;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-S 240&lt;/code&gt; sends the drives into spindown after 20 minutes of idle (the unit for this hdparm flag is 5 seconds, so 240 × 5 = 1200s = 20min). There&#39;s deliberately no acoustic management command here — Seagate IronWolf drives don&#39;t support AAM (the feature was dropped in ATA-8, and NAS/enterprise drives have been silently ignoring the command for years). Earlier versions of my config still had &lt;code&gt;hdparm -M 128&lt;/code&gt;, until I verified with &lt;code&gt;hdparm -I&lt;/code&gt; that the drives report &quot;AAM not supported&quot; — so the effect was nil.&lt;/p&gt;
&lt;h2 id=&quot;fan-control-the-biggest-pain-point&quot;&gt;Fan Control: The Biggest Pain Point&lt;/h2&gt;
&lt;p&gt;I spent a weekend on this one.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;/blog/nixos-nas-setup/back.jpg&quot; alt=&quot;Rear of the Ugreen DXP4800 Plus with system fan and removable dust filter&quot; width=&quot;1440&quot; height=&quot;957&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
  &lt;figcaption&gt;The rear system fan and the removable dust filter. Photo: techreviewer.de&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The DXP4800 Plus uses an ITE IT8613E Super-I/O chip for fan control. The mainline kernel ships an it87 module, but the auto-detection for the IT8613E isn&#39;t included there — without a force ID, the driver simply doesn&#39;t find the chip. The community module from &lt;a rel=&quot;noreferrer external&quot; href=&quot;https://github.com/frankcrawford/it87&quot;&gt;frankcrawford/it87&lt;/a&gt; on GitHub collects patches for exactly these chip variants and is the de-facto standard solution in the lm-sensors community for ITE chips without mainline support. To make the driver load cleanly on this specific hardware, you need three workarounds that all have to play together:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ACPI reserves the I/O ports the driver needs (&lt;code&gt;0x0a00-0x0a3f&lt;/code&gt;) and won&#39;t release them. With the kernel parameter &lt;code&gt;acpi_enforce_resources=lax&lt;/code&gt; you tell the kernel to chill out a bit on this one.&lt;/li&gt;
&lt;li&gt;The chip ID has to be passed to the module at load time via &lt;code&gt;force_id&lt;/code&gt;. Here&#39;s where it gets interesting: originally I used &lt;code&gt;force_id=0x8613&lt;/code&gt; (the actual chip ID), and that worked fine across multiple kernel versions. With kernel 6.12.90 the detection path for IT8613E silently broke — the module loads, but &lt;code&gt;modprobe&lt;/code&gt; just returns &quot;No such device&quot;, no useful error message. The solution is in frankcrawford/it87 Issue #46: force &lt;code&gt;0x8622&lt;/code&gt; (IT8622E) instead. The IT8622E is hardware-equivalent enough that all the relevant PWM and temperature registers respond identically — the driver just thinks it&#39;s a different chip and takes a detection path that isn&#39;t affected by the regression. A working hack, no custom patch required.&lt;/li&gt;
&lt;li&gt;At the driver level there&#39;s still an ACPI conflict on top of that. &lt;code&gt;ignore_resource_conflict=1&lt;/code&gt; ignores it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here&#39;s the config:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #D3C6AA); background-color: light-dark(#FFFFFF, #2D353B);&quot; &gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;boot&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;kernelParams&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;acpi_enforce_resources=lax&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;boot&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;extraModulePackages&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  (&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;boot&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;kernelPackages&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;callPackage&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span style=&quot;color: light-dark(#FF9800, #D3C6AA);&quot;&gt; stdenv&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color: light-dark(#FF9800, #D3C6AA);&quot;&gt; fetchFromGitHub&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color: light-dark(#FF9800, #D3C6AA);&quot;&gt; kernel&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    stdenv&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;mkDerivation&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;      pname&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;it87&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;      src&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; fetchFromGitHub&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;        owner&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;frankcrawford&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;        repo&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;it87&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;        rev&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;master&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;        sha256&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;sha256-iWyOctK+TFhVCOw2LiV4NiNFEAqNXOpSdGY//VwO8Ko=&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;      nativeBuildInputs&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; kernel&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;moduleBuildDependencies&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#C2C3C5, #859289);&quot;&gt;      #&lt;/span&gt;&lt;span style=&quot;color: light-dark(#C2C3C5, #859289);&quot;&gt; ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;boot&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;extraModprobeConfig&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  options it87 force_id=0x8622 ignore_resource_conflict=1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;boot&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;kernelModules&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;it87&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This builds &lt;code&gt;it87.ko&lt;/code&gt; fresh from GitHub against the running kernel. On every kernel update it gets rebuilt automatically — and that&#39;s exactly the reason you do it this way instead of just dumping a binary somewhere.&lt;/p&gt;
&lt;p&gt;Once the module is up, there&#39;s still a fun bug. In pure auto mode, the chip stops the rear system fan as soon as the CPU drops below a certain threshold. The CPU then heats up by 1-2°C without airflow, the chip starts the fan again, ten seconds later it stops it again. You can hear it ramping up and down the whole time. Fan gets loud, fan gets quiet, fan gets loud, fan gets quiet. In an endless loop. Drives you nuts.&lt;/p&gt;
&lt;p&gt;So I&#39;ve since moved to a temperature-triggered hysteresis solution (see the update block at the end). Originally it was a simpler variant: &lt;code&gt;pwm2&lt;/code&gt; (HDD cage fan) stayed in auto mode, &lt;code&gt;pwm3&lt;/code&gt; (rear) got a fixed manual value. PWM 50 out of 255 was plenty for cooling and pleasantly quiet:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #D3C6AA); background-color: light-dark(#FFFFFF, #2D353B);&quot; &gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  for attempt in $(seq 1 10); do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    for hwmon in /sys/class/hwmon/hwmon*; do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;      if [ &amp;quot;$(cat $hwmon/name 2&amp;gt;/dev/null)&amp;quot; = &amp;quot;it8622&amp;quot; ]; then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;        echo 2  &amp;gt; $hwmon/pwm2_enable   # auto&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;        echo 1  &amp;gt; $hwmon/pwm3_enable   # manual&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;        echo 50 &amp;gt; $hwmon/pwm3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;        echo 2  &amp;gt; $hwmon/pwm4_enable&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;        echo 2  &amp;gt; $hwmon/pwm5_enable&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;        exit 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;      fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    done&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    sleep 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  done&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  exit 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The retry loop is in there because the hwmon device doesn&#39;t always show up immediately after the module loads. Up to 10 attempts at one-second intervals. Belt and suspenders, you know how it goes.&lt;/p&gt;
&lt;h2 id=&quot;ugos-protection&quot;&gt;UGOS Protection&lt;/h2&gt;
&lt;p&gt;As we now know, the DXP4800 Plus ships with a fixed 128GB SSD pre-loaded with UGOS (UgreenOS). I want to leave it untouched for two reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Warranty.&lt;/strong&gt; If I ever have to send the device in, I don&#39;t want to be the guy explaining why the factory OS is gone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resale.&lt;/strong&gt; Restoring the original state if I ever sell the NAS is then just a change in the BIOS boot order.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So under no circumstances should this SSD get written to. At the same time I don&#39;t want to have to hit the boot menu every reboot and pick NixOS. For that there are four independent layers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;disko-config.nix&lt;/code&gt; doesn&#39;t list it.&lt;/strong&gt; Disko can&#39;t touch what it doesn&#39;t know about.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A udev rule&lt;/strong&gt; matches the device by its serial number and sets it read-only at the block level the moment it&#39;s detected.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A systemd &lt;code&gt;oneshot&lt;/code&gt;&lt;/strong&gt; sets the read-only flag again at every boot, in case something races with udev.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No fstab entries.&lt;/strong&gt; Nothing tries to mount it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So I effectively have four layers protecting the UgreenOS install. Here&#39;s the snippet:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #D3C6AA); background-color: light-dark(#FFFFFF, #2D353B);&quot; &gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;services&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;udev&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;extraRules&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;  ACTION==&amp;quot;add|change&amp;quot;, SUBSYSTEM==&amp;quot;block&amp;quot;, \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    ENV{ID_SERIAL}==&amp;quot;YSO128GTLCW-E3C-2_511250701135025164&amp;quot;, \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    RUN+=&amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #A7C080);&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;pkgs&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;util-linux&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #A7C080);&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;/bin/blockdev --setro /dev/%k&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;systemd&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;services&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;ugos-protect&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  wantedBy&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;multi-user.target&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  after&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;systemd-udev-settle.service&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  serviceConfig&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt; Type&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;oneshot&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt; RemainAfterExit&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  script&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    UGOS=&amp;quot;/dev/disk/by-id/nvme-YSO128GTLCW-E3C-2_511250701135025164&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    if [ -e &amp;quot;$UGOS&amp;quot; ]; then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;      DEV=$(readlink -f &amp;quot;$UGOS&amp;quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;      blockdev --setro &amp;quot;$DEV&amp;quot; 2&amp;gt;/dev/null || true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;    fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;  &amp;#39;&amp;#39;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The match runs by serial number — so it survives whenever a drive ends up with a different number at the kernel level. The only moments the SSD is briefly writable are during a &lt;code&gt;nixos-rebuild&lt;/code&gt;, so the bootloader sync can update its ESP partition. After that it goes straight back to read-only.&lt;/p&gt;
&lt;h2 id=&quot;the-dashboard&quot;&gt;The Dashboard&lt;/h2&gt;
&lt;p&gt;I wanted something central. Something like a dashboard, a web UI, that kind of thing. Where I see the important stuff at a glance instead of running a stack of cat commands against random log files. So I cobbled together a small replacement in the form of a bash script, with some help from AI.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nixnas-status&lt;/code&gt; is what it&#39;s called and it&#39;s roughly 350 lines of bash that, over SSH, give me a live overview: CPU/RAM/load, network throughput and ping, storage with usage bars, RAID status, every drive with SMART and temperature, NFS exports and which of them are currently mounted by clients, and the status of all the services I care about. The thing refreshes every five seconds without flickering — instead of &lt;code&gt;clear&lt;/code&gt; it does &lt;code&gt;tput cup 0 0&lt;/code&gt;, so it just moves the cursor to the top and renders over what&#39;s there.&lt;/p&gt;
&lt;p&gt;The whole thing is wired in via &lt;code&gt;writeShellScriptBin&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #D3C6AA); background-color: light-dark(#FFFFFF, #2D353B);&quot; &gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;environment&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;systemPackages&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; with&lt;/span&gt;&lt;span&gt; pkgs&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#C2C3C5, #859289);&quot;&gt;  #&lt;/span&gt;&lt;span style=&quot;color: light-dark(#C2C3C5, #859289);&quot;&gt; ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  (&lt;/span&gt;&lt;span&gt;writeShellScriptBin&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;nixnas-status&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt;builtins&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;readFile&lt;/span&gt;&lt;span style=&quot;color: light-dark(#2B5581, #DBBC7F);&quot;&gt; ./scripts/nixnas-status&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Meaning: no installation. After every &lt;code&gt;nixos-rebuild&lt;/code&gt;, &lt;code&gt;nixnas-status&lt;/code&gt; is just on the &lt;code&gt;PATH&lt;/code&gt;. The script is read in at build time and the resulting wrapper lives in &lt;code&gt;/run/current-system&lt;/code&gt;. If I want to change something, I edit the file in &lt;code&gt;/etc/nixos&lt;/code&gt; and rebuild. No deployment, no service, no cron. Nothing.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;/blog/nixos-nas-setup/dashboard.png&quot; alt=&quot;Screenshot of the nixnas-status dashboard in a terminal&quot; width=&quot;1600&quot; height=&quot;607&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
  &lt;figcaption&gt;The nixnas-status dashboard — everything important at a glance over SSH.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;hardening&quot;&gt;Hardening&lt;/h2&gt;
&lt;p&gt;This is one of my favorite areas, security. It&#39;s also a bottomless pit you could probably never finish digging in. Since this is &quot;just&quot; a NAS that&#39;s also protected by other things — firewall rules in my LAN, VLANs, and so on — I kept things pretty basic. The firewall opens exactly two ports (22 SSH, 2049 NFS). Fail2Ban is running and watching sshd: 5 failed attempts get a 1-hour ban, and &lt;code&gt;bantime-increment&lt;/code&gt; escalates that for repeat offenders up to 48 hours. Should keep brute force and the like at bay. Excerpt from the configs:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#24292E, #D3C6AA); background-color: light-dark(#FFFFFF, #2D353B);&quot; &gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;services&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt;.&lt;/span&gt;&lt;span&gt;fail2ban&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  enable&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  maxretry&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; 5&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  bantime&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;1h&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  bantime-increment&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    enable&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    maxtime&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;48h&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;  jails&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;sshd&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;settings&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    enabled&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    port&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;ssh&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    filter&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #DBBC7F);&quot;&gt;sshd&lt;/span&gt;&lt;span style=&quot;color: light-dark(#22863A, #D3C6AA);&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#6F42C1, #DBBC7F);&quot;&gt;    maxretry&lt;/span&gt;&lt;span style=&quot;color: light-dark(#D32F2F, #E69875);&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color: light-dark(#1976D2, #D699B6);&quot;&gt; 5&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PermitRootLogin = &quot;no&quot;&lt;/code&gt;, &lt;code&gt;PasswordAuthentication = false&lt;/code&gt;. Login only via SSH keys now — the &quot;password auth is still on&quot; mentioned earlier in this post has since been taken care of, a step that had been sitting on the todo list far too long with no good reason to keep being postponed.&lt;/p&gt;
&lt;p&gt;The NAS sits on its own VLAN with no inbound from the internet, so this is more about catching my own mistakes (mine or my girlfriend&#39;s) than fending off determined attackers.&lt;/p&gt;
&lt;h2 id=&quot;update-may-2026-what-s-changed-since-publication&quot;&gt;Update May 2026 — What&#39;s Changed Since Publication&lt;/h2&gt;
&lt;p&gt;Three weeks after writing this blog post, the live config has moved on a bit, because real-world operation surfaced issues that weren&#39;t there in April. So nobody copies stale code 1:1, here are the honest updates:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kernel 6.12.90 broke the it87 workaround.&lt;/strong&gt; With the update to kernel 6.12.90, &lt;code&gt;force_id=0x8613&lt;/code&gt; suddenly stopped working — the driver just returned &quot;No such device&quot;, no useful error message. The solution is in frankcrawford/it87 Issue #46: force &lt;code&gt;force_id=0x8622&lt;/code&gt; (IT8622E) instead. Hardware-equivalent enough that the driver addresses all the relevant registers identically — it just takes a detection path that isn&#39;t affected by the regression. The init script&#39;s name check was accordingly migrated from &quot;it8613&quot; to &quot;it8622&quot; — the code block above in this post already shows the current state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Temperature-triggered hysteresis instead of a fixed PWM 50.&lt;/strong&gt; The static solution in the original post (&lt;code&gt;pwm3 = 50&lt;/code&gt; as a constant) worked, but was blind to actual load. My current controller reads the HDD temperatures every 3 minutes and switches between Level 0 (PWM 100), Level 1 (PWM 160), Level 2 (PWM 220), and Level 3 (PWM 255) — with hysteresis so it doesn&#39;t oscillate: Level 1 only steps back down at ≤44°C even though it steps up at ≥45°C. A deliberate trade against the original auto-mode bug.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Realization: pwm4 and pwm5 are dead.&lt;/strong&gt; While debugging the new detection, I noticed: the DXP4800 Plus actually has only two fans installed (on &lt;code&gt;pwm2&lt;/code&gt; and &lt;code&gt;pwm3&lt;/code&gt;). The driver exposes &lt;code&gt;pwm1&lt;/code&gt;, &lt;code&gt;pwm4&lt;/code&gt;, and &lt;code&gt;pwm5&lt;/code&gt; too, but there&#39;s simply nothing behind them. My older init code still tried to configure all five — under the IT8622 detection path that gives EBUSY errors, because &lt;code&gt;pwm4&lt;/code&gt;/&lt;code&gt;5&lt;/code&gt; are initialized read-only in auto mode there. Now I only touch &lt;code&gt;pwm2&lt;/code&gt; and &lt;code&gt;pwm3&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;smartd schedule moved to 13:00.&lt;/strong&gt; Originally the long test ran Sundays at 02:00 — which collided with the backup window of my Proxmox Backup Server. Double drive load, double backup duration. Moved to 13:00, no collisions since.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;-M 128&lt;/code&gt; (AAM) removed.&lt;/strong&gt; It was in the original version as a &quot;quieter seek mode&quot;. When verifying, it turned out: (a) 128 isn&#39;t the quiet value (that would be 254), (b) IronWolf drives don&#39;t support AAM in the first place — the command was dropped in ATA-8, modern NAS drives silently ignore it. So this was a no-op in my config that pretended to do something. Now removed.&lt;/p&gt;
&lt;h2 id=&quot;what-i-d-do-differently&quot;&gt;What I&#39;d Do Differently&lt;/h2&gt;
&lt;p&gt;A few honest notes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8 GB of RAM is borderline.&lt;/strong&gt; For what I&#39;m doing, it&#39;s fine — NFS, smartd, a btrfs scrub once a month, otherwise idle services. But tight enough that I can&#39;t pile anything else on top. If I wanted to put Jellyfin or Nextcloud directly on the NAS, 16 GB would be the absolute minimum. Right now those services live on a separate Proxmox host and just mount &lt;code&gt;/data/media&lt;/code&gt; over NFS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The initial password is &lt;code&gt;changeme&lt;/code&gt;.&lt;/strong&gt; Yes, flagged in the comments, yes, I change it after the first boot. But the better play is baking a deployment SSH key in from the start — the &quot;install, panic-change password, then set up keys&quot; pattern just isn&#39;t clean and isn&#39;t where I want to end up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No off-site backup yet.&lt;/strong&gt; RAID5 protects against one drive failing. It does not protect against fire, theft, or me firing off &lt;code&gt;rm -rf&lt;/code&gt; in the wrong directory. The plan is a second NAS at a different location. The whole thing is on the planning board and will probably happen later this year.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fan curve could be more granular.&lt;/strong&gt; The 4-stage hysteresis described in the update block (levels 0–3 driven by HDD temperatures) replaces the original &lt;code&gt;pwm=50&lt;/code&gt; hack, but it&#39;s still coarse — a proper continuous curve (PWM scaling linearly over temperature instead of discrete steps) would be the more elegant long-term solution. As it stands, the controller steps a level up or down maybe 4× a day, which you can hear. Not bad, just not perfect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The bootloader-on-UGOS thing feels fragile.&lt;/strong&gt; It works, though. All three ESPs stay in sync. But if the UGOS SSD ever physically dies, I&#39;m locked out of booting until I figure something else out. I should walk through the recovery case before I actually need it.&lt;/p&gt;
&lt;p&gt;Overall, though — I&#39;m really happy with how this turned out. In six months I can &lt;code&gt;git diff&lt;/code&gt; and still know what every line does. Rebuilding from scratch on different hardware would be a handful of search-and-replace operations on disk IDs.&lt;/p&gt;
&lt;p&gt;The repo is on GitHub at &lt;a rel=&quot;noreferrer external&quot; href=&quot;https://github.com/daskladas/nasdots&quot;&gt;daskladas/nasdots&lt;/a&gt; if you want to dig further or grab a few bits. Help yourself.&lt;/p&gt;
</content>
        
    </entry>
</feed>
