<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://paperless.blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://paperless.blog/" rel="alternate" type="text/html" /><updated>2026-04-09T19:04:05+00:00</updated><id>https://paperless.blog/feed.xml</id><title type="html">Paperless</title><author><name>Victor Engmark</name></author><entry><title type="html">Nix function from string to pseudorandom integer</title><link href="https://paperless.blog/string-to-pseudorandom-integer-in-nix" rel="alternate" type="text/html" title="Nix function from string to pseudorandom integer" /><published>2025-09-13T00:00:00+00:00</published><updated>2025-09-13T00:00:00+00:00</updated><id>https://paperless.blog/string-to-pseudorandom-integer-in-nix</id><content type="html" xml:base="https://paperless.blog/string-to-pseudorandom-integer-in-nix"><![CDATA[<p>Or: Load balancing remote builders with pseudorandom numbers.</p>

<h2 id="background">Background<sup id="fnref:newbie-caveat"><a href="#fn:newbie-caveat" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></h2>

<p>Nix remote builders are fantastic - a machine reachable by SSH plus
<a href="https://search.nixos.org/options?channel=unstable&amp;query=nix.buildMachines.*.">a few lines of configuration</a>
(and avoiding some
<a href="https://paperless.blog/remote-nix-build-footguns">footguns</a>), and you got
yourself seamless remote builds. The main issue is the <em>network traffic</em> between
the builder and client host, but in case that will slow things down, disabling
remote builders is usually as simple as adding <code class="language-plaintext highlighter-rouge">--builders ''</code> or a similar
option to the command. Conversely, <code class="language-plaintext highlighter-rouge">--max-jobs 0</code> <em>forces</em> remote builds.</p>

<p>Another issue is when a group of people, each on their own machine, are sharing
a pool of builders. Since none of these are aware of each other, and there’s no
central scheduling, it’s easy for a build machine to end up as a <em>choke point,</em>
having every client asking it for resources before trying the next host in the
list. This can delay build start-up. Even worse, if a builder is configured to
run more than one job in parallel, it might end up doing that even when other
builders are doing nothing. This means there will be unnecessary resource
contention.</p>

<p>Finally, we want to minimise download time. It would be ideal for each build
from a single user to always go to the same builder, so that the builder doesn’t
have to download anything which is already in the Nix store. But since builders
are shared, we can’t always guarantee this. <strong>Each user should have a unique
list of builder priorities.</strong> With a lot of users and a lot of builders, this
should result in a decent spread of load across the machines, and minimal
downloads for each individual user.</p>

<h2 id="the-hack">The hack</h2>

<p>An equivalent to a “priority” for builders is the
<a href="https://search.nixos.org/options?channel=unstable&amp;show=nix.buildMachines.*.speedFactor&amp;query=nix.buildMachines.*.speedFactor"><em>speed factor</em></a>.
The higher the speed factor, the more preferred the host. We could generate a
pseudo-random list of speed factors for each user, and achieve crude load
balancing this way. (If we configured each user’s machine <em>centrally</em> we could
of course <em>assign</em> each of them a list of speed factors, but this is not the
scenario I’m dealing with.)</p>

<p>All we need now is a pseudo-random number generator which is likely to be unique
per combination of user and builder. Thanks to
<a href="https://discourse.nixos.org/t/string-int-function/69291/6">Joel McCracken pointing out the relevant building blocks</a>,
this should do the trick:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">pkgs</span><span class="o">.</span><span class="nv">lib</span><span class="o">.</span><span class="nv">fromHexString</span> <span class="p">(</span>
  <span class="kr">builtins</span><span class="o">.</span><span class="nv">substring</span> <span class="mi">0</span> <span class="mi">15</span> <span class="p">(</span>
    <span class="kr">builtins</span><span class="o">.</span><span class="nv">hashString</span> <span class="s2">"md5"</span> <span class="s2">"your string"</span><span class="p">));</span>
</code></pre></div></div>

<p>How it can be used in the build machine configuration<sup id="fnref:same-user"><a href="#fn:same-user" class="footnote" rel="footnote" role="doc-noteref">2</a></sup><sup id="fnref:above-one"><a href="#fn:above-one" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nv">config</span><span class="p">,</span> <span class="nv">pkgs</span><span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="p">{</span>
  <span class="nv">config</span><span class="o">.</span><span class="nv">nix</span><span class="o">.</span><span class="nv">buildMachines</span> <span class="o">=</span>
    <span class="kd">let</span>
        <span class="nv">pseudoRandomSpeedFactor</span> <span class="o">=</span>
          <span class="nv">content</span><span class="p">:</span> <span class="mi">2</span> <span class="o">+</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">lib</span><span class="o">.</span><span class="nv">fromHexString</span> <span class="p">(</span><span class="kr">builtins</span><span class="o">.</span><span class="nv">substring</span> <span class="mi">0</span> <span class="mi">15</span> <span class="p">(</span><span class="kr">builtins</span><span class="o">.</span><span class="nv">hashString</span> <span class="s2">"md5"</span> <span class="nv">content</span><span class="p">));</span>
    <span class="kn">in</span>
    <span class="p">[</span>
      <span class="kr">rec</span> <span class="p">{</span>
        <span class="nv">hostName</span> <span class="o">=</span> <span class="s2">"[…]"</span><span class="p">;</span>
        <span class="nv">speedFactor</span> <span class="o">=</span> <span class="nv">pseudoRandomSpeedFactor</span> <span class="p">(</span><span class="nv">hostName</span> <span class="o">+</span> <span class="nv">sshUser</span><span class="p">);</span>
        <span class="nv">sshUser</span> <span class="o">=</span> <span class="s2">"[…]"</span><span class="p">;</span>
        <span class="c"># Other properties omitted</span>
      <span class="p">}</span>
      <span class="c"># Other builders omitted</span>
    <span class="p">];</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To verify the new configuration, we can pull out the mapping from client host to
builder host and speed factor with
<code class="language-plaintext highlighter-rouge">nix eval .#nixosConfigurations --apply 'clientHosts: builtins.mapAttrs (_clientHost: props: map (builder: {${builder.hostName} = builder.speedFactor;}) props.config.nix.buildMachines) clientHosts'</code>.
For example, with two NixOS host configurations “weak” and “strong”, where
“strong” doesn’t have a remote builder, and “weak” uses “strong” as the builder:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ nix <span class="nb">eval</span> .#nixosConfigurations <span class="nt">--apply</span> <span class="s1">'clientHosts: builtins.mapAttrs (_clientHost: props: map (builder: {${builder.hostName} = builder.speedFactor;}) props.config.nix.buildMachines) clientHosts'</span>
<span class="o">{</span> strong <span class="o">=</span> <span class="o">[</span> <span class="o">]</span><span class="p">;</span> weak <span class="o">=</span> <span class="o">[</span> <span class="o">{</span> strong <span class="o">=</span> 290089632656129126<span class="p">;</span> <span class="o">}</span> <span class="o">]</span><span class="p">;</span> <span class="o">}</span>
</code></pre></div></div>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:newbie-caveat">

      <p>Caveat: I’m relatively new to remote builders. They are pretty simple, so I
haven’t actually read the Nix code to verify all my assertions, and have
made assumptions about how they work based on the “the simplest thing which
could possibly work” heuristic. Please let me know if you have any factual
corrections. <a href="#fnref:newbie-caveat" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:same-user">

      <p>If everyone is using the same SSH user, you could use
<code class="language-plaintext highlighter-rouge">config.networking.hostName</code> instead of <code class="language-plaintext highlighter-rouge">sshUser</code>. <a href="#fnref:same-user" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:above-one">

      <p>The 16 hex characters (“0” through “9”, “a” through “f”) representing four
bits each, and the 15 character limit of <code class="language-plaintext highlighter-rouge">fromHexString</code>, means that the
final number is between 0 and 2^(15×4)-1, inclusive. Since a speed factor of
0 would be pointless, and a speed factor of 1 presumably makes the remote
builder equally likely to be used to the local host, I’m adding 2 to the
result so that it’s between 2 and 2^(15×4)+1, inclusive. Yes, this is a
silly thing to worry about, but my brain would not shut up about the corner
case. <a href="#fnref:above-one" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Victor Engmark</name></author><category term="Nix" /><summary type="html"><![CDATA[Or: Load balancing remote builders with pseudorandom numbers.]]></summary></entry><entry><title type="html">Remote Nix build footguns</title><link href="https://paperless.blog/remote-nix-build-footguns" rel="alternate" type="text/html" title="Remote Nix build footguns" /><published>2025-08-09T00:00:00+00:00</published><updated>2025-08-09T00:00:00+00:00</updated><id>https://paperless.blog/remote-nix-build-footguns</id><content type="html" xml:base="https://paperless.blog/remote-nix-build-footguns"><![CDATA[<p>Remote builds are one of the biggest superpowers of Nix. But while it is in
theory easy to set up, there are a fair number of footguns. I have no feet, but
I must walk. Here are a couple of tips for making sure everything is set up
properly.</p>

<p>First, a quick recap of what’s needed: the <em>root</em> user on the client needs to be
able to SSH <em>non-interactively</em> into the builder as a user who is allowed to run
Nix builds<sup id="fnref:not-trusted"><a href="#fn:not-trusted" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. We can verify different aspects of this on the command
line.</p>

<p>On the client host:</p>

<ul>
  <li>Get the builders configuration with <code class="language-plaintext highlighter-rouge">nix config show builders</code>, and if it
points to a file, <code class="language-plaintext highlighter-rouge">cat</code> it to get the usernames, host names, and SSH key
paths. If this configuration is missing, you might need to restart the Nix
daemon.</li>
  <li>Verify that your SSH keys don’t have a passphrase with
<code class="language-plaintext highlighter-rouge">ssh-keygen -f PRIVATE_KEY -y</code>. Otherwise remove it with
<code class="language-plaintext highlighter-rouge">ssh-keygen -f PRIVATE_KEY -p</code>.</li>
</ul>

<p>On the builder host:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">nix config show allowed-users</code> lists who can trigger Nix builds. The user
listed in the relevant <code class="language-plaintext highlighter-rouge">builders</code> entry on the client (or one of its groups if
there are any <code class="language-plaintext highlighter-rouge">@GROUP</code> entries) needs to be in the allowed users list. That
is, if the <em>client</em> <code class="language-plaintext highlighter-rouge">builders</code> configuration has an entry starting with
<code class="language-plaintext highlighter-rouge">ssh-ng://alice@big</code>, and <code class="language-plaintext highlighter-rouge">nix config show allowed-users</code> on big lists
<code class="language-plaintext highlighter-rouge">root @wheel</code>, then <code class="language-plaintext highlighter-rouge">groups alice</code> on big must include “wheel”.</li>
</ul>

<p>It’s a bit clunky to verify the connection from the client to the builder, but
the following should do:
<code class="language-plaintext highlighter-rouge">sudo ssh -o 'IdentityAgent none' -i PRIVATE_KEY USER@HOST true</code>. <code class="language-plaintext highlighter-rouge">sudo</code> is
necessary because the Nix daemon by default runs as the root user. And the
<code class="language-plaintext highlighter-rouge">IdentityAgent none</code> setting is necessary to avoid forwarding keys from the SSH
agent of the user running <code class="language-plaintext highlighter-rouge">sudo</code> (via <code class="language-plaintext highlighter-rouge">$SSH_AUTH_SOCK</code>).</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:not-trusted">

      <p><a href="https://discourse.nixos.org/t/ssh-works-but-remote-building-says-it-cant-connect/67023/3">@waffle8946 points out</a>
that I should use
<a href="https://nix.dev/manual/nix/stable/command-ref/conf-file.html#conf-allowed-users"><code class="language-plaintext highlighter-rouge">allowed-users</code></a>
for security reasons. In my case I’m already root on the builder host, so I
use
<a href="https://nix.dev/manual/nix/stable/command-ref/conf-file.html#conf-trusted-users"><code class="language-plaintext highlighter-rouge">trusted-users</code></a>. <a href="#fnref:not-trusted" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Victor Engmark</name></author><category term="Nix" /><summary type="html"><![CDATA[Remote builds are one of the biggest superpowers of Nix. But while it is in theory easy to set up, there are a fair number of footguns. I have no feet, but I must walk. Here are a couple of tips for making sure everything is set up properly.]]></summary></entry><entry><title type="html">Hardening systemd services via system call filter</title><link href="https://paperless.blog/hardening-systemd-services-via-system-call-filter" rel="alternate" type="text/html" title="Hardening systemd services via system call filter" /><published>2025-08-02T00:00:00+00:00</published><updated>2025-08-02T00:00:00+00:00</updated><id>https://paperless.blog/hardening-systemd-services-via-system-call-filter</id><content type="html" xml:base="https://paperless.blog/hardening-systemd-services-via-system-call-filter"><![CDATA[<p>No code is perfect, so it’s useful to limit its potential for doing harm.
systemd provides a
<a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#SystemCallFilter=">system call filter</a>
which we can use to do just that, but it’s easy to limit it so much that it
breaks the service.</p>

<p>Let’s start with <code class="language-plaintext highlighter-rouge">systemd-analyze security SERVICE</code> to get a list of settings
which can limit how much risk the service is to the system (“exposure”). (We’re
only looking into system calls in this article, but I would recommend looking
into everything it lists.) Here’s an example analysing the SSH daemon with
<code class="language-plaintext highlighter-rouge">systemd-analyze security sshd</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  NAME                                                        DESCRIPTION                                                             EXPOSURE
✓ AmbientCapabilities=                                        Service process does not receive ambient capabilities
✗ CapabilityBoundingSet=~CAP_AUDIT_*                          Service has audit subsystem access                                           0.1
✗ CapabilityBoundingSet=~CAP_BLOCK_SUSPEND                    Service may establish wake locks                                             0.1
✗ CapabilityBoundingSet=~CAP_BPF                              Service may load BPF programs                                                0.1
✗ CapabilityBoundingSet=~CAP_(CHOWN|FSETID|SETFCAP)           Service may change file ownership/access mode/capabilities unrestricted      0.2
✗ CapabilityBoundingSet=~CAP_(DAC_*|FOWNER|IPC_OWNER)         Service may override UNIX file/IPC permission checks                         0.2
✗ CapabilityBoundingSet=~CAP_IPC_LOCK                         Service may lock memory into RAM                                             0.1
✗ CapabilityBoundingSet=~CAP_KILL                             Service may send UNIX signals to arbitrary processes                         0.1
✗ CapabilityBoundingSet=~CAP_LEASE                            Service may create file leases                                               0.1
✗ CapabilityBoundingSet=~CAP_LINUX_IMMUTABLE                  Service may mark files immutable                                             0.1
✗ CapabilityBoundingSet=~CAP_MAC_*                            Service may adjust SMACK MAC                                                 0.1
✗ CapabilityBoundingSet=~CAP_MKNOD                            Service may create device nodes                                              0.1
✗ CapabilityBoundingSet=~CAP_NET_ADMIN                        Service has network configuration privileges                                 0.2
✗ CapabilityBoundingSet=~CAP_NET_(BIND_SERVICE|BROADCAST|RAW) Service has elevated networking privileges                                   0.1
✗ CapabilityBoundingSet=~CAP_SET(UID|GID|PCAP)                Service may change UID/GID identities/capabilities                           0.3
✗ CapabilityBoundingSet=~CAP_SYS_ADMIN                        Service has administrator privileges                                         0.3
✗ CapabilityBoundingSet=~CAP_SYS_BOOT                         Service may issue reboot()                                                   0.1
✗ CapabilityBoundingSet=~CAP_SYS_CHROOT                       Service may issue chroot()                                                   0.1
✗ CapabilityBoundingSet=~CAP_SYSLOG                           Service has access to kernel logging                                         0.1
✗ CapabilityBoundingSet=~CAP_SYS_MODULE                       Service may load kernel modules                                              0.2
✗ CapabilityBoundingSet=~CAP_SYS_(NICE|RESOURCE)              Service has privileges to change resource use parameters                     0.1
✗ CapabilityBoundingSet=~CAP_SYS_PACCT                        Service may use acct()                                                       0.1
✗ CapabilityBoundingSet=~CAP_SYS_PTRACE                       Service has ptrace() debugging abilities                                     0.3
✗ CapabilityBoundingSet=~CAP_SYS_RAWIO                        Service has raw I/O access                                                   0.2
✗ CapabilityBoundingSet=~CAP_SYS_TIME                         Service processes may change the system clock                                0.2
✗ CapabilityBoundingSet=~CAP_SYS_TTY_CONFIG                   Service may issue vhangup()                                                  0.1
✗ CapabilityBoundingSet=~CAP_WAKE_ALARM                       Service may program timers that wake up the system                           0.1
✓ Delegate=                                                   Service does not maintain its own delegated control group subtree
✗ DeviceAllow=                                                Service has no device ACL                                                    0.2
✗ IPAddressDeny=                                              Service does not define an IP address allow list                             0.2
✓ KeyringMode=                                                Service doesn't share key material with other services
✗ LockPersonality=                                            Service may change ABI personality                                           0.1
✗ MemoryDenyWriteExecute=                                     Service may create writable executable memory mappings                       0.1
✗ NoNewPrivileges=                                            Service processes may acquire new privileges                                 0.2
✓ NotifyAccess=                                               Service child processes cannot alter service state
✗ PrivateDevices=                                             Service potentially has access to hardware devices                           0.2
✗ PrivateMounts=                                              Service may install system mounts                                            0.2
✗ PrivateNetwork=                                             Service has access to the host's network                                     0.5
✗ PrivateTmp=                                                 Service has access to other software's temporary files                       0.2
✗ PrivateUsers=                                               Service has access to other users                                            0.2
✗ ProcSubset=                                                 Service has full access to non-process /proc files (/proc subset=)           0.1
✗ ProtectClock=                                               Service may write to the hardware clock or system clock                      0.2
✗ ProtectControlGroups=                                       Service may modify the control group file system                             0.2
✗ ProtectHome=                                                Service has full access to home directories                                  0.2
✗ ProtectHostname=                                            Service may change system host/domainname                                    0.1
✗ ProtectKernelLogs=                                          Service may read from or write to the kernel log ring buffer                 0.2
✗ ProtectKernelModules=                                       Service may load or read kernel modules                                      0.2
✗ ProtectKernelTunables=                                      Service may alter kernel tunables                                            0.2
✗ ProtectProc=                                                Service has full access to process tree (/proc hidepid=)                     0.2
✗ ProtectSystem=                                              Service has full access to the OS file hierarchy                             0.2
  RemoveIPC=                                                  Service runs as root, option does not apply
✗ RestrictAddressFamilies=~AF_(INET|INET6)                    Service may allocate Internet sockets                                        0.3
✗ RestrictAddressFamilies=~AF_NETLINK                         Service may allocate netlink sockets                                         0.1
✗ RestrictAddressFamilies=~AF_PACKET                          Service may allocate packet sockets                                          0.2
✗ RestrictAddressFamilies=~AF_UNIX                            Service may allocate local sockets                                           0.1
✗ RestrictAddressFamilies=~…                                  Service may allocate exotic sockets                                          0.3
✗ RestrictNamespaces=~cgroup                                  Service may create cgroup namespaces                                         0.1
✗ RestrictNamespaces=~ipc                                     Service may create IPC namespaces                                            0.1
✗ RestrictNamespaces=~mnt                                     Service may create file system namespaces                                    0.1
✗ RestrictNamespaces=~net                                     Service may create network namespaces                                        0.1
✗ RestrictNamespaces=~pid                                     Service may create process namespaces                                        0.1
✗ RestrictNamespaces=~user                                    Service may create user namespaces                                           0.3
✗ RestrictNamespaces=~uts                                     Service may create hostname namespaces                                       0.1
✗ RestrictRealtime=                                           Service may acquire realtime scheduling                                      0.1
✗ RestrictSUIDSGID=                                           Service may create SUID/SGID files                                           0.2
✗ RootDirectory=/RootImage=                                   Service runs within the host's root directory                                0.1
  SupplementaryGroups=                                        Service runs as root, option does not matter
✗ SystemCallArchitectures=                                    Service may execute system calls with all ABIs                               0.2
✗ SystemCallFilter=~@clock                                    Service does not filter system calls                                         0.2
✗ SystemCallFilter=~@cpu-emulation                            Service does not filter system calls                                         0.1
✗ SystemCallFilter=~@debug                                    Service does not filter system calls                                         0.2
✗ SystemCallFilter=~@module                                   Service does not filter system calls                                         0.2
✗ SystemCallFilter=~@mount                                    Service does not filter system calls                                         0.2
✗ SystemCallFilter=~@obsolete                                 Service does not filter system calls                                         0.1
✗ SystemCallFilter=~@privileged                               Service does not filter system calls                                         0.2
✗ SystemCallFilter=~@raw-io                                   Service does not filter system calls                                         0.2
✗ SystemCallFilter=~@reboot                                   Service does not filter system calls                                         0.2
✗ SystemCallFilter=~@resources                                Service does not filter system calls                                         0.2
✗ SystemCallFilter=~@swap                                     Service does not filter system calls                                         0.2
✗ UMask=                                                      Files created by service are world-readable by default                       0.1
✗ User=/DynamicUser=                                          Service runs as root user                                                    0.4

→ Overall exposure level for sshd.service: 9.6 UNSAFE 😨
</code></pre></div></div>

<p>As we can see, any issues with the SSH daemon would open up the system to a lot
of scary side effects. Based on discussions it seems that limiting the
capabilities of the SSH daemon without breaking any of the <em>vast</em> array of
functionality it provides is actually quite difficult. But most services are
much simpler and can be hardened a lot more, for example:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  NAME                             DESCRIPTION                                                                                         EXPOSURE
✓ SystemCallFilter=~@clock         System call allow list defined for service, and @clock is not included
✓ SystemCallFilter=~@cpu-emulation System call allow list defined for service, and @cpu-emulation is not included
✓ SystemCallFilter=~@debug         System call allow list defined for service, and @debug is not included
✓ SystemCallFilter=~@module        System call allow list defined for service, and @module is not included
✓ SystemCallFilter=~@mount         System call allow list defined for service, and @mount is not included
✓ SystemCallFilter=~@obsolete      System call allow list defined for service, and @obsolete is not included
✓ SystemCallFilter=~@privileged    System call allow list defined for service, and @privileged is not included
✓ SystemCallFilter=~@raw-io        System call allow list defined for service, and @raw-io is not included
✓ SystemCallFilter=~@reboot        System call allow list defined for service, and @reboot is not included
✗ SystemCallFilter=~@resources     System call allow list defined for service, and @resources is included (e.g. ioprio_set is allowed)      0.2
✓ SystemCallFilter=~@swap          System call allow list defined for service, and @swap is not included
</code></pre></div></div>

<p>Not bad! This service only needs access to some of the system calls in the
<code class="language-plaintext highlighter-rouge">resources</code> group, such as <code class="language-plaintext highlighter-rouge">ioprio_set</code>. We can use
<code class="language-plaintext highlighter-rouge">systemctl show --property=SystemCallFilter SERVICE</code> to show the full list.</p>

<p>Anything not allowed will result in signal 31, aka. “SYS”, which can be
inspected in the core dump log. Below is an example from a service which I had
changed without updating the hardening settings:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">❯ journalctl --identifier=systemd-coredump --lines=1 --output=cat
Process 582 (sed) of user 1000 dumped core.

Module [omitted]/sed without build-id.
Stack trace of thread 582:
</span><span class="gp">#</span>0  0x00007f8f72d1051b fchown <span class="o">(</span>libc.so.6 + 0x11051b<span class="o">)</span>
<span class="gp">#</span>1  0x000000000040817b closedown <span class="o">([</span>omitted]/sed + 0x817b<span class="o">)</span>
<span class="gp">#</span>2  0x0000000000408d70 read_pattern_space <span class="o">([</span>omitted]/sed + 0x8d70<span class="o">)</span>
<span class="gp">#</span>3  0x000000000040ad4d process_files <span class="o">([</span>omitted]/sed + 0xad4d<span class="o">)</span>
<span class="gp">#</span>4  0x0000000000403abf main <span class="o">([</span>omitted]/sed + 0x3abf<span class="o">)</span>
<span class="gp">#</span>5  0x00007f8f72c2a4d8 __libc_start_call_main <span class="o">(</span>libc.so.6 + 0x2a4d8<span class="o">)</span>
<span class="gp">#</span>6  0x00007f8f72c2a59b __libc_start_main@@GLIBC_2.34 <span class="o">(</span>libc.so.6 + 0x2a59b<span class="o">)</span>
<span class="gp">#</span>7  0x0000000000403be5 _start <span class="o">([</span>omitted]/sed + 0x3be5<span class="o">)</span>
<span class="go">ELF object binary architecture: AMD x86-64
</span></code></pre></div></div>

<p>In this case we can see that <code class="language-plaintext highlighter-rouge">sed</code> tried to run <code class="language-plaintext highlighter-rouge">fchown</code>. We can use
<code class="language-plaintext highlighter-rouge">systemd-analyze syscall-filter</code> to see which system calls (and other groups)
are in each group, and searching for <code class="language-plaintext highlighter-rouge">fchown</code> we can see that the <code class="language-plaintext highlighter-rouge">chown</code> group
contains <code class="language-plaintext highlighter-rouge">fchown</code>. We’ll need to either</p>

<ol>
  <li>add <code class="language-plaintext highlighter-rouge">fchown</code> or <code class="language-plaintext highlighter-rouge">@chown</code> to <code class="language-plaintext highlighter-rouge">SystemCallFilter</code> (easy, and low risk since the
service runs as my user);</li>
  <li>change the <code class="language-plaintext highlighter-rouge">sed</code> command to not need to call <code class="language-plaintext highlighter-rouge">fchown</code> (potentially impossible
without changing <code class="language-plaintext highlighter-rouge">sed</code> itself); or</li>
  <li>use some other command than <code class="language-plaintext highlighter-rouge">sed</code> in the service (potentially lots of work).</li>
</ol>

<p>The first option is easy, and the risk is tolerable, so that’s what I ended up
doing.</p>

<p>Knowing which commands are available and how to find relevant debugging info is
half the battle, so I hope this was useful.</p>

<p>Thanks to
<a href="https://discourse.nixos.org/t/changing-systemd-systemcallfilter-to-make-sed-i-to-work/39546/2?u=l0b0">_Andrew on the NixOS Discourse</a>
for the <code class="language-plaintext highlighter-rouge">journalctl</code> tip!</p>]]></content><author><name>Victor Engmark</name></author><category term="systemd" /><category term="hardening" /><category term="security" /><summary type="html"><![CDATA[No code is perfect, so it’s useful to limit its potential for doing harm. systemd provides a system call filter which we can use to do just that, but it’s easy to limit it so much that it breaks the service.]]></summary></entry><entry><title type="html">nixf-tidy pre-commit hook</title><link href="https://paperless.blog/nixf-tidy-pre-commit-hook" rel="alternate" type="text/html" title="nixf-tidy pre-commit hook" /><published>2025-04-21T00:00:00+00:00</published><updated>2025-04-21T00:00:00+00:00</updated><id>https://paperless.blog/nixf-tidy-pre-commit-hook</id><content type="html" xml:base="https://paperless.blog/nixf-tidy-pre-commit-hook"><![CDATA[<p><code class="language-plaintext highlighter-rouge">nixf-tidy</code>
(<a href="https://discourse.nixos.org/t/nixf-tidy-static-semantic-linter-for-nix-help-me-to-integrate-it-with-github-actions-pr-reviews/43911">introduction</a>,
<a href="https://github.com/nix-community/nixd/blob/3aa27fde1edcf7b126c70a62aad05d120209363c/libnixf/README.md#nixf-tidy">docs</a>)
is a simple Nix linter CLI tool which takes Nix code on standard input and emits
a JSON array of issues. This mode of operation makes it non-trivial to use as a
<a href="https://pre-commit.com/">pre-commit</a> hook, so I’ve written a small wrapper:</p>

<figure>
  <figcaption>nixf-tidy.bash</figcaption>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c">#!/usr/bin/env bash</span>

<span class="nb">set</span> <span class="nt">-o</span> errexit <span class="nt">-o</span> noclobber <span class="nt">-o</span> nounset <span class="nt">-o</span> pipefail
<span class="nb">shopt</span> <span class="nt">-s</span> failglob inherit_errexit

<span class="k">for </span>file<span class="p">;</span> <span class="k">do
    </span><span class="nv">result</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>nixf-tidy <span class="nt">--pretty-print</span> <span class="nt">--variable-lookup</span> &lt; <span class="s2">"</span><span class="k">${</span><span class="nv">file</span><span class="k">}</span><span class="s2">"</span><span class="si">)</span><span class="s2">"</span>
    <span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">result</span><span class="k">}</span><span class="s2">"</span> <span class="o">!=</span> <span class="s1">'[]'</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">printf</span> <span class="s1">'%s: %s\n'</span> <span class="s2">"</span><span class="k">${</span><span class="nv">file</span><span class="k">}</span><span class="s2">"</span> <span class="s2">"</span><span class="k">${</span><span class="nv">result</span><span class="k">}</span><span class="s2">"</span> <span class="o">&gt;</span>&amp;2
        <span class="nv">exit_code</span><span class="o">=</span>3
    <span class="k">fi
done

</span><span class="nb">exit</span> <span class="s2">"</span><span class="k">${</span><span class="nv">exit_code</span><span class="p">-0</span><span class="k">}</span><span class="s2">"</span></code></pre></figure>

</figure>

<h2 id="use">Use</h2>

<p>Save <code class="language-plaintext highlighter-rouge">nixf-tidy.bash</code> above somewhere in your repository and use
<a href="https://gitlab.com/engmark/engmark.gitlab.io/blob/131f4463f6f4ad8a9c441848f4d0cd3fde0e095e/.pre-commit-config.yaml#L52-58">the following snippet</a>:</p>

<figure>
  <figcaption>.pre-commit-config.yaml</figcaption>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="na">repos</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">repo</span><span class="pi">:</span> <span class="s">local</span>
    <span class="na">hooks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">id</span><span class="pi">:</span> <span class="s">nixf-tidy</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">nixf-tidy</span>
        <span class="na">entry</span><span class="pi">:</span> <span class="s">./path/to/nixf-tidy.bash</span>
        <span class="na">files</span><span class="pi">:</span> <span class="s">\.nix$</span>
        <span class="na">language</span><span class="pi">:</span> <span class="s">system</span>
        <span class="na">stages</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">pre-commit</span><span class="pi">]</span></code></pre></figure>

</figure>]]></content><author><name>Victor Engmark</name></author><category term="quality assurance" /><category term="Nix" /><category term="linting" /><summary type="html"><![CDATA[nixf-tidy (introduction, docs) is a simple Nix linter CLI tool which takes Nix code on standard input and emits a JSON array of issues. This mode of operation makes it non-trivial to use as a pre-commit hook, so I’ve written a small wrapper:]]></summary></entry><entry><title type="html">“Treadmill vs. Real Hill” response</title><link href="https://paperless.blog/treadmill-vs-real-hill-response" rel="alternate" type="text/html" title="“Treadmill vs. Real Hill” response" /><published>2025-04-12T00:00:00+00:00</published><updated>2025-04-12T00:00:00+00:00</updated><id>https://paperless.blog/treadmill-vs-real-hill-response</id><content type="html" xml:base="https://paperless.blog/treadmill-vs-real-hill-response"><![CDATA[<p>In
<a href="https://www.youtube.com/watch?v=PAOpkv0fpik">Treadmill vs. Real Hill: Which is harder to run</a>,
Steve Mould and Jared Reabow ran an interesting experiment: does it take more
energy to run up a hill or to run in place on a treadmill with the same
inclination? The <a href="https://www.youtube.com/watch?v=PAOpkv0fpik&amp;t=1077s">result</a>
was interesting, but also a bit of a let-down: the treadmill run used 9.1 W,
while the ramp run used 10.1 W, 11% more. More research is clearly needed 😅!
What could improve the experimental setup?</p>

<p>The friction of a regular treadmill and a wooden slope is going to be different,
so using the <strong>same surface material</strong> would eliminate that option. Using a
<strong>hard surface material</strong> would also reduce friction, but could increase the
amount of slip. We can avoid the slip by using a
<a href="https://en.wikipedia.org/wiki/Rack_railway">rack railway</a>, which would also
allow testing with much bigger inclines, even beyond 45°. A
<a href="https://www.youtube.com/watch?v=lLXNc7unIT4">Lego treadmill</a> should work
beautifully for this: run on the treadmill for the stationary test, and lay the
tread out on an incline to do the moving test.</p>

<p>Comparing with different inclinations should also help. If we see an X%
difference between the energy use on the treadmill and the stationary surface
both at a positive angle and when running <strong>horizontally</strong>, we can conclude that
the difference is not because of the incline. A <strong>bigger incline</strong> would also be
interesting.</p>

<p>Even a <strong>negative incline</strong> could be interesting, if instead of running the
engine we used engine breaking to <em>generate</em> power. Would this result be
relevant for the experiment? I’m not sure, but intuitively I would expect the
power <em>use</em> on a slope of X° to be similar to the power <em>generated</em> on a slope
of -X°. Unfortunately this result would probably be dominated by the
engine/dynamo efficiency, which are both going to be much less than 100%. The
engine and dynamo efficiencies could also be completely different, say, 80% and
50%.</p>

<p>Another easy win would be to use an <strong>aerodynamic</strong> vehicle to minimise
differences due to air resistance. In a similar vein, a <strong>heavy</strong> vehicle would
make the air resistance a smaller factor of the total energy use. And since drag
is proportional to the <em>square</em> of the speed, using a <strong>slow speed</strong> would
further reduce the contribution from air resistance. Slowing down would also
reduce the risk of transferring extra energy to the vehicle because of rattling
in the treadmill system itself (thanks Adon!).</p>

<p>In summary and conclusion, I’ll need about 500,000 moon dollars for Lego parts,
a to-be-negotiated sum to contract <a href="https://deadsimpletech.com/">a friend</a> to
“find a Lagrangian and minimise the action”, and three months to produce a Git
repo and YouTube video with the results.</p>]]></content><author><name>Victor Engmark</name></author><category term="physics" /><category term="experiment" /><summary type="html"><![CDATA[In Treadmill vs. Real Hill: Which is harder to run, Steve Mould and Jared Reabow ran an interesting experiment: does it take more energy to run up a hill or to run in place on a treadmill with the same inclination? The result was interesting, but also a bit of a let-down: the treadmill run used 9.1 W, while the ramp run used 10.1 W, 11% more. More research is clearly needed 😅! What could improve the experimental setup?]]></summary></entry><entry><title type="html">Getting started with automated quality assurance</title><link href="https://paperless.blog/getting-started-with-automated-quality-assurance" rel="alternate" type="text/html" title="Getting started with automated quality assurance" /><published>2024-12-17T00:00:00+00:00</published><updated>2024-12-17T00:00:00+00:00</updated><id>https://paperless.blog/getting-started-with-automated-quality-assurance</id><content type="html" xml:base="https://paperless.blog/getting-started-with-automated-quality-assurance"><![CDATA[<p>The bad news is that every discussion we’ve had about tabs vs spaces was a waste
of time, and neither of us learned anything useful. The <em>good</em> news is that we
won’t need to have that discussion ever again - there are tools which apply
idiomatic indentation for pretty much any language under the sun. But the good
news don’t stop there. That discussion about whether entries in a long lists
should all be on a single line, broken into long lines, or broken into one item
per line? There’s a tool for that. Newline at end of file? There’s a tool for
that. How imports should be sorted and grouped? Detecting overly general catch
statements, unused variables, or bad spelling? You guessed it. In this article
I’ll go through why you might want to automate QA, and some practicalities of
how to adopt automated QA tools, so that the team can concentrate on the
actually important work.</p>

<h2 id="why">Why</h2>

<p>Do any of the following apply to you?</p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Participated in a discussion about tabs vs. spaces.</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Found a bug in production which would’ve been obvious at review time if
    the code had been properly formatted.</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Created a function which had more cyclomatic complexity than the London
    Underground.</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Committed a file with a syntax error.</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Committed a syntactically valid file with schema errors. For example, a
    JSON file with a <code class="language-plaintext highlighter-rouge">$schema</code> property, or an XML file with an
    <code class="language-plaintext highlighter-rouge">schemaLocation</code> attribute.</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Committed a spelling mistake, risking making a future <code class="language-plaintext highlighter-rouge">git grep</code> miss
    important information, or looking bad in front of clients.</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Committed a script with the wrong line endings, causing incomprehensible
    runtime errors.</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Received a complaint about misleading, overly complex, or non-idiomatic
    code.</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Silently cursed someone, maybe even yourself, for using such weird
    formatting.</li>
</ul>

<p>You see where this is going - all of these can be avoided by using tools which
are widely available, robust, and generally have excellent defaults.</p>

<h2 id="why-not">Why not?</h2>

<ul>
  <li>More maintenance burden, making sure the tools keep working,</li>
  <li>Each tool has quirks.</li>
  <li>Some tools use your least favourite configuration format.</li>
  <li>Some big commits. (The next section discusses ways to minimise that.)</li>
</ul>

<h2 id="strategy">Strategy</h2>

<p>tl;dr: Start early, improve steadily.</p>

<p>When learning a new set of tools it’s not a good idea to try to adopt all of
them at once. Automated QA tools are still a relatively new phenomenon, so
conventions are still being developed, and learning how to use one tool does not
usually translate into an easier time with the next one.</p>

<p>Make sure to get buy-in before introducing a tool. If someone is sceptical, try
a quick demo to see if they like it. One important thing to keep in mind at this
stage is that today’s tools are generally extremely robust. Gone are the days
when auto-formatting a piece of code had a significant chance of breaking it. Of
course I can’t speak for <em>all</em> tools, but anything mainstream has almost by
definition been tested on thousands of projects already.</p>

<p>After introducing one tool I’d recommend looking for another one which is a
small but useful step forward. Any time we find some particular part of
development tedious, chances are someone has developed a tool to avoid most of
that work. A quick search for “CI [the task]” (without quotes) in a search
engine should find something relevant. For example:</p>

<ul>
  <li><a href="https://kagi.com/search?q=CI+Rust+linter">“CI Rust linter”</a> finds Clippy</li>
  <li><a href="https://kagi.com/search?q=CI+Bash+formatter">“CI Bash formatter”</a> finds shfmt</li>
</ul>

<p>Make sure everyone working on the project has time to get familiar with new
tools before introducing another one. Otherwise you risk becoming the “Keeper of
the Tool”, leading a solitary life in a silo.</p>

<p>Don’t be afraid to revert or change tools. Sometimes the tool is too painful to
work with. Many years ago I tried a Java formatter. But rather than sensible
defaults, the first thing I had to do was to choose between a bunch of
formatting standards, none of which I was familiar with because <em>I was just
getting started.</em> I won’t name names, but I still run into this with “modern”
tools, having to do a bunch of obscure configuration just to get started. Other
times the community default changes or crystallises, such as <code class="language-plaintext highlighter-rouge">nixfmt-rfc-style</code>
recently becoming the default Nix formatting tool.</p>

<p>Sometimes two tools overlap in functionality. For example, I’d recommend using
isort with Black, even though we have to manually configure isort to use a style
compatible with Black. Other times the tools refuse to work together, and we
have to choose between them. 🤷</p>

<p>If a few hours with a tool doesn’t give much benefit, just stash that work.
Maybe look into it again in a month or two, when the original experience has
faded a bit. At the same time, we shouldn’t feel obliged to introduce all the
tools we possibly can - some might just be more trouble than they are worth, or
don’t fit how we want to work. For example, I like the idea of prose style
checkers, but not of excising the word “is” from my blog!</p>

<p>The earlier in a project automation is introduced, the better. Introducing a
formatter usually results in a single commit with a big diff, which can make it
harder to explore the version control log. That said, if a project is in active
development it’s probably going to last yet another long time, so we should try
to judge fairly the cost of such a one-time big diff against the repeated return
on investment from automation. In the golden words of Randall Munroe,
<a href="https://xkcd.com/1205/">Is It Worth the Time?</a> (But also, please also consider
the developer experience improvement! Time is not the only dimension worth
optimising for.)</p>

<p>When working on a project with many developers, we need to be careful to make
the introduction of a new tool as painless as possible. Which means we need to
be prepared to learn the tool in some depth before even suggesting it for
production use. We might have to showcase it, discuss any quirks (slow speed,
bad defaults, workarounds for common issues), and create a plan for how to
introduce it. This might involve setting up temporary logic to only apply the
tool to new files, so that the team can get used to it before committing to the
big diff resulting from applying it to the entire repository. Then we might
apply the tool to all <em>changed</em> files. Make sure developers apply these changes
in a separate commit or branch <em>before</em> the changes they are working on, so that
it’s easy to review the formatting changes separate from any semantic changes.
This should organically lead to a better state, and after a while we can
introduce a single commit (or small series of commits) to finish the job and
tear down any temporary code.</p>

<h2 id="which-tools">Which tools?</h2>

<p>This isn’t really the best place to go into any depth (future articles,
perhaps), but I’d recommend these tools to anyone who wants their team to be
able to concentrate on the important parts of the work:</p>

<ul>
  <li><a href="https://pre-commit.com/">pre-commit</a> can be used in CI to run all linters and
formatters on the entire repo with a single command,
<code class="language-plaintext highlighter-rouge">pre-commit run --all-files</code>. Locally, <code class="language-plaintext highlighter-rouge">pre-commit install</code> will set up hooks
to run only on the changed files when committing, to fix issues quickly before
committing. All of the following tools work with pre-commit; see
<a href="https://gitlab.com/engmark/engmark.gitlab.io/-/blob/a3fb3b23b9da198c537b8674a2bed974d097d11e/.pre-commit-config.yaml">my pre-commit configuration</a>
for some examples.</li>
  <li><a href="https://github.com/pre-commit/pre-commit-hooks">pre-commit-hooks</a> also has a
grab-bag of useful hooks.</li>
  <li><a href="https://editorconfig.org/">EditorConfig</a> is a simple way to tell all modern
editors how to do the basics.</li>
  <li><a href="https://check-jsonschema.readthedocs.io/">check-jsonschema</a> can verify
conformance with both JSON and YAML schemas out of the box.</li>
  <li><a href="https://jorisroovers.com/gitlint/">gitlint</a> checks that commit messages
conform to your requirements.</li>
  <li><a href="https://prettier.io/">Prettier</a> formats not just code, but also data files
and markup.</li>
  <li><a href="https://vale.sh/">Vale</a> checks prose rules.</li>
</ul>]]></content><author><name>Victor Engmark</name></author><category term="quality assurance" /><category term="automation" /><summary type="html"><![CDATA[The bad news is that every discussion we’ve had about tabs vs spaces was a waste of time, and neither of us learned anything useful. The good news is that we won’t need to have that discussion ever again - there are tools which apply idiomatic indentation for pretty much any language under the sun. But the good news don’t stop there. That discussion about whether entries in a long lists should all be on a single line, broken into long lines, or broken into one item per line? There’s a tool for that. Newline at end of file? There’s a tool for that. How imports should be sorted and grouped? Detecting overly general catch statements, unused variables, or bad spelling? You guessed it. In this article I’ll go through why you might want to automate QA, and some practicalities of how to adopt automated QA tools, so that the team can concentrate on the actually important work.]]></summary></entry><entry><title type="html">Automating ticket progression</title><link href="https://paperless.blog/automating-ticket-progression" rel="alternate" type="text/html" title="Automating ticket progression" /><published>2024-12-15T00:00:00+00:00</published><updated>2024-12-15T00:00:00+00:00</updated><id>https://paperless.blog/automating-ticket-progression</id><content type="html" xml:base="https://paperless.blog/automating-ticket-progression"><![CDATA[<p>I forget to move tickets around the board when I’m supposed to. After working
with 10+ ticketing systems for some 20 years, it’s still too alien to have to
perform a <em>manual</em> step in a completely separate system at the same time as
trying to concentrate on the development. Why not automate this process a bit?</p>

<p>This article is a brain dump for my future self to implement, and therefore
makes a bunch of simplifying assumptions, glossing over a bunch of details and
considerations like team size. Still, the central idea might be useful.</p>

<h2 id="assumptions">Assumptions</h2>

<ul>
  <li>Tickets are tracked <em>electronically,</em> or you have a robot and sufficient time
to automate your physical Kanban board 😉.</li>
  <li>The ticket tracking system has a sane API.</li>
  <li>Tickets all have a unique ID, so that you don’t have to do fancy heuristics to
connect them to change sets.</li>
  <li>You use feature branches. It would be possible to adapt this approach to
trunk-based development, but it would probably require some rethinking and
heuristics.</li>
  <li>Each branch corresponds to exactly one ticket. This is doable if tickets are
small.</li>
  <li>Some work is not related to a specific ticket, and that’s OK. We have finite
time, after all.</li>
</ul>

<h2 id="discussion">Discussion</h2>

<p>Tickets are often created with just some hastily scribbled notes. This may be OK
in a one-person team, but implicit knowledge can’t generally be inferred from
such notes, so we probably want to make sure there’s separate “(not) ready [for
an arbitrary developer to start working on without further context]” states.
Only once the ticket is ready should anyone start working on it.</p>

<p>In standard “make the change easy, then make the easy change” fashion, feature
branches mean that preparatory work such as refactoring should probably be in
separate commits in the same branch as the actual feature. These commits should
<em>not</em> be squashed when merging, since that makes reverts extremely painful.</p>

<h2 id="process">Process</h2>

<ol>
  <li>Work starts when creating the branch<sup id="fnref:in-progress"><a href="#fn:in-progress" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. This can be signalled by
implementing a <code class="language-plaintext highlighter-rouge">create-feature-branch</code> Git alias. This alias would take just
the ticket ID, then perform something like the following process:
    <ol>
      <li>Download the ticket metadata. If it’s not in the “ready” state, either
warn the user or abort, depending on the state and how strict you want to
be with the process. For example, it’s probably not expected that a
cancelled ticket moves into “in progress”, so any such attempt can
probably be assumed to be a typo.</li>
      <li>Create a
<a href="https://en.wikipedia.org/w/index.php?title=Clean_URL&amp;oldid=1262474658#Slug">slug</a>
containing the ticket title and ID, in such a way that it’s easy to
programmatically extract the ID from the slug. For example, a ticket with
a title of “Alert on excess CPU use” and ID of “1234” might get a slug of
“alert-on-excess-cpu-use-1234”.</li>
      <li>Create a branch with the name of the slug. As in,
<code class="language-plaintext highlighter-rouge">git branch SLUG DEFAULT_REMOTE/DEFAULT_BRANCH</code>.</li>
      <li>Change to the new branch.</li>
      <li>Call the ticketing system API to change the ticket state to “in progress”.</li>
    </ol>
  </li>
  <li>Create a draft merge request when pushing the first time. This could use the
name of the ticket as the title. This shows how work is progressing, and
should give the right idea about the fact that the branch is not ready to
base other work on, because the commit history may change. Signalling that
the history might change encourages atomic commits (as opposed to fix-up
commits for linting issues) and a clean history through rebasing onto the
target branch rather than merging from it.</li>
  <li>Move the ticket to “in review” when a merge request is no longer in draft,
and/or when deployed to a user acceptance environment.</li>
  <li>Move the ticket to “in production” (often called “done”, which is misleading)
when it’s been deployed to production.</li>
  <li>Move the ticket to “in use” when log monitors detect that the relevant code
has been triggered.</li>
  <li>Move the ticket to “cancelled” if the branch is deleted before merging.</li>
  <li>Create a new ticket to <em>remove</em> the functionality when log monitors detect
that the relevant code has not been triggered for a Long Time™. This might
be too much work, but for a complex system it could be one way to keep the
maintenance burden manageable.</li>
</ol>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:in-progress">

      <p>It might be simpler to implement a server-side hook to set the state to “in
progress” when someone first pushes a branch with a ticket ID, but that
pushes a lot of information-gathering much later into the process. Part of
the appeal of the process above is that information is updated ASAP. <a href="#fnref:in-progress" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Victor Engmark</name></author><category term="automation" /><category term="Git" /><category term="project management" /><summary type="html"><![CDATA[I forget to move tickets around the board when I’m supposed to. After working with 10+ ticketing systems for some 20 years, it’s still too alien to have to perform a manual step in a completely separate system at the same time as trying to concentrate on the development. Why not automate this process a bit?]]></summary></entry><entry><title type="html">TLS certificate setup on NixOS</title><link href="https://paperless.blog/tls-certificate-setup-on-nixos" rel="alternate" type="text/html" title="TLS certificate setup on NixOS" /><published>2024-12-01T00:00:00+00:00</published><updated>2024-12-01T00:00:00+00:00</updated><id>https://paperless.blog/tls-certificate-setup-on-nixos</id><content type="html" xml:base="https://paperless.blog/tls-certificate-setup-on-nixos"><![CDATA[<p>Info dump on how to set up a HTTPS-enabled service on NixOS.</p>

<p>Prerequisites:</p>

<ul>
  <li>API access to change DNS entries for your own domain</li>
  <li>NixOS</li>
  <li>Static IP or dynamic DNS setup</li>
</ul>

<p>Starting off <code class="language-plaintext highlighter-rouge">configuration.nix</code>:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nv">modulesPath</span><span class="p">,</span>
  <span class="nv">config</span><span class="p">,</span>
  <span class="nv">lib</span><span class="p">,</span>
  <span class="nv">pkgs</span><span class="p">,</span>
  <span class="o">...</span>
<span class="p">}:</span>
<span class="kd">let</span>
  <span class="nv">certName</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">config</span><span class="o">.</span><span class="nv">networking</span><span class="o">.</span><span class="nv">hostName</span><span class="si">}</span><span class="s2">-dot-</span><span class="si">${</span><span class="nv">config</span><span class="o">.</span><span class="nv">networking</span><span class="o">.</span><span class="nv">domain</span><span class="si">}</span><span class="s2">-wildcard"</span><span class="p">;</span>
<span class="kn">in</span>
<span class="p">{</span>
  <span class="c"># TODO: See below</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Networking:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">networking</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nv">domain</span> <span class="o">=</span> <span class="s2">"example.org"</span><span class="p">;</span> <span class="c"># TODO: Replace with your own domain</span>
  <span class="nv">firewall</span><span class="o">.</span><span class="nv">allowedTCPPorts</span> <span class="o">=</span> <span class="p">[</span>
    <span class="mi">22</span> <span class="c"># TODO: Configure SSH (not shown)</span>
    <span class="c"># I've intentionally left out port 80 since all modern clients support TLS</span>
    <span class="mi">443</span>
  <span class="p">];</span>
  <span class="nv">hostName</span> <span class="o">=</span> <span class="s2">"your-hostname"</span><span class="p">;</span> <span class="c"># TODO: Replace with the name you want for your server</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Let’s Encrypt (ACME) setup:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">security</span><span class="o">.</span><span class="nv">acme</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nv">acceptTerms</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
  <span class="nv">certs</span><span class="o">.</span><span class="s2">"</span><span class="si">${</span><span class="nv">certName</span><span class="si">}</span><span class="s2">"</span> <span class="o">=</span> <span class="p">{</span>
    <span class="c"># TODO: Replace with a real path or something like `config.sops.secrets.acmeCredentials.path` if you enable SOPS</span>
    <span class="nv">credentialsFile</span> <span class="o">=</span> <span class="sx">/path/to/acme/credentials</span><span class="p">;</span>
    <span class="c"># TODO: Replace with a value from https://search.nixos.org/options?query=security.acme.certs.%3Cname%3E.dnsProvider</span>
    <span class="nv">dnsProvider</span> <span class="o">=</span> <span class="s2">"some-provider"</span><span class="p">;</span>
    <span class="nv">domain</span> <span class="o">=</span> <span class="s2">"*.</span><span class="si">${</span><span class="nv">config</span><span class="o">.</span><span class="nv">networking</span><span class="o">.</span><span class="nv">hostName</span><span class="si">}</span><span class="s2">.</span><span class="si">${</span><span class="nv">config</span><span class="o">.</span><span class="nv">networking</span><span class="o">.</span><span class="nv">domain</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
  <span class="p">};</span>
  <span class="nv">defaults</span><span class="o">.</span><span class="nv">email</span> <span class="o">=</span> <span class="s2">"jdoe@example.org"</span><span class="p">;</span> <span class="c"># TODO: Replace with your email address</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Enable any HTTP service. In this case I’ll be setting
<code class="language-plaintext highlighter-rouge">services.audiobookshelf.enable = true;</code> to serve audiobooks locally. Then I’ll
use nginx to serve audiobookshelf to the world:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">nginx</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nv">clientMaxBodySize</span> <span class="o">=</span> <span class="s2">"4G"</span><span class="p">;</span> <span class="c"># Allow uploading big audiobooks</span>
  <span class="nv">enable</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
  <span class="nv">recommendedGzipSettings</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
  <span class="nv">recommendedOptimisation</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
  <span class="nv">recommendedProxySettings</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
  <span class="nv">recommendedTlsSettings</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
  <span class="nv">virtualHosts</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"audiobookshelf.</span><span class="si">${</span><span class="nv">config</span><span class="o">.</span><span class="nv">networking</span><span class="o">.</span><span class="nv">fqdn</span><span class="si">}</span><span class="s2">"</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nv">forceSSL</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
      <span class="nv">locations</span><span class="o">.</span><span class="s2">"/"</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nv">proxyPass</span> <span class="o">=</span> <span class="s2">"http://127.0.0.1:</span><span class="si">${</span><span class="kr">builtins</span><span class="o">.</span><span class="kr">toString</span> <span class="nv">config</span><span class="o">.</span><span class="nv">services</span><span class="o">.</span><span class="nv">audiobookshelf</span><span class="o">.</span><span class="nv">port</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
      <span class="nv">proxyWebsockets</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
      <span class="nv">extraConfig</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">        proxy_redirect http:// $scheme://;</span><span class="err">
</span><span class="s2">      ''</span><span class="p">;</span>
    <span class="p">};</span>
    <span class="nv">useACMEHost</span> <span class="o">=</span> <span class="nv">certName</span><span class="p">;</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Using <a href="https://github.com/Mic92/sops-nix">sops-nix</a> is <em>optional,</em> but it is a
good way to automate secrets management. The Nix part of the configuration
should look something like this:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">sops</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nv">age</span><span class="o">.</span><span class="nv">sshKeyPaths</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"/etc/ssh/ssh_host_ed25519_key"</span> <span class="p">];</span>
  <span class="nv">defaultSopsFile</span> <span class="o">=</span> <span class="sx">./secrets/default.yaml</span><span class="p">;</span>
  <span class="nv">secrets</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">acmeCredentials</span> <span class="o">=</span> <span class="p">{</span> <span class="p">};</span>
  <span class="p">};</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Then there’s the file configuration in <code class="language-plaintext highlighter-rouge">.sops.yaml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">keys</span><span class="pi">:</span>
  <span class="c1"># TODO: Use `age-keygen --output ~/.config/sops/age/keys.txt` to replace the value below</span>
  <span class="pi">-</span> <span class="nl">&amp;admin</span> <span class="s">age125nlnhal9c90u8vwtveurccf5emtdk2u5nr3vv3yyu5kfmldgsusre2n8k</span>
  <span class="c1"># TODO: Use `ssh-keyscan HOST | ssh-to-age` to replace the value below</span>
  <span class="pi">-</span> <span class="nl">&amp;host_audiobookshelf</span> <span class="s">age1hr9vcf9jlfgxf4f6c3f9yq2kklzj7tplxv7cyulhqzfvhx42le4ssnq89j</span>
<span class="na">creation_rules</span><span class="pi">:</span> <span class="c1"># sops updatekeys secrets/*</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">audiobookshelf/secrets/[^/]+\.(yaml|json|env|ini|sops)$</span>
    <span class="na">key_groups</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">age</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="nv">*admin</span>
          <span class="pi">-</span> <span class="nv">*host_audiobookshelf</span>
</code></pre></div></div>

<p>You can then add encrypted credentials in <code class="language-plaintext highlighter-rouge">audiobookshelf/secrets/default.yaml</code>,
which would look something like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">acmeCredentials</span><span class="pi">:</span> <span class="s">ENC[AES256_GCM,data:…,type:str]</span>
<span class="na">sops</span><span class="pi">:</span>
  <span class="na">kms</span><span class="pi">:</span> <span class="pi">[]</span>
  <span class="na">gcp_kms</span><span class="pi">:</span> <span class="pi">[]</span>
  <span class="na">azure_kv</span><span class="pi">:</span> <span class="pi">[]</span>
  <span class="na">hc_vault</span><span class="pi">:</span> <span class="pi">[]</span>
  <span class="na">age</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">recipient</span><span class="pi">:</span> <span class="s">age125nlnhal9c90u8vwtveurccf5emtdk2u5nr3vv3yyu5kfmldgsusre2n8k</span>
      <span class="na">enc</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">-----BEGIN AGE ENCRYPTED FILE-----</span>
        <span class="s">…</span>
        <span class="s">-----END AGE ENCRYPTED FILE-----</span>
    <span class="pi">-</span> <span class="na">recipient</span><span class="pi">:</span> <span class="s">age1hr9vcf9jlfgxf4f6c3f9yq2kklzj7tplxv7cyulhqzfvhx42le4ssnq89j</span>
      <span class="na">enc</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">-----BEGIN AGE ENCRYPTED FILE-----</span>
        <span class="s">…</span>
        <span class="s">-----END AGE ENCRYPTED FILE-----</span>
  <span class="na">lastmodified</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2024-11-20T04:20:13Z"</span>
  <span class="na">mac</span><span class="pi">:</span> <span class="s">ENC[AES256_GCM,data:…,type:str]</span>
  <span class="na">pgp</span><span class="pi">:</span> <span class="pi">[]</span>
  <span class="na">unencrypted_suffix</span><span class="pi">:</span> <span class="s">_unencrypted</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">3.9.1</span>
</code></pre></div></div>

<p>Finally, to allow nginx to read the TLS certificate:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">users</span><span class="o">.</span><span class="nv">users</span><span class="o">.</span><span class="nv">nginx</span><span class="o">.</span><span class="nv">extraGroups</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"acme"</span> <span class="p">];</span>
</code></pre></div></div>

<p>Apply this configuration, and https://audiobookshelf.your-hostname.example.org
should be accessible online.</p>]]></content><author><name>Victor Engmark</name></author><category term="HTTPS" /><category term="TLS" /><category term="NixOS" /><category term="Let&apos;s Encrypt" /><summary type="html"><![CDATA[Info dump on how to set up a HTTPS-enabled service on NixOS.]]></summary></entry><entry><title type="html">The lost taglines</title><link href="https://paperless.blog/the-lost-taglines" rel="alternate" type="text/html" title="The lost taglines" /><published>2024-09-18T00:00:00+00:00</published><updated>2024-09-18T00:00:00+00:00</updated><id>https://paperless.blog/the-lost-taglines</id><content type="html" xml:base="https://paperless.blog/the-lost-taglines"><![CDATA[<p>Software documentation usually does not include the most important facts you
want to know as a beginner. The kind of stuff you might internalise after using
something for years, but you <em>could not</em> have learned by reading the
documentation. The stuff which should show up as a blinking orange &amp; teal banner
on top of the documentation page — the lost taglines:</p>

<ul>
  <li>Bash: “Use More Quotes™!”</li>
  <li>CSV: Your data <em>will</em> contain commas.</li>
  <li>Git: <code class="language-plaintext highlighter-rouge">bisect</code> — you’ll never be the same again.</li>
  <li>JSON: Add a schema!</li>
  <li>Markdown: The parser is the limiting factor.</li>
  <li>Nix: It’s worth the pain.</li>
  <li>PHP: The comments are the most useful parts of the docs.</li>
  <li>Perl: The first thing you must do is add every safety pragma.</li>
  <li>Python: <code class="language-plaintext highlighter-rouge">mypy --strict</code> and <code class="language-plaintext highlighter-rouge">ruff</code> FTW.</li>
  <li>Rust: The compiler loves you, and only wants you to be happy.</li>
  <li>vCard: v3 is the only one supported anywhere.</li>
  <li>YAML: Have you considered JSON?</li>
</ul>]]></content><author><name>Victor Engmark</name></author><category term="documentation" /><category term="silliness" /><category term="software" /><summary type="html"><![CDATA[Software documentation usually does not include the most important facts you want to know as a beginner. The kind of stuff you might internalise after using something for years, but you could not have learned by reading the documentation. The stuff which should show up as a blinking orange &amp; teal banner on top of the documentation page — the lost taglines:]]></summary></entry><entry><title type="html">Nightly Rust development with Nix</title><link href="https://paperless.blog/nightly-rust-development-with-nix" rel="alternate" type="text/html" title="Nightly Rust development with Nix" /><published>2024-09-09T00:00:00+00:00</published><updated>2024-09-09T00:00:00+00:00</updated><id>https://paperless.blog/nightly-rust-development-with-nix</id><content type="html" xml:base="https://paperless.blog/nightly-rust-development-with-nix"><![CDATA[<p>This one took a while to get simple enough; enjoy!</p>

<h2 id="background">Background</h2>

<p>While
<a href="https://bevyengine.org/learn/quick-start/getting-started/">getting started with Bevy</a>
I wanted to make sure I had an easily reproducible development environment by
using only Nix as the package manager. However, the latest version of Bevy was
not compatible with stable Rust, so looked into Nix development environments for
the nightly version. Unfortunately the instructions I could find either
recommended using Rustup (that is, involving another package manager and
probably sacrificing reproducibility), were “left as an exercise to the
reader”-style vague, or looked complex to maintain. I also wanted to start out
with stable integration with JetBrains IDEA, my IDE of choice, so that when I
upgrade any part of the setup it would have a good chance to Just Keep
Working™.</p>

<p>I tried starting from a standard Nix shell, but nightly Rust integration seemed
like it would need one of the aforementioned complex setups or some third party
involvement. I didn’t want to add another framework, but finally caved and
started using one. This happily turned out to be the right choice.</p>

<h2 id="prerequisites">Prerequisites</h2>

<ul>
  <li><a href="https://nixos.org/">Nix</a></li>
  <li><a href="https://devenv.sh/">devenv</a> (<code class="language-plaintext highlighter-rouge">pkgs.devenv</code>)</li>
  <li><a href="https://direnv.net/">direnv</a> (<code class="language-plaintext highlighter-rouge">programs.direnv.enable = true</code> in NixOS)</li>
  <li>Optionally <a href="https://www.jetbrains.com/rust/">RustRover</a>
(<code class="language-plaintext highlighter-rouge">pkgs.jetbrains.rust-rover</code>) or
<a href="https://www.jetbrains.com/idea/">JetBrains IDEA Ultimate</a>
(<code class="language-plaintext highlighter-rouge">pkgs.jetbrains.idea-ultimate</code>) with the official Rust plugin<sup id="fnref:tested-ide"><a href="#fn:tested-ide" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></li>
</ul>

<h2 id="setup">Setup</h2>

<ol>
  <li>Go to your project directory</li>
  <li>Run <code class="language-plaintext highlighter-rouge">devenv init</code> to create devenv and direnv configuration</li>
  <li>Run <code class="language-plaintext highlighter-rouge">devenv inputs add fenix github:nix-community/fenix --follows nixpkgs</code> to
add nightly Rust support</li>
  <li>
    <p>Add the following in <code class="language-plaintext highlighter-rouge">devenv.nix</code>:</p>

    <div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">languages</span><span class="o">.</span><span class="nv">rust</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nv">channel</span> <span class="o">=</span> <span class="s2">"nightly"</span><span class="p">;</span>
  <span class="nv">components</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"cargo"</span>
    <span class="s2">"rust-src"</span>
    <span class="s2">"rustc"</span>
  <span class="p">];</span>
  <span class="nv">enable</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div>    </div>
  </li>
</ol>

<p>At this point <code class="language-plaintext highlighter-rouge">cargo --version</code> and <code class="language-plaintext highlighter-rouge">rustc --version</code> should print <code class="language-plaintext highlighter-rouge">-nightly</code>
version numbers, and you can run <code class="language-plaintext highlighter-rouge">cargo init</code> to start the Rust journey.</p>

<h2 id="optional-ide-setup">Optional IDE setup</h2>

<p>Start your IDE inside the project directory<sup id="fnref:run-idea"><a href="#fn:run-idea" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> to have all the tools you
just installed available on the path. In <em>Languages &amp; Frameworks</em> → <em>Rust</em>, you
can now set the relevant paths for the project:</p>

<ul>
  <li><em>Toolchain location:</em> <code><em>[path to
project]</em>/.devenv/profile/bin</code></li>
  <li><em>Standard library:</em> <code><em>[path to
project]</em>/.devenv/profile/lib/rustlib/src/rust/library</code></li>
</ul>

<p>This should be enough to build and run the code, and to follow references to
Rust core, within the IDE.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:tested-ide">

      <p>I’ve only tested this with IDEA, but it should work similarly in RustRover. <a href="#fnref:tested-ide" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:run-idea">

      <p>I use <code class="language-plaintext highlighter-rouge">nohup idea-ultimate nosplash . &amp;&gt;/dev/null &amp; disown</code> to be able to
use the shell immediately after starting IDEA. <a href="#fnref:run-idea" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Victor Engmark</name></author><category term="Nix" /><category term="Rust" /><category term="development" /><category term="IDE" /><summary type="html"><![CDATA[This one took a while to get simple enough; enjoy!]]></summary></entry></feed>