<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Gaelan Lloyd</title><description>Personal website of Gaelan Lloyd showcasing projects, creative work, and insights in web design, technology, and digital innovation.</description><link>https://www.gaelanlloyd.com/</link><atom:link href="https://www.gaelanlloyd.com/feed.xml" rel="self" type="application/rss+xml"/><pubDate>Wed, 17 Jun 2026 21:10:58 +0000</pubDate><lastBuildDate>Wed, 17 Jun 2026 21:10:58 +0000</lastBuildDate><generator>Jekyll v4.4.1</generator><item><title>Published my second official WordPress plugin: E20 Page Tree</title><description>&lt;p&gt;As I gave thought to becoming an officially-published WordPress plugin author, thinking about what I’d release, I decided that I’d start by publishing a small, relatively simple plugin. That became &lt;a href=&quot;/blog/published-wordpress-plugin-edit-by-id/&quot;&gt;Quick Edit Post by ID&lt;/a&gt;. But that was just the starter project so I could figure out everything needed to publish a more complex tool, so I could iron out the kinks in my workflow.&lt;/p&gt; &lt;p&gt;For plugin #2, I decided to publish another tool that I’ve been adding to WordPress sites for years. It’s something I find valuable, and something that helps me in my day-to-day as a site admin. It’s a page tree visualizer with some powerful data export features. I gave the plugin a major overhaul, rewriting almost the entire thing from scratch. This is the third iteration of the plugin and the most polished. Had to get it all buttoned up and pretty to submit to the official WordPress plugin repo!&lt;/p&gt; &lt;p&gt;I think this plugin offers some handy features that I believe WordPress power users are sure to love.&lt;/p&gt; &lt;h2 id=&quot;the-backstory&quot;&gt;The backstory&lt;/h2&gt; &lt;p&gt;The default WordPress Pages screen is useful, but only just barely so. It’s difficult to use on larger sites and doesn’t provide any useful visualization of the site structure. Parent/child relationships are visible, but not always easy to deduce. Page templates are hidden unless you go looking for them. Drafts, scheduled pages, and pending review items can be easy to miss. And on many professional web teams, you’re often making project lists or analyzing the site page hierarchy, which means getting the site’s page structure into a spreadsheet. In WordPress core, there isn’t a simple, built-in way to do any of those things.&lt;/p&gt; &lt;p&gt;So, I built a small plugin for exactly that workflow.&lt;/p&gt; &lt;p&gt;Here it is: &lt;a href=&quot;https://wordpress.org/plugins/e20-page-tree/&quot;&gt;E20 Page Tree&lt;/a&gt;&lt;/p&gt; &lt;p&gt;Here’s a brief demo of the plugin in action:&lt;/p&gt; &lt;p&gt;&lt;img src=&quot;/assets/images/posts/2026-05-28/demo-wordpress-page-tree-plugin.gif&quot; alt=&quot;&quot; class=&quot;border-dark&quot; width=&quot;&quot; style=&quot;max-width: 100%; height: auto; &quot; /&gt;&lt;/p&gt; &lt;h2 id=&quot;the-challenge-i-faced&quot;&gt;The challenge I faced&lt;/h2&gt; &lt;p&gt;A lot of WordPress sites use Pages as their primary site structure.&lt;/p&gt; &lt;p&gt;That works well, especially for sites with a clear information architecture: Home, section landing pages, subpages, policy pages, campaign pages, and so on. But once a site grows beyond a dozen or so pages, it’s difficult to get a mental model of the site’s page hierarchy from the WordPress dashboard.&lt;/p&gt; &lt;p&gt;As a web systems engineer, I often need to answer questions like:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;What does the full WordPress page tree look like?&lt;/li&gt; &lt;li&gt;Which pages are nested under a given section?&lt;/li&gt; &lt;li&gt;Which pages use a certain page template?&lt;/li&gt; &lt;li&gt;Which pages have not been updated in a long time?&lt;/li&gt; &lt;li&gt;Are there draft or scheduled pages hiding somewhere in the structure?&lt;/li&gt; &lt;li&gt;Are there any orphaned pages lingering anywhere?&lt;/li&gt; &lt;li&gt;Are there any forgotten one-off or test pages that should be cleaned up?&lt;/li&gt; &lt;li&gt;Can I get this page hierarchy into a spreadsheet for cleanup, migration planning, or SEO review?&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;The normal Pages screen is fine for everyday editing. But when I’m doing content audits, technical SEO reviews, site migrations, information architecture planning, or editorial cleanup, I need a broader, structural view.&lt;/p&gt; &lt;p&gt;I want the site’s page tree, along with useful metadata, in one place.&lt;/p&gt; &lt;h2 id=&quot;page-tree-to-the-rescue&quot;&gt;Page Tree, to the rescue!&lt;/h2&gt; &lt;p&gt;E20 Page Tree adds a new &lt;strong&gt;Page Tree&lt;/strong&gt; screen under the WordPress Pages menu.&lt;/p&gt; &lt;p&gt;It shows the site’s page hierarchy in a clean, expandable tree view. Parent and child pages are visually connected, pages can be expanded or collapsed, and each row includes useful metadata right in the table.&lt;/p&gt; &lt;p&gt;The tree includes:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Page title&lt;/li&gt; &lt;li&gt;ID&lt;/li&gt; &lt;li&gt;Slug&lt;/li&gt; &lt;li&gt;Page template&lt;/li&gt; &lt;li&gt;Created date&lt;/li&gt; &lt;li&gt;Last modified date&lt;/li&gt; &lt;li&gt;Status&lt;/li&gt; &lt;li&gt;Quick edit and view actions&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;The status column uses a colored key, bringing fast attention to any pages that aren’t published:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;P&lt;/strong&gt; = Published&lt;/li&gt; &lt;li&gt;&lt;strong&gt;D&lt;/strong&gt; = Draft&lt;/li&gt; &lt;li&gt;&lt;strong&gt;R&lt;/strong&gt; = Pending Review&lt;/li&gt; &lt;li&gt;&lt;strong&gt;F&lt;/strong&gt; = Scheduled&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;If the site uses a static homepage, it’s shown first, at the top, just where you’d expect the homepage to be in a sitemap. That’s a small detail, but it helps orient the rest of the structure.&lt;/p&gt; &lt;h2 id=&quot;exporting-the-wordpress-page-tree&quot;&gt;Exporting the WordPress page tree&lt;/h2&gt; &lt;p&gt;The part I find most useful is the export functionality. My page tree plugin includes several output formats:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;A spreadsheet-friendly table&lt;/li&gt; &lt;li&gt;CSV&lt;/li&gt; &lt;li&gt;TSV&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;The table view can be copied and pasted directly into Excel, Google Sheets, LibreOffice Calc, or Numbers. The CSV and TSV tabs are useful if you want plain-text output for another tool, script, or manual processing step.&lt;/p&gt; &lt;p&gt;Having the sitemap in a spreadsheet is handy during content audits and migrations. Once the data is in a spreadsheet, with pages paired to their ID and metadata, you can filter by template, sort by modified date, review page depth, assign sections to team members, build a cleanup plan… or accomplish literally any number of jobs that pro teams do as part of long-term website operational maintenance.&lt;/p&gt; &lt;p&gt;Those were some of the main workflows I wanted to support. The interactive WordPress page tree is useful on its own, but the spreadsheet export turns the page tree into a practical planning tool that teams can use for any number of projects.&lt;/p&gt; &lt;h2 id=&quot;a-focused-plugin&quot;&gt;A focused plugin&lt;/h2&gt; &lt;p&gt;Like my first plugin, this one is small and intentionally focused.&lt;/p&gt; &lt;p&gt;It only works with WordPress Pages right now. It does not try to become a full content inventory system, migration framework, editorial workflow manager, or SEO platform.&lt;/p&gt; &lt;p&gt;It just gives you a better WordPress page tree and makes it easy to copy that structure into a spreadsheet so you can do some serious work on your site.&lt;/p&gt; &lt;p&gt;The plugin has no external dependencies, and its CSS and JavaScript only load on its own admin page. Users need the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edit_pages&lt;/code&gt; capability to access it.&lt;/p&gt; &lt;h2 id=&quot;my-second-official-wordpress-plugin&quot;&gt;My second official WordPress plugin&lt;/h2&gt; &lt;p&gt;Publishing my first official WordPress plugin earlier this year was partly about learning the WordPress.org plugin submission process. As my second, larger published plugin, I learned more about what it took to get something more complex submitted. Next is the advertising process… spreading the word, so to speak.&lt;/p&gt; &lt;p&gt;My hope is that you’ll find this a useful tool and get some insights about your website that aren’t possible with the barebones WordPress backend.&lt;/p&gt; &lt;p&gt;&lt;a href=&quot;https://wordpress.org/plugins/e20-page-tree/&quot;&gt;E20 Page Tree&lt;/a&gt; is available now in the official WordPress plugin directory.&lt;/p&gt; </description><pubDate>Thu, 28 May 2026 00:00:00 +0000</pubDate><link>https://www.gaelanlloyd.com/blog/wordpress-plugin-page-tree/</link><guid isPermaLink="true">https://www.gaelanlloyd.com/blog/wordpress-plugin-page-tree/</guid><category>plugin</category><category>productivity</category><category>wordpress</category></item><item><title>Meet &amp;quot;Good Boy,&amp;quot; My Zero-Dependency FreeBSD Bootstrapper</title><description>&lt;p&gt;I built a little tool that I’ve lovingly named &lt;a href=&quot;https://github.com/gaelanlloyd/good-boy&quot;&gt;Good Boy&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;It is a tiny, zero-dependency bootstrapper for fresh FreeBSD installs, written as one plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sh&lt;/code&gt; script.&lt;/p&gt; &lt;p&gt;I made it over the last two days for my homelab, where I spin up jails often. I wanted a faster, simpler way to get these environments up and running.&lt;/p&gt; &lt;h2 id=&quot;whyd-i-make-this&quot;&gt;Why’d I make this?&lt;/h2&gt; &lt;p&gt;I’ve written provisioning scripts before, but they were always a little more complicated than I wanted.&lt;/p&gt; &lt;p&gt;In the name of being DRY, I had split things into multiple function files. Each playbook lived in its own file. Everything lived in a Git repo. That was clean, sure… but before you could do anything useful, you had to do lots of steps:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Install and configure Git&lt;/li&gt; &lt;li&gt;Generate SSH keys and add them to your Git account&lt;/li&gt; &lt;li&gt;Clone the repo&lt;/li&gt; &lt;li&gt;Install Bash&lt;/li&gt; &lt;li&gt;Then, you could get started&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;I wanted to make a simpler version. Something that could run on a baseline FreeBSD install with no extra dependencies. No Bash, no Git, no cloned repo. Just the native &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sh&lt;/code&gt; shell and the tools already available on the system.&lt;/p&gt; &lt;h2 id=&quot;the-idea&quot;&gt;The idea&lt;/h2&gt; &lt;p&gt;&lt;em&gt;Good Boy&lt;/em&gt; is one self-contained script.&lt;/p&gt; &lt;p&gt;All the playbooks live inside that one file. Supporting source files, like dotfiles or config files, can live somewhere remote… but only the script itself needs to be downloaded to kick things off.&lt;/p&gt; &lt;p&gt;That makes the script a little bigger and a little more obtuse initially, but it reduces the overall complexity dramatically.&lt;/p&gt; &lt;p&gt;All you need to get started (after customizing your script, of course…)&lt;/p&gt; &lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;fetch https://example.com/good-boy.sh &lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x good-boy.sh ./good-boy.sh base ./good-boy.sh user ./good-boy.sh famp &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;One file, one command. Multiple playbooks. &lt;em&gt;Good boy!&lt;/em&gt;&lt;/p&gt; &lt;h2 id=&quot;what-it-does&quot;&gt;What it does&lt;/h2&gt; &lt;p&gt;Right now, &lt;em&gt;Good Boy&lt;/em&gt; is built around the things I tend to do on fresh FreeBSD systems. It has playbooks for baseline system setup, user setup, and initializing &lt;a href=&quot;/blog/create-a-freebsd-web-server/&quot;&gt;a FAMP stack&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;The Base playbook runs package updates, installs my favorite utilities, configures &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doas&lt;/code&gt;, primes the locate database, and cleans up junk leftover from some VPS install snapshots.&lt;/p&gt; &lt;p&gt;The User playbook handles my user-specific setup: Shell aliases, dotfiles, SSH authorized keys, hushlogin, and generates an SSH key for the box.&lt;/p&gt; &lt;p&gt;The FAMP playbook installs and configures MariaDB, Apache, PHP and php-fpm… then ties everything together with a demo virtualhost.&lt;/p&gt; &lt;h2 id=&quot;partially-idempotent-in-a-good-enough-way&quot;&gt;Partially idempotent, in a “good-enough” way&lt;/h2&gt; &lt;p&gt;&lt;em&gt;Good Boy&lt;/em&gt; aims to be partially idempotent.&lt;/p&gt; &lt;p&gt;Some commands are safe to run again. There’s some built-in sanity checks, like whether files or directories already exist. Changed config files get backed up with a timestamp before being replaced.&lt;/p&gt; &lt;p&gt;However… some commands may be destructive if run repeatedly. So, use &lt;em&gt;Good Boy&lt;/em&gt; with care.&lt;/p&gt; &lt;h2 id=&quot;why-not-just-use-ansible&quot;&gt;Why not just use Ansible?&lt;/h2&gt; &lt;p&gt;&lt;em&gt;Good Boy&lt;/em&gt; is not trying to be Ansible or Salt. It’s not trying to be an enterprise configuration management platform. It’s not built to manage fleets of servers from a single pane of glass.&lt;/p&gt; &lt;p&gt;It’s meant to solve a smaller problem: Getting a fresh FreeBSD system from “brand new” to “ready enough.” It’s just a &lt;em&gt;smol&lt;/em&gt; bootstrapper that does the tedious early setup work for you… and does it fast!&lt;/p&gt; &lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h2&gt; &lt;p&gt;&lt;em&gt;Good Boy&lt;/em&gt; started as an experiment to see if I could make a useful FreeBSD bootstrapper with zero dependencies, staying within the limitations of the built-in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sh&lt;/code&gt; shell. It was a little fun labor of love for the last two days. And now I’m happy and proud to release it into the wild!&lt;/p&gt; &lt;p&gt;So far, &lt;em&gt;Good Boy&lt;/em&gt; does exactly what I wanted: Makes fresh systems much faster and much less tedious.&lt;/p&gt; &lt;p&gt;Is it perfect? …No.&lt;/p&gt; &lt;p&gt;Is it production-grade enterprise configuration management? …Also no.&lt;/p&gt; &lt;p&gt;Does it sit, stay, and bootstrap a fresh jail without needing Git first? …Yes.&lt;/p&gt; &lt;p&gt;&lt;em&gt;What a Good Boy!&lt;/em&gt;&lt;/p&gt; </description><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><link>https://www.gaelanlloyd.com/blog/good-boy-zero-dependency-freebsd-bootstrapper/</link><guid isPermaLink="true">https://www.gaelanlloyd.com/blog/good-boy-zero-dependency-freebsd-bootstrapper/</guid><category>productivity</category><category>freebsd</category></item><item><title>Published my first official WordPress plugin: Quick Edit Post by ID</title><description>&lt;p&gt;For about ten years, I’ve been adding a simple WordPress admin bar utility to the websites I work on. It’s a simple text entry field that allows you to jump directly into editing any ID-based item just by typing the ID and pressing Enter.&lt;/p&gt; &lt;h2 id=&quot;the-challenge-i-faced&quot;&gt;The challenge I faced&lt;/h2&gt; &lt;p&gt;Some sites have posts or pages with very similar names. WooCommerce sites may have several duplicates of a given product, with different variations.&lt;/p&gt; &lt;p&gt;In these cases, the ID is always unique. The ID is the thing you can count on to reliably get you editing the proper item.&lt;/p&gt; &lt;p&gt;After working with a site for a while, you may start to memorize IDs, or maybe even have a little cheat sheet of the ones you work with the most. On many sites I work on, I’ll expose the WordPress post ID for items in HTML comments or data attributes so I can view the site source and quickly see which post ID a thing is related to.&lt;/p&gt; &lt;p&gt;Once you start looking at IDs, it helps to have a quick way to jump to edit an item by its ID. So, this little feature became a valuable power tool.&lt;/p&gt; &lt;h2 id=&quot;quick-edit-field-to-the-rescue&quot;&gt;Quick Edit field, to the rescue!&lt;/h2&gt; &lt;p&gt;Here it is! &lt;a href=&quot;https://wordpress.org/plugins/edit-by-id/&quot;&gt;Quick Edit Post by ID (Admin Bar)&lt;/a&gt;&lt;/p&gt; &lt;p&gt;The entire plugin is just a few lines of code. It’s really quite simple. But it brings a lot of utility.&lt;/p&gt; &lt;p&gt;This plugin is handy for anyone who is working day-in and day-out on WordPress sites. E-commerce site merchandisers, content management teams, site admins, event staff… anyone interacting with existing posts often can benefit from this little addition to the WordPress admin bar.&lt;/p&gt; &lt;p&gt;Here’s a brief demo of the plugin in action:&lt;/p&gt; &lt;p&gt;&lt;img src=&quot;/assets/images/posts/2026-03-25/demo-wordpress-plugin-quick-edit-post-by-id.gif&quot; alt=&quot;&quot; class=&quot;border-dark&quot; width=&quot;&quot; style=&quot;max-width: 100%; height: auto; &quot; /&gt;&lt;/p&gt; &lt;h2 id=&quot;my-first-official-wordpress-plugin&quot;&gt;My first official WordPress plugin&lt;/h2&gt; &lt;p&gt;I’ve been adding this plugin to sites manually for years. But recently, I wanted to learn how to become an official WordPress plugin author.&lt;/p&gt; &lt;p&gt;There was a lot to learn:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Plugin naming conventions&lt;/li&gt; &lt;li&gt;Licensing considerations&lt;/li&gt; &lt;li&gt;Code validation and safety checks&lt;/li&gt; &lt;li&gt;Packaging the plugin for review&lt;/li&gt; &lt;li&gt;Communicating with the official plugin team&lt;/li&gt; &lt;li&gt;SVN&lt;/li&gt; &lt;li&gt;Making screenshots and animated example GIFs&lt;/li&gt; &lt;li&gt;Making a good-looking Readme&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;It was a challenging process, and rather stressful from a personal aspect… exposing your private work to the world, having your code running in environments you can’t control and you haven’t ever seen… But I learned a lot in the last couple weeks and am very excited to see if anyone else finds this tool helpful!&lt;/p&gt; </description><pubDate>Wed, 25 Mar 2026 00:00:00 +0000</pubDate><link>https://www.gaelanlloyd.com/blog/published-wordpress-plugin-edit-by-id/</link><guid isPermaLink="true">https://www.gaelanlloyd.com/blog/published-wordpress-plugin-edit-by-id/</guid><category>plugin</category><category>productivity</category><category>wordpress</category></item><item><title>Choosing a note system: My Experience with Syncthing, BookStack, and Git</title><description>&lt;p&gt;A close friend and I have an inside joke centering around someone asking her the vague question, &lt;em&gt;&quot;Where do you keep your files?&quot;&lt;/em&gt;&lt;/p&gt; &lt;p&gt;Well… Certainly everyone has a unique answer to that question. I’ve been storing personal notes on my computer for years and years.&lt;/p&gt; &lt;p&gt;The manner in which I’ve stored those notes has gone through several phases:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;TXT files stored locally on my computer (1990’s - 2015)&lt;/li&gt; &lt;li&gt;TXT files shared between computers via Syncthing (2015 - 2021)&lt;/li&gt; &lt;li&gt;Self-hosted BookStack on a &lt;a href=&quot;/blog/create-a-freebsd-web-server/&quot;&gt;FAMP-stack&lt;/a&gt; VPS (2021 - 2025 Q3)&lt;/li&gt; &lt;li&gt;Git-backed Markdown files with a local Forgejo and external Codeberg remotes (2025 Q3 - Present)&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;The recent move to Git-backed Markdown has felt like such a significant improvement over my prior systems that I felt like writing about it.&lt;/p&gt; &lt;p&gt;I had read posts that some folks were fond of this setup, but at first I was doubtful because I felt like I’d be stuck forever running tons of commit commands and dealing with merge conflicts. In actuality, that hasn’t been as big of an issue as I thought it would be. I’ve got some tips and tricks to speed up working with Git listed at the bottom of this post.&lt;/p&gt; &lt;h2 id=&quot;some-notes-about-my-particular-use-case&quot;&gt;Some notes about my particular use case&lt;/h2&gt; &lt;p&gt;Here’s some background information to give you context about how I’m using and interacting with these tools:&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;I’m the only user&lt;/strong&gt;: Many knowledgebase platforms provide support for multiple users, and that adds layers of complexity. Since complexity leads to risk, that introduced anxiety. I’m looking for stability, simplicity, and a sense of being “at ease” when interacting with the overall system.&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;99% of my use happens at home&lt;/strong&gt;: Since I work from home, only rarely do I need to access the information remotely. Do I really need to host complex infrastructure and have my data living offsite when the vast majority of my usage is done within my home LAN?&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;thoughts-on-note-taking-products&quot;&gt;Thoughts on note-taking products&lt;/h2&gt; &lt;p&gt;I flirted with Microsoft OneNote, Evernote, Ulysses, and several others.&lt;/p&gt; &lt;p&gt;All of these have nice interfaces, and some have cool and interesting features. However, I was never comfortable with any of them, because they all suffer from one or more of the following dealbreakers:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Paid software&lt;/li&gt; &lt;li&gt;Vendor lock-in&lt;/li&gt; &lt;li&gt;Telemetry&lt;/li&gt; &lt;li&gt;Part of bulky office suites&lt;/li&gt; &lt;li&gt;Data stored in an opaque format&lt;/li&gt; &lt;li&gt;Data synced to third-party sources I had zero control over&lt;/li&gt; &lt;li&gt;AI-littered interfaces&lt;/li&gt; &lt;li&gt;Private data used to train LLM models&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;comparing-txt-files-in-syncthing-bookstack-and-git-backed-markdown-files-for-storing-personal-notes&quot;&gt;Comparing TXT files in Syncthing, BookStack, and Git-backed Markdown files for storing personal notes&lt;/h2&gt; &lt;p&gt;Ok, so let’s get down to it! I’m going to rate these three ways of storing and sharing content on a 0-5 scale:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;5 being the highest marks for a particular measurement&lt;/li&gt; &lt;li&gt;1 being the lowest&lt;/li&gt; &lt;li&gt;0 means “this feature is not applicable or not available on the platform”&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Here’s how I feel the platforms stack up:&lt;/p&gt; &lt;table class=&quot;table table-ratings&quot;&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Feature&lt;/th&gt; &lt;th&gt;Syncthing TXT&lt;/th&gt; &lt;th&gt;BookStack&lt;/th&gt; &lt;th&gt;Git-backed Markdown&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;th class=&quot;ratings-title&quot;&gt; &lt;a href=&quot;#multiple-devices&quot;&gt; Available on multiple devices &lt;/a&gt; &lt;/th&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;th class=&quot;ratings-title&quot;&gt; &lt;a href=&quot;#operational-ease&quot;&gt; Ease of hosting and operating &lt;/a&gt; &lt;/th&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;th class=&quot;ratings-title&quot;&gt; &lt;a href=&quot;#kb-features&quot;&gt; Knowledgebase features &lt;/a&gt; &lt;/th&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;th class=&quot;ratings-title&quot;&gt; &lt;a href=&quot;#work-computer&quot;&gt; Usable on your work computer &lt;/a&gt; &lt;/th&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;th class=&quot;ratings-title&quot;&gt; &lt;a href=&quot;#mobile-friendly&quot;&gt; Mobile-friendly web interface &lt;/a&gt; &lt;/th&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;nil&quot;&gt;-&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;th class=&quot;ratings-title&quot;&gt; &lt;a href=&quot;#revision-control&quot;&gt; Revision control &lt;/a&gt; &lt;/th&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;th class=&quot;ratings-title&quot;&gt; &lt;a href=&quot;#data-safety&quot;&gt; Data safety at rest &lt;/a&gt; &lt;/th&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;th class=&quot;ratings-title&quot;&gt; &lt;a href=&quot;#accessible-when-broken&quot;&gt; Content accessible if platform unavailable &lt;/a&gt; &lt;/th&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;nil&quot;&gt;-&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;td class=&quot;ratings&quot;&gt; &lt;div class=&quot;star-ratings&quot;&gt; &lt;span class=&quot;numeral&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;stars&quot;&gt;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&amp;bull;&lt;/span&gt; &lt;/div&gt; &lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;tfoot&gt; &lt;tr&gt; &lt;th&gt;Total ratings score&lt;/th&gt; &lt;th&gt;20/40 (50%)&lt;/th&gt; &lt;th&gt;28/40 (70%)&lt;/th&gt; &lt;th&gt;38/40 (95%)&lt;/th&gt; &lt;/tr&gt; &lt;/tfoot&gt; &lt;/table&gt; &lt;h2 id=&quot;ratings-explained&quot;&gt;Ratings, explained&lt;/h2&gt; &lt;p&gt;Let’s go over each of the ratings in the table above.&lt;/p&gt; &lt;h3 id=&quot;available-on-multiple-devices&quot;&gt;&lt;a name=&quot;multiple-devices&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Available on multiple devices&lt;/h3&gt; &lt;p&gt;All three solutions are available on multiple devices. So, it’s a tie here.&lt;/p&gt; &lt;h3 id=&quot;ease-of-hosting-and-operating&quot;&gt;&lt;a name=&quot;operational-ease&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Ease of hosting and operating&lt;/h3&gt; &lt;p&gt;&lt;strong&gt;Syncthing&lt;/strong&gt; is free. You can simply run the software on two or more personal computers to get your shares working, and it’s not too complex to set up. You don’t need to involve a dedicated “server” unless you want an always-on replica, or you want a remote device that’s available independently from your physical home location. Every so often I ran into bizarre conflicts with out-of-sync files, or remotes that would appear to be stuck syncing at “99% complete” forever, but otherwise Syncthing is reliable and works well. There’s some security risk as you are indeed hosting a service with externally-open ports.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;BookStack&lt;/strong&gt; has hosted options, but they’re costly for a personal knowledgebase for a solo user. If you choose to go self-hosted, BookStack requires a stunningly-complex setup to run:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;You’ll need to build, manage, and operate a VPS instance.&lt;/li&gt; &lt;li&gt;Then, you’ll need to understand how to operate and maintain: SSH, ZeroTier†, Apache, PHP, MySQL, BookStack, Composer, Laravel, all the various and changing BookStack dependences, Let’s Encrypt, Cron. († Optional, but I recommend using it.)&lt;/li&gt; &lt;li&gt;If you choose to store your backups on AWS S3, you’ll need an AWS account and need to configure and operate AWS CLI.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Once BookStack is up and running, it’s easy to operate. But then, updates come along. From a long-term maintenance perspective, there are some further drawbacks:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Updating the platform is a challenge, as the ever-changing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;composer&lt;/code&gt; dependencies can get frustrating to work with over time. On more than one occasion, a required &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;composer&lt;/code&gt; package wasn’t available on FreeBSD, or had issues running, causing platform updates to fail.&lt;/li&gt; &lt;li&gt;You are responsible to ensuring the platform is backed up, ideally to an off-site, third party source (like AWS S3). I was reasonably sure the backups were working, but I wasn’t ever able to feel fully at-ease knowing so much of my valuable personal information was teetering on a single point of failure.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;Git-backed Markdown&lt;/strong&gt; hosting is trivial and free on many platforms out there, like Codeberg, GitHub, etc. There’s nothing to maintain and no services to run. Just keep the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git&lt;/code&gt; package up-to-date on your machine and interact with the files with your favorite text editor. If you want, you can self-host a Git instance on something like Forgejo. This setup is more secure than Syncthing, as there are no services listening on open ports.&lt;/p&gt; &lt;h3 id=&quot;knowledgebase-features&quot;&gt;&lt;a name=&quot;kb-features&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Knowledgebase features&lt;/h3&gt; &lt;p&gt;&lt;strong&gt;Syncthing&lt;/strong&gt; text files have zero ability for cross-linking, showing images, etc. You could certainly use Markdown files here, editing them in an IDE much like I’ll talk about in a moment for Git-backed Markdown files… So in some ways this is a decent option. However, you won’t have the online read-only interface provided by a Git host, which adds a decent amount of utility.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;BookStack&lt;/strong&gt; wins here, hands-down. There are many useful features for organizing, formatting, and embedding rich content. I had some really slick HTML and CSS formatting on some pages.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Git-backed Markdown files&lt;/strong&gt; don’t have many features per se, but if you’re viewing them in an IDE like VSCodium or VSCode, the Markdown will start to become more useful. Header levels, code blocks, and callout blocks help your content become more readable. And once published to a Git hosting platform, you’ll be able to browse the content in a read-only mode and see rich formatting, hyperlinks, and image support. I had to give up the ability to make some really custom HTML and CSS tweaks, but that was an OK tradeoff considering the other wins.&lt;/p&gt; &lt;h3 id=&quot;usable-on-your-work-computer&quot;&gt;&lt;a name=&quot;work-computer&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Usable on your work computer&lt;/h3&gt; &lt;p&gt;&lt;strong&gt;Syncthing&lt;/strong&gt; runs as a service on your local machine, and it needs to open port connections to peers. Most workplaces probably won’t want you installing a service like Syncthing on a work computer.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;BookStack&lt;/strong&gt; is accessible over the web, so it’s “just another website” to access.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Git-backed Markdown&lt;/strong&gt; is probably A-OK to use on a work machine. If you’re geeky enough to want a personal knowledgebase with this setup, you probably have Git running on your work computer, too.&lt;/p&gt; &lt;h3 id=&quot;mobile-friendly-web-interface&quot;&gt;&lt;a name=&quot;mobile-friendly&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Mobile-friendly web interface&lt;/h3&gt; &lt;p&gt;&lt;strong&gt;Syncthing&lt;/strong&gt; is not suitable for personal knowledgebase content on a mobile device. It’s technically possible, and I do run the Syncthing client on my personal devices for media syncing (photos, mostly). But, editing text files on a mobile device is a tedious affair. Only true sadists need apply for this lifestyle.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;BookStack&lt;/strong&gt; has a fantastic web interface, and you can even edit on mobile devices.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Git-backed Markdown&lt;/strong&gt;, if synced up to a publicly-accessible service like Codeberg, GitHub, etc., gives you a “good-enough” web interface that can show your content. You can even make changes to the content on mobile, which again is “good-enough” for basic changes on the go. I think BookStack’s editor wins over online Git edits here, so I rated it a bit higher.&lt;/p&gt; &lt;h3 id=&quot;revision-control&quot;&gt;&lt;a name=&quot;revision-control&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Revision control&lt;/h3&gt; &lt;p&gt;&lt;strong&gt;Syncthing&lt;/strong&gt; can store copies of changed files, but you have to set up a revision scheme. In most cases, after a certain amount of time or number of revisions, older content is purged.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;BookStack&lt;/strong&gt; has very good revision control for pages. You can see a page’s history and revert to older versions right within the web interface.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Git-backed Markdown files&lt;/strong&gt; wins in terms of revision control, with powerful features like blame and the UI’s of online repos. All changes from the beginning of the repo’s creation are stored on every machine that works with the repo.&lt;/p&gt; &lt;h3 id=&quot;data-safety-at-rest&quot;&gt;&lt;a name=&quot;data-safety&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Data safety at rest&lt;/h3&gt; &lt;p&gt;&lt;strong&gt;Syncthing&lt;/strong&gt; introduces a few subtle sources of risk for the data at rest:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;When viewing a file, stray keystrokes could accidentally modify a file you’re simply reading. An accidental save would then change the file. Without a Git-style blame system, you might not notice until it was too late. This could be as simple as an errant key stroke or an accidental deletion of an entire section of content.&lt;/li&gt; &lt;li&gt;It’s possible for infection on a remote to silently spread to and corrupt local files on all replicas.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;BookStack&lt;/strong&gt; is a website that is typically operated in a publicly-accessible state (unless you lock it down with any number of methods). If the server is compromised, your data is at risk. If the server’s backups aren’t configured properly, your content is lost. You must be forever vigilant operating this setup, and that adds a slight mental tax to your life. I never felt that my data here was truly safe.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Git-backed Markdown&lt;/strong&gt; is utterly immune to almost all disasters. Absolutely nothing can happen to plain-text files sitting on your machine. And in even the most spectacular failure scenarios, any machine with a repo clone becomes a point-in-time recovery backup of the entire state and history of the content.&lt;/p&gt; &lt;h3 id=&quot;content-accessible-if-platform-unavailable&quot;&gt;&lt;a name=&quot;accessible-when-broken&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Content accessible if platform unavailable&lt;/h3&gt; &lt;p&gt;&lt;strong&gt;Syncthing&lt;/strong&gt; and &lt;strong&gt;Git-backed Markdown&lt;/strong&gt; content is available at all times, regardless of the platform’s operational status, so this is never a problem.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;BookStack&lt;/strong&gt; content is not accessible if the platform goes offline, leaving you in a serious bind. Most people will run a single instance of BookStack without a mirror, so there’s a painful single point of failure. Several times while updating BookStack or the various service dependencies, a wonky &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;composer&lt;/code&gt; update or a random problem causing Apache or SSL certs to go haywire would make all of my notes inaccessible. At a time when I needed my notes the most, I couldn’t get to them. I had a pre-update ritual of opening up every page I thought I might need in separate browser tabs so that I could have my notes ready in case BookStack went offline. This felt like some sort of silly voodoo practice and I’m glad to no longer need to do that.&lt;/p&gt; &lt;h2 id=&quot;summary-thoughts&quot;&gt;Summary thoughts&lt;/h2&gt; &lt;p&gt;I was excited for the features that BookStack provided, but as I moved content into the platform I was left with a vague feeling of unease. Moving content into BookStack never left me feeling totally secure. I figured the platform itself was trustworthy, but what if the platform was discontinued? What if the server got hacked? What if my backups weren’t working and I was humming along with ever onlyone copy of my most precious data in existence?&lt;/p&gt; &lt;p&gt;I noticed as I began to move content out of BookStack and into Git that I started feeling a sense of security and safety. It felt safe because I knew nothing could ever happen to my files. I didn’t have to worry about backups to S3 failing or any number of other “silent killers” that could be of hidden risk. I know my content is cold and at rest, static and immutable unless manually invoked to change by my hand. And… knowing those changes are fully tracked over the content’s entire lifetime gives me a sense of quiet and ease that I truly value.&lt;/p&gt; &lt;h2 id=&quot;appendix-miscellaneous-thoughts-on-the-platforms&quot;&gt;Appendix: Miscellaneous thoughts on the platforms&lt;/h2&gt; &lt;h3 id=&quot;bookstack&quot;&gt;BookStack&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;If you’re working in the HTML editor, accidental presses of ESC or clicks outside the editor’s popup window will close the editor modal and cause you to lose any changes you’ve made. This happened to me more than once and was quite frustrating.&lt;/li&gt; &lt;li&gt;I have a lot of tabular content, and BookStack writes really pedantic HTML tables with width percentages on every cell in the table. It was ugly and obtuse to work with.&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&quot;git-backed-markdown&quot;&gt;Git-backed Markdown&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;Rendered Markdown output is possible directly from within VSCodium or VSCode with the &lt;a href=&quot;https://open-vsx.org/extension/jonathan-yeung/mark-sharp&quot;&gt;Mark Sharp&lt;/a&gt; extension from Jonathan Yeung. I can usually visually parse Markdown in edit mode just fine, but sometimes “you just want pretty.”&lt;/li&gt; &lt;li&gt;Obviously, you aren’t relegated to just storing Markdown files in a Git repo or a Syncthing share. You can add all sorts of content easily, including folders of source code for projects. I’ve also been storing the pure Markdown output of LLM conversations. Now everything is searchable and stored safely.&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&quot;tip-save-time-making-commits&quot;&gt;Tip: Save time making commits&lt;/h3&gt; &lt;p&gt;Since this is a repo only used by me, I can get away with some shortcuts:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;I don’t use PRs, I just work directly on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt;.&lt;/li&gt; &lt;li&gt;I don’t bother with descriptive commit messages unless there’s a major change.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;I wrote a little helper alias &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gcp&lt;/code&gt; that allows me to quickly commit staged files with a boilerplate message.&lt;/p&gt; &lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;alias &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;gcp&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;git commit -m &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Updates&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &amp;amp;&amp;amp; git push origin main&quot;&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;So yeah, my commit history looks like this…&lt;/p&gt; &lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;d38be3a (HEAD -&amp;gt; main, origin/main, origin/HEAD, codeberg/main, codeberg/HEAD) Updates ce5e530 Updates b0f1bc3 CLEANUP OF MIGRATED CONTENT 9c0013d Updates 50c4354 Updates 9717e83 Updates baffaa0 Updates 2c64ee6 MAJOR REORGANIZATION bfcfb1f Updates c15075a Updates 22d6444 Updates 883f25c Updates eb4b504 Updates 679fe8c Updates ff28c48 Updates 6a7cc24 Updates 8bb9b71 Add old training files 51b09a0 Updates c6e9d53 Add archived notes faf8868 Updates &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;…but it’s ok because tools like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blame&lt;/code&gt; and the remotes’ web interfaces help me investigate any file histories or changes I’m interested in.&lt;/p&gt; &lt;h3 id=&quot;tip-host-a-local-forgejo-instance&quot;&gt;Tip: Host a local Forgejo instance&lt;/h3&gt; &lt;p&gt;This is totally optional, but hosting a Forgejo instance locally is relatively easily (I operate a homelab with a &lt;a href=&quot;/projects/ripley/&quot;&gt;local VM server&lt;/a&gt;). Day-to-day changes are pushed almost instantly, which keeps me in mental flow while working. At the end of each day, I push all changes out to a third-party remote for disaster recovery and to ensure I can access my notes while outside of my home.&lt;/p&gt; </description><pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate><link>https://www.gaelanlloyd.com/blog/note-taking-system-comparison-git-markdown-syncthing-bookstack/</link><guid isPermaLink="true">https://www.gaelanlloyd.com/blog/note-taking-system-comparison-git-markdown-syncthing-bookstack/</guid><category>productivity</category><category>self-hosted</category></item><item><title>How to create an IoT VLAN with OPNsense and TP-Link Omada (with IPv6 Prefix Delegation)</title><description>&lt;p&gt;In this post I’ll be talking about the journey I went on to strengthen my home network by adding an isolated VLAN for IoT devices and properly assigning IPv6 addresses for all devices on my network.&lt;/p&gt; &lt;p&gt;This guide will cover: IPv6 prefix delegation; Creating VLAN interfaces in OPNsense; DHCP configuration; Router advertisements; Firewall isolation rules; Managed switch VLAN tagging; and Omada VLAN Wi-Fi configuration.&lt;/p&gt; &lt;h2&gt;Table of contents&lt;/h2&gt; &lt;ol class=&quot;toc&quot;&gt; &lt;li&gt; &lt;a href=&quot;#intro&quot;&gt;Introduction&lt;/a&gt; &lt;ol class=&quot;toc&quot;&gt; &lt;li&gt; &lt;a href=&quot;#intro-hardware-used&quot;&gt;Hardware used&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#intro-why-vlan-iot-devices&quot;&gt;Why you should put IoT devices on a separate VLAN&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#intro-before&quot;&gt;Before: The original network layout&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#intro-after&quot;&gt;Afterwards: The improved network layout&lt;/a&gt; &lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#ipv6-prefix-delegation&quot;&gt;Get your ISP to provision you a /60 or /56 block of IPv6 addresses&lt;/a&gt; &lt;ol class=&quot;toc&quot;&gt; &lt;li&gt; &lt;a href=&quot;#ipv6-prefix-delegation-request-isp-change&quot;&gt;Requesting the change from the ISP&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#ipv6-prefix-delegation-validating-isp-settings&quot;&gt;Validating the prefix your ISP is issuing&lt;/a&gt; &lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#opnsense-create-vlan&quot;&gt;OPNsense VLAN setup&lt;/a&gt; &lt;ol class=&quot;toc&quot;&gt; &lt;li&gt; &lt;a href=&quot;#create-iot-vlan&quot;&gt;Create the IoT VLAN in OPNsense&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#add-network-interface&quot;&gt;Assign the VLAN interface&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#assign-dhcp-ranges&quot;&gt;Configure DHCP for the VLAN&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#enable-router-advertisements&quot;&gt;Enable IPv6 router advertisements&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#opnsense-vlan-firewall-rules&quot;&gt;Create firewall rules&lt;/a&gt; &lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#managed-switch-configure-vlan&quot;&gt;Configure VLANs on your managed switch&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#omada-create-vlan-wifi&quot;&gt;Configure VLANs in TP-Link Omada&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#troubleshooting&quot;&gt;Common problems/Troubleshooting&lt;/a&gt; &lt;ol class=&quot;toc&quot;&gt; &lt;li&gt; &lt;a href=&quot;#troubleshooting-ipv6-prefix-id-out-of-range&quot;&gt;Error: &apos;IPv6 prefix ID is out of range&apos;&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#troubleshooting-vlan-wifi-incorrect-addresses&quot;&gt;VLAN Wi-Fi devices are not receiving IP addresses&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href=&quot;#troubleshooting-iot-devices-cant-see-others&quot;&gt;IoT devices cannot see each other&lt;/a&gt; &lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;/ol&gt; &lt;h2 id=&quot;introduction&quot;&gt;&lt;a name=&quot;intro&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Introduction&lt;/h2&gt; &lt;p&gt;I decided to tackle a project I had been putting off for a while: &lt;em&gt;Building out a separate IoT VLAN for my home network.&lt;/em&gt;&lt;/p&gt; &lt;p&gt;Prior to this, I had been using Omada’s guest network for IoT devices in my home. But doing so caused me some headaches:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;It prevented the devices on that network from talking to each other.&lt;/li&gt; &lt;li&gt;It was cluttered: Both visitor devices and IoT devices were part of the same WLAN.&lt;/li&gt; &lt;li&gt;It kept me from having full visibility into the network’s firewall rules and hid activity for those devices on my network.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;The Omada guest network feature is great, and I want to keep using it for when I have visitors over. I want guests to have access to our Wi-Fi, but not to our home network. And I don’t want anything running on their devices to get to see anything on our trusted network. &lt;em&gt;No funny business!&lt;/em&gt;&lt;/p&gt; &lt;p&gt;Instead of using the Omada guest network for IoT devices, creating a dedicated VLAN provides better firewall control, device visibility, and network segmentation.&lt;/p&gt; &lt;p&gt;On a personal note, prior to this I really didn’t know much about IPv6. Frankly, I just disabled it more often than not, because I didn’t want to deal with it. It turns out that giving each device on your network a truly-global address unlocks a lot of simplicity and allows services like ZeroTier, Syncthing, and other direct-connection services to work quickly without dealing with weird legacy workarounds like IPv4 NAT.&lt;/p&gt; &lt;p&gt;This project helped me learn a bunch about IPv6, too. It was a long and arduous project but I’m glad I hunkered down and tackled it!&lt;/p&gt; &lt;h3 id=&quot;hardware-used&quot;&gt;&lt;a name=&quot;intro-hardware-used&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Hardware used&lt;/h3&gt; &lt;p&gt;In my home network, I have the following services and devices:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Xfinity/Comcast ISP (Residential cable)&lt;/li&gt; &lt;li&gt;Motorola Nighthawk CM2000 cable modem&lt;/li&gt; &lt;li&gt;OPNsense firewall/router running on an HP T730 thin client with an Intel I340-T4 NIC&lt;/li&gt; &lt;li&gt;MikroTik CSS318-16G-2S+IN managed switch&lt;/li&gt; &lt;li&gt;TP-Link Omada Wi-Fi APs (2x EAP610-v3, 1x EAP650-v1)&lt;/li&gt; &lt;li&gt;And a bunch of devices all connected in via ethernet and over Wi-Fi&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&quot;why-you-should-put-iot-devices-on-a-separate-vlan&quot;&gt;&lt;a name=&quot;intro-why-vlan-iot-devices&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Why you should put IoT devices on a separate VLAN&lt;/h3&gt; &lt;p&gt;I’m fairly security-minded and want to help protect the devices on my network. Smart-home devices can offer useful functionality, but I don’t believe should have full access to my trusted home LAN. How many times have we heard news stories about botnets and other things running amok in things like Smart Light Bulbs?&lt;/p&gt; &lt;p&gt;IoT devices should be isolated from your trusted home network because many of them:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Receive infrequent security updates and/or run outdated firmware.&lt;/li&gt; &lt;li&gt;Do naughty things, like scan your network and phone home all sorts of telemetry.&lt;/li&gt; &lt;li&gt;Have glaring issues like using hardcoded admin credentials that can eventually be discovered and exploited.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Isolating IoT devices lets you use them for their convenience while preventing them from accessing your trusted network. That limits the damage blast radius in the event they’re compromised.&lt;/p&gt; &lt;h3 id=&quot;before-the-original-network-layout&quot;&gt;&lt;a name=&quot;intro-before&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Before: The original network layout&lt;/h3&gt; &lt;p&gt;Here’s the original network layout before I started.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;For simplicity’s sake, I’m only illustrating the Wi-Fi parts of the network. In reality, there’s a bunch of hard-wired devices connected up to the network.&lt;/li&gt; &lt;li&gt;Network diagram icons courtesy of &lt;a href=&quot;https://github.com/cryptofuture/vrt-sheet-for-dia&quot;&gt;vrt-sheet-for-dia&lt;/a&gt; on GitHub, with a little of my own zhuzhing up.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src=&quot;/assets/images/posts/2026-03-04/opnsense-iot-vlan-network-diagram-before-vlan.webp&quot; alt=&quot;A diagram showing a home network and two wifi networks. IoT devices are connected to the guest network.&quot; class=&quot;border-dark&quot; width=&quot;100%&quot; style=&quot;max-width: 100%; height: auto; &quot; /&gt;&lt;/p&gt; &lt;p&gt;As you can see, everything non-trusted was just all tossed onto the Omada guest WLAN. There wasn’t any way to segment the devices in any meaningful way… it was either an “Fully trusted” or “Guest only” setup.&lt;/p&gt; &lt;h3 id=&quot;afterwards-the-improved-network-layout&quot;&gt;&lt;a name=&quot;intro-after&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Afterwards: The improved network layout&lt;/h3&gt; &lt;p&gt;&lt;img src=&quot;/assets/images/posts/2026-03-04/home-network-opnsense-iot-vlan-network-diagram.webp&quot; alt=&quot;A diagram showing a home network and three wifi networks. IoT devices are now connected to the new IoT-specific network which has its own VLAN.&quot; class=&quot;border-dark&quot; width=&quot;100%&quot; style=&quot;max-width: 100%; height: auto; &quot; /&gt;&lt;/p&gt; &lt;p&gt;In the improved network layout:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;IoT devices are now on their own VLAN’d WLAN, isolated from the home network. They can now see each other.&lt;/li&gt; &lt;li&gt;Physical network devices that are IoT related, or that I otherwise felt should be isolated (printers, etc.),are also tagged on the same VLAN to keep them isolated from the trusted network.&lt;/li&gt; &lt;/ul&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Network&lt;/th&gt; &lt;th&gt;VLAN&lt;/th&gt; &lt;th&gt;Purpose&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Trusted network&lt;/td&gt; &lt;td&gt;1&lt;/td&gt; &lt;td&gt;Personal computers, phones, and tablets&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;IoT network&lt;/td&gt; &lt;td&gt;10&lt;/td&gt; &lt;td&gt;Smart-home devices, Ring home security devices, etc.&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Guest Wi-Fi&lt;/td&gt; &lt;td&gt;n/a (Omada controlled)&lt;/td&gt; &lt;td&gt;Visitor devices&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;h2 id=&quot;get-your-isp-to-provision-you-a-60-or-56-block-of-ipv6-addresses&quot;&gt;&lt;a name=&quot;ipv6-prefix-delegation&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Get your ISP to provision you a /60 or /56 block of IPv6 addresses&lt;/h2&gt; &lt;p&gt;As I started going through the process to enable VLANs in OPNsense, you’ll need to assign separate IPv6 address blocks for each VLAN. But this isn’t something you have full control over… these addresses aren’t something you can create yourself. They come from your ISP.&lt;/p&gt; &lt;p&gt;Your ISP probably delegates you a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/64&lt;/code&gt; block of IPv6 addresses, because (a) it’s easy for them, and (b) most residential customers won’t need anything different as they’ll just have everything on one network anyway.&lt;/p&gt; &lt;p&gt;Your ISP may listen for and honor requests to issue different prefixes. Unfortunately for me, this wasn’t the case. No matter what I tried, I was only ever issued a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/64&lt;/code&gt; network.&lt;/p&gt; &lt;p&gt;If you don’t have the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/60&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/56&lt;/code&gt; prefix delegation, you’ll get an error in OPNsense at the point where you enable the VLAN interface’s IPv6 Identity Association. You won’t be able to assign a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Prefix ID&lt;/code&gt; and will get the error &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;You specified an IPv6 prefix ID that is out of range.&lt;/code&gt;&lt;/p&gt; &lt;h3 id=&quot;requesting-the-change-from-the-isp&quot;&gt;&lt;a name=&quot;ipv6-prefix-delegation-request-isp-change&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Requesting the change from the ISP&lt;/h3&gt; &lt;p&gt;Since this was only something my ISP could change, I had to work with them to request the change. This ended up taking the most time and being the most frustrating part of my journey. Working with Xfinity/Comcast’s Tier-1 support proved to be a challenge because they aren’t trained to help with IPv6 prefix delegation. Escalation paths with their support team are also tricky. I was told several times by support reps that, “The matter was being worked on and that I’d hear back from the team in 3-4 hours.” But after a day went by I had to follow up with support again, starting all over in the explanation each time. I had to repeat that cycle for several days before I got a ticket number for my request.&lt;/p&gt; &lt;p&gt;In total, it took about a dozen calls to Xfinity support over the course of one week to get this changed. Eventually, they did provision me a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/60&lt;/code&gt;, and I was up and running!&lt;/p&gt; &lt;h3 id=&quot;validating-the-prefix-your-isp-is-issuing&quot;&gt;&lt;a name=&quot;ipv6-prefix-delegation-validating-isp-settings&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Validating the prefix your ISP is issuing&lt;/h3&gt; &lt;p&gt;To validate the IPv6 prefix you’re receiving, go to OPNsense and navigate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;System &amp;gt; Log Files &amp;gt; General&lt;/code&gt;. Search for keyword &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prefix&lt;/code&gt; under log level &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Notice&lt;/code&gt;. Look at the most recent log entries and look at the last digits to see what network prefix you’re being issued. You want to see either &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/60&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/56&lt;/code&gt;.&lt;/p&gt; &lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;dhcp6c_script: REQUEST on igb0 prefix now 2001:db8:1234:5670::/56 # Good dhcp6c_script: REQUEST on igb0 prefix now 2001:db8:1234:5670::/60 # Good dhcp6c_script: REQUEST on igb0 prefix now 2001:db8:1234:5670::/64 # Not good &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/56&lt;/code&gt; is good, too! That means you’re being issued 256 different network groups. I requested a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/60&lt;/code&gt; to be more conservative, as the 16 different networks provided by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/60&lt;/code&gt; should be more than sufficient for my needs. I also wanted to be conservative with my request to Xfinity in case they pushed back on the larger allocation.&lt;/p&gt; &lt;h2 id=&quot;opnsense-vlan-setup&quot;&gt;&lt;a name=&quot;opnsense-create-vlan&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;OPNsense VLAN setup&lt;/h2&gt; &lt;p&gt;This section walks through the complete process of configuring a VLAN in OPNsense. We’ll create the VLAN interface, assign IPv4 and IPv6 networks, enable DHCP, and configure router advertisements so devices can obtain IPv6 addresses.&lt;/p&gt; &lt;p&gt;The steps below assume you already have:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;A managed switch capable of VLAN tagging&lt;/li&gt; &lt;li&gt;Wi-Fi access points that support VLANs&lt;/li&gt; &lt;li&gt;An IPv6 prefix delegation from your ISP (see above)&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Once your ISP is providing you a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/56&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/60&lt;/code&gt;, you can continue actually setting up the VLANs on your network.&lt;/p&gt; &lt;p&gt;We’ll start by building that out in OPNsense, then roll out the VLAN elsewhere on the network.&lt;/p&gt; &lt;h3 id=&quot;create-the-iot-vlan-in-opnsense&quot;&gt;&lt;a name=&quot;create-iot-vlan&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Create the IoT VLAN in OPNsense&lt;/h3&gt; &lt;p&gt;Within OPNsense, navigate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Interfaces &amp;gt; Devices &amp;gt; VLAN&lt;/code&gt;.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Device: (Leave blank, it’ll be auto-generated)&lt;/li&gt; &lt;li&gt;Parent interface: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;igb1 (LAN)&lt;/code&gt; &lt;ul&gt; &lt;li&gt;(Or whatever your LAN interface is)&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt;VLAN Tag: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10&lt;/code&gt; &lt;ul&gt; &lt;li&gt;(I used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10&lt;/code&gt; here, but you can use something different if you’d like. Just be sure you keep this value consistent with the steps later on in this guide.)&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt;Description: (Something useful, like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IoT Devices&lt;/code&gt;)&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Add the new VLAN interfaces and enable them.&lt;/p&gt; &lt;h3 id=&quot;assign-the-vlan-interface&quot;&gt;&lt;a name=&quot;add-network-interface&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Assign the VLAN interface&lt;/h3&gt; &lt;p&gt;First, add the new interface.&lt;/p&gt; &lt;p&gt;Navigate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Interfaces &amp;gt; Assignments&lt;/code&gt;.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Assign a new interface&lt;/code&gt;, find the new VLAN interface you created in the prior step. (For instance, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vlan01&lt;/code&gt;).&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Then, enable the interface.&lt;/p&gt; &lt;p&gt;Navigate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Interfaces &amp;gt; VLAN01&lt;/code&gt;.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Basic &lt;ul&gt; &lt;li&gt;Enable interface = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Checked&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt;Generic &lt;ul&gt; &lt;li&gt;IPv4 Config Type = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Static IP&lt;/code&gt;&lt;/li&gt; &lt;li&gt;IPv6 Config Type = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Identity association&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt;Static IPv4 Config &lt;ul&gt; &lt;li&gt;IPv4 address = Provide an IP range here to use for the VLAN devices, like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;192.168.10.1/24&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt;IPv6 Identity Association &lt;ul&gt; &lt;li&gt;Parent interface = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAN&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Assign Prefix ID = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;If at this step you get the error “You specified an IPv6 prefix ID that is out of range,” then your ISP might be provisioning you a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/64&lt;/code&gt; IPv6 instead of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/60&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/56&lt;/code&gt;.&lt;/p&gt; &lt;h3 id=&quot;configure-dhcp-for-the-vlan&quot;&gt;&lt;a name=&quot;assign-dhcp-ranges&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Configure DHCP for the VLAN&lt;/h3&gt; &lt;p&gt;Adapt the values here to suit your needs. For this example, we’re using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;192.168.10.1/24&lt;/code&gt; as the address pool, and I’m giving &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.100-.254&lt;/code&gt; to be open slots of the DHCP pool. We’ll keep &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.2-.99&lt;/code&gt; for static IP reservations. You can of course change that allocation however you want!&lt;/p&gt; &lt;p&gt;Navigate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Services &amp;gt; Dnsmasq DNS &amp;amp; DHCP &amp;gt; DHCP ranges&lt;/code&gt;.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Interface = VLAN10 &lt;ul&gt; &lt;li&gt;Start address = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;192.168.10.100&lt;/code&gt;&lt;/li&gt; &lt;li&gt;End address = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;192.168.10.254&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Enable DHCP server/service on new interfaces&lt;/p&gt; &lt;p&gt;Navigate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Services &amp;gt; Dnsmasq DNS &amp;amp; DHCP &amp;gt; General&lt;/code&gt;.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Interface: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LAN&lt;/code&gt; + Add new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VLAN&lt;/code&gt; network (it’s a multi-select field)&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&quot;enable-ipv6-router-advertisements&quot;&gt;&lt;a name=&quot;enable-router-advertisements&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Enable IPv6 router advertisements&lt;/h3&gt; &lt;div class=&quot;alert alert-info&quot;&gt; &lt;p&gt;&lt;strong&gt;Helpful hint:&lt;/strong&gt; The default OPNsense configuration doesn&apos;t enable this for you on the LAN. Enabling this (even just for the LAN network) fixed a bunch of weird issues I was seeing on my network!&lt;/p&gt; &lt;/div&gt; &lt;p&gt;Next, we’ll enable router advertisements (which provides IPv6 routing and address information for devices on your network.&lt;/p&gt; &lt;p&gt;Navigate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Services &amp;gt; Router Advertisements&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;Make an RA for the base LAN.&lt;/p&gt; &lt;p&gt;New:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Enabled = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Checked&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Interface = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Your LAN&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Mode = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Assisted&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Then, make an RA for each VLAN you created earlier.&lt;/p&gt; &lt;p&gt;New:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Enabled = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Checked&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Interface = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VLAN10&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Mode = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Assisted&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&quot;create-firewall-rules&quot;&gt;&lt;a name=&quot;opnsense-vlan-firewall-rules&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Create firewall rules&lt;/h3&gt; &lt;p&gt;There’s a few schools of thought here on how you can secure the VLAN traffic. We’ll of course prevent the VLAN from initiating connections to the trusted network, but as far as outbound traffic goes, we could…&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Prevent all outbound traffic on all ports and protocols, except on specific ports. &lt;ul&gt; &lt;li&gt;The most secure, but the most time consuming to set up and maintain over time.&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt;Allow all outbound traffic on all ports and protocols, except to protected networks and addresses. &lt;ul&gt; &lt;li&gt;Secure as long as you are careful!&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ol&gt; &lt;p&gt;I wanted to naturally go with “total blacklist except what I specifically allow,” but then I realized I’d be forever maintaining the list of ports that need to be open for all these random IoT devices to operate. So, I opted for the second choice.&lt;/p&gt; &lt;p&gt;OPNsense allows for the creation of &lt;em&gt;Aliases&lt;/em&gt; so we can assign a single place to store all the trusted network addresses and network interface names. Aliases can only hold items of a certain type, so we’ll be making three of them:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;A list of all trusted networks.&lt;/li&gt; &lt;li&gt;A list of all trusted IP addresses.&lt;/li&gt; &lt;li&gt;A joint list combining all items in #1 and #2, which we’ll apply in the firewall rules.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;First, we’ll create an Alias for networks to trust.&lt;/p&gt; &lt;p&gt;Navigate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Firewall &amp;gt; Aliases &amp;gt; Create&lt;/code&gt;.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Enabled = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Checked&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Name = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUSTED_NETWORKS&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Type = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Network(s)&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Content = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__lan_network&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Description = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Specific networks to trust&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Then, the Alias for IP addresses.&lt;/p&gt; &lt;p&gt;Navigate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Firewall &amp;gt; Aliases &amp;gt; Create&lt;/code&gt;.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Enabled = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Checked&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Name = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUSTED_ADDRS&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Type = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Host(s)&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Content = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;192.168.1.1&lt;/code&gt; (or your OPNsense router management IP), then the IP address of any other management devices or other things you want to block.&lt;/li&gt; &lt;li&gt;Description = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Specific IP addresses to trust&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Create an Alias to join those two.&lt;/p&gt; &lt;p&gt;Navigate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Firewall &amp;gt; Aliases &amp;gt; Create&lt;/code&gt;.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Enabled = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Checked&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Name = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUSTED_COMBINED&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Type = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Network Group&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Content = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUSTED_ADDRS&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUSTED_NETWORKS&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Description = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;The combined group of all trusted entities&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Next, we’ll set up the firewall rules.&lt;/p&gt; &lt;p&gt;Start by allowing traffic out of VLAN.&lt;/p&gt; &lt;p&gt;Navigate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Firewall &amp;gt; Rules &amp;gt; [Your VLAN Interface]&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;Add rule:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Action = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PASS&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Interface = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[Your VLAN Interface]&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Direction = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IN&lt;/code&gt;&lt;/li&gt; &lt;li&gt;TCP/IP Version = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IPv4+IPv6&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Protocol = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ANY&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Source = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[Your VLAN Interface] net&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Destination = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ANY&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Description = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Allow [Your VLAN Interface] out to any internet destination&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Then, block traffic from the VLAN to the joint alias of protected network resources.&lt;/p&gt; &lt;p&gt;Navigate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Firewall &amp;gt; Rules &amp;gt; [Your VLAN Interface]&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;Add rule:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Action = Either &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;REJECT&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BLOCK&lt;/code&gt; (see note below)&lt;/li&gt; &lt;li&gt;Interface = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[Your VLAN Interface]&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Direction = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IN&lt;/code&gt;&lt;/li&gt; &lt;li&gt;TCP/IP Version = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IPv4+IPv6&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Protocol = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ANY&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Source = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[Your VLAN Interface] net&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Destination = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUSTED_COMBINED&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Description = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Block [Your VLAN Interface] to protected network entities&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Then, move the block rule above the allow rule.&lt;/p&gt; &lt;p&gt;About the block rule:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;Make sure that the block rule is ABOVE the allow rule.&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;REJECT&lt;/code&gt; if you want fast rejections upon accessing protected resources. That’s good for devices on internal networks so the devices don’t hang forever waiting to timeout when they hit the firewall. Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BLOCK&lt;/code&gt; if you just want devices trying to reach protected resources to never hear back. It’s more secure per-se, but could lead to long timeouts and possible network congestion from IoT devices waiting on hold.&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;configure-vlans-on-your-managed-switch&quot;&gt;&lt;a name=&quot;managed-switch-configure-vlan&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Configure VLANs on your managed switch&lt;/h2&gt; &lt;p&gt;We probably have different devices here, so you’ll need to follow whatever instructions your switch’s vendor provides for setting up the VLANs.&lt;/p&gt; &lt;p&gt;The most important parts are:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Set up a base VLAN for trusted traffic (I used VLAN &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1&lt;/code&gt;).&lt;/li&gt; &lt;li&gt;Tag any physical ports for devices that must be VLAN’d (I put our network printer on the IoT VLAN.)&lt;/li&gt; &lt;li&gt;Tag any physical ports for Omada APs to have both VLAN &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10&lt;/code&gt;.&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;configure-vlans-in-tp-link-omada&quot;&gt;&lt;a name=&quot;omada-create-vlan-wifi&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Configure VLANs in TP-Link Omada&lt;/h2&gt; &lt;p&gt;To get Wi-Fi devices onto this new VLAN, you’ll need to create a new WLAN for them. Use a completely separate, strong password for this network.&lt;/p&gt; &lt;p&gt;While setting up the new WLAN, under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Advanced Settings&lt;/code&gt;, use:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;VLAN = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Custom&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Add VLAN = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;By VLAN ID&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;When you connect devices, check that their IPv4 addresses are part of the new VLAN range. You’ll also want to check the IPv6 addresses and see that their addresses are part of the VLAN’s group, indicated by the different prefix (in this case, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b500&lt;/code&gt; for devices in the trusted LAN, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b501&lt;/code&gt; for devices in the IoT VLAN with prefix &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1&lt;/code&gt;).&lt;/p&gt; &lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Device on Trusted LAN 2001:db8:100:b500::25 Device on IoT VLAN 2001:db8:100:b501::25 &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h2 id=&quot;common-problemstroubleshooting&quot;&gt;&lt;a name=&quot;troubleshooting&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Common problems/Troubleshooting&lt;/h2&gt; &lt;h3 id=&quot;error-ipv6-prefix-id-is-out-of-range&quot;&gt;&lt;a name=&quot;troubleshooting-ipv6-prefix-id-out-of-range&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;Error: “IPv6 prefix ID is out of range”&lt;/h3&gt; &lt;p&gt;This usually means your ISP is only delegating a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/64&lt;/code&gt; IPv6 prefix instead of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/60&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/56&lt;/code&gt;. OPNsense requires a larger delegated prefix when assigning IPv6 subnets to multiple VLAN interfaces.&lt;/p&gt; &lt;p&gt;Solution:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Contact your ISP and request a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/60&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/56&lt;/code&gt; prefix delegation (&lt;a href=&quot;#ipv6-prefix-delegation&quot;&gt;see above&lt;/a&gt;).&lt;/li&gt; &lt;li&gt;Confirm the prefix using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;System &amp;gt; Log Files &amp;gt; General&lt;/code&gt; in OPNsense (&lt;a href=&quot;#ipv6-prefix-delegation-validating-isp-settings&quot;&gt;see above&lt;/a&gt;).&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&quot;vlan-wi-fi-devices-are-not-receiving-ip-addresses&quot;&gt;&lt;a name=&quot;troubleshooting-vlan-wifi-incorrect-addresses&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;VLAN Wi-Fi devices are not receiving IP addresses&lt;/h3&gt; &lt;p&gt;Check the following:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;The VLAN ID configured on the Omada WLAN matches the VLAN tag on the switch.&lt;/li&gt; &lt;li&gt;The switch port connected to the AP is configured as a &lt;strong&gt;tagged trunk&lt;/strong&gt;.&lt;/li&gt; &lt;li&gt;DHCP is enabled for the VLAN interface in OPNsense.&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&quot;iot-devices-cannot-see-each-other&quot;&gt;&lt;a name=&quot;troubleshooting-iot-devices-cant-see-others&quot; class=&quot;anchor&quot;&gt;&lt;/a&gt;IoT devices cannot see each other&lt;/h3&gt; &lt;p&gt;Make sure &lt;strong&gt;Client Isolation&lt;/strong&gt; is disabled on the VLAN WLAN in Omada.&lt;/p&gt; </description><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><link>https://www.gaelanlloyd.com/blog/iot-vlan-opnsense-omada-ipv6/</link><guid isPermaLink="true">https://www.gaelanlloyd.com/blog/iot-vlan-opnsense-omada-ipv6/</guid><category>firewall</category><category>how-to</category><category>networking</category></item><item><title>Upgrade MariaDB from 10.6 LTS to 11.8 LTS on FreeBSD</title><description>&lt;p&gt;Here’s my process to upgrade MariaDB from 10.6 LTS to 11.8 LTS on FreeBSD. It’s very straighforward. I’ll be adding in some safety checks along the way, like DB and config file backups, to help ensure this upgrade goes smoothly.&lt;/p&gt; &lt;p&gt;First, review the &lt;a href=&quot;https://mariadb.com/docs/server/server-management/install-and-upgrade-mariadb/upgrading/platform-specific-upgrade-guides/upgrading-on-linux/upgrading-between-major-mariadb-versions&quot;&gt;official upgrade notes&lt;/a&gt;. There have been some changes to be aware of, including &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;my.cnf&lt;/code&gt; option deprecations that could throw errors after the upgrade.&lt;/p&gt; &lt;h2 id=&quot;upgrade-process&quot;&gt;Upgrade process&lt;/h2&gt; &lt;p&gt;Launch a root console.&lt;/p&gt; &lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;su - &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Backup all databases:&lt;/p&gt; &lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mysqldump &lt;span class=&quot;nt&quot;&gt;--all-databases&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /root/mysql-upgrade-backup-&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; +%Y%m%d&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;.sql &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Back up all config files:&lt;/p&gt; &lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;tar&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-cvzf&lt;/span&gt; /root/mysql_config_backup_&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; +%Y%m%d&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;.tar.gz /usr/local/etc/mysql &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Stop the MariaDB service:&lt;/p&gt; &lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;service mysql-server stop &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;There’s no need to remove the old MariaDB packages. They’ll be automatically removed when the new version is installed.&lt;/p&gt; &lt;p&gt;Install the new version (this also installs the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-client&lt;/code&gt; package):&lt;/p&gt; &lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pkg &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;mariadb118-server &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;You’ll see output like the following, showing the old packages will be removed. Enter &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;y&lt;/code&gt; to proceed.&lt;/p&gt; &lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Checking integrity... &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;3 conflicting&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; - mariadb118-client-11.8.5 &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;FreeBSD] conflicts with mariadb106-client-10.6.24 &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;installed] on /usr/local/bin/mariadb - mariadb118-client-11.8.5 &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;FreeBSD] conflicts with mariadb106-server-10.6.24 &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;installed] on /usr/local/bin/mariadb-dumpslow - mariadb118-server-11.8.5 &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;FreeBSD] conflicts with mariadb106-server-10.6.24 &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;installed] on /usr/local/bin/aria_chk Checking integrity... &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0 conflicting&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; Conflicts with the existing packages have been found. One more solver iteration is needed to resolve them. The following 5 package&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;s&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; will be affected &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;of 0 checked&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;: New packages to be INSTALLED: libfmt: 12.1.0 &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;FreeBSD] mariadb118-client: 11.8.5 &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;FreeBSD] mariadb118-server: 11.8.5 &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;FreeBSD] Installed packages to be REMOVED: mariadb106-client: 10.6.24 mariadb106-server: 10.6.24 Number of packages to be removed: 2 Number of packages to be installed: 3 The process will require 52 MiB more space. Proceed with this action? &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;y/N]: &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Start the MariaDB service:&lt;/p&gt; &lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;service mysql-server start &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Run the post-install upgrade:&lt;/p&gt; &lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mariadb-upgrade &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Inspect your sites and applications to validate the systems are working as intended!&lt;/p&gt; </description><pubDate>Tue, 03 Feb 2026 00:00:00 +0000</pubDate><link>https://www.gaelanlloyd.com/blog/upgrade-mariadb-106-to-118/</link><guid isPermaLink="true">https://www.gaelanlloyd.com/blog/upgrade-mariadb-106-to-118/</guid><category>database</category><category>freebsd</category><category>how-to</category></item><item><title>Fix weird lag issues with Intel graphics on FreeBSD 14.3 with i915 driver</title><description>&lt;p&gt;I noticed that my laptop’s minimal Xorg environment was suffering from a series of strange lag issues:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Typing delays: Typed characters would show up only after other characters were typed.&lt;/li&gt; &lt;li&gt;Screen refresh delays: &lt;ul&gt; &lt;li&gt;Instead of showing immediately on hover, Openbox’s main menu submenus would only show after the cursor was moved around inside the parent item’s menu a bit.&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vim&lt;/code&gt; error messages like “No write since last change” wouldn’t show up until additional keys were pressed after &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:q&amp;lt;enter&amp;gt;&lt;/code&gt;. The computer sat motionless for more than 10 seconds and then only showed the error message after I pressed an arrow key!&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;These issues happened in MATE and Openbox, and even plagued basic &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tty&lt;/code&gt; consoles. It didn’t make sense, because the system was not under load and is utterly minimal (just plain-old Xorg running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lightdm&lt;/code&gt; and Openbox with no other programs running). It’s a a Dell E7470 with an i5-6200U Skylake CPU with Intel HD Graphics 520… plenty of power for this DE.&lt;/p&gt; &lt;p&gt;The lag went away when booted into single-user mode, or if I disabled &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;i915&lt;/code&gt; and had Xorg use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scfb&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;After some hunting around, turns out that PSR (Panel Self Refresh) was to blame. Disabling PSR fixed the lag issues immediately. But, finding out the exact &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sysctl&lt;/code&gt; settings was tricky because they have to go under the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;compat.linuxkpi&lt;/code&gt; section, and they have a unique naming convention that wasn’t immediately apparent from the documentation I could find online.&lt;/p&gt; &lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt; &lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/boot/loader.conf&lt;/code&gt;, set:&lt;/p&gt; &lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Disable PSR (panel self-refresh)&lt;/span&gt; compat.linuxkpi.i915_enable_psr&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;0&quot;&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Reboot and check things out. That fixed it for me!&lt;/p&gt; &lt;h2 id=&quot;whats-psr&quot;&gt;What’s PSR?&lt;/h2&gt; &lt;p&gt;PSR is a power-saving feature that helps put the GPU into a low-power state if it believes nothing on-screen has changed and, thus, the screen doesn’t need a redraw. Apparently, the PSR implementation from Skylake-era systems was immature in hardware and in software, leading to buggy issues like this.&lt;/p&gt; &lt;p&gt;This setting to disable PSR is likely provided by some distros or pre-built environments… but since I’m rolling my own barebones Xorg environment, I had to learn about the setting the hard way!&lt;/p&gt; </description><pubDate>Thu, 13 Nov 2025 00:00:00 +0000</pubDate><link>https://www.gaelanlloyd.com/blog/fix-freebsd-lag-issues-intel-i915/</link><guid isPermaLink="true">https://www.gaelanlloyd.com/blog/fix-freebsd-lag-issues-intel-i915/</guid><category>freebsd</category><category>how-to</category></item><item><title>How to rebuild a ZFS pool</title><description>&lt;p&gt;I recently discovered that my server’s storage pool had much more capacity than it should, because I created a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stripe&lt;/code&gt; pool across two disks instead of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mirror&lt;/code&gt; as I had originally intended.&lt;/p&gt; &lt;p&gt;Unfortunately, the only way to correct this is to destroy the pool and start over. With ZFS, it’s actually not as painful as it sounds, and with ZFS’s built-in checksumming and scrubbing features you can help ensure that your data transfers without a hitch.&lt;/p&gt; &lt;p&gt;I’ll admit, I’ve had to rebuild or recreate pools a few times now for various reasons. It almost feels like a right of passage as a sysadmin.&lt;/p&gt; &lt;p&gt;Here’s some tips to help you do this process safely and thoroughly. Please consider this a starting point for your own project… there might be some areas where you need to adapt these steps to better suit your individual circumstances. And hey, this is meant to be a friendly guide of tips and suggestions from one storage geek to another. So, &lt;strong&gt;be careful&lt;/strong&gt;… and &lt;strong&gt;don’t sue me&lt;/strong&gt; if you nuke your pool!&lt;/p&gt; &lt;h2 id=&quot;what-youll-need&quot;&gt;What you’ll need&lt;/h2&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;A backup storage device with enough capacity to store the entire pool’s data.&lt;/strong&gt; In my case, I had two spare drives laying around that were the same capacity as my pool, so I made a new temporary ZFS &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mirror&lt;/code&gt; pool using them.&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;A tested, working UPS.&lt;/strong&gt; You’re already using one for this server, right?&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;(Optional) A second copy of your data.&lt;/strong&gt; I did an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rsync&lt;/code&gt; of all the actual files in my pool to &lt;em&gt;another&lt;/em&gt; external drive, for yet one more layer of redundancy in case something messed up. No such thing as being too careful here!&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;before-you-begin&quot;&gt;Before you begin&lt;/h2&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;I assume you have intermediate knowledge of ZFS.&lt;/strong&gt; This guide doesn’t cover the deeper nuances of ZFS exhaustively. For instance, I assume you understand what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tank&lt;/code&gt; is. I’ll do my best to walk you through the process below, but before you begin you might want to consider reading &lt;a href=&quot;https://www.tiltedwindmillpress.com/product/fmzfs/&quot;&gt;FreeBSD Mastery: ZFS&lt;/a&gt; by Michael W Lucas, or at a bare minimum the ZFS section of the &lt;a href=&quot;https://docs.freebsd.org/en/books/handbook/zfs/&quot;&gt;FreeBSD Handbook&lt;/a&gt;.&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;Decide on how many snapshots you want to keep.&lt;/strong&gt; I decided to not save all of my pool’s daily snapshots, and instead transfer a single point-in-time migration snapshot. Decide if you want to retain your daily snapshots or if this single snapshot is sufficient.&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;The transfer will take lots of time.&lt;/strong&gt; It took about 5½ hours to transfer about 3.1 TB of data out of the pool, and another 5½ hours to transfer it back. Make sure that when you do this process you aren’t rushed and have ample time to do the job slowly and carefully.&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;some-additional-thoughts&quot;&gt;Some additional thoughts&lt;/h2&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;ECC RAM helped give me confidence in the transfer job.&lt;/strong&gt; During the transfer, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;htop&lt;/code&gt; reported that the anonymous ARC cache was being used heavily to shuttle data around. I could hear the drive activity and confirm that there was a lot of read pre-fetching going on, and that when the copy commands ended the ARC took a few moments to “empty out” before the target drive stopped working. I know there’s a lot of debate out there about ECC RAM not being necessary for ZFS, but in this case with so much data flying back and forth through RAM, having ECC helped reassure me that that random bit flips wouldn’t happen and introduce issues. (&lt;em&gt;I’m sure a ZFS expert could weigh in here and reassure me more about checksum validations that might happen during data transport… If you are one, please contact me!&lt;/em&gt;)&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;I avoided using USB drives and went direct SATA → SATA for speed and stability.&lt;/strong&gt; I suppose there’s nothing wrong with using an external USB drive if you must, but if this is your primary storage pool then at least plug that drive into a UPS! Your goal is to minimize external issues and safeguard your data during this delicate process.&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;This pool only had datasets, but volumes work basically the same way.&lt;/strong&gt; If you need to export data from volumes, the process is mostly the same. The instructions below focus on datasets only, but you could easily adapt this process to include volumes.&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;how-to-rebuild-a-zfs-pool&quot;&gt;How to rebuild a ZFS pool&lt;/h2&gt; &lt;h3 id=&quot;stop-services-and-cronjobs&quot;&gt;Stop services and cronjobs&lt;/h3&gt; &lt;p&gt;First, stop anything running on the server that might allow access to the source pool. This includes file shares, FTP servers, external SSH connections by people other than you, etc.&lt;/p&gt; &lt;p&gt;Also, consider stopping any cronjobs on the system. Since this process may take several hours, you don’t want your nightly backup job or a weekly scrub kicking off in the middle of things.&lt;/p&gt; &lt;h3 id=&quot;collect-data&quot;&gt;Collect data&lt;/h3&gt; &lt;p&gt;Let’s collect some data that will be handy to have as we work. Save the results of these commands somewhere.&lt;/p&gt; &lt;p&gt;Start out by exporting a list of all datasets, their sizes, and mountpoints.&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;zfs list &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; tank &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Export a list of just the dataset names to help build scripts to go over all the datasets. Also useful to have if you enjoy making checklists!&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;zfs list &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; name &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; tank &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Save the result of these commands too. They’ll help provide context for the disk layout and mountpoint names, permissions, etc.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lsblk&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ls -lha&lt;/code&gt; of the pool root directory&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zpool status tank&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Properties for the pool root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zfs get all tank&lt;/code&gt; and all datasets (run this command for each dataset: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zfs get all tank/dataset&lt;/code&gt;). Pipe that output to text files and keep that info for reference.&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&quot;set-up-the-new-temporary-storage&quot;&gt;Set up the new temporary storage&lt;/h3&gt; &lt;p&gt;Making another ZFS storage pool and dataset is best because the ZFS exports we’ll do below will almost certainly exceed the maximum filesize of many filesystems.&lt;/p&gt; &lt;p&gt;When building the new temporary storage pool, be sure to label the disks with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpart&lt;/code&gt; labels so that you can be absolutely sure about which disks you’re addressing. (&lt;strong&gt;DO NOT&lt;/strong&gt; rely on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/&lt;/code&gt; labels like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ada0&lt;/code&gt; to address your disks, as those labels may change across reboots.)&lt;/p&gt; &lt;h3 id=&quot;create-snapshots&quot;&gt;Create snapshots&lt;/h3&gt; &lt;p&gt;Now it’s time to create snapshots of the datasets to capture the data as it exists at this point in time. For each dataset, run:&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;zfs snapshot tank/dataset@YYYY-MM-DD-MIGRATION &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;You can adapt that snapshot naming scheme to something meaningful to you. I like using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;YYYY-MM-DD&lt;/code&gt; format because it sorts nicely, and then use a name in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALL CAPS&lt;/code&gt; so this important snapshot stands out when you’re looking through a list of hundreds of snapshots.&lt;/p&gt; &lt;h3 id=&quot;transfer-snapshots-to-the-temporary-storage&quot;&gt;Transfer snapshots to the temporary storage&lt;/h3&gt; &lt;div class=&quot;alert alert-info&quot;&gt; &lt;p&gt;&lt;strong&gt;PRO TIP:&lt;/strong&gt; Are you SSH&apos;ing in to the box you&apos;re working on? If so, run the rest of these commands in a &lt;code&gt;tmux&lt;/code&gt; session. This keeps the remote session running even if your network connection is interrupted.&lt;/p&gt; &lt;/div&gt; &lt;p&gt;Each snapshot now needs to be sent to the temporary storage drive. For each dataset, you’ll want to run:&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;zfs send tank/dataset@YYYY-MM-DD-MIGRATION &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /mnt/temporary/dataset.zfs &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;As this process may take hours to run, consider batching up all of those commands in a shell script. This will allow you to leave the house, go to sleep, etc. while the job runs dutifully in the background.&lt;/p&gt; &lt;h3 id=&quot;verify-everything-was-copied&quot;&gt;Verify everything was copied&lt;/h3&gt; &lt;p&gt;Use the list of datasets you wrote down earlier in the first steps of this process. Did you transfer all of them to the temporary storage location? Did you see any errors during the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zfs send&lt;/code&gt; jobs? Do you want to send a second copy of the data somewhere, just in case? You could &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrub&lt;/code&gt; the temporary location if you really want, but that’s probably overkill…&lt;/p&gt; &lt;p&gt;The point is, take your time here. Double- and triple-check everything, because we’re about to destroy the source pool. There’s no going back after that!&lt;/p&gt; &lt;h3 id=&quot;destroy-and-rebuild-the-pool&quot;&gt;Destroy and rebuild the pool&lt;/h3&gt; &lt;p&gt;First, you’ll want to export the source pool, to tell ZFS to stop using it.&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;zpool &lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;tank &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;If that command gives you any guff about the pool being in use, make sure you’re not in a working directory inside the pool, that all services that might be using the pool are stopped, etc. Sometimes also the pool just takes a minute to free up before it can be exported, so try again after a minute.&lt;/p&gt; &lt;p&gt;If you’ve passed entire disks to ZFS, you’ll need to clear their labels to dissociate the disks from the pool.&lt;/p&gt; &lt;p&gt;Run this command on each drive, &lt;strong&gt;being very careful&lt;/strong&gt; to supply the proper &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/&lt;/code&gt; names (or, try using GPT labels, etc.)&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;zpool labelclear /dev/x zpool labelclear /dev/y &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Once the labels are cleared from the drives, you’re ready to create the new pool. Follow the steps appropriate to the pool setup you want.&lt;/p&gt; &lt;p&gt;In my case, I wanted a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mirror&lt;/code&gt; of two disks, so I ran:&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;zpool create tank mirror /dev/label/x /dev/label/y &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Before you proceed, check the status of the newly-created pool.&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;zpool status tank &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Were you trying to make a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mirror&lt;/code&gt;? Be sure right now, right this very instant, that the output looks like this:&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pool: tank state: ONLINE config: NAME STATE READ WRITE CKSUM tank ONLINE 0 0 0 mirror-0 ONLINE 0 0 0 &amp;lt;&lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; ada0 ONLINE 0 0 0 ada1 ONLINE 0 0 0 &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Make sure that line marked with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;--&lt;/code&gt; arrow is there, otherwise you have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stripe&lt;/code&gt;! This is what bit me earlier… it’s very easy to miss.&lt;/p&gt; &lt;p&gt;After doing a sanity check on the pool architecture, set up any pool-specific settings, like: mountpoint, ACL settings, and other ZFS properties you want to configure at the pool level.&lt;/p&gt; &lt;h3 id=&quot;transfer-snapshots-from-the-temporary-storage&quot;&gt;Transfer snapshots from the temporary storage&lt;/h3&gt; &lt;p&gt;Now it’s time to repopulate the new pool with the snapshots you sent to the temporary storage. You’ll need to run this command for each dataset, so consider making a shell script, and make sure you’re executing it from within &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tmux&lt;/code&gt; if you’re working remotely.&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; /mnt/temporary/dataset.zfs | zfs receive tank/dataset &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Once all the data is transferred, you’ll want to check and set:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Dataset properties&lt;/li&gt; &lt;li&gt;Mountpoints&lt;/li&gt; &lt;li&gt;Owners, groups, and permissions&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;You can use the information exported in the first few steps to ensure everything is set up as you had it.&lt;/p&gt; &lt;h3 id=&quot;verify-the-pool-data&quot;&gt;Verify the pool data&lt;/h3&gt; &lt;p&gt;At this point, I ran a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrub&lt;/code&gt; on the new pool to make sure everything was in order. Scrubbing my pool took 4½ hours, so this is another opportunity to let the system run unattended until it’s done.&lt;/p&gt; &lt;h3 id=&quot;re-start-services-cronjobs-and-the-server&quot;&gt;Re-start services, cronjobs, and the server&lt;/h3&gt; &lt;p&gt;Make sure you restart any services you shut off and any cronjobs you commented out earlier.&lt;/p&gt; &lt;p&gt;Once that’s done, give the system a reboot. It’s not absolutely necessary, but it helps validate that you have everything in order so you can prove that the next boot will succeed.&lt;/p&gt; &lt;p&gt;Remove the temporary storage drives and hang on to them for a bit until you are sure everything’s working.&lt;/p&gt; &lt;p&gt;All that’s left is to test your system, make sure the various services are working, and then you’re done – one more pool rebuild is now under your belt!&lt;/p&gt; </description><pubDate>Thu, 09 Jan 2025 00:00:00 +0000</pubDate><link>https://www.gaelanlloyd.com/blog/how-to-rebuild-a-zfs-pool/</link><guid isPermaLink="true">https://www.gaelanlloyd.com/blog/how-to-rebuild-a-zfs-pool/</guid><category>how-to</category><category>storage</category><category>zfs</category></item><item><title>Announcing Bildaro, a self-hosted photo gallery</title><description>&lt;p&gt;It’s been a busy past couple of days! An idea I had over the weekend spawned a new little project that I’m eager to share.&lt;/p&gt; &lt;p&gt;I’m excited to announce &lt;a href=&quot;https://github.com/gaelanlloyd/bildaro&quot;&gt;Bildaro&lt;/a&gt;: A new self-hosted, minimalist, teeny tiny, mobile-friendly photo gallery and image processor and uploader.&lt;/p&gt; &lt;p&gt;&lt;img src=&quot;/assets/images/posts/2024-11-11/screenshot-01.webp&quot; alt=&quot;Bildaro, a static site photo gallery written in Jekyll&quot; class=&quot;border-dark&quot; width=&quot;100%&quot; style=&quot;max-width: 100%; height: auto; &quot; /&gt;&lt;/p&gt; &lt;p&gt;Bildaro is a small-footprint Jekyll blog that, when combined with AWS S3 and AWS Amplify, provides a simple, performant, serverless way to self-host your photo collection at a very low cost.&lt;/p&gt; &lt;p&gt;You retain control over the platform and your content, and your site visitors get a fast, streamlined, simple experience.&lt;/p&gt; &lt;p&gt;The small codebase is easy to understand and welcomes tinkering so you can adjust it to your needs:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;30 lines of JS to power the image lightbox modal.&lt;/li&gt; &lt;li&gt;260 lines of CSS (Sass) that Jekyll will compile for you on build.&lt;/li&gt; &lt;li&gt;170 lines of code for the image processor and file uploader, written as a simple Bash script with no dependencies other than the tools it uses (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;imagemagick&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aws cli&lt;/code&gt; via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip&lt;/code&gt;).&lt;/li&gt; &lt;li&gt;Other dependencies are minimal and are only needed if you want to run Jekyll locally (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ruby&lt;/code&gt; and a few small Ruby gems).&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;And just to reiterate: The numbers above are not &lt;em&gt;KB&lt;/em&gt; of code, they’re individual &lt;em&gt;lines&lt;/em&gt; of code. Bildaro is &lt;em&gt;smol&lt;/em&gt;.&lt;/p&gt; &lt;h2 id=&quot;why-a-self-hosted-serverless-photo-gallery&quot;&gt;Why a self-hosted serverless photo gallery?&lt;/h2&gt; &lt;p&gt;Running your own web server requires expertise, takes considerable effort to get running, and requires an investment in long-term maintenance.&lt;/p&gt; &lt;p&gt;Many popular self-hosted solutions, like my previous all-time favorite &lt;a href=&quot;https://lycheeorg.github.io/&quot;&gt;Lychee&lt;/a&gt;, have grown into behemoths that require all sorts of dependencies just for a baseline install. And since they run on a live system exposed to the internet, you’ll have to be vigilant and keep current with updates to keep your system secure.&lt;/p&gt; &lt;p&gt;A static website: doesn’t require any maintenance, is practically immune to almost all of the typical vulnerability channels websites face because it doesn’t have any “moving parts,” and can be served quickly and at massive scale for very little money.&lt;/p&gt; &lt;h2 id=&quot;why-did-i-make-bildaro&quot;&gt;Why did I make Bildaro?&lt;/h2&gt; &lt;p&gt;I’ve hosted a photo gallery in the past, but I was tired of maintaining it. I took it offline for a while to see if Dropbox would be sufficient. Turns out Dropbox is “just fine,” but it’s not an ideal experience. The web interface is heavy and bulky, full of log in dialogs, cookie notices, upsells, and now all this AI bullshit. It left me wanting to self-host again.&lt;/p&gt; &lt;p&gt;This past weekend I spun up a new web server to host Lychee, and it quickly became apparent that the project had just gotten really big for my rather basic needs. Composer, Laravel, Artisan, code linters, local development tools, CSS frameworks, Vue, Typescript, Font Awesome, gesture libraries… All in all about 400 MB of dependencies to download just to get started.&lt;/p&gt; &lt;p&gt;On top of that, don’t forget that you have the complexity of the baseline OS install to support the software stack in the first place. You’ll need Apache/nginx/lighttpd, PHP, and a database engine. Do you want MariaDB or SQLite? You have to keep all of these things updated. And backed up. Then there’s SSH, ZeroTier, Certbot and SSL certs and renewals, software dependencies…&lt;/p&gt; &lt;p&gt;That’s a lot of moving parts for something that feels so simple.&lt;/p&gt; &lt;p&gt;It got me thinking that there’s gotta be a better way.&lt;/p&gt; &lt;p&gt;My brain started turning:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;My photo gallery website doesn’t change, except when I add or remove photos.&lt;/li&gt; &lt;li&gt;So if it rarely changes, why do I need a dynamically-generated, database-backed, full-blown web server running with dedicated CPU and RAM just to host a basic static site?&lt;/li&gt; &lt;li&gt;Could this be a static website in disguise? Should it have been one this entire time?&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;jekyll-to-the-rescue&quot;&gt;Jekyll to the rescue&lt;/h2&gt; &lt;p&gt;Jekyll feels like an old friend. I’ve used the platform for years to create this blog, and other websites for business and hobbies. It’s a platform I know and trust. It’s performant and has powerful and useful features. It does a thing with focus and it does it well. And… it took on absolute superpowers when served with AWS Amplify.&lt;/p&gt; &lt;p&gt;As soon as I realized the photo gallery site could be static, the flood happened. Everything fell into place.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;The photo albums can just be posts, with YAML data storing all of the images in the album along with some basic metadata.&lt;/li&gt; &lt;li&gt;The images can live on AWS S3, where they can take up as much space as I need, and it’ll cost next-to-nothing. They can be decoupled from the blog, so I don’t have to worry about working with the images in the repo.&lt;/li&gt; &lt;li&gt;AWS Amplify can host the Jekyll site in a serverless fashion, like I do with this site. And it’ll be served from a CDN edge, with more performance than I could ever get out of the web server. (Frankly, more performance than I’ll ever need for my simple site.) And best of all, I won’t have to manage a single thing.&lt;/li&gt; &lt;li&gt;I can write a simple Bash script to convert the source JPGs into web-optimized &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webp&lt;/code&gt; thumbnails and the display size variants, create the album &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zip&lt;/code&gt; download file for people that want it, have it export an image manifest, and even upload all the files to S3 for me.&lt;/li&gt; &lt;li&gt;Changes happen as repo commits and can trigger build events, updating the gallery site.&lt;/li&gt; &lt;li&gt;The site can &lt;em&gt;just fucking sit there&lt;/em&gt; and serve and serve and serve for pennies until I’m ready to change it. Like how this blog does. No moving parts. No worrying about software updates, Certbot renewal triggers, or anything that comes with running your own server. Ever.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;So, I got to work!&lt;/p&gt; &lt;h2 id=&quot;bonvenon-bildaro&quot;&gt;Bonvenon, Bildaro!&lt;/h2&gt; &lt;p&gt;I wrote Bildaro in the course of two days. Two good “sit down and focus” sessions is all it took. It all came together very quickly, because the premise is so simple: Bildaro is basically a Jekyll blog, nothing more.&lt;/p&gt; &lt;p&gt;Albums are simply posts, and they contain a YAML list of the photos in the album. The view page simply iterates over those images and creates the photo gallery output. The image URLs get prepended with the S3 bucket URL. And even better, this file is generated from the Bash script. All you need to do is edit the title and the cover image, commit the changes, and Amplify takes care of the rest.&lt;/p&gt; &lt;p&gt;Example album &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_posts/2023-09-17-satsop-nuclear-plant.md&lt;/code&gt;&lt;/p&gt; &lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Satsop Nuclear Plant&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;cover&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DSC07949.jpg&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;download_size&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;4264&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;pictures&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DSC07949.jpg&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DSC08116.jpg&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DSC08264.jpg&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DSC08378.jpg&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DSC08392.jpg&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DSC08403.jpg&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DSC08426.jpg&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DSC08429.jpg&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Since the page is static and written from scratch, it’s lightweight and performant. Check out these Pagespeed scores and see just how lightweight the gallery homepage is:&lt;/p&gt; &lt;p&gt;&lt;img src=&quot;/assets/images/posts/2024-11-11/pagespeed-mobile.webp&quot; alt=&quot;Tens across the board!&quot; class=&quot;border-dark&quot; width=&quot;100%&quot; style=&quot;max-width: 100%; height: auto; &quot; /&gt;&lt;/p&gt; &lt;p&gt;I’m sure there are other Jekyll photo galleries out there, but it felt so rewarding to write one for myself, from scratch. Tuned to do exactly what I need it to do, and nothing more.&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;“This is my gallery. There are many like it, but this one is mine.”&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;Bildaro is open source, released under the permissive MIT license. It’s given me great pleasure to write this little thing. You’re welcome to take it for a spin!&lt;/p&gt; &lt;p&gt;&lt;a href=&quot;https://photos.gaelanlloyd.com&quot;&gt;Take a look at my photo gallery&lt;/a&gt; to see it in action.&lt;/p&gt; </description><pubDate>Mon, 11 Nov 2024 00:00:00 +0000</pubDate><link>https://www.gaelanlloyd.com/blog/bildaro-self-hosted-photo-gallery/</link><guid isPermaLink="true">https://www.gaelanlloyd.com/blog/bildaro-self-hosted-photo-gallery/</guid><category>photography</category><category>self-hosted</category><category>software</category><category>jekyll</category></item><item><title>Fix macOS Finder problems with Samba file shares</title><description>&lt;p&gt;Using MacOS’s Finder to work with large file share directories on my FreeBSD Samba server was sometimes giving me very strange, erratic behavior. It felt like the server had amnesia or was having hallucinations.&lt;/p&gt; &lt;p&gt;Symptoms included:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Directory contents would take a very long time to load.&lt;/li&gt; &lt;li&gt;Directory contents would disappear after browsing between sibling folders.&lt;/li&gt; &lt;li&gt;Directories would show the contents of &lt;em&gt;different&lt;/em&gt; sibling folders.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;The problem was really annoying and took me quite a while to figure out.&lt;/p&gt; &lt;p&gt;At first, I thought it was an issue with Finder. However, third-party file browsers like &lt;em&gt;Commander One&lt;/em&gt; and &lt;em&gt;Marta&lt;/em&gt; showed the same symptoms.&lt;/p&gt; &lt;p&gt;Since I built the file server myself, I assumed it must be a problem I inadvertently caused. However, Windows, Linux, Android, nor other FreeBSD clients didn’t suffer from these issues. Also, protocols other than SMB were not affected (FTP and SSH were working just fine).&lt;/p&gt; &lt;p&gt;Some articles online suggested that Samba itself was to blame, or that MacOS’s implementation of the Samba client was buggy. I even considered switching away from Samba to some other protocol, but that wouldn’t be ideal.&lt;/p&gt; &lt;p&gt;After doing a lot of reading and testing, I learned that Finder aggressively caches directory listings, and that cache system can be unreliable with large fileshares over Samba. And I also learned that out-of-the-box Samba configurations need to be tweaked slightly to improve connections for MacOS clients.&lt;/p&gt; &lt;p&gt;The fixes below were scoured from many places on the web, including Reddit posts, blog posts, Github repos, and a deep dive of the Samba configuration documentation. It took me a few months to track down the various configuration combinations needed, but now everything’s been working properly.&lt;/p&gt; &lt;h2 id=&quot;server-changes&quot;&gt;Server changes&lt;/h2&gt; &lt;p&gt;On the server, make the following changes to your distribution’s Samba config file. On FreeBSD, the file is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local/etc/smb4.conf&lt;/code&gt;:&lt;/p&gt; &lt;div class=&quot;language-conf highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[&lt;span class=&quot;n&quot;&gt;global&lt;/span&gt;] &lt;span class=&quot;c&quot;&gt;# Enforce a minimum of Samba v2 (Vista / Server 2008) for client connections &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;min&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;protocol&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;SMB2&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;server&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;min&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;protocol&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;SMB2&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# Performance improvements &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sendfile&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ea&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;support&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt; = &lt;span class=&quot;m&quot;&gt;2500&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# Improve compatibility with Macs &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vfs&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;catia&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fruit&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;streams_xattr&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fruit&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;aapl&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fruit&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;nfs_aces&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;no&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fruit&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;zero_file_id&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fruit&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;metadata&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fruit&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;resource&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fruit&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;encoding&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;native&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spotlight&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;backend&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;tracker&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;readdir_attr&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;aapl_rsize&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;no&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;readdir_attr&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;aapl_finder_info&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;no&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;readdir_attr&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;aapl_max_access&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;no&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fruit&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;MacSamba&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fruit&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;posix_rename&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fruit&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;veto_appledouble&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;no&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fruit&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;wipe_intentionally_left_blank_rfork&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fruit&lt;/span&gt;:&lt;span class=&quot;n&quot;&gt;delete_empty_adfiles&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# 2026: No longer recommended as this causes Finder error -8062. # veto files = /.DS_Store/.Trashes/ # delete veto files = yes &lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# (OPTIONAL) If this server isn&apos;t sharing printers, # disable printer support to speed things up. &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;load&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;printers&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;no&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;printing&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;bsd&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;printcap&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; = /&lt;span class=&quot;n&quot;&gt;dev&lt;/span&gt;/&lt;span class=&quot;n&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# NOTE: &apos;spoolss&apos; is not a typo. &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;disable&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spoolss&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Spotlight searching of Samba shares is disabled by default, but you can add this line to each of your Samba share definitions in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;smb.conf&lt;/code&gt; to be absolutely sure:&lt;/p&gt; &lt;div class=&quot;language-conf highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[&lt;span class=&quot;n&quot;&gt;yourshare&lt;/span&gt;] &lt;span class=&quot;n&quot;&gt;spotlight&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;no&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h2 id=&quot;client-changes&quot;&gt;Client changes&lt;/h2&gt; &lt;p&gt;On the MacOS client computers, create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/nsmb.conf&lt;/code&gt;:&lt;/p&gt; &lt;div class=&quot;language-conf highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[&lt;span class=&quot;n&quot;&gt;default&lt;/span&gt;] &lt;span class=&quot;c&quot;&gt;# Disable SMB v1 &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;protocol_vers_map&lt;/span&gt;=&lt;span class=&quot;m&quot;&gt;6&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# Disable NetBIOS &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;port445&lt;/span&gt;=&lt;span class=&quot;n&quot;&gt;no_netbios&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# Use NTFS streams if supported &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;streams&lt;/span&gt;=&lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# Disable directory caching &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dir_cache_max_cnt&lt;/span&gt;=&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dir_cache_max&lt;/span&gt;=&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dir_cache_off&lt;/span&gt;=&lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# Disable packet signing &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;signing_required&lt;/span&gt;=&lt;span class=&quot;n&quot;&gt;no&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# Disable multi-channel connections and prioritize the wired ethernet connection &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mc_prefer_wired&lt;/span&gt;=&lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mc_on&lt;/span&gt;=&lt;span class=&quot;n&quot;&gt;no&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# Disable SMB session signing &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;validate_neg_off&lt;/span&gt;=&lt;span class=&quot;n&quot;&gt;yes&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h2 id=&quot;applying-the-changes&quot;&gt;Applying the changes&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;Restart the Samba service on the server (FreeBSD: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;service samba_server restart&lt;/code&gt;)&lt;/li&gt; &lt;li&gt;Restart each client computer&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;When you reconnect to the server shares on your machines, these issues shouldn’t occur anymore.&lt;/p&gt; &lt;h2 id=&quot;update-2026-04-28-note-on-prior-veto-files-configuration-directives&quot;&gt;Update 2026-04-28: Note on prior &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veto files&lt;/code&gt; configuration directives&lt;/h2&gt; &lt;p&gt;Initially, I had configured Samba to prevent macOS clients from writing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.DS_Store&lt;/code&gt; and other cruft to server shares to keep things clean and tidy. However, as of March 2026, I started encountering errors preventing folder copy operations from succeeding. See the solution below under &lt;em&gt;Troubleshooting&lt;/em&gt;.&lt;/p&gt; &lt;p&gt;As this is a battle that I appear to have lost, and I can’t prevent the files from being written without causing errors, I’m considering writing a small cron job to purge all cruft files from my server shares on a regular basis.&lt;/p&gt; &lt;h2 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h2&gt; &lt;dl&gt; &lt;dt&gt;When copying files from a macOS computer to the server, Finder reports the error “The operation can’t be completed because an unexpected error occurred (error code -8062).”&lt;/dt&gt; &lt;dd&gt;Finder reports this error when it isn’t able to write the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.DS_Store&lt;/code&gt; files to the remote location. Disable the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veto files&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete veto files&lt;/code&gt; configuration lines, restart Samba, and try again.&lt;/dd&gt; &lt;/dl&gt; </description><pubDate>Sat, 03 Aug 2024 00:00:00 +0000</pubDate><link>https://www.gaelanlloyd.com/blog/fix-mac-os-finder-samba-problems/</link><guid isPermaLink="true">https://www.gaelanlloyd.com/blog/fix-mac-os-finder-samba-problems/</guid><category>annoyances</category><category>mac</category><category>samba</category><category>how-to</category></item></channel></rss>