<?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-05-07T00:21:42+00:00</updated><id>https://heywoodlh.io/feed.xml</id><title type="html">@heywoodlh</title><subtitle>heywoodlh thoughts</subtitle><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><entry><title type="html">Hacks for dealing with open source burnout</title><link href="https://heywoodlh.io/open-source-burnout/" rel="alternate" type="text/html" title="Hacks for dealing with open source burnout" /><published>2025-02-19T00:00:00+00:00</published><updated>2025-02-19T00:00:00+00:00</updated><id>https://heywoodlh.io/open-source-burnout</id><content type="html" xml:base="https://heywoodlh.io/open-source-burnout/"><![CDATA[<p>I’ve been blessed with a career enabled by my enthusiasm for contributing to open source. While I’ve never held a job dedicated to contributing to open source professionally, my open source contributions have been a significant factor in obtaining <em>all</em> of my senior technical roles – specifically, <a href="https://github.com/heywoodlh">my GitHub account</a> and <a href="https://heywoodlh.io">this blog</a> have been talking points in most of my successful job interviews.</p>

<p>I’ve been thinking about strategies that have helped me maintain momentum in my open source contributions and think they may help any new or existing person in the realm of open source. This isn’t as much a mental health post on how to avoid burnout, more of an unstructured list of hacks that have helped me over the years.</p>

<h2 id="disclaimers">Disclaimers</h2>

<p>Some disclaimers feel necessary before proceeding to temper expectations.</p>

<p>I am not claiming to be a model of mental health or a significant figure in the open source space. As a married person, my career has taken a toll on myself and my marriage. At points I struggle with managing healthy boundaries around my career and I think it will always be something to calibrate. My strategies may not work for everyone. Maybe I’ll publish this post and burnout the next day.</p>

<p>As with everyone on the internet, take my opinions and advice with a grain of salt and be confident that I know nothing. :)</p>

<h2 id="you-should-be-your-target-audience">You should be your target audience</h2>

<p>If you write anything open source, do it for yourself before anyone else. Do not expect that anyone will actually care about your contributions. Write for yourself in the moment, yourself in the past, or yourself in the future. Write code and documentation for yourself to reference. For example, I’m writing this post with myself as the target audience. If nobody reads this, it’s an interesting thing <em>for me</em> to process for myself and by itself that is a win. Additionally, reading a post like this when I first started working toward my career would have been really entertaining to me.</p>

<p>Another example: contribute to projects that <em>you</em> use and implement features that <em>you</em> need. Chances are, if you need it, someone else needs it! And if nobody else needs it, it can be a reference for yourself and a potential brag to a future employer.</p>

<p>As I have gotten older, I have noticed that people doing things for a long period of time – no matter what it is – is impressive by itself. If you write for yourself consistently, it will be a self-contained achievement and will likely be interesting to others on its own!</p>

<h2 id="small-contributions">Small contributions</h2>

<p>It can be easy to get overwhelmed with all the grand contributions you anticipate making. Keep your scope small first and understand your own limits before you try to conquer the world.</p>

<p>Here are some examples of small contributions to start:</p>
<ul>
  <li>Improve documentation on a tool you’re using (even if it’s only for you to read)</li>
  <li>Publish “Hello, world” projects as a reference for a language you’re learning</li>
  <li>For larger projects, break it down into tons of tiny tasks and accomplish one task at a time</li>
</ul>

<p>If your tasks remain small, you can be done with that task when you eventually run out of dopamine/excitement for the larger project and revisit after a break.</p>

<h2 id="discoverability">Discoverability</h2>

<p>I’ve found GitHub to be an incredible place for enabling the discovery of my own code for others as well as my own discovery of other projects. Most of my senior roles have used GitHub in some capacity, so it has been extra convenient to use.</p>

<p>An example demonstrating discoverability is my GitHub Stars list: <a href="https://github.com/heywoodlh?tab=stars">heywoodlh GitHub Stars</a></p>

<p>Many people don’t like GitHub being “social media” – which I can understand. For me, though, I’ve been able to find community with strangers interested in similar projects and it has helped employers easily parse how active I am with coding when they can look at my GitHub profile.</p>

<blockquote>
  <p>Note: I think that GitHub alternatives like GitLab, SourceHut, Codeberg, etc. are absolutely incredible – don’t use GitHub if you prefer something else!</p>
</blockquote>

<h2 id="reducing-tech-debt">Reducing tech debt</h2>

<p>For someone hoping to contribute to open source, reducing tech debt drastically reduces excessive mental load.</p>

<h3 id="tools">Tools</h3>

<p>Many of the tools I use and my mentality around them are captured in this post:</p>

<p><a href="https://heywoodlh.io/stack/">heywoodlh tech stack</a></p>

<p>Specifically around mitigating burnout, I implement the following which really helps:</p>
<ul>
  <li>Avoid visual overstimulation</li>
  <li>Live in the command line – a single pane of glass really helps me!</li>
  <li>Try to use keybindings to reduce mental overload and increase speed</li>
</ul>

<h3 id="offload-technical-overhead">Offload technical overhead</h3>

<p>One of the most impactful changes for reducing my open source load has been using external platforms for mission critical tooling. Nothing is more stressful than a self-induced outage that you have to fix that blocks you from getting meaningful work done.</p>

<p>Here are some things I’ve offloaded to an external service that’s drastically reduced my stress:</p>
<ol>
  <li>Switching to <a href="https://tailscale.com">Tailscale</a> away from self-hosted Wireguard</li>
  <li>Using GitHub Actions for regularly scheduled actions I always need to run</li>
  <li>Using <em>fly.io</em> for network services that I need highly available</li>
  <li>Publishing container images to Docker Hub instead of a private registry</li>
</ol>

<p>I’m proud to report that with the above items, my homelab can go down with minimal impact to my immediate family (spouse and kids). But, I still (mostly) have as much control as I need.</p>

<h3 id="self-documenting-as-a-lifestyle">Self-documenting as a lifestyle</h3>

<p>As a person with a DevOps career, I definitely have more passion than a normal person about utilizing self-documenting tools such as Nix, Terraform, etc. I won’t belabor the point outside of saying: it helps to write projects you can read the code for to quickly understand what’s happening.</p>

<p>For tools or projects that aren’t inherently self-documenting by design, I try to write documentation for myself to read later when I’m working on a project because <em>I forget everything</em>. Documenting stuff sucks, but saves so much time when revisiting a project. At worst, too much documentation is not referenced – but I’ve found it’s usually difficult to ever come close to <em>too much</em> documentation.</p>

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

<p>I hope these tips help!</p>]]></content><author><name></name></author><category term="linux" /><category term="open-source" /><category term="open" /><category term="burnout" /><category term="mental" /><category term="health" /><summary type="html"><![CDATA[I’ve been blessed with a career enabled by my enthusiasm for contributing to open source. While I’ve never held a job dedicated to contributing to open source professionally, my open source contributions have been a significant factor in obtaining all of my senior technical roles – specifically, my GitHub account and this blog have been talking points in most of my successful job interviews.]]></summary></entry><entry><title type="html">S.T.A.L.K.E.R. 2 Settings on Linux (Steam)</title><link href="https://heywoodlh.io/stalker-2-linux-settings/" rel="alternate" type="text/html" title="S.T.A.L.K.E.R. 2 Settings on Linux (Steam)" /><published>2025-01-09T00:00:00+00:00</published><updated>2025-01-09T00:00:00+00:00</updated><id>https://heywoodlh.io/stalker-2-linux</id><content type="html" xml:base="https://heywoodlh.io/stalker-2-linux-settings/"><![CDATA[<p>I’ve recently switched my gaming machine from Windows 11 to Linux. S.T.A.L.K.E.R. 2 is one of my new favorite games and seemed to run noticeably better on the same hardware running Windows 11. So, this is my attempt to document what I’ve done to fix performance under Linux.</p>

<p>One note: I’m lame and try to target 60FPS on 1080p so I can conveniently stream over my network via Sunshine and Moonlight.</p>

<h1 id="my-gaming-machine-specs">My Gaming Machine Specs</h1>

<p>Operating System: NixOS Unstable, 6.11.2-zen kernel
CPU: AMD Ryzen 5 5600G with Radeon Graphics
GPU: Nvidia 3080 Ti
Nvidia Driver version: <code class="language-plaintext highlighter-rouge">565.57.01</code> with <a href="https://github.com/keylase/nvidia-patch">nvidia-patch</a>
RAM: 32 GB
Drive: 1TB NVMe
Proton Version: <a href="https://github.com/GloriousEggroll/proton-ge-custom/releases/tag/GE-Proton9-22">Proton-GloriousEggroll 9-22</a>
Game Version: 1.1.3 (Steam)</p>

<p>One final thing is that booting with the kernel parameter of <code class="language-plaintext highlighter-rouge">mitigations=off</code> to disable CPU mitigations seemed to make a big impact in my configuration. I won’t go into whether or not you should, this article was informative on about that topic: <a href="https://fosspost.org/disable-cpu-mitigations-on-linux">How to Disable CPU Mitigations on Linux</a></p>

<p>For more details on my NixOS setup, here’s the relevant config files:</p>
<ul>
  <li><a href="https://github.com/heywoodlh/nixos-configs/blob/f31689a83632677b08108e65ee8c79c5e4b7c6f2/nixos/hosts/zalman/configuration.nix">Host-specific configuration</a></li>
  <li><a href="https://github.com/heywoodlh/nixos-configs/blob/f31689a83632677b08108e65ee8c79c5e4b7c6f2/nixos/roles/gaming/nvidia-patch.nix">Nvidia-Patch implementation</a></li>
  <li><a href="https://github.com/heywoodlh/nixos-configs/blob/f31689a83632677b08108e65ee8c79c5e4b7c6f2/nixos/roles/gaming/sunshine.nix">Sunshine, steam and KDE configuration</a></li>
  <li><a href="https://github.com/heywoodlh/nixos-configs/blob/f31689a83632677b08108e65ee8c79c5e4b7c6f2/nixos/roles/gaming/proton-ge.nix">Proton-GE installer package</a></li>
</ul>

<h1 id="steam-launch-options">Steam Launch Options:</h1>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DXVK_FRAME_RATE=60 PROTON_ENABLE_NVAPI=true %command% -xgeshadercompile -nothreadtimeout -NoVerifyGC
</code></pre></div></div>

<blockquote>
  <p>Note: change <code class="language-plaintext highlighter-rouge">DXVK_FRAME_RATE=60</code> to your desired frame rate.</p>
</blockquote>

<p>These options may be specific to GloriousEggroll Proton, so mileage may vary if you try a different Proton version.</p>

<h1 id="mods">Mods:</h1>

<p>The only proper mod I’m using specific to performance is the base version of <a href="https://www.nexusmods.com/stalker2heartofchornobyl/mods/7?tab=files">Optimized Tweaks S.2 - Improved Performance Reduced Stutter Lower Latency Better Frametimes</a></p>

<h1 id="configuration-files">Configuration files</h1>

<p>On Linux with Proton, these files can be found in this folder in your steam library: <code class="language-plaintext highlighter-rouge">steamapps/compatdata/1643320/pfx/drive_c/users/steamuser/AppData/Local/Stalker2/Saved/Config/Windows</code></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Engine.ini</code>:</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; https://steamcommunity.com/app/1643320/discussions/0/4626980689722585014/
[SystemSettings]
r.DynamicGlobalIlluminationMethod=2
r.ContactShadows=0
r.Lumen.Reflections.Allow=0
r.MaterialQualityLevel=0
r.Nanite.MaxPixelsPerEdge=4
r.Shadow.CSM.MaxCascades=0
r.ShadowQuality=0
r.VolumetricCloud=0
r.Volumetricfog=0
r.OneFrameThreadLag=0

[/script/engine.renderersettings]
r.GraphicsAdapter=0
r.TextureStreaming=0
r.DepthOfFieldQuality=0
r.BloomQuality=0
r.FilmGrain=0
r.DisableDistortion=1
r.LensFlareQuality=0
r.Fog=0

[/script/engine.gameengine]
DisplayGamma=2.056000
GlobalNetTravelCount=6
bEnableMouseSmoothing=False
bViewAccelerationEnabled=False
bDisableMouseAcceleration=False
</code></pre></div></div>

<blockquote>
  <p>The <code class="language-plaintext highlighter-rouge">r.OneFrameThreadLag=0</code> parameter seems to make a big difference with input lag when using frame generation.</p>
</blockquote>

<h1 id="in-game-settings">In-Game Settings:</h1>

<p>Preset: Medium</p>

<p>These are the settings I changed outside of the preset:</p>
<ul>
  <li>Textures: High</li>
  <li>Hair: low</li>
  <li>Motion blur strength: 0</li>
  <li>Depth of field: low</li>
  <li>Light shafts: disabled</li>
  <li>Upscaling method: DLSS</li>
  <li>Upscaling quality: Performance</li>
</ul>

<p>It also seemed to help my FPS just a bit to switch to Fullscreen Bordless instead of Exclusive Fullscreen.</p>]]></content><author><name></name></author><category term="linux" /><category term="steam" /><category term="stalker2" /><category term="gaming" /><summary type="html"><![CDATA[I’ve recently switched my gaming machine from Windows 11 to Linux. S.T.A.L.K.E.R. 2 is one of my new favorite games and seemed to run noticeably better on the same hardware running Windows 11. So, this is my attempt to document what I’ve done to fix performance under Linux.]]></summary></entry></feed>