<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>dKade's notes</title><link>https://dkade.com/</link><description>Recent content on dKade's notes</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>dkade@dkade.com (Daniel Loureiro)</managingEditor><webMaster>dkade@dkade.com (Daniel Loureiro)</webMaster><lastBuildDate>Fri, 21 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://dkade.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Removing a Lenovo ThinkPad T540p Supervisor Password with a CH341A Programmer</title><link>https://dkade.com/posts/lenovo_t540p_bios_unlock/</link><pubDate>Fri, 21 Aug 2026 00:00:00 +0000</pubDate><author>dkade@dkade.com (Daniel Loureiro)</author><guid>https://dkade.com/posts/lenovo_t540p_bios_unlock/</guid><description>A complete walkthrough of dumping, patching and re-signing a Lenovo T540p BIOS with a CH341A clip programmer to remove a &amp;#34;forgotten&amp;#34; supervisor password — including the Insyde H2O RSA signature pitfall</description><content:encoded><![CDATA[<p>My ThinkPad T540p ended up with a supervisor (BIOS) password after a Lenovo BIOS update&hellip; The machine still powered on, but the BIOS setup was locked and the hard drive was SVP-locked. Rather than replacing the mainboard or sending it to a repair shop, I and AI removed the password by dumping the BIOS flash chip, patching it, and re-flashing — using a cheap CH341A USB programmer and a SOIC-8 test clip.</p>
<p>I was following the process from <a href="https://www.badcaps.net/forum/troubleshooting-hardware-devices-and-electronics-theory/troubleshooting-laptops-tablets-and-mobile-devices/bios-requests-only/78215-lenovo-bios-auto-patcher-for-supervisor-password-removal">BADCAPS</a> but it turned out to be more involved than the usual &ldquo;run the auto-patcher and flash&rdquo; recipe, because this particular BIOS (GMET91WW, a 2021 build) enforces an <strong>RSA signature check</strong> on the patched image and refuses to boot otherwise. This post documents the full procedure that worked.</p>
<h2 id="the-hardware">The hardware</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Lenovo ThinkPad T540p
</span></span><span class="line"><span class="cl">└── BIOS flash: MX25L3273E  (4 MiB, SOIC-8 SPI NOR)
</span></span><span class="line"><span class="cl">        └── CH341A USB SPI programmer + SOIC-8 clip
</span></span></code></pre></div><p>The board connection CH341A and the SOIC-8 clip</p>
<p><img loading="lazy" src="/assets/board_connections.jpeg" type="" alt=""  /></p>
<p>The T540p is a Haswell-era machine, it carries an 8 MiB chip (MX25L6405 family) and the proper BIOS one 4 MiB MX25L3273E. Either way, the procedure is the same — you just need the right chip definition.</p>
<p>Chips on board, the BIOS is the right one</p>
<p><img loading="lazy" src="/assets/chips_board.jpeg" type="" alt=""  /></p>
<h2 id="the-plan">The plan</h2>
<ol>
<li>Dump the BIOS chip with <code>flashrom</code></li>
<li>Verify the dump (multiple reads, identical hashes)</li>
<li>Patch the dump with the Lenovo BIOS auto-patcher (injects two DXE drivers)</li>
<li><strong>Re-sign the modified BIOS</strong> so the machine actually boots it</li>
<li>Flash the signed image and trigger the unlock challenge</li>
<li>Restore the original BIOS</li>
</ol>
<p>Most guides stop at step 3 and 5. The signature step in between is what makes the difference on newer BIOS builds.</p>
<h2 id="dumping-the-chip">Dumping the chip</h2>
<p>A SOIC-8 clip sits on top of the chip without desoldering. The CH341A talks to it over SPI, and <code>flashrom</code> drives the whole thing:</p>
<p>Listing chips found</p>
<p><img loading="lazy" src="/assets/1_found_chips.png" type="" alt=""  /></p>
<p>Check if we identified the correct chip</p>
<p><img loading="lazy" src="/assets/2_check_chip.png" type="" alt=""  /></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sudo flashrom -c <span class="s2">&#34;MX25L3233F/MX25L3273E&#34;</span> -p ch341a_spi -r t540p_original.bin
</span></span></code></pre></div><p><img loading="lazy" src="/assets/3_dump_bios.png" type="" alt=""  /></p>
<p>The chip is small enough that the clip is finicky. Read it three times and confirm the hashes match before touching anything:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sudo flashrom -c <span class="s2">&#34;MX25L3233F/MX25L3273E&#34;</span> -p ch341a_spi -r t540p_original_b2.bin
</span></span><span class="line"><span class="cl">sudo flashrom -c <span class="s2">&#34;MX25L3233F/MX25L3273E&#34;</span> -p ch341a_spi -r t540p_original_b3.bin
</span></span><span class="line"><span class="cl">sha256sum t540p_original*.bin
</span></span></code></pre></div><p>That verified dump is the single recovery path if anything goes wrong later — keep it somewhere safe.</p>
<h2 id="patching-the-bios">Patching the BIOS</h2>
<p>The Lenovo auto-patcher from the Badcaps forum community (<code>lenovo-bios-autopatcher</code>) does the heavy lifting: it replaces two DXE drivers in the firmware image (<code>BootOption.ffs</code> and <code>LenovoTranslateService.ffs</code>). On boot, the modified translation service presents a password challenge instead of a hard lock, which lets you generate a one-time unlock code.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">cd</span> lenovo-bios-autopatcher
</span></span><span class="line"><span class="cl">./autopatch /path/to/t540p_original.bin
</span></span></code></pre></div><p>This produces <code>t540p_original_PATCHED.bin</code>. On its own, though, this image would not boot on my machine — it black-screened every time.</p>
<p><img loading="lazy" src="/assets/5_patch_bios.png" type="" alt=""  /></p>
<h2 id="the-black-screen-trap-insyde-h2o-rsa-signature">The black-screen trap: Insyde H2O RSA signature</h2>
<p>ThinkPads of this era use an Insyde H2O firmware. Starting around certain BIOS builds, the firmware carries an RSA-signed block called <code>TCPABIOS</code> that stores a SHA1 checksum for the main firmware volume. When the auto-patcher injects the two DXE drivers, the volume bytes change, the SHA1 no longer matches what <code>TCPABIOS</code> records, and the BIOS treats the image as tampered.</p>
<p>On older BIOS builds the machine just beeps five times and continues. On the newer GMET91WW build it <strong>hard-stops with a black screen</strong>.</p>
<p>This is where the AI just simplifies so much our life, if I had to do this manually it would have taken ages, lots of trial and error. So lets use AI for what it is really usefull I started this with a local model (Qwen3.6-35B-A3B) running on my Mac M1, it worked really well, but has there was a lot of trial and error even on AI side, I switched to deepseek-flash-v4 and it went fast and smooth.</p>
<p>The fix is to re-sign the modified image with a new RSA key and swap the corresponding public key into the image. The signature scheme is:</p>
<ul>
<li><code>TCPABIOS</code> block stores the volume checksum and is itself RSA-signed (<code>e = 3</code>, 1024-bit)</li>
<li>The signing input is <code>0x00 * 108 + SHA1(block_data)</code></li>
<li>The 128-byte signature follows a <code>FF FF 83</code> separator</li>
<li>The public key lives at a fixed offset in the same NVRAM region</li>
</ul>
<p>It wrote a small tool (<code>resign_bios.py</code>) that does all of this automatically:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">python3 resign_bios.py t540p_original_PATCHED.bin
</span></span></code></pre></div><p>This generates a fresh RSA keypair, updates the volume checksum in <code>TCPABIOS</code>, re-signs both <code>TCPABIOS</code> and <code>TCPABBLK</code>, and replaces the public key in the image. The output is <code>t540p_original_PATCHED_SIGNED.bin</code>, which boots cleanly.</p>
<h2 id="flashing-and-triggering-the-unlock">Flashing and triggering the unlock</h2>
<p>Flash the signed image:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sudo flashrom -c <span class="s2">&#34;MX25L3233F/MX25L3273E&#34;</span> -p ch341a_spi -w t540p_original_PATCHED_SIGNED.bin
</span></span></code></pre></div><p>Disconnect the programmer completely, then boot.</p>
<ol>
<li>At the Lenovo splash, press <strong>F1</strong> to enter BIOS setup</li>
<li>At the supervisor password prompt, type any character and press <strong>Enter</strong></li>
<li>Reboot, enter BIOS again, type the same character</li>
<li>The screen shows a <strong>Hardware ID</strong></li>
<li>Press <strong>Enter</strong>, then press <strong>Spacebar twice</strong></li>
</ol>
<p>On many guides, this is where an unlock code appears. On GMET91WW the machine instead reported <strong>&ldquo;TPM written&rdquo;</strong> — meaning the password was cleared directly without issuing a code. Either way, write down whatever the BIOS displays.</p>
<h2 id="restoring-the-original-bios">Restoring the original BIOS</h2>
<p>Once the unlock routine is done, flash the verified original dump back:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sudo flashrom -c <span class="s2">&#34;MX25L3233F/MX25L3273E&#34;</span> -p ch341a_spi -w t540p_original.bin
</span></span><span class="line"><span class="cl">sudo flashrom -c <span class="s2">&#34;MX25L3233F/MX25L3273E&#34;</span> -p ch341a_spi --verify t540p_original.bin
</span></span></code></pre></div><p>Then power on. If an unlock code was issued, enter BIOS and press <strong>F9</strong> to restore factory defaults, entering the code when prompted. On my unit the password was simply gone after the &ldquo;TPM written&rdquo; pass.</p>
<p><img loading="lazy" src="/assets/6_restore_original.png" type="" alt=""  /></p>
<h2 id="gotchas">Gotchas</h2>
<ul>
<li><strong>The clip is the weak link.</strong> Erase failures at a fixed sector almost always mean marginal contact on one of the address lines — reads still work, but writes fail. Reseat the clip firmly and clean the pads.</li>
<li><strong>Chip auto-detection lies.</strong> Running <code>flashrom</code> without <code>-c</code> sometimes reported an 8 MiB MX25L6405-family chip (a neighbouring part or marginal contact). Always pass <code>-c &quot;MX25L3233F/MX25L3273E&quot;</code> explicitly.</li>
<li><strong>The NVRAM volume patch is not needed here.</strong> The auto-patcher ships a 1 MiB NVRAM volume file; this BIOS uses a 408 KiB NVRAM, so that step correctly reports &ldquo;not found&rdquo;. Leave the NVRAM alone — the DXE injection plus re-sign is what does the work.</li>
<li><strong>Backup first.</strong> The original dump is the only way back. I kept three verified copies before patching.</li>
</ul>
<h2 id="summary">Summary</h2>
<p>Removing a forgotten supervisor password from a ThinkPad T540p is very doable with a CH341A programmer and a SOIC-8 clip.</p>
<p>The straightforward recipe is: dump → verify → auto-patch → flash → unlock → restore. On newer BIOS builds you additionally have to re-sign the patched image, because the Insyde H2O firmware verifies an RSA signature over the firmware volumes. Once I added that step, the machine booted, presented the unlock challenge, and the password was gone in one pass.</p>
<p>The whole procedure, including the re-signing tool and step-by-step workflow, is available in my <a href="https://codeberg.org/dkade/lenovo-bios-unlock"><code>lenovo-bios-unlock</code></a> repository.</p>
<hr>
<p><em>Disclaimer: This work was done purely for research and to recover from a problem caused by an official Lenovo BIOS update, on my own hardware. It is not intended to facilitate bypassing security on equipment you do not own, and it should not be used for any illegal purpose.</em></p>
<p><em>Disclaimer: I use AI as a productivity tool. For a senior engineer, AI is incredibly powerful as one can focus on the solution design and conceptualization and leave the boring part that is implementation to the AI.</em></p>
]]></content:encoded></item><item><title>Running NetBox in a FreeBSD Jail with BastilleBSD</title><link>https://dkade.com/posts/netbox_freebsd_jail/</link><pubDate>Wed, 12 Aug 2026 00:00:00 +0000</pubDate><author>dkade@dkade.com (Daniel Loureiro)</author><guid>https://dkade.com/posts/netbox_freebsd_jail/</guid><description>A complete guide to running NetBox natively inside a FreeBSD 15.1 VNET jail managed with BastilleBSD — no Docker and no Linux VM</description><content:encoded><![CDATA[<p>I wanted a simple source of truth for my home network: physical hosts, virtual machines, VLANs, prefixes, IP addresses, interfaces and switch connections. I was already keeping some of this information in NocoDB, but once the relationships between devices, interfaces and IPs become important, NetBox is a much better fit.</p>
<p>The official installation documentation targets Linux, but NetBox itself is a Python/Django application backed by PostgreSQL and Redis. With a few FreeBSD-specific adjustments it runs cleanly inside a jail.</p>
<h2 id="architecture">Architecture</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">FreeBSD 15.1-RELEASE host
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">└── netbox VNET jail
</span></span><span class="line"><span class="cl">    │
</span></span><span class="line"><span class="cl">    ├── nginx          :80
</span></span><span class="line"><span class="cl">    │    └── Gunicorn  127.0.0.1:8001
</span></span><span class="line"><span class="cl">    │         └── NetBox
</span></span><span class="line"><span class="cl">    │
</span></span><span class="line"><span class="cl">    ├── PostgreSQL 17
</span></span><span class="line"><span class="cl">    ├── Redis
</span></span><span class="line"><span class="cl">    └── NetBox RQ worker
</span></span></code></pre></div><p>Everything runs natively in one jail using normal FreeBSD services.</p>
<h2 id="why-a-jail">Why a jail?</h2>
<ul>
<li>Native FreeBSD deployment without Docker</li>
<li>No Linux VM just for NetBox</li>
<li>PostgreSQL, Redis and nginx come from normal FreeBSD packages</li>
<li>ZFS snapshots make rollback easy</li>
<li>VNET gives the jail its own LAN address</li>
<li>The application can be managed with standard service commands</li>
</ul>
<h2 id="creating-the-jail">Creating the jail</h2>
<p>Create a VNET jail on the FreeBSD host. Adjust the address and interface for your network:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">bastille create -V netbox 15.1-RELEASE 192.168.2.55/24 vtnet0
</span></span></code></pre></div><p>Before starting PostgreSQL, give the jail private System V IPC namespaces:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">bastille config netbox <span class="nb">set</span> sysvmsg new
</span></span><span class="line"><span class="cl">bastille config netbox <span class="nb">set</span> sysvsem new
</span></span><span class="line"><span class="cl">bastille config netbox <span class="nb">set</span> sysvshm new
</span></span><span class="line"><span class="cl">bastille restart netbox
</span></span></code></pre></div><p>Without these settings PostgreSQL initdb fails with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">FATAL: could not create shared memory segment: Function not implemented
</span></span><span class="line"><span class="cl">DETAIL: Failed system call was shmget(...)
</span></span></code></pre></div><p>Enter the jail:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">bastille console netbox
</span></span></code></pre></div><h2 id="installing-netbox">Installing NetBox</h2>
<p>The bootstrap script installs the full stack:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">fetch https://codeberg.org/dkade/BSD/raw/branch/main/FreeBSD/netbox/install_netbox_freebsd.sh
</span></span><span class="line"><span class="cl">chmod +x install_netbox_freebsd.sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">NETBOX_HOST</span><span class="o">=</span>netbox.home.lan <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nv">NETBOX_IP</span><span class="o">=</span>192.168.2.55 <span class="se">\
</span></span></span><span class="line"><span class="cl">./install_netbox_freebsd.sh
</span></span></code></pre></div><p>The current script is pinned to NetBox 4.6.1 and installs:</p>
<ul>
<li>Python 3.12</li>
<li>PostgreSQL 17</li>
<li>Redis</li>
<li>Gunicorn</li>
<li>nginx</li>
</ul>
<p>NetBox itself lives under:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/usr/local/netbox/
</span></span><span class="line"><span class="cl">├── src/
</span></span><span class="line"><span class="cl">├── venv/
</span></span><span class="line"><span class="cl">└── gunicorn.py
</span></span></code></pre></div><p>The installer creates native FreeBSD services for:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">netbox
</span></span><span class="line"><span class="cl">netbox_rq
</span></span></code></pre></div><p>and enables the standard services:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">postgresql
</span></span><span class="line"><span class="cl">redis
</span></span><span class="line"><span class="cl">nginx
</span></span></code></pre></div><h2 id="freebsd-service-handling">FreeBSD service handling</h2>
<p>This was the main FreeBSD-specific part.</p>
<p>A first implementation used:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">daemon
</span></span><span class="line"><span class="cl">└── su
</span></span><span class="line"><span class="cl">    └── gunicorn
</span></span></code></pre></div><p>It started successfully, but stop and restart could hang because the FreeBSD service framework was tracking a supervisor process instead of the actual Gunicorn master.</p>
<p>The working version lets Gunicorn daemonize itself and maintain:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/var/run/netbox/netbox.pid
</span></span></code></pre></div><p>The rc.d script then signals the real Gunicorn master directly.</p>
<p>The RQ worker uses the same principle and stores its real PID at:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/var/run/netbox/netbox-rq.pid
</span></span></code></pre></div><p>This gives normal FreeBSD behaviour:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">service netbox start
</span></span><span class="line"><span class="cl">service netbox stop
</span></span><span class="line"><span class="cl">service netbox restart
</span></span><span class="line"><span class="cl">service netbox status
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">service netbox_rq start
</span></span><span class="line"><span class="cl">service netbox_rq restart
</span></span><span class="line"><span class="cl">service netbox_rq status
</span></span></code></pre></div><h2 id="nginx">nginx</h2>
<p>nginx listens on port 80 and proxies to Gunicorn on localhost:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">client
</span></span><span class="line"><span class="cl">  |
</span></span><span class="line"><span class="cl">  v
</span></span><span class="line"><span class="cl">nginx :80
</span></span><span class="line"><span class="cl">  |
</span></span><span class="line"><span class="cl">  v
</span></span><span class="line"><span class="cl">Gunicorn 127.0.0.1:8001
</span></span><span class="line"><span class="cl">  |
</span></span><span class="line"><span class="cl">  v
</span></span><span class="line"><span class="cl">NetBox
</span></span></code></pre></div><p>The generated configuration lives at:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/usr/local/etc/nginx/conf.d/netbox.conf
</span></span></code></pre></div><h2 id="creating-the-administrator">Creating the administrator</h2>
<p>After installation:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">cd</span> /usr/local/netbox/src/netbox
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">su -m netbox -c <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s1">&#39;/usr/local/netbox/venv/bin/python manage.py createsuperuser&#39;</span>
</span></span></code></pre></div><p>Then open the hostname or jail IP in a browser.</p>
<h3 id="allowed_hosts-and-csrf">ALLOWED_HOSTS and CSRF</h3>
<p>Django validates both request hosts and POST origins.</p>
<p>The installer adds the configured hostname and optional jail IP to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">ALLOWED_HOSTS</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;netbox.home.lan&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;192.168.2.55&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;localhost&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;127.0.0.1&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">CSRF_TRUSTED_ORIGINS</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;http://netbox.home.lan&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;http://192.168.2.55&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></div><p>If the origin is missing, the UI can load normally but login fails with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Forbidden (403)
</span></span><span class="line"><span class="cl">CSRF verification failed. Request aborted.
</span></span></code></pre></div><p>If HTTPS is added later, add the matching <code>https://</code> origin as well.</p>
<h2 id="checking-the-stack">Checking the stack</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">service netbox status
</span></span><span class="line"><span class="cl">service netbox_rq status
</span></span><span class="line"><span class="cl">service postgresql status
</span></span><span class="line"><span class="cl">service redis status
</span></span><span class="line"><span class="cl">service nginx status
</span></span></code></pre></div><p>Gunicorn should only listen on localhost:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sockstat -4 -l <span class="p">|</span> grep <span class="m">8001</span>
</span></span></code></pre></div><p>nginx should listen on port 80:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sockstat -4 -l <span class="p">|</span> grep <span class="s1">&#39;:80&#39;</span>
</span></span></code></pre></div><h2 id="upgrading">Upgrading</h2>
<p>Before upgrading I would take a ZFS snapshot and dump PostgreSQL:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">pg_dump -U netbox -h 127.0.0.1 netbox &gt; /root/netbox.sql
</span></span></code></pre></div><p>Then stop the application:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">service netbox_rq stop
</span></span><span class="line"><span class="cl">service netbox stop
</span></span></code></pre></div><p>For now the installer intentionally stays pinned to a known working NetBox release rather than automatically following the latest tag. A separate upgrade script can later handle release download, Python dependency updates, migrations, static file collection and service restart.</p>
<h2 id="gotchas">Gotchas</h2>
<ul>
<li><strong>System V IPC</strong>: PostgreSQL needs <code>sysvmsg=new</code>, <code>sysvsem=new</code> and <code>sysvshm=new</code> in the Bastille jail.</li>
<li><strong>Service user shell</strong>: <code>/bin/sh</code> avoids the <code>su</code> and user-environment problems we hit with <code>/usr/sbin/nologin</code>.</li>
<li><strong>Gunicorn 26 runtime path</strong>: set <code>XDG_RUNTIME_DIR=/var/run/netbox</code> so its control socket is created in a directory owned by the NetBox user.</li>
<li><strong>Avoid <code>daemon -&gt; su -&gt; gunicorn</code></strong>: startup works, but signal propagation makes stop/restart unreliable.</li>
<li><strong>Track the real RQ worker PID</strong>: the same supervisor problem applies to the background worker.</li>
<li><strong>CSRF matters</strong>: any hostname or IP used in the browser must be represented in the Django host/origin configuration.</li>
<li><strong>Static docs directory</strong>: release archives may omit <code>project-static/docs</code>; creating it avoids Django&rsquo;s static-files warning.</li>
<li><strong>FreeBSD is not NetBox&rsquo;s officially documented installation target</strong>: future upgrades may need extra ports/packages when Python dependencies change.</li>
</ul>
<h2 id="summary">Summary</h2>
<p>NetBox runs well natively inside a Bastille VNET jail once the service supervision and PostgreSQL IPC details are handled.</p>
<p>The final stack is simple:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">FreeBSD jail
</span></span><span class="line"><span class="cl">├── nginx
</span></span><span class="line"><span class="cl">├── NetBox / Gunicorn
</span></span><span class="line"><span class="cl">├── NetBox RQ
</span></span><span class="line"><span class="cl">├── PostgreSQL
</span></span><span class="line"><span class="cl">└── Redis
</span></span></code></pre></div><p>No Docker, no Linux VM, and the whole application behaves like a normal FreeBSD service.</p>
<hr>
<p><em>Disclaimer: I use AI as a productivity tool. For a senior engineer, AI is incredibly powerful as one can focus on the solution design and conceptualization and leave the boring part that is implementation to the AI.</em></p>
]]></content:encoded></item><item><title>Running Wiki.js in a FreeBSD Jail with BastilleBSD</title><link>https://dkade.com/posts/wikijs_freebsd_jail/</link><pubDate>Tue, 11 Aug 2026 00:00:00 +0000</pubDate><author>dkade@dkade.com (Daniel Loureiro)</author><guid>https://dkade.com/posts/wikijs_freebsd_jail/</guid><description>A complete guide to running Wiki.js natively inside a FreeBSD 15.1 jail managed with BastilleBSD — no Docker and no Linux VM</description><content:encoded><![CDATA[<p>I wanted a self-hosted knowledge base for my home network and lab documentation. Wiki.js was a good fit — a modern wiki with a clean editor, Markdown support, and a searchable API — and it runs fine on a modest FreeBSD jail.</p>
<p>The official Wiki.js release targets Linux, but there is one important catch: it ships a prebuilt Linux <code>sqlite3</code> native module. On FreeBSD this needs to be rebuilt locally before Wiki.js will start.</p>
<p>This post covers a simple Wiki.js 2.5.314 installation on FreeBSD 15.1 using SQLite, Node.js 24, and a native <code>rc.d</code> service.</p>
<h2 id="setup">Setup</h2>
<p>The jail is a normal FreeBSD 15.1-RELEASE jail with its own IP on the LAN.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">FreeBSD 15.1-RELEASE Host
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">└── wiki jail
</span></span><span class="line"><span class="cl">    └── Wiki.js :3000
</span></span></code></pre></div><p>No Docker or Linux emulation is needed. Wiki.js itself runs natively under FreeBSD Node.js.</p>
<h2 id="why-a-jail">Why a jail?</h2>
<ul>
<li>Native FreeBSD deployment without Docker</li>
<li>No Linux VM just for Wiki.js</li>
<li>Node.js, npm and SQLite come from normal FreeBSD packages</li>
<li>ZFS snapshots make rollback easy</li>
<li>VNET gives the jail its own LAN address</li>
<li>The application can be managed with standard service commands</li>
</ul>
<h2 id="installing-wikijs">Installing Wiki.js</h2>
<p>I created a small bootstrap script which handles the whole installation.</p>
<p>Inside the jail as root:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">fetch https://codeberg.org/dkade/BSD/raw/branch/main/FreeBSD/wikijs/install-wikijs-freebsd-15.1.sh
</span></span><span class="line"><span class="cl">chmod +x install-wikijs-freebsd-15.1.sh
</span></span><span class="line"><span class="cl">./install-wikijs-freebsd-15.1.sh
</span></span></code></pre></div><p>The script installs:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">node24
</span></span><span class="line"><span class="cl">npm-node24
</span></span><span class="line"><span class="cl">python311
</span></span><span class="line"><span class="cl">gmake
</span></span><span class="line"><span class="cl">pkgconf
</span></span><span class="line"><span class="cl">sqlite3
</span></span><span class="line"><span class="cl">ca_root_nss
</span></span></code></pre></div><p>It then downloads Wiki.js 2.5.314 and extracts it under:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/usr/local/wikijs
</span></span></code></pre></div><p>A dedicated <code>wikijs</code> user is created to run the service instead of running Wiki.js as root.</p>
<h2 id="the-sqlite-freebsd-gotcha">The SQLite FreeBSD gotcha</h2>
<p>The official Wiki.js release includes its <code>node_modules</code>, including a prebuilt <code>sqlite3</code> native module.</p>
<p>Trying to start it directly on FreeBSD results in errors such as:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Shared object &#34;libc.musl-x86_64.so.1&#34; not found,
</span></span><span class="line"><span class="cl">required by &#34;node_sqlite3.node&#34;
</span></span></code></pre></div><p>Running <code>ldd</code> makes the problem obvious:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ldd /usr/local/wikijs/node_modules/sqlite3/build/Release/node_sqlite3.node
</span></span></code></pre></div><p>returns:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">not a FreeBSD ELF shared object
</span></span></code></pre></div><p>The bundled module is a Linux/musl binary, so it cannot be used natively on FreeBSD.</p>
<p>The solution is to rebuild only the SQLite native module:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">cd</span> /usr/local/wikijs
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">rm -rf node_modules/sqlite3/build
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">env <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="nv">npm_config_python</span><span class="o">=</span>/usr/local/bin/python3.11 <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="nv">npm_config_build_from_source</span><span class="o">=</span><span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="nv">MAKE</span><span class="o">=</span>gmake <span class="se">\
</span></span></span><span class="line"><span class="cl">  npm rebuild sqlite3
</span></span></code></pre></div><p>Python 3.11 is used because the version of <code>node-gyp</code> bundled with Wiki.js 2.5.x still depends on Python&rsquo;s old <code>distutils</code> module, which was removed in Python 3.12.</p>
<p>After the rebuild:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">file node_modules/sqlite3/build/Release/node_sqlite3.node
</span></span></code></pre></div><p>should identify it as a FreeBSD ELF shared object.</p>
<p>Test it directly:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">node -e <span class="s2">&#34;const s=require(&#39;sqlite3&#39;); console.log(s.VERSION); console.log(&#39;sqlite3 OK&#39;)&#34;</span>
</span></span></code></pre></div><h2 id="configuration">Configuration</h2>
<p>Wiki.js uses:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/usr/local/wikijs/config.yml
</span></span></code></pre></div><p>For a simple SQLite setup:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">3000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">sqlite</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">storage</span><span class="p">:</span><span class="w"> </span><span class="l">/usr/local/wikijs/data/wiki.db</span><span class="w">
</span></span></span></code></pre></div><p>Make sure the database directory is writable by the Wiki.js user:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">mkdir -p /usr/local/wikijs/data
</span></span><span class="line"><span class="cl">chown -R wikijs:wikijs /usr/local/wikijs
</span></span></code></pre></div><h2 id="freebsd-rcd-service">FreeBSD rc.d service</h2>
<p>The installer creates:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/usr/local/etc/rc.d/wikijs
</span></span></code></pre></div><p>Wiki.js is started using FreeBSD&rsquo;s <code>daemon(8)</code> and Node.js:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">/usr/local/bin/node /usr/local/wikijs/server
</span></span></code></pre></div><p>The service is enabled with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sysrc <span class="nv">wikijs_enable</span><span class="o">=</span>YES
</span></span></code></pre></div><p>Start it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">service wikijs start
</span></span></code></pre></div><p>Check the status:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">service wikijs status
</span></span></code></pre></div><p>Restart it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">service wikijs restart
</span></span></code></pre></div><p>Logs are written to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/var/log/wikijs.log
</span></span></code></pre></div><p>so they can be followed with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tail -f /var/log/wikijs.log
</span></span></code></pre></div><p>Once running, Wiki.js is available at:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">http://&lt;jail-ip&gt;:3000
</span></span></code></pre></div><p>The first connection opens the Wiki.js setup wizard.</p>
<h2 id="upgrading">Upgrading</h2>
<p>Before upgrading, take a ZFS snapshot or at least back up:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/usr/local/wikijs/config.yml
</span></span><span class="line"><span class="cl">/usr/local/wikijs/data/
</span></span></code></pre></div><p>After replacing the Wiki.js application files with a newer release, rebuild the SQLite module again:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">cd</span> /usr/local/wikijs
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">env <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="nv">npm_config_python</span><span class="o">=</span>/usr/local/bin/python3.11 <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="nv">npm_config_build_from_source</span><span class="o">=</span><span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="nv">MAKE</span><span class="o">=</span>gmake <span class="se">\
</span></span></span><span class="line"><span class="cl">  npm rebuild sqlite3
</span></span></code></pre></div><p>Then restart Wiki.js:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">service wikijs restart
</span></span></code></pre></div><p>Avoid blindly running:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">npm update
</span></span></code></pre></div><p>or:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">npm audit fix --force
</span></span></code></pre></div><p>inside the Wiki.js application tree. Wiki.js ships with a specific dependency set and changing package versions independently can break the application.</p>
<h2 id="gotchas">Gotchas</h2>
<ul>
<li><strong>SQLite native module:</strong> the bundled <code>node_sqlite3.node</code> is Linux/musl and must be rebuilt for FreeBSD.</li>
<li><strong>Python version:</strong> use Python 3.11 when rebuilding with the older bundled <code>node-gyp</code>.</li>
<li><strong>Node.js:</strong> Wiki.js 2.5.314 runs correctly with the FreeBSD <code>node24</code> package.</li>
<li><strong>Do not copy old <code>node_modules</code>:</strong> when upgrading, use a clean Wiki.js release tree and preserve only your configuration and database.</li>
<li><strong>rc.d PID tracking:</strong> when using <code>daemon(8)</code>, set <code>procname</code> to <code>/usr/local/bin/node</code> so <code>service wikijs status</code> correctly detects the running process.</li>
</ul>
<h2 id="summary">Summary</h2>
<p>Wiki.js runs cleanly inside a FreeBSD 15.1 jail without Docker or Linux emulation.</p>
<p>The only non-obvious part is rebuilding the bundled SQLite native module for FreeBSD. Once that is done, Wiki.js behaves like any other native FreeBSD service: a dedicated user, an <code>rc.d</code> script, automatic startup, and straightforward ZFS snapshots for backups and upgrades.</p>
<p>The bootstrap script automates the complete process from a fresh jail.</p>
<hr>
<p><em>Disclaimer: I use AI as a productivity tool. For a senior engineer, AI is incredibly powerful as one can focus on the solution design and conceptualization and leave the boring part that is implementation to the AI.</em></p>
]]></content:encoded></item><item><title>Running HomeAssistant Core in FreeBSD Jails with BastilleBSD</title><link>https://dkade.com/posts/homeassistant_freebsd_jail/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><author>dkade@dkade.com (Daniel Loureiro)</author><guid>https://dkade.com/posts/homeassistant_freebsd_jail/</guid><description>A complete guide to running HomeAssistant Core in a VNET FreeBSD jail managed with BastilleBSD, with separate jails for Mosquitto MQTT and Zigbee2MQTT with USB passthrough</description><content:encoded><![CDATA[<p>I run HomeAssistant Core on FreeBSD inside a jail managed with BastilleBSD — no Docker, no VM, just a lightweight VNET jail with its own IP on the LAN. This post covers the full setup: three jails, a bootstrap script, USB passthrough for Zigbee, and WiFi devices on the same subnet.</p>
<h2 id="architecture">Architecture</h2>
<pre tabindex="0"><code>FreeBSD 15.1-RELEASE Host
│
├── homeassistant (192.168.2.50)  — HA Core :8123
├── mosquitto    (192.168.2.52)  — MQTT broker :1883
└── zigbee2mqtt  (192.168.2.51)  — Zigbee2MQTT, Sonoff USB passthrough
</code></pre><p>All three jails are VNET on the same <code>/24</code> subnet. HA Core talks to Mosquitto over MQTT, Zigbee2MQTT publishes device states to Mosquitto, and HA subscribes. Shelly devices connect directly over WiFi and are auto-discovered by HA on the LAN.</p>
<h2 id="why-jails">Why jails?</h2>
<ul>
<li>Native FreeBSD — no Docker overlay, no Linux emulation</li>
<li>VNET gives each jail a real IP on the LAN — devices discover HA directly</li>
<li>ZFS snapshots for quick rollbacks before upgrades</li>
<li>Each service in its own jail — clean isolation, restart one without affecting others</li>
</ul>
<h2 id="creating-the-jails">Creating the jails</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">bastille create -V homeassistant 15.1-RELEASE 192.168.2.50/24 vtnet0
</span></span><span class="line"><span class="cl">bastille create -V mosquitto 15.1-RELEASE 192.168.2.52/24 vtnet0
</span></span><span class="line"><span class="cl">bastille create -V zigbee2mqtt 15.1-RELEASE 192.168.2.51/24 vtnet0
</span></span></code></pre></div><p>The <code>-V</code> flag gives each jail a VNET interface with its own IP and routing table — they live on the same broadcast domain as the host.</p>
<h2 id="homeassistant-jail">HomeAssistant jail</h2>
<p>A bootstrap script handles the full setup:</p>
<ul>
<li><strong>Repository</strong>: <a href="https://codeberg.org/dkade/BSD/src/branch/main/FreeBSD/install_ha_freebsd.sh">codeberg.org/dkade/BSD</a></li>
<li><strong>Method</strong>: Python 3.14 virtual environment under <code>/usr/local/etc/homeassistant/venv</code></li>
<li><strong>User</strong>: dedicated <code>hass</code> system user</li>
<li><strong>Service</strong>: FreeBSD rc.d script using <code>daemon -u hass</code></li>
<li><strong>Enable</strong>: <code>sysrc homeassistant_enable=YES</code></li>
</ul>
<p>What the script does:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Inside the jail as root</span>
</span></span><span class="line"><span class="cl">fetch https://codeberg.org/dkade/BSD/raw/branch/main/FreeBSD/install_ha_freebsd.sh
</span></span><span class="line"><span class="cl">sh install_ha_freebsd.sh
</span></span></code></pre></div><p>It installs system packages (<code>python314</code>, <code>gcc12</code>, <code>rust</code>, <code>dbus</code>, <code>libffi</code>, <code>openssl</code>, etc.), creates a venv, and installs <code>homeassistant</code> along with <code>numpy</code>, <code>zlib_ng</code>, and <code>isal</code> for performance. The rc.d script starts HA as the <code>hass</code> user and listens on port 8123.</p>
<p>After first boot, complete the onboarding at <code>http://192.168.2.50:8123</code>.</p>
<h2 id="mosquitto-jail">Mosquitto jail</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">bastille console mosquitto
</span></span><span class="line"><span class="cl">pkg install mosquitto
</span></span><span class="line"><span class="cl">sysrc <span class="nv">mosquitto_enable</span><span class="o">=</span>YES
</span></span><span class="line"><span class="cl">service mosquitto start
</span></span></code></pre></div><p>No authentication is needed on a local subnet — configure <code>listener 1883 192.168.2.52</code> in <code>/usr/local/etc/mosquitto/mosquitto.conf</code> to bind to the jail IP.</p>
<h2 id="zigbee2mqtt-jail">Zigbee2MQTT jail</h2>
<p>This jail needs access to the Sonoff Zigbee coordinator plugged into the host.</p>
<h3 id="usb-passthrough-with-devfs">USB passthrough with devfs</h3>
<p>On the <strong>host</strong> (<code>/etc/devfs.rules</code>):</p>
<pre tabindex="0"><code>[bastille_usb=101]
add path &#39;usbctl&#39; mode 0660
add path &#39;usb/*&#39; mode 0660
add path &#39;ugen*&#39; mode 0660
add path &#39;cuaU*&#39; mode 0660
add path &#39;ttyU*&#39; mode 0660
</code></pre><p>Set the rule in <code>/usr/local/bastille/jails/zigbee2mqtt/jail.conf</code>:</p>
<pre tabindex="0"><code>devfs_ruleset = &#34;bastille_usb&#34;;
</code></pre><p>Then find the Sonoff device:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># On the host</span>
</span></span><span class="line"><span class="cl">ls /dev/cuaU*
</span></span><span class="line"><span class="cl"><span class="c1"># /dev/cuaU0 &lt;- this is the Sonoff</span>
</span></span></code></pre></div><p>Restart the jail and you should see the device inside:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">bastille restart zigbee2mqtt
</span></span><span class="line"><span class="cl">bastille console zigbee2mqtt
</span></span><span class="line"><span class="cl">ls /dev/cuaU*
</span></span></code></pre></div><h3 id="installing-and-configuring">Installing and configuring</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">bastille console zigbee2mqtt
</span></span><span class="line"><span class="cl">pkg install node npm mosquitto-libs
</span></span><span class="line"><span class="cl">npm install -g zigbee2mqtt
</span></span></code></pre></div><p>Create <code>/usr/local/etc/zigbee2mqtt/configuration.yaml</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">mqtt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">server</span><span class="p">:</span><span class="w"> </span><span class="l">mqtt://192.168.2.52:1883</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">serial</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="l">/dev/cuaU0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">frontend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span></code></pre></div><p>rc.d script at <code>/usr/local/etc/rc.d/zigbee2mqtt</code> to start via <code>daemon</code>, then <code>sysrc zigbee2mqtt_enable=YES &amp;&amp; service zigbee2mqtt start</code>.</p>
<h2 id="device-integration">Device integration</h2>
<ul>
<li><strong>Shelly</strong> (WiFi): connect them to your WiFi. HA auto-discovers them via the LAN once the Shelly integration is added in the UI.</li>
<li><strong>Zigbee</strong> (Sonoff sensors, bulbs, etc.): pair through the Zigbee2MQTT frontend (<code>http://192.168.2.51:8080</code>). HA receives device states via MQTT auto-discovery.</li>
</ul>
<h2 id="upgrading">Upgrading</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># HA Core</span>
</span></span><span class="line"><span class="cl">bastille console homeassistant
</span></span><span class="line"><span class="cl">service homeassistant stop
</span></span><span class="line"><span class="cl">su -l hass -c <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s1">&#39;HOME=/usr/local/etc/homeassistant PIP_CACHE_DIR=/tmp/pip-cache \
</span></span></span><span class="line"><span class="cl"><span class="s1">   /usr/local/etc/homeassistant/venv/bin/pip install --upgrade homeassistant&#39;</span>
</span></span><span class="line"><span class="cl">service homeassistant start
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># zigbee2mqtt</span>
</span></span><span class="line"><span class="cl">bastille console zigbee2mqtt
</span></span><span class="line"><span class="cl">service zigbee2mqtt stop
</span></span><span class="line"><span class="cl">npm update -g zigbee2mqtt
</span></span><span class="line"><span class="cl">service zigbee2mqtt start
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># mosquitto</span>
</span></span><span class="line"><span class="cl">bastille console mosquitto
</span></span><span class="line"><span class="cl">pkg upgrade mosquitto
</span></span><span class="line"><span class="cl">service mosquitto restart
</span></span></code></pre></div><h2 id="gotchas">Gotchas</h2>
<ul>
<li><strong>Python compilation</strong>: HA Core compiles C and Rust extensions during <code>pip install</code>. Make sure <code>gcc12</code>, <code>rust</code>, <code>libffi</code>, and <code>openssl</code> are installed in the jail before installing HA.</li>
<li><strong>dbus</strong>: HA Core expects D-Bus to be running (<code>service dbus start</code>). The rc.d script has <code>REQUIRE: dbus</code>, so dbus starts first.</li>
<li><strong>USB devfs</strong>: The devfs ruleset must be applied <em>before</em> the jail starts. Double-check the ruleset number matches in both <code>/etc/devfs.rules</code> and the jail config.</li>
<li><strong>VNET routing</strong>: If jails can&rsquo;t reach the internet for <code>pkg install</code>, check that <code>gateway_enable=&quot;YES&quot;</code> is set on the host and NAT/pf is configured for the VNET subnet.</li>
<li><strong>Persistent pip cache</strong>: The script sets <code>PIP_CACHE_DIR=/tmp/pip-cache</code> so you don&rsquo;t re-download wheels on every upgrade.</li>
</ul>
<h2 id="summary">Summary</h2>
<p>Three FreeBSD jails managed with BastilleBSD running HA Core, Mosquitto, and Zigbee2MQTT — each with its own IP, rc.d service, and clean isolation. The bootstrap script at <code>codeberg.org/dkade/BSD</code> automates the HA jail from scratch, and the rest is just standard FreeBSD package management.</p>
<hr>
<p><em>Disclaimer: I use AI as a productivity tool. For a senior engineer, AI is incredibly powerful as one can focus on the solution design and conceptualization and leave the boring part that is implementation to the AI.</em></p>
]]></content:encoded></item><item><title>Tips and lessons learnt by using AWS FMS and WAFv2</title><link>https://dkade.com/posts/aws_tips_waf_fms/</link><pubDate>Thu, 11 Mar 2021 00:00:00 +0000</pubDate><author>dkade@dkade.com (Daniel Loureiro)</author><guid>https://dkade.com/posts/aws_tips_waf_fms/</guid><description>&lt;ul&gt;
&lt;li&gt;Properly plan for your WAF logs, this will be the hardest part;&lt;/li&gt;
&lt;li&gt;Logs are activated per WebACL so each account needs to have a properly configured Kinesis Data Firehose. It is possible to enable the logs from FMS&lt;/li&gt;
&lt;li&gt;When creating Cloudformation stack make sure you make resources dependent on another or WAF will quickly rate limit Cloudformation and your stack will fail&lt;/li&gt;
&lt;li&gt;Use JSON in Cloudformation if you have accounts starting with 0. PyYAML has a &lt;a href="https://github.com/yaml/pyyaml/issues/98"&gt;bug&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Instead of Cloudformation you can use Terraform (when I started building the solution this wasn&amp;rsquo;t possible)&lt;/li&gt;
&lt;li&gt;Before activating an account on a FMS Policy, make sure that account has AWS Config enabled&lt;/li&gt;
&lt;li&gt;Use AWS Managed rules as they are created with standards in mind for example protecting from &lt;a href="https://owasp.org/www-project-top-ten/"&gt;OWASP Top 10&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;AWS WAF uses web ACL capacity units and you are limited to 1500, you can request increase but it&amp;rsquo;s not simple, so plan accordingly because if you need to change the WCU capacity of a Rule group you have to create a new one and delete the old one&lt;/li&gt;
&lt;li&gt;Rate limit rules can ONLY be added to WebACLs and are related to that WebACL. There is no option at the moment to add them to FMS, we are working with AWS to change this;&lt;/li&gt;
&lt;/ul&gt;</description><content:encoded><![CDATA[<ul>
<li>Properly plan for your WAF logs, this will be the hardest part;</li>
<li>Logs are activated per WebACL so each account needs to have a properly configured Kinesis Data Firehose. It is possible to enable the logs from FMS</li>
<li>When creating Cloudformation stack make sure you make resources dependent on another or WAF will quickly rate limit Cloudformation and your stack will fail</li>
<li>Use JSON in Cloudformation if you have accounts starting with 0. PyYAML has a <a href="https://github.com/yaml/pyyaml/issues/98">bug</a></li>
<li>Instead of Cloudformation you can use Terraform (when I started building the solution this wasn&rsquo;t possible)</li>
<li>Before activating an account on a FMS Policy, make sure that account has AWS Config enabled</li>
<li>Use AWS Managed rules as they are created with standards in mind for example protecting from <a href="https://owasp.org/www-project-top-ten/">OWASP Top 10</a></li>
<li>AWS WAF uses web ACL capacity units and you are limited to 1500, you can request increase but it&rsquo;s not simple, so plan accordingly because if you need to change the WCU capacity of a Rule group you have to create a new one and delete the old one</li>
<li>Rate limit rules can ONLY be added to WebACLs and are related to that WebACL. There is no option at the moment to add them to FMS, we are working with AWS to change this;</li>
</ul>
]]></content:encoded></item><item><title>How OLX Europe Fights Millions of Bots with AWS</title><link>https://dkade.com/posts/olx_aws_post_/</link><pubDate>Mon, 08 Mar 2021 00:00:00 +0000</pubDate><author>dkade@dkade.com (Daniel Loureiro)</author><guid>https://dkade.com/posts/olx_aws_post_/</guid><description>&lt;p&gt;My latest work done in OLX was featured by AWS on their own blog! I&amp;rsquo;m really happy with it. I talk about using AWS Firewall Manager + WAFv2 and in house tools to fight attacks. A big kudos to &lt;a href="https://www.linkedin.com/in/gabrielsoltz/"&gt;Gabril Soltz&lt;/a&gt; for his amazing work with the WAFBot.&lt;/p&gt;
&lt;p&gt;Read more at -&amp;gt; &lt;a href="https://aws.amazon.com/blogs/architecture/field-notes-how-olx-europe-fights-millions-of-bots-with-aws/"&gt;https://aws.amazon.com/blogs/architecture/field-notes-how-olx-europe-fights-millions-of-bots-with-aws/&lt;/a&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<p>My latest work done in OLX was featured by AWS on their own blog! I&rsquo;m really happy with it. I talk about using AWS Firewall Manager + WAFv2 and in house tools to fight attacks.  A big kudos to <a href="https://www.linkedin.com/in/gabrielsoltz/">Gabril Soltz</a> for his amazing work with the WAFBot.</p>
<p>Read more at -&gt; <a href="https://aws.amazon.com/blogs/architecture/field-notes-how-olx-europe-fights-millions-of-bots-with-aws/">https://aws.amazon.com/blogs/architecture/field-notes-how-olx-europe-fights-millions-of-bots-with-aws/</a></p>
]]></content:encoded></item></channel></rss>