<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://heywoodlh.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://heywoodlh.io/" rel="alternate" type="text/html" /><updated>2026-07-16T00:20:21+00:00</updated><id>https://heywoodlh.io/feed.xml</id><title type="html">@heywoodlh</title><subtitle>heywoodlh thoughts</subtitle><entry><title type="html">Demo: Lima Virtual Machines on MacOS and Linux</title><link href="https://heywoodlh.io/lima-demo/" rel="alternate" type="text/html" title="Demo: Lima Virtual Machines on MacOS and Linux" /><published>2026-06-26T00:00:00+00:00</published><updated>2026-06-26T00:00:00+00:00</updated><id>https://heywoodlh.io/lima-vms</id><content type="html" xml:base="https://heywoodlh.io/lima-demo/"><![CDATA[<p>This is a demo of how I use Lima for Docker and NixOS virtual machines on MacOS and Linux:</p>

<script src="https://asciinema.org/a/1257753.js" id="asciicast-1257753" async="true"></script>

<p>Specifically, I use Lima for providing a Docker socket and a NixOS virtual machine when I’m not on NixOS.</p>

<p>Relevant Home-Manager module: <a href="https://tangled.org/heywoodlh.io/nixos-configs/blob/5c53f838926eab9608ea7713e63debe7e8f84b4d/home/modules/lima.nix">lima.nix</a></p>]]></content><author><name></name></author><category term="linux" /><category term="macos" /><category term="productivity" /><category term="cli" /><category term="vms" /><category term="docker" /><category term="nixos" /><summary type="html"><![CDATA[This is a demo of how I use Lima for Docker and NixOS virtual machines on MacOS and Linux:]]></summary></entry><entry><title type="html">My 2026 Linux (NixOS) gaming setup</title><link href="https://heywoodlh.io/linux-gaming-2026/" rel="alternate" type="text/html" title="My 2026 Linux (NixOS) gaming setup" /><published>2026-06-26T00:00:00+00:00</published><updated>2026-06-26T00:00:00+00:00</updated><id>https://heywoodlh.io/linux-gaming</id><content type="html" xml:base="https://heywoodlh.io/linux-gaming-2026/"><![CDATA[<p>I use Linux, specifically NixOS, for the majority of my gaming, so I wanted to compile a list of references for anyone interested in replicating my setup.</p>

<h2 id="my-general-purpose-linux-gaming-recommendations">My general-purpose Linux gaming recommendations</h2>

<p>Before getting into the specifics of my setup, I’ll provide some general-purpose advice.</p>

<h3 id="use-cachyos">Use CachyOS</h3>

<p>If you came here for a recommendation on what Linux gaming distro to use, I would recommend <a href="https://cachyos.org/">CachyOS</a> for the following reasons:</p>
<ul>
  <li>It is based on Arch, which makes it easy to get bleeding edge updates</li>
  <li>The CachyOS Gaming Wiki is an awesome Linux gaming resource (even if you don’t use CachyOS): <a href="https://wiki.cachyos.org/configuration/gaming/">Gaming with CachyOS Guide</a></li>
  <li>CachyOS’ publicly documented kernel settings are excellent: <a href="https://github.com/CachyOS/CachyOS-Settings">github:CachyOS/CachyOS-Settings</a></li>
</ul>

<h3 id="a-quick-rant-dont-use-nvidia-50xx-gpus-for-linux-gaming">A quick rant: don’t use Nvidia 50xx GPUs for Linux gaming</h3>

<p>If you’re exploring buying a dedicated GPU for a Linux gaming rig, I cannot recommend buying a 50xx series GPU because of Nvidia’s driver UX. When I first got my 50xx series GPU, driver updates were absolutely horrendous, often breaking things on Linux <em>and</em> Windows. Thankfully, as of June 2026 my anecdotal experience is that driver updates are far less disruptive, but I would never recommend a 50xx series GPU if you have options.</p>

<p>Additionally, to use the Nvidia driver on Linux, you have to disable secure boot. On my one system that I dual-boot Windows, I cannot play Battlefield 6 because Secure Boot has to be disabled for the Linux Nvidia driver to work. I’m not a fan of how invasive these anti-cheat engines are – but I’m also not a fan of my GPU driver on Linux preventing me from playing specific games at all.</p>

<p>The feeling I am left with is that Linux gaming experience is definitely second-class. AMD sucks, too, for various reasons, but Linux support is far better and a dedicated AMD GPU would be my recommendation for a dedicated Linux gaming machine.</p>

<h3 id="my-overall-linux-gpu-recommendation-intel-arc-battlemage">My overall Linux GPU recommendation: Intel Arc Battlemage</h3>

<p>Even with the current AI bubble inflating costs of computer parts, the Intel Arc GPUs are far more affordable than the AMD and Nvidia GPUs. Additionally, the Intel Arc GPU driver is bundled with the Linux kernel – so a relatively modern Linux kernel version should allow the Intel Arc to <em>just work</em> on Linux.</p>

<p>The Intel Arc won’t exceed AMD and Nvidia GPUs, but it will be a far cheaper option and a better RoI.</p>

<h2 id="my-gaming-machines">My gaming machines</h2>

<p>I have three gaming machines I use (ordered by how much I play on them):</p>
<ol>
  <li><a href="https://tangled.org/heywoodlh.io/nixos-configs/blob/5c53f838926eab9608ea7713e63debe7e8f84b4d/nixos/hosts/gaming.nix">nixos-gaming (custom dedicated gaming machine) configuration</a></li>
  <li><a href="https://tangled.org/heywoodlh.io/nixos-configs/blob/5c53f838926eab9608ea7713e63debe7e8f84b4d/flake.nix#L1011-1043">nixos-blade (Razer Blade 14) configuration</a></li>
  <li><a href="https://tangled.org/heywoodlh.io/nixos-configs/blob/5c53f838926eab9608ea7713e63debe7e8f84b4d/nixos/hosts/steam-deck.nix">steam-deck configuration</a></li>
</ol>

<p>I mostly play on my <code class="language-plaintext highlighter-rouge">nixos-gaming</code> machine which sits in a closet, but find myself falling back to my Razer Blade 14 when I play away from home, or my Steam Deck when I need an ultra-portable setup.</p>

<p>There are some hardware-specific deviations I’ll share below, but the majority of the configuration is shared amongst these machines.</p>

<h2 id="dedicated-gaming-nixos-gaming-machine-hardware">Dedicated gaming <code class="language-plaintext highlighter-rouge">nixos-gaming</code> machine hardware</h2>

<p>My dedicated gaming machine is a custom build with the following hardware:</p>
<ul>
  <li>CPU: AMD Ryzen 7 5700X3D</li>
  <li>GPU: Nvidia 5070 Ti (16 GB VRAM)</li>
  <li>RAM: 32 GB DDR5 RAM</li>
</ul>

<p>For game streaming with Sunshine I also have a 120Hz HDMI dummy dongle that I leave plugged into my GPU.</p>

<p>For more details on the specific hardware configuration I have see: <a href="https://pcpartpicker.com/user/heywoodlh/saved/#view=qrD7vK">https://pcpartpicker.com/user/heywoodlh/saved/#view=qrD7vK</a></p>

<h3 id="dual-booting-nixos-and-windows">Dual booting NixOS and Windows</h3>

<p>I dual-boot Windows on my dedicated gaming machine and have a nice little shell script to easily reboot into Windows if I ever want to play games that are unsupported on Linux: <a href="https://tangled.org/heywoodlh.io/nixos-configs/blob/5c53f838926eab9608ea7713e63debe7e8f84b4d/nixos/hosts/gaming.nix#L42-44">reboot-windows</a></p>

<h2 id="nixos-steam-deck">NixOS Steam Deck</h2>

<p>I have a first generation Steam Deck. I wrote a post a while ago on how I installed NixOS on my Steam Deck (that is mostly still applicable): <a href="https://heywoodlh.io/nixos-steamdeck">Installing NixOS on the Steam Deck</a></p>

<p>It is enabled by Jovian NixOS: <a href="https://github.com/Jovian-Experiments/Jovian-NixOS">github:Jovian-Experiments/Jovian-NixOS</a></p>

<p>Overall, if you like NixOS and you like the Steam Deck, I highly recommend installing NixOS on it – otherwise SteamOS is awesome, too!</p>

<h2 id="nixos-configuration">NixOS configuration</h2>

<p>I have my configuration separated into custom NixOS modules:</p>
<ul>
  <li>General purpose gaming configuration: <a href="https://tangled.org/heywoodlh.io/nixos-configs/blob/5c53f838926eab9608ea7713e63debe7e8f84b4d/nixos/modules/gaming.nix">gaming.nix</a></li>
  <li>Nvidia driver configuration with nvidia-patch: <a href="https://tangled.org/heywoodlh.io/nixos-configs/blob/5c53f838926eab9608ea7713e63debe7e8f84b4d/nixos/modules/nvidia-patch.nix">nvidia-patch.nix</a></li>
  <li>Sunshine for game streaming: <a href="https://tangled.org/heywoodlh.io/nixos-configs/blob/5c53f838926eab9608ea7713e63debe7e8f84b4d/nixos/modules/sunshine.nix">sunshine.nix</a></li>
</ul>

<p>I do the majority of my gaming via Moonlight + Sunshine on an Apple TV with a controller or an M4 iPad Pro with an Xbox Elite 2 controller.</p>

<h3 id="desktop-environments-kde-and-hyprland">Desktop environments: KDE and Hyprland</h3>

<p>I’m using KDE as my desktop environment for dedicated gaming machines, with a relatively minimal config here: <a href="https://tangled.org/heywoodlh.io/nixos-configs/blob/5c53f838926eab9608ea7713e63debe7e8f84b4d/nixos/modules/kde.nix">kde.nix</a></p>

<p>For general-purpose Linux machines I use Hyprland (i.e. on my Razer Blade 14 away from home):</p>
<ul>
  <li><a href="https://tangled.org/heywoodlh.io/nixos-configs/blob/5c53f838926eab9608ea7713e63debe7e8f84b4d/nixos/modules/hyprland.nix">[nixos] hyprland.nix</a></li>
  <li><a href="https://tangled.org/heywoodlh.io/nixos-configs/blob/5c53f838926eab9608ea7713e63debe7e8f84b4d/home/modules/hyprland.nix">[home-manager] hyprland.nix</a></li>
</ul>

<p>I used to daily drive GNOME, but when it came to gaming, I found it could be inconsistent. KDE has felt far better when I want things to <em>just work</em>. Hyprland has been a good experience as well, although, certain games’ windowing can behave a bit strangely with Hyprland’s automatic window tiling.</p>

<h2 id="cachyos-settings">CachyOS settings</h2>

<p>The following links will be helpful for anyone wanting to reference CachyOS settings:</p>
<ul>
  <li><a href="https://github.com/CachyOS/CachyOS-Settings">CachyOS-Settings repository</a></li>
  <li><a href="https://wiki.cachyos.org/features/kernel/">CachyOS Kernel</a> on NixOS via <a href="https://github.com/xddxdd/nix-cachyos-kernel">github:xddxdd/nix-cachyos-kernel</a></li>
  <li><a href="https://github.com/CachyOS/proton-cachyos">Proton CachyOS</a> fetched via my custom script <a href="https://tangled.org/heywoodlh.io/nixos-configs/blob/82b05e519f236e9b9fc75fafd94c920a6fef5276/nixos/modules/gaming.nix#L17-82">proton-custom.sh</a></li>
  <li>My <code class="language-plaintext highlighter-rouge">git</code> diff for my NixOS configuration replicating their settings: <a href="https://github.com/heywoodlh/nixos-configs/compare/68cdc39..f43d984">https://github.com/heywoodlh/nixos-configs/compare/68cdc39..f43d984</a></li>
</ul>]]></content><author><name></name></author><category term="linux" /><category term="steam" /><category term="gaming" /><summary type="html"><![CDATA[I use Linux, specifically NixOS, for the majority of my gaming, so I wanted to compile a list of references for anyone interested in replicating my setup.]]></summary></entry><entry><title type="html">Tremendously Stupid: Age-Verification Laws for Operating Systems in the United States</title><link href="https://heywoodlh.io/us-age-verification-laws/" rel="alternate" type="text/html" title="Tremendously Stupid: Age-Verification Laws for Operating Systems in the United States" /><published>2026-03-03T00:00:00+00:00</published><updated>2026-03-03T00:00:00+00:00</updated><id>https://heywoodlh.io/us-age-verification-laws</id><content type="html" xml:base="https://heywoodlh.io/us-age-verification-laws/"><![CDATA[<p>As a US citizen, self-identifying Linux and privacy enthusiast, and a human being I am very against all the proposed legislation in California and Colorado to enforce age verification on the operating system level. I wanted to share my perspective on the legislation as a contributor to NixOS.</p>

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

<p>Colorado legislation: <a href="https://leg.colorado.gov/bills/SB26-051">SB26-051 Age Attestation on Computing Devices</a></p>

<p>California legislation: <a href="https://leginfo.legislature.ca.gov/faces/billTextClient.xhtml?bill_id=202520260AB1043">AB-1043 Age verification signals: software applications and online services</a></p>

<p>The Colorado legislation is mostly the same as CA from what I’ve read so I’m just going to highlight the points for CA.</p>

<h2 id="operating-system-providers">“Operating system providers”</h2>

<blockquote>
  <p>“Operating system provider” means a person or entity that develops, licenses, or controls the operating system software on a computer, mobile device, or any other general purpose computing device.</p>
</blockquote>

<p>With this definition, anyone who forks <a href="https://github.com/nixos/nixpkgs">nixpkgs</a> could potentially fall under the category of “operating system developer” because that fork could be used as a source of truth to build NixOS. Clearly this bill did not have open source operating systems/development pipelines in mind. As implied by other users in this thread, I suspect the only real point of enforcement will be hardware vendors and their out-of-the-box operating systems.</p>

<h3 id="macos">MacOS</h3>

<p>While MacOS doesn’t have anything rolled out yet, Apple will surely comply with this and bake it into MacOS. Perhaps this will enable Apple to go the route of Windows and force users on threat of death to login with an Apple account.</p>

<p>As a nixpkgs Darwin contributor, I’m sad about this, but am not surprised and will be exiting the Apple ecosystem even further.</p>

<h2 id="covered-application-stores">“Covered application stores”</h2>

<blockquote>
  <p>“Covered application store” means a publicly available internet website, software application, online service, or platform that distributes and facilitates the download of applications</p>
</blockquote>

<p>So, in my view, nixpkgs probably falls into this “covered application store” category – but again, anyone can fork it, modify it, etc. – there’s no reasonable way to enforce this as literally anybody could fork nixpkgs, redistribute, modify, etc.</p>

<h2 id="constraintsrequirements">Constraints/requirements</h2>

<p>Ages they care about:</p>

<blockquote>
  <p>(1) Whether a user is under 13 years of age.</p>

  <p>(2) Whether the user is at least 13 years of age and under 16 years of age.</p>

  <p>(3) Whether the user is at least 16 years of age and under 18 years of age.</p>

  <p>(4) Whether the user is at least 18 years of age.</p>
</blockquote>

<p>Thee assertions they make for operating system providers:</p>
<ol>
  <li>Provide a way for a user to indicate their age bracket in the operating system</li>
  <li>Provide a way for a developer to consume the age of bracket of the user</li>
  <li>The operating system/app store must enforce restrictions around apps and age limits (I’m assuming not allowing users to install apps that are not appropriate relative to their age)</li>
</ol>

<h2 id="penalties">Penalties</h2>

<p>People – I’m guessing operating system developers – in violation of the title shall be subject to a fine:</p>

<blockquote>
  <p>not more than two thousand five hundred dollars ($2,500) per affected child for each negligent violation or not more than seven thousand five hundred dollars ($7,500) per affected child for each intentional violation</p>
</blockquote>

<h2 id="due-date-in-ca">Due date in CA</h2>

<blockquote>
  <p>This title shall become operative on January 1, 2027.</p>
</blockquote>

<h2 id="specific-linux-distributions-discourse">Specific Linux distributions discourse</h2>

<p>Ubuntu: <a href="https://lists.ubuntu.com/archives/ubuntu-devel/2026-March/043510.html">On the unfortunate need for an “age verification” API for legal compliance reasons in some U.S. states</a></p>

<p>Pop! OS: <a href="https://www.reddit.com/r/pop_os/comments/1rfqyf8/comment/o7q34wu/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button">System76 Principal Engineer response</a></p>

<blockquote>
  <p>Note: Pop! OS is made by System76, which is based out of Denver, Colorado – therefore, they are uniquely impacted.</p>
</blockquote>

<p>Arch Linux: <a href="https://bbs.archlinux.org/viewtopic.php?id=312459">California Age verification craziness</a></p>

<p>NixOS: <a href="https://discourse.nixos.org/t/compliance-with-u-s-age-verification-laws/75791">Compliance with U.S. age verification laws</a></p>

<p>Even if your preferred Linux distribution isn’t based in the United States, this will surely have ripple effects with large corporations like Red Hat and Canonical contributing and maintaining much of the modern Linux applications and services that enable its widespread use.</p>

<h2 id="why-this-is-stupid">Why this is stupid</h2>

<p>First, it violates privacy, and doesn’t offer any meaningful protections for children. Applications greedy for user data will surely abuse the data about the verified ages of users (children and adults) on computers.</p>

<p>Second, open source operating systems can be forked by anybody and re-distributed. The legislation clearly isn’t designed to scale with something like NixOS.</p>

<p>Third, there’s no meaningful way to scale enough to actually enforce this for open source operating systems.</p>

<h2 id="what-this-means">What this means</h2>

<p>In theory, this means that anybody distributing an operating system could be liable for this (i.e. <a href="github.com/NixOS/nixpkgs/forks">any one of the 18,000+ forks of nixpkgs on GitHub</a>). In practice, I think this will be enforced against hardware vendors and the operating systems they bundle with brand new hardware.</p>

<p>But, as a US Citizen I consider this a big downgrade for my privacy.</p>

<h2 id="misc">Misc</h2>

<blockquote>
  <p>I posted a less opinionated version of this on the NixOS Discourse post regarding this topic: <a href="https://discourse.nixos.org/t/compliance-with-u-s-age-verification-laws/75791/16?u=heywoodlh">Compliance with U.S. age verification laws</a></p>
</blockquote>

<p>Obligatory note: none of my perspective represents my employer.</p>]]></content><author><name></name></author><category term="linux" /><category term="privacy" /><category term="macos" /><summary type="html"><![CDATA[As a US citizen, self-identifying Linux and privacy enthusiast, and a human being I am very against all the proposed legislation in California and Colorado to enforce age verification on the operating system level. I wanted to share my perspective on the legislation as a contributor to NixOS.]]></summary></entry><entry><title type="html">Demo: Task and GitHub Project Management on the Command-line</title><link href="https://heywoodlh.io/todo-project-mgmt-cli/" rel="alternate" type="text/html" title="Demo: Task and GitHub Project Management on the Command-line" /><published>2026-02-09T00:00:00+00:00</published><updated>2026-02-09T00:00:00+00:00</updated><id>https://heywoodlh.io/todo-project-mgmt-cli</id><content type="html" xml:base="https://heywoodlh.io/todo-project-mgmt-cli/"><![CDATA[<p>This is a demo of how I’m managing personal tasks and tech-related items with GitHub Issues.</p>

<script src="https://asciinema.org/a/6xQg5eLSjRDqgAYv.js" id="asciicast-6xQg5eLSjRDqgAYv" async="true"></script>

<p>Relevant Home-Manager configurations for the curious:</p>
<ul>
  <li>Todoman wrappers: <a href="https://github.com/heywoodlh/nixos-configs/blob/f177611b0b6c57e1cbab1ee1d04cebec1f8ae632/home/base.nix#L12-L29">github:heywoodlh/nixos-configs - home/base.nix#L12-L29</a></li>
  <li>Todoman configuration: <a href="https://github.com/heywoodlh/nixos-configs/blob/f177611b0b6c57e1cbab1ee1d04cebec1f8ae632/home/base.nix#L613-L624">github:heywoodlh/nixos-configs - home/base.nix#L613-L624</a></li>
  <li>GitHub CLI module: <a href="https://github.com/heywoodlh/nixos-configs/blob/f177611b0b6c57e1cbab1ee1d04cebec1f8ae632/home/modules/gh.nix">github:heywoodlh/nixos-configs - home/modules/gh.nix</a></li>
</ul>]]></content><author><name></name></author><category term="caldav" /><category term="linux" /><category term="macos" /><category term="productivity" /><category term="cli" /><category term="todo" /><category term="todoman" /><category term="gh" /><category term="gh-dash" /><summary type="html"><![CDATA[This is a demo of how I’m managing personal tasks and tech-related items with GitHub Issues.]]></summary></entry><entry><title type="html">iCloud as a Cross-Platform Source of Truth (CalDav, CardDav)</title><link href="https://heywoodlh.io/cross-platform-icloud/" rel="alternate" type="text/html" title="iCloud as a Cross-Platform Source of Truth (CalDav, CardDav)" /><published>2026-01-21T00:00:00+00:00</published><updated>2026-01-21T00:00:00+00:00</updated><id>https://heywoodlh.io/cross-platform-icloud</id><content type="html" xml:base="https://heywoodlh.io/cross-platform-icloud/"><![CDATA[<p>This is a summary of how to use iCloud for syncing contacts, calendars and reminders on non-Apple clients/platforms.</p>

<h2 id="tldr-use-these-settings-for-caldavcarddav-on-icloud">TL;DR use these settings for CalDav/CardDav on iCloud</h2>

<p>Remember to set up an <a href="https://support.apple.com/en-us/102654">App-specific Password</a> for authentication.</p>

<ul>
  <li>https://contacts.apple.com: Contacts</li>
  <li>https://caldav.icloud.com*: Calendar, Reminders</li>
</ul>

<p>* In order to use the same calendars/reminders on iOS and other platforms, you must configure an external CardDav/CalDav account for iCloud on iOS. The built-in iCloud Reminders/Calendar that is setup after logging into doesn’t use traditional CalDav.</p>

<h2 id="why-use-icloud">Why use iCloud?</h2>

<p>I recently switched away from an iPhone as my daily driver to GrapheneOS on a Pixel 8. However, I still have an iPad and switch regularly between Linux and MacOS when I’m on a workstation. So, I wanted to use iCloud due to Apple’s (often overhyped) adherence to privacy but relying on endpoints that will work with clients that support CalDav and CardDav.</p>

<h3 id="as-a-long-time-proton-user-why-not-use-proton">As a long-time Proton user, why not use Proton?</h3>

<p>* Traditional CalDav and CardDav clients don’t work with Proton.</p>

<p>Proton offers a variety of end-to-end encrypted cloud services rivaling iCloud. However, Proton isn’t as portable since they break the protocols to ensure end-to-end encryption. While I think Proton is the superior suite of products from a privacy perspective, they are less functional for traditional protocols like CardDav and CalDav.</p>

<p>* Check out <a href="https://github.com/acheong08/ferroxide">Ferroxide</a> for a third-party, open source solution that exposes CalDav and CardDav endpoints that integrate with Proton.</p>

<h2 id="platforms-and-clients-im-using">Platforms and clients I’m using</h2>

<h3 id="reminders-calendars">Reminders, calendars</h3>

<ul>
  <li>Linux, MacOS: <code class="language-plaintext highlighter-rouge">vdirsyncer</code>, <code class="language-plaintext highlighter-rouge">todoman</code></li>
  <li>Android: <tasks.org>, [DAVx⁵](https://www.davx5.com)</tasks.org></li>
  <li>iOS*: Native clients/settings</li>
</ul>

<p>* As stated in the first TL;DR in order for iOS to show the same reminders and calendars as the other platforms, you need to add an “external” CalDav account for iCloud since the built-in iCloud integration for Reminders and Calendars doesn’t use the same CalDav endpoint.</p>
<h3 id="contacts">Contacts</h3>

<ul>
  <li>Linux, MacOS: <code class="language-plaintext highlighter-rouge">vdirsyncer</code>, <code class="language-plaintext highlighter-rouge">khard</code></li>
  <li>Android: <a href="https://www.davx5.com">DAVx⁵</a></li>
  <li>iOS: Native clients/settings</li>
</ul>

<h2 id="android-setup-with-davx">Android setup with DAVx⁵</h2>

<p>I won’t be going in-depth on Android setup, use the settings in the TL;DR at the beginning and use DAVx⁵’s wizard to set them up.</p>

<h3 id="tasksorg-integration">Tasks.org integration</h3>

<p>Use the following article to setup tasks.org with DAVx⁵: <a href="https://tasks.org/docs/davx5.html">tasks.org: DAVx⁵</a></p>

<h2 id="linuxmacos-configuration">Linux/MacOS configuration</h2>

<h3 id="vdirsyncer-configuration">Vdirsyncer configuration</h3>

<p>Vdirsyncer is a tool for downloading CalDav and CardDav data locally.</p>

<p>Here is my configuration – using 1Password’s CLI for retrieving credentials – at the time of writing:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[general]
status_path = "/home/heywoodlh/.config/vdirsyncer/status/"

## Calendar/Reminders

[pair icloud_calendar]
a = "apple_local"
b = "apple_remote"
collections = ["from b", "from a"]

[storage apple_local]
type = "filesystem"
path = "~/.todo/apple"
fileext = ".ics"

# Reminder, this is not the iOS-provided iCloud integration
# All devices (i.e. Apple devices) who want to see this should be using caldav
[storage apple_remote]
type = "caldav"
item_types = ["VTODO"]
url = "https://caldav.icloud.com"
username.fetch = ["shell", "op item get '&lt;some-item&gt;' --fields label=username --reveal"]
password.fetch = ["shell", "op item get '&lt;some-item&gt;' --fields label=app-password --reveal"]

## Contacts

[pair icloud_contacts]
a = "apple_remote_contacts"
b = "apple_local_contacts"
collections = [["icloud_contacts", "card", "main"]]
conflict_resolution = "a wins"
metadata = ["displayname"]

[storage apple_local_contacts]
type = "filesystem"
path = "~/.contacts/apple"
fileext = ".vcf"

[storage apple_remote_contacts]
type = "carddav"
url = "https://contacts.icloud.com"
username.fetch = ["shell", "op item get '&lt;some-item&gt;' --fields label=username --reveal"]
password.fetch = ["shell", "op item get '&lt;some-item&gt;' --fields label=app-password --reveal"]
</code></pre></div></div>

<p>Once Vdirsyncer is configured, run the following to download data:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vdirsyncer discover
vdirsyncer sync
</code></pre></div></div>

<h3 id="khard-configuration">Khard configuration</h3>

<p>Here is my khard configuration at the time of writing:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[addressbooks]
[[personal]]
path = ~/.contacts/apple/main
</code></pre></div></div>

<p>List all contacts with this command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>khard list
</code></pre></div></div>

<h3 id="aerc-configuration">Aerc configuration</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[protonmail]
address-book-cmd = khard email -a personal --parsable --remove-first-line %s
aliases = Spencer Heywood &lt;*@protonmail.com&gt;,Spencer Heywood &lt;*@pm.me&gt;, Spencer Heywood &lt;heywoodlh@heywoodlh.io&gt;
copy-to = Sent
default = INBOX
from = Spencer Heywood &lt;spencer@heywoodlh.io&gt;
outgoing = smtp+insecure://&lt;some-user&gt;%40protonmail.com@protonmail-bridge:25
outgoing-cred-cmd = op read '&lt;some-item&gt;'
source = imap+insecure://&lt;some-user&gt;%40protonmail.com@protonmail-bridge:143
source-cred-cmd = op read '&lt;some-item&gt;'
</code></pre></div></div>

<p>I use Protonmail Bridge over Tailscale for SMTP+IMAP.</p>

<p>Here is my Kubernetes deployment for Protonmail Bridge at the time of writing: <a href="https://github.com/heywoodlh/nixos-configs/blob/38d871e19245b054d6812ed90ab53200d6be2916/flakes/kube/manifests/protonmail-bridge.yaml">protonmail-bridge.yaml</a></p>]]></content><author><name></name></author><category term="carddav" /><category term="caldav" /><category term="apple" /><category term="linux" /><category term="ios" /><category term="icloud" /><category term="macos" /><category term="email" /><category term="imap" /><category term="smtp" /><category term="protonmail" /><category term="proton" /><summary type="html"><![CDATA[This is a summary of how to use iCloud for syncing contacts, calendars and reminders on non-Apple clients/platforms.]]></summary></entry><entry><title type="html">Aerc, Protonmail Bridge and Catch-all Addresses</title><link href="https://heywoodlh.io/aerc-protonmail-catch-all/" rel="alternate" type="text/html" title="Aerc, Protonmail Bridge and Catch-all Addresses" /><published>2026-01-05T00:00:00+00:00</published><updated>2026-01-05T00:00:00+00:00</updated><id>https://heywoodlh.io/aerc-proton-catch-all</id><content type="html" xml:base="https://heywoodlh.io/aerc-protonmail-catch-all/"><![CDATA[<p>This is how I configured <a href="https://aerc-mail.org/">Aerc</a>, my email client, to work with Protonmail Bridge after I setup my custom domain with a catch-all address:</p>

<p>In <code class="language-plaintext highlighter-rouge">accounts.conf</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[protonmail]
from = Spencer Heywood &lt;spencer@heywoodlh.io&gt;
aliases = Spencer Heywood &lt;*@protonmail.com&gt;,Spencer Heywood &lt;*@pm.me&gt;
&lt;snip&gt;
</code></pre></div></div>

<p>This configuration will assume I want to use <code class="language-plaintext highlighter-rouge">spencer@heywoodlh.io</code> as my reply address (the default address for this custom domain) <em>unless</em> the recipient of the email is my <code class="language-plaintext highlighter-rouge">protonmail.com</code> email address or my additional <code class="language-plaintext highlighter-rouge">pm.me</code> address. Alternatively, you could provide the full address of your Protonmail addresses in the <code class="language-plaintext highlighter-rouge">aliases</code> section.</p>

<p>This is required because Protonmail has an irritating limitation on sending addresses.</p>

<h2 id="bonus">BONUS</h2>

<p>Aerc configuration at the time of writing: <a href="https://github.com/heywoodlh/nixos-configs/blob/744934cdfdfa5d9b033a4f5092a5d263548f4b2a/home/base.nix#L464-L490">home/base.nix#programs.aerc</a></p>

<p>My Protonmail Bridge Kubernetes deployment at the time of writing: <a href="https://github.com/heywoodlh/nixos-configs/blob/744934cdfdfa5d9b033a4f5092a5d263548f4b2a/flakes/kube/manifests/protonmail-bridge.yaml">protonmail-bridge.yaml</a></p>]]></content><author><name></name></author><category term="email" /><category term="imap" /><category term="smtp" /><category term="protonmail" /><category term="proton" /><summary type="html"><![CDATA[This is how I configured Aerc, my email client, to work with Protonmail Bridge after I setup my custom domain with a catch-all address:]]></summary></entry><entry><title type="html">K3s + Intel Arc A770 GPU passthru on NixOS</title><link href="https://heywoodlh.io/k3s-gpu-nixos/" rel="alternate" type="text/html" title="K3s + Intel Arc A770 GPU passthru on NixOS" /><published>2025-10-14T00:00:00+00:00</published><updated>2025-10-14T00:00:00+00:00</updated><id>https://heywoodlh.io/k3s-intel-gpu-passthru</id><content type="html" xml:base="https://heywoodlh.io/k3s-gpu-nixos/"><![CDATA[<blockquote>
  <p>This is mostly a reposting of my PR to nixpkgs: <a href="https://github.com/NixOS/nixpkgs/pull/452052h">k3s: add intel gpu docs</a></p>
</blockquote>

<h1 id="intel-gpu-support-in-k3s">Intel GPU Support in k3s</h1>

<p>This post makes the following assumptions:</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">services.k3s.enable</code> is already set to true</li>
  <li>The Linux kernel running is modern enough to support your GPU out of the box</li>
  <li>The desired driver is <code class="language-plaintext highlighter-rouge">i915</code> – modify as needed for other drivers</li>
</ol>

<blockquote>
  <p>Note: at the time of writing, I was using an Intel Arc A770 in k3s. The majority of this guide likely should work on other Kubernetes distributions, and will likely work identically for integrated graphics capabilities.</p>
</blockquote>

<h3 id="enable-the-intel-driver-in-nixos">Enable the Intel driver in NixOS</h3>

<p>Add the following NixOS configuration to enable the Intel driver (necessary on headless deployments):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>services.xserver.videoDrivers = [ "i915" ];
</code></pre></div></div>

<p>After rebuilding the configuration, reboot the host for the GPU driver to be assigned to the GPU. Use the following command to ensure the GPU is using the i915 kernel:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo lspci -k
</code></pre></div></div>

<p>i.e. the output looks like this on a host with the Intel Arc A770:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ sudo lspci -k | grep -A 3 'Arc'
03:00.0 VGA compatible controller: Intel Corporation DG2 [Arc A770] (rev 08)
        Subsystem: ASRock Incorporation Device 6010
        Kernel driver in use: i915
        Kernel modules: i915, xe
</code></pre></div></div>

<h2 id="install-intel-node-feature-discovery-nfd-in-k3s">Install Intel Node Feature Discovery (NFD) in k3s</h2>

<p>Intel’s device plugin for kubernetes provides Node Feature Discovery (NFD). NFD allows for GPU capabilities on a node to be automatically discovered if a discrete GPU is installed and the Intel drivers have been properly assigned.</p>

<blockquote>
  <p>Documentation for Intel NFD installation is here for reference: <a href="https://intel.github.io/intel-device-plugins-for-kubernetes/cmd/gpu_plugin/README.html#install-with-nfd">Install with NFD</a></p>
</blockquote>

<p>The following commands will install NFD in the cluster (assumes <code class="language-plaintext highlighter-rouge">curl</code>, <code class="language-plaintext highlighter-rouge">jq</code> and <code class="language-plaintext highlighter-rouge">kubectl</code> are all installed/configured):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Use the latest release
export LATEST_RELEASE=$(curl -s https://api.github.com/repos/intel/intel-device-plugins-for-kubernetes/releases/latest | jq -r '.tag_name')

# Use Kustomize to deploy the configuration
kubectl apply -k "https://github.com/intel/intel-device-plugins-for-kubernetes/deployments/nfd?ref=$LATEST_RELEASE"
kubectl apply -k "https://github.com/intel/intel-device-plugins-for-kubernetes/deployments/nfd/overlays/node-feature-rules?ref=$LATEST_RELEASE"
kubectl apply -k "https://github.com/intel/intel-device-plugins-for-kubernetes/deployments/gpu_plugin/overlays/nfd_labeled_nodes?ref=$LATEST_RELEASE"
</code></pre></div></div>

<p>NFD should automatically apply relevant labels to your node. This can be verified with the following command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get nodes -o yaml | grep gpu.intel.com | sort -u
</code></pre></div></div>

<p>Output should look similar to the following:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ kubectl get nodes -o yaml | grep gpu.intel.com | sort -u
      gpu.intel.com/device-id.0300-56a0.count: "1"
      gpu.intel.com/device-id.0300-56a0.present: "true"
      gpu.intel.com/family: A_Series
      gpu.intel.com/i915: "1"
      gpu.intel.com/i915_monitoring: "0"
      nfd.node.kubernetes.io/feature-labels: gpu.intel.com/device-id.0300-56a0.count,gpu.intel.com/device-id.0300-56a0.present,gpu.intel.com/family,intel.feature.node.kubernetes.io/gpu
</code></pre></div></div>

<blockquote>
  <p>Note: <code class="language-plaintext highlighter-rouge">gpu.intel.com/i915: "1"</code> indicates only one pod can use the GPU – see below for a fix.</p>
</blockquote>

<p>Now, GPU-enabled pods can be run with this configuration:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spec:
  containers:
    resources:
      requests:
        gpu.intel.com/i915: "1"
      limits:
        gpu.intel.com/i915: "1"
</code></pre></div></div>

<h3 id="allowing-more-than-one-pod-to-use-the-gpu">Allowing more than one pod to use the GPU</h3>

<p>In the default configuration, only one pod can use the GPU. To enable multiple pods to use the GPU, apply the following Kustomize patch:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>patches:
- target:
    kind: DaemonSet
    name: intel-gpu-plugin
  patch: |
    - op: add
      path: /spec/template/spec/containers/0/args
      value:
        - -shared-dev-num=10
</code></pre></div></div>

<p>Or, manually edit the <code class="language-plaintext highlighter-rouge">intel-gpu-plugin</code> DaemonSet to run with <code class="language-plaintext highlighter-rouge">-shared-dev-num=10</code> (or however many max pods can access the GPU), like so:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: intel-gpu-plugin
spec:
    spec:
      containers:
      - args:
        - -shared-dev-num=10
</code></pre></div></div>

<p>Verify the number has been applied like so:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get nodes -o yaml | grep gpu.intel.com/i915 | sort -u
</code></pre></div></div>

<p>i.e. in this configuration, up to 10 pods can use the GPU:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ kubectl get nodes -o yaml | grep gpu.intel.com/i915 | sort -u
    gpu.intel.com/i915: "10"
</code></pre></div></div>

<h3 id="test-pod">Test pod</h3>

<p>This is a complete pod configuration for reference/testing:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---
apiVersion: v1
kind: Pod
metadata:
  name: intel-gpu-test
  namespace: default
spec:
  containers:
  - name: intel-gpu-test
    image: docker.io/ubuntu:24.04
    command: [ "/bin/bash", "-c", "--" ]
    args: [ "while true; do sleep 30; done;" ]
    resources:
      requests:
        gpu.intel.com/i915: "1"
      limits:
        gpu.intel.com/i915: "1"
</code></pre></div></div>

<p>Once the pod is running, use the following command to test that the GPU is available:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl exec -n default -it pod/intel-gpu-test -- ls /dev/dri
</code></pre></div></div>

<p>If the GPU is available, the output will look like the following:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ kubectl exec -n default -it pod/intel-gpu-test -- ls /dev/dri
by-path  card1  renderD128
</code></pre></div></div>

<p>Delete the pod so as to not count against the GPU limit:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete -n default pod/intel-gpu-test
</code></pre></div></div>

<h2 id="additional-deployment-examples">Additional deployment examples:</h2>

<p><a href="https://github.com/heywoodlh/flakes/blob/d14d559e5e814f102feb0f0c42d0c3808dccb205/kube/manifests/media.yaml#L2-L93">Plex with Intel GPU passthru</a></p>

<p><a href="https://github.com/heywoodlh/flakes/blob/d14d559e5e814f102feb0f0c42d0c3808dccb205/kube/manifests/ollama.yaml">Ollama with Intel GPU passthru</a></p>]]></content><author><name></name></author><category term="linux" /><category term="devops" /><category term="kubernetes" /><category term="k3s" /><category term="intel" /><category term="gpu" /><category term="nixos" /><summary type="html"><![CDATA[This is mostly a reposting of my PR to nixpkgs: k3s: add intel gpu docs]]></summary></entry><entry><title type="html">Introduction to Ansible (using Docker)</title><link href="https://heywoodlh.io/ansible-intro-docker/" rel="alternate" type="text/html" title="Introduction to Ansible (using Docker)" /><published>2025-08-21T00:00:00+00:00</published><updated>2025-08-21T00:00:00+00:00</updated><id>https://heywoodlh.io/intro-to-ansible</id><content type="html" xml:base="https://heywoodlh.io/ansible-intro-docker/"><![CDATA[<blockquote>
  <p>This is a reposting of a meeting from the <a href="https://culug.group">Unix User Group I help run</a></p>
</blockquote>

<h1 id="getting-started-with-ansible">Getting Started with Ansible</h1>

<p>This tutorial will walk you through some basic Ansible principles. All resources used in this tutorial can be found here:</p>

<p><a href="https://github.com/central-utah-lug/meetings/tree/main/2023/May/25">https://github.com/central-utah-lug/meetings/tree/main/2023/May/25</a></p>

<p>By the end of the tutorial, you will use Ansible to deploy a server on DigitalOcean, do some basic maintenance on the server, and destroy the server.</p>

<h2 id="requirements">Requirements</h2>

<p>Use my Docker container:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run -it --rm docker.io/heywoodlh/ansible-demo
</code></pre></div></div>

<p>In the Docker container, all of the playbooks in this post are located in the <code class="language-plaintext highlighter-rouge">/ansible-demo/playbooks</code> directory.</p>

<p>Additionally, the following packages are available in the container:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">ansible</code></li>
  <li><code class="language-plaintext highlighter-rouge">doctl</code></li>
  <li><code class="language-plaintext highlighter-rouge">edit</code> (<a href="https://github.com/microsoft/edit">Microsoft Edit</a>)</li>
  <li><code class="language-plaintext highlighter-rouge">nano</code></li>
  <li><code class="language-plaintext highlighter-rouge">vim</code></li>
</ul>

<p>A DigitalOcean account:</p>
<ul>
  <li>Create an API token with Read and Write permission to use for this tutorial (save it somewhere like a password manager): https://cloud.digitalocean.com/account/api/tokens</li>
</ul>

<h2 id="deploy-a-server-with-digitaloceans-ansible-module">Deploy a server with DigitalOcean’s Ansible module</h2>

<p>First, let’s create a new directory and move into it for all of our commands:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mkdir -p /tmp/ansible-demo
cd /tmp/ansible-demo
</code></pre></div></div>

<p>Install the Ansible DigitalOcean collection, using <code class="language-plaintext highlighter-rouge">ansible-galaxy</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible-galaxy collection install community.digitalocean
</code></pre></div></div>

<p>Before we can run any Ansible things against DigitalOcean’s API, we need to set an API token environment variable for Ansible to use:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export DO_API_TOKEN="contents-of-api-token"
</code></pre></div></div>

<h3 id="deploy-the-server">Deploy the server</h3>

<p>Now, create a file at <code class="language-plaintext highlighter-rouge">playbooks/create-server.yml</code> with the following commands:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mkdir -p playbooks

cat &gt;playbooks/create-server.yml &lt;&lt;EOL
---
- hosts: localhost
  tasks:

  - name: gather information about digitalocean ssh keys
    community.digitalocean.digital_ocean_sshkey_info:
    register: ssh_keys

  - name: set sshkey_pub_id when ansible-demo key exists
    set_fact:
      sshkey_pub_id: "{{ ssh_keys.data | selectattr('name', 'equalto', 'ansible-demo') | map(attribute='id') | first }}"
    ignore_errors: true

  - name: create ssh key without passphrase at /tmp/ansible-demo-id_rsa
    ansible.builtin.command: ssh-keygen -b 2048 -t rsa -f /tmp/ansible-demo-id_rsa -N ""
    when: sshkey_pub_id is not defined

  - name: read contents of /tmp/ansible-demo-id_rsa.pub
    ansible.builtin.command: cat /tmp/ansible-demo-id_rsa.pub
    register: sshkey_pub
    when: sshkey_pub_id is not defined

  - name: upload ssh key to digitalocean
    community.digitalocean.digital_ocean_sshkey:
      name: "ansible-demo"
      ssh_pub_key: "{{ sshkey_pub.stdout }}"
      state: present
    register: result
    when: sshkey_pub_id is not defined

  - name: set sshkey_pub_id when ansible-demo key exists
    set_fact:
      sshkey_pub_id: "{{ result.data.ssh_key.id }}"
    when: sshkey_pub_id is not defined

  - name: create a new digitalocean server
    community.digitalocean.digital_ocean_droplet:
      state: present
      name: ansible-demo
      size: s-1vcpu-1gb
      region: sfo3
      image: ubuntu-22-04-x64
      wait_timeout: 500
      ssh_keys: 
      - "{{ sshkey_pub_id }}" 
      unique_name: true
    register: my_droplet
EOL
</code></pre></div></div>

<p>Now, run the playbook:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible-playbook playbooks/create-server.yml
</code></pre></div></div>

<h2 id="add-the-server-to-inventory">Add the server to inventory</h2>

<p>Ansible uses something called “inventory” to track remote servers that you want to run tasks against. Now that the server is created, let’s create an inventory file manually (you can use Ansible to programmatically do this, but for the sake of learning, let’s do it manually).</p>

<p>Create an inventory file named <code class="language-plaintext highlighter-rouge">hosts</code> with the IP of the demo server with the following one-liner:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>echo "ansible-demo ansible_host=$(doctl --access-token="${DO_API_TOKEN}" compute droplet list | grep ansible-demo | awk '{print $3}') ansible_user=root ansible_ssh_private_key_file=/tmp/ansible-demo-id_rsa" | tee hosts
</code></pre></div></div>

<p>Now, we can run Ansible playbooks against the <code class="language-plaintext highlighter-rouge">ansible-demo</code> server!</p>

<p>Test connectivity to the <code class="language-plaintext highlighter-rouge">ansible-demo</code> server defined in the inventory with Ansible’s ping module:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible ansible-demo -i hosts -m ping
</code></pre></div></div>

<h2 id="some-simple-playbook-examples-to-run-against-your-host">Some simple playbook examples to run against your host</h2>

<p>We will now create some example playbooks to run against the new <code class="language-plaintext highlighter-rouge">ansible-demo</code> server in our Ansible inventory.</p>

<h3 id="create-a-user">Create a user</h3>

<p>A common system administration task on remote servers is to manage users. So let’s create a new user named <code class="language-plaintext highlighter-rouge">tempuser</code> with Ansible.</p>

<p>Create a file at <code class="language-plaintext highlighter-rouge">playbooks/adduser-tempuser.yml</code> with the following commands:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat &gt;playbooks/adduser-tempuser.yml &lt;&lt;EOL
---
- hosts: ansible-demo
  tasks:
  - name: create tempuser 
    ansible.builtin.user:
      name: tempuser
      uid: 1050
      shell: /bin/bash
  - name: create /home/tempuser/.ssh
    ansible.builtin.file:
      state: directory
      path: /home/tempuser/.ssh
      owner: tempuser
      mode: 0700
  - name: use previously generated ssh key as ~/.ssh/authorized_keys file for tempuser
    ansible.builtin.copy:
      src: /tmp/ansible-demo-id_rsa.pub
      dest: /home/tempuser/.ssh/authorized_keys
      owner: tempuser
      mode: 0600
EOL
</code></pre></div></div>

<p>Now, let’s run the playbook, using the newly created <code class="language-plaintext highlighter-rouge">hosts</code> file as inventory:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible-playbook -i hosts playbooks/adduser-tempuser.yml
</code></pre></div></div>

<p>To ensure that this worked, let’s use Ansible’s <code class="language-plaintext highlighter-rouge">ping</code> module, but let’s supply a different user this time.</p>

<p>First, let’s supply an invalid user so we can see how it looks when this fails:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible ansible-demo -e "ansible_user=invalid-user" -i hosts -m ping
</code></pre></div></div>

<p>Now, let’s try with the new <code class="language-plaintext highlighter-rouge">tempuser</code> user that we created earlier:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible ansible-demo -e "ansible_user=tempuser" -i hosts -m ping
</code></pre></div></div>

<h3 id="install-some-packages">Install some packages</h3>

<p>Another common systems administration task is to install packages. So, let’s use Ansible to ensure the latest version of a couple of packages is installed for anyone on the <code class="language-plaintext highlighter-rouge">ansible-demo</code> system to use.</p>

<p>Before we install the packages, run this command to see if the packages currently exist on the system (replace <code class="language-plaintext highlighter-rouge">server-ip</code> with the public IP address of your server):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible ansible-demo -i hosts -m ansible.builtin.command -a "dpkg -l" | grep -E 'emacs|neofetch|python3-pip'
</code></pre></div></div>

<p>The command should return nothing, meaning that none of the packages we are about to install are currently installed on the <code class="language-plaintext highlighter-rouge">ansible-demo</code> system.</p>

<p>Create <code class="language-plaintext highlighter-rouge">playbooks/install-packages.yml</code> with the following command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat &gt;playbooks/install-packages.yml &lt;&lt;EOL
---
- hosts: ansible-demo
  tasks:
  - name: install latest version of various packages with apt
    ansible.builtin.apt:
      update_cache: yes
      pkg:
      - emacs
      - neofetch
      - python3-pip
EOL
</code></pre></div></div>

<p>Now, run the playbook:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible-playbook -i hosts playbooks/install-packages.yml
</code></pre></div></div>

<p>To be sure that the packages were actually installed, let’s run a command on the server to verify each of those packages exist:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible ansible-demo -i hosts -m ansible.builtin.command -a "dpkg -l" | grep -E 'emacs|neofetch|python3-pip'
</code></pre></div></div>

<h2 id="destroy-the-server">Destroy the server:</h2>

<p>Create <code class="language-plaintext highlighter-rouge">destroy-server.yml</code> with the following content to remove the SSH key and server created in <code class="language-plaintext highlighter-rouge">create-server.yml</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat &gt;playbooks/destroy-server.yml &lt;&lt;EOL
---
- hosts: localhost
  tasks:

  - name: gather information about digitalocean ssh keys
    community.digitalocean.digital_ocean_sshkey_info:
    register: ssh_keys

  - name: set sshkey_pub_id when ansible-demo key exists
    set_fact:
      sshkey_pub_id: "{{ ssh_keys.data | selectattr('name', 'equalto', 'ansible-demo') | map(attribute='id') | first }}"
    ignore_errors: true

  - name: remove ssh key from digitalocean
    community.digitalocean.digital_ocean_sshkey:
      name: "ansible-demo"
      id: "{{ sshkey_pub_id }}"
      state: absent
    when: sshkey_pub_id is defined

  - name: destroy ansible-demo digitalocean server
    community.digitalocean.digital_ocean_droplet:
      state: absent
      name: ansible-demo
      wait_timeout: 500
      unique_name: true
EOL
</code></pre></div></div>

<p>Now, run the playbook:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible-playbook -i hosts playbooks/destroy-server.yml
</code></pre></div></div>

<p>Alternatively, these commands could be used to clean everything up with <code class="language-plaintext highlighter-rouge">doctl</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>doctl compute --access-token="${DO_API_TOKEN}" droplet delete $(doctl compute --access-token="${DO_API_TOKEN}" droplet list | grep ansible-demo | awk '{print $1}')
doctl compute --access-token="${DO_API_TOKEN}" ssh-key delete $(doctl compute --access-token="${DO_API_TOKEN}" ssh-key list | grep ansible-demo | awk '{print $1}')
</code></pre></div></div>]]></content><author><name></name></author><category term="linux" /><category term="ansible" /><category term="devops" /><summary type="html"><![CDATA[This is a reposting of a meeting from the Unix User Group I help run]]></summary></entry><entry><title type="html">Lightweight security stack (SIEM, vulnerabilities, logging) for homelab</title><link href="https://heywoodlh.io/lightweight-siem/" rel="alternate" type="text/html" title="Lightweight security stack (SIEM, vulnerabilities, logging) for homelab" /><published>2025-07-23T00:00:00+00:00</published><updated>2025-07-23T00:00:00+00:00</updated><id>https://heywoodlh.io/lightweight-security-stack</id><content type="html" xml:base="https://heywoodlh.io/lightweight-siem/"><![CDATA[<p>As someone with a career orbiting around security, I deeply enjoy finding effective security tools that are minimal. This post will serve as a index of what I’m doing for security monitoring at home at the time of writing. These tools are mostly all running in Kubernetes.</p>

<h2 id="notification-infrastructure">Notification Infrastructure</h2>

<p>It’s important for significant security events to be made visible to administrators.</p>

<h3 id="ntfy">NTFY</h3>

<p>I use NTFY to receive push notifications – although, as described below, I mirror those notifications to Signal.</p>

<p>NTFY deployment in Kubernetes: <a href="https://github.com/heywoodlh/flakes/blob/02d6a8c47b428331f634f866cad72b5e7134a930/kube/manifests/ntfy.yaml">ntfy.yaml</a></p>

<p>As an example, here is how I monitor all SSH login attempts on my NixOS servers with NTFY: <a href="https://github.com/heywoodlh/nixos-configs/blob/f3c35b13038cf37d8ab782cf5b0ac969ee78a7da/nixos/roles/security/sshd-monitor.nix">sshd-monitor.nix</a></p>

<h3 id="ntfy-signal-mirror">NTFY Signal Mirror</h3>

<p>I wrote the following tool to mirror notifications I get in specific ntfy topics to Signal: <a href="https://github.com/heywoodlh/signal-ntfy-mirror">github:heywoodlh/signal-ntfy-mirror</a></p>

<p>I won’t describe how it all works, but here’s the NixOS implementation for those willing to figure it out themselves: <a href="https://github.com/heywoodlh/nixos-configs/blob/2ff872cb56e1734f738d3d26e0ccc1e4544440a4/nixos/roles/monitoring/ntfy-signal.nix">ntfy-signal.nix</a></p>

<p>The resulting Signal alert messages look like this:</p>

<p><img src="../images/signal-ntfy.png" alt="signal-ntfy" title="signal-ntfy" /></p>

<h2 id="logging">Logging</h2>

<p>Good logging infrastructure is a critical security component.</p>

<h3 id="syslog-ng">Syslog-ng</h3>

<p>Syslog-ng, in my opinion, is the underrated GOAT of logging. It’s minimal, yet can do so much.</p>

<p>Syslog-ng deployment in Kubernetes: <a href="https://github.com/heywoodlh/flakes/blob/28c183bc2a1c4e75d1bd9caf1e6aa30f218e315c/kube/manifests/syslog.yaml">syslog.yaml</a></p>

<p>The deployment also contains configurations for the following two tools to quickly search the logs:</p>
<ul>
  <li><a href="https://github.com/heywoodlh/logbash">logbash</a></li>
  <li><a href="https://github.com/tstack/lnav">lnav</a></li>
</ul>

<p>Here is how looking up failed SSH login attempts with my <code class="language-plaintext highlighter-rouge">logbash</code> deployment looks:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ logbash linux ssh-failed-login
/logs/linux/2025_07_23.log:Jul 23 21:30:56 ts-syslog-6d6kv-0 &lt;86&gt;1 2025-07-23T15:30:56-06:00 nix-nvidia sshd-session 2354768 - - Failed keyboard-interactive/pam for invalid user fakeuser from 192.168.1.48 port 63139 ssh2
/logs/linux/2025_07_23.log:Jul 23 21:30:56 ts-syslog-6d6kv-0 &lt;86&gt;1 2025-07-23T15:30:56-06:00 nix-nvidia sshd-session 2354768 - - Connection closed by invalid user fakeuser 192.168.1.48 port 63139 [preauth]
/logs/linux/2025_07_23.log:Jul 23 21:30:56 ts-syslog-6d6kv-0 &lt;30&gt;1 2025-07-23T15:30:56-06:00 nix-nvidia s7wypw6qzlva5z7m58zl2szjq7p0c1ks-sshd-monitor 2354781 - - {"id":"xvFYOYQpzn53","time":1753306256,"expires":1753349456,"event":"message","topic":"ssh-notifications","message":"Jul 23 15:30:56 nix-nvidia sshd-session[2354768]: Failed keyboard-interactive/pam for invalid user fakeuser from 192.168.1.48 port 63139 ssh2"}
/logs/linux/2025_07_23.log:Jul 23 21:30:56 ts-syslog-6d6kv-0 &lt;30&gt;1 2025-07-23T15:30:56-06:00 nix-nvidia s7wypw6qzlva5z7m58zl2szjq7p0c1ks-sshd-monitor 2354785 - - {"id":"hOCAbts9lJ6X","time":1753306256,"expires":1753349456,"event":"message","topic":"ssh-notifications","message":"Jul 23 15:30:56 nix-nvidia sshd-session[2354768]: Connection closed by invalid user fakeuser 192.168.1.48 port 63139 [preauth]"}
</code></pre></div></div>

<h3 id="syslog-ng-to-ntfy-alertnotification">Syslog-ng to NTFY alert/notification</h3>

<p>I want to draw special attention to the fact that syslog-ng can run actions on specific matches. This is especially powerful when you want certain log messages to trigger events.</p>

<p>In my case, I want specific logs to trigger NTFY alerts. I couldn’t find any reference examples when trying to set this up, so I hope this saves anybody wanting to do this some time.</p>

<p>Here’s a snippet from my syslog-ng configuration that triggers an NTFY notification on a match:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>source unifi_remote {
  udp(ip(0.0.0.0) port(514));
};

destination ntfy {
  http(
    url("http://ntfy.default.svc.cluster.local/security-notifications")
    method("POST")
    user-agent("syslog-ng User Agent")
    headers("Title: syslog-ng alert ${HOST}")
    body("${ISODATE} ${MESSAGE}")
  );
};

# All noteworthy ssh events
filter ssh_events {
  message("(Failed password|Invalid verification code|Invalid user|Accepted publickey|Accepted password|Accepted keyboard-interactive|Failed keyboard-interactive).*");
};

# unifi ssh notifications
log {
  source(unifi_remote);
  filter(ssh_events);
  destination(ntfy);
};
</code></pre></div></div>

<h2 id="endpoint-monitoring">Endpoint monitoring</h2>

<p>I use a handful of tools for monitoring my servers and workstations.</p>

<h3 id="using-fleetdm-policies-for-determining-endpoint-posture">Using FleetDM Policies for determining endpoint posture</h3>

<p><a href="https://fleetdm.com/">FleetDM</a> is an open source endpoint monitoring solution built around <a href="https://www.osquery.io/">OSQuery</a>. OSQuery is perhaps the only solution I know of wherein one can get the state of a machine <em>without</em> administrative shell access and with minimal invasion of privacy.</p>

<p>FleetDM deployment in Kubernetes: <a href="https://github.com/heywoodlh/flakes/blob/7ac7f166e0856b7a63e226f8dccb0b0b20ad41c4/kube/manifests/fleetdm.yaml">fleetdm.yaml</a></p>

<h3 id="macos-fleetdm-configuration">MacOS FleetDM configuration</h3>

<p>Installation of the <code class="language-plaintext highlighter-rouge">fleetctl</code>-generated installer for MacOS, hosted on a web server in my Tailnet: <a href="https://github.com/heywoodlh/nixos-configs/blob/90fed4b193d3c5b6556c9dddb8cb766787142c5a/darwin/roles/base.nix#L58-L73">base.nix</a></p>

<p>As a Nix-Darwin and Home-Manager here’s where I configure my Mac relevant to Fleet:</p>
<ul>
  <li>Fleet installation: <a href="https://github.com/heywoodlh/nixos-configs/blob/d5e3e32444409b5f2d6eb388b45f32f8e3e03c85/darwin/roles/base.nix#L62-L77">github:heywoodlh/nixos-configs – darwin/roles/base.nix#L62-77</a></li>
  <li>Defaults implementation: <a href="https://github.com/heywoodlh/nixos-configs/blob/d5e3e32444409b5f2d6eb388b45f32f8e3e03c85/home/modules/darwin-defaults.nix">github:heywoodlh/nixos-configs – home/modules/darwin-defaults.nix</a></li>
</ul>

<p>And, here’s how the Macs show up on Fleet after they are configured:</p>

<p><img src="../images/fleet-macos.png" alt="fleetdm-macos" title="Mac posture in FleetDM" /></p>

<h3 id="linux-configuration">Linux configuration</h3>

<p>NixOS configuration for OSQuery: <a href="https://github.com/heywoodlh/nixos-configs/blob/a88939bae4f576690b2e8b26e70e47cfea96490f/nixos/roles/monitoring/osquery.nix">osquery.nix</a></p>

<p>Ubuntu servers installing the FleetDM generated package with Ansible: <a href="https://github.com/heywoodlh/flakes/blob/3a7ca920470bfea0697728886f4451335a5cf8bd/ansible/server/tasks/linux/security.yml#L130-L146">security.yml</a></p>

<h2 id="vulnerability-scanning">Vulnerability scanning</h2>

<p>I have at least two solutions for vulnerability scanning at the time of writing.</p>

<h3 id="nuclei-for-web-scanning">Nuclei for web scanning</h3>

<p>I use <a href="https://projectdiscovery.io/nuclei">Nuclei from Project Discovery</a> for automated vulnerability scanning in my cluster.</p>

<p>Nuclei deployment in Kubernetes: <a href="https://github.com/heywoodlh/flakes/blob/28c183bc2a1c4e75d1bd9caf1e6aa30f218e315c/kube/manifests/nuclei.yaml">nuclei.yaml</a></p>

<p>The features in my deployment are the following:</p>
<ul>
  <li>Automatic enumeration of Kubernetes services as targets for Nuclei to scan</li>
  <li>Upload of results to projectdiscovery.io’s dashboard</li>
</ul>

<p><img src="../images/project-discovery.png" alt="Project Discovery Dashboard" title="Project Discovery Dashboard" /></p>

<h3 id="flan-scan-nmap--vulners-for-network-vulnerability-scanning">Flan Scan (nmap + vulners) for network vulnerability scanning</h3>

<p>For a simple, less web-focused vulnerability scan, I use Cloudflare’s <a href="https://github.com/cloudflare/flan">Flan scanner</a> which is as a wrapper around <code class="language-plaintext highlighter-rouge">nmap</code> and the <code class="language-plaintext highlighter-rouge">vulners</code> script that produces a report of the findings.</p>

<p>Flan scan deployment in Kubernetes: <a href="https://github.com/heywoodlh/flakes/blob/02d6a8c47b428331f634f866cad72b5e7134a930/kube/manifests/flan-scan.yaml">flan-scan.yaml</a></p>

<p><img src="../images/flan-scan.png" alt="Flan Scan Results" title="Flan Scan Results" /></p>]]></content><author><name></name></author><category term="linux" /><category term="syslog" /><category term="siem" /><category term="security" /><category term="logs" /><category term="osquery" /><summary type="html"><![CDATA[As someone with a career orbiting around security, I deeply enjoy finding effective security tools that are minimal. This post will serve as a index of what I’m doing for security monitoring at home at the time of writing. These tools are mostly all running in Kubernetes.]]></summary></entry><entry><title type="html">Disabling laptop fingerprint reader when clamshell on Linux</title><link href="https://heywoodlh.io/disable-fprint-clamshell-laptop/" rel="alternate" type="text/html" title="Disabling laptop fingerprint reader when clamshell on Linux" /><published>2025-03-16T00:00:00+00:00</published><updated>2025-03-16T00:00:00+00:00</updated><id>https://heywoodlh.io/disable-fprint-docked</id><content type="html" xml:base="https://heywoodlh.io/disable-fprint-clamshell-laptop/"><![CDATA[<p>This post will briefly outline my fix for Fprint trying to use the built-in fingerprint reader when the laptop lid is closed.</p>

<h1 id="fprint--laptop-fingerprint-reader--clamshell-on-linuxs-problem">Fprint + laptop fingerprint reader + clamshell on Linux’s problem</h1>

<p>I have multiple Linux laptops with built-in fingerprint readers that I use with Fprint to login via my fingerprint on Linux. Clamshell mode (having the laptop closed but still usable with an external display, keyboard, and trackpad) with Fprint presents a very annoying default behavior: if you use Fprint for <code class="language-plaintext highlighter-rouge">sudo</code> (i.e. in a terminal), it will prompt you for a fingerprint when the laptop is closed and will not time out for about 20 seconds. This is very annoying! Additionally, I use 1Password’s system authentication option to be able to use my fingerprint to login to 1Password and it suffers from the same issue.</p>

<h1 id="brief-nixos-plug">Brief NixOS plug:</h1>

<p>At the time of writing, I have a pull request opened in nixpkgs to fix this: <a href="https://github.com/NixOS/nixpkgs/pull/342676">nixos/pam: option to disable fprint if laptop lid is closed</a></p>

<p>This is what the implementation looks like for my X13: <a href="https://github.com/heywoodlh/nixos-configs/blob/92b3c5357c98cb9427753fd0d72385aefe099dcf/nixos/hosts/x13/configuration.nix#L85-L91">nixos/hosts/x13/configuration.nix</a></p>

<p>I won’t cover in this post how to consume my branch in NixOS.</p>

<h1 id="script-to-detect-laptop-lid-state-lidsh">Script to detect laptop lid state: lid.sh</h1>

<p>One could use something like this script to detect the state of the laptop lid:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/usr/bin/env bash

lid_state="/proc/acpi/button/lid/LID/state"

# Exit with failure if lid is closed, else true
grep -q closed ${lid_state} &amp;&amp; exit 1; true
</code></pre></div></div>

<h1 id="pam-configuration">PAM configuration</h1>

<p>Assuming <code class="language-plaintext highlighter-rouge">lid.sh</code> was placed at <code class="language-plaintext highlighter-rouge">/opt/scripts/lid.sh</code>, your PAM configuration <code class="language-plaintext highlighter-rouge">/etc/pam.d/sudo</code> might look like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>auth [success=ignore default=1] /usr/lib/aarch64-linux-gnu/security/pam_exec.so quiet /opt/scripts/lid.sh # fprintd-lid (order 11400)
</code></pre></div></div>

<p>This should be populated to each PAM configuration you’d like this to work.</p>

<p>For context/reference, here’s the realized configuration of <code class="language-plaintext highlighter-rouge">/etc/pam.d/sudo</code> on my NixOS machine – ignore the Nix store paths if you’re unfamiliar with NixOS:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Account management.
account required /nix/store/g928dngdfy30jyi1cs2m2a5wfimxgnkr-linux-pam-1.6.1/lib/security/pam_unix.so # unix (order 10900)

# Authentication management.
auth [success=ignore default=1] /nix/store/g928dngdfy30jyi1cs2m2a5wfimxgnkr-linux-pam-1.6.1/lib/security/pam_exec.so quiet /opt/scripts/lid.sh # fprintd-lid (order 11400)
auth sufficient /nix/store/7kjh2p1pzbibr9cj08kbczr4vzh3dyxv-fprintd-tod-1.90.9/lib/security/pam_fprintd.so # fprintd (order 11500)
auth sufficient /nix/store/g928dngdfy30jyi1cs2m2a5wfimxgnkr-linux-pam-1.6.1/lib/security/pam_unix.so likeauth try_first_pass # unix (order 11700)
auth required /nix/store/g928dngdfy30jyi1cs2m2a5wfimxgnkr-linux-pam-1.6.1/lib/security/pam_deny.so # deny (order 12500)

# Password management.
password sufficient /nix/store/g928dngdfy30jyi1cs2m2a5wfimxgnkr-linux-pam-1.6.1/lib/security/pam_unix.so nullok yescrypt # unix (order 10200)

# Session management.
session required /nix/store/g928dngdfy30jyi1cs2m2a5wfimxgnkr-linux-pam-1.6.1/lib/security/pam_env.so conffile=/etc/pam/environment readenv=0 # env (order 10100)
session required /nix/store/g928dngdfy30jyi1cs2m2a5wfimxgnkr-linux-pam-1.6.1/lib/security/pam_unix.so # unix (order 10200)
session required /nix/store/g928dngdfy30jyi1cs2m2a5wfimxgnkr-linux-pam-1.6.1/lib/security/pam_limits.so conf=/nix/store/wn252azs7hgq9q1m6k4jlwclclswgwrh-limits.conf # limits (order 12200)
</code></pre></div></div>

<h1 id="conclusion">Conclusion</h1>

<p>Using my <code class="language-plaintext highlighter-rouge">lid.sh</code> script in PAM to detect if your laptop lid is open should bypass your built-in fingerprint reader when your laptop lid is closed but will allow it when your lid is open.</p>]]></content><author><name></name></author><category term="linux" /><category term="nixos" /><category term="fprint" /><category term="fprintd" /><category term="clamshell" /><category term="laptop" /><summary type="html"><![CDATA[This post will briefly outline my fix for Fprint trying to use the built-in fingerprint reader when the laptop lid is closed.]]></summary></entry></feed>