<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://hadow.fr/feed.xml" rel="self" type="application/atom+xml" /><link href="https://hadow.fr/" rel="alternate" type="text/html" /><updated>2026-10-12T12:24:04+02:00</updated><id>https://hadow.fr/feed.xml</id><title type="html">hadow.fr</title><entry><title type="html">Bad Usb With A Digispark Attiny85</title><link href="https://hadow.fr/blog/bad-usb-with-a-digispark-attiny85.html" rel="alternate" type="text/html" title="Bad Usb With A Digispark Attiny85" /><published>2026-08-30T00:00:00+02:00</published><updated>2026-08-30T00:00:00+02:00</updated><id>https://hadow.fr/blog/bad-usb-with-a-digispark-attiny85</id><content type="html" xml:base="https://hadow.fr/blog/bad-usb-with-a-digispark-attiny85.html"><![CDATA[<p>This short blog post explains how to use a Digispark Attiny85 as a <a href="https://en.wikipedia.org/wiki/BadUSB">bad USB</a> device.</p>

<h1 id="steps">Steps</h1>

<h2 id="0-the-board-itself">0. The board itself</h2>

<p>The board looks like this:</p>

<p><img src="/assets/img/2026-08-30-bad-usb-with-a-digispark-attiny85/1.png" alt="Picture 1" />
<img src="/assets/img/2026-08-30-bad-usb-with-a-digispark-attiny85/2.png" alt="Picture 2" /></p>

<p>And with a 3d printed case like <a href="https://www.printables.com/model/509019-digispark-attiny85-enclosure-bad-usb-stick-fake-me">this one</a> it can even look like a somewhat genuine USB stick:</p>

<p><img src="/assets/img/2026-08-30-bad-usb-with-a-digispark-attiny85/3.png" alt="Picture 3" /></p>

<h2 id="1-preparing-the-environment">1. Preparing the environment</h2>

<h3 id="11-installing-arduino-ide-and-the-required-libraries">1.1. Installing Arduino IDE and the required libraries</h3>

<p>First install <code class="language-plaintext highlighter-rouge">micronucleus</code>, on Archlinux this can be done with the command <code class="language-plaintext highlighter-rouge">sudo pacman -S micronucleus</code><br />
Then install the <a href="https://github.com/arduino/arduino-ide">Arduino IDE</a>. On Archlinux it’s available in the AUR as the package <code class="language-plaintext highlighter-rouge">arduino-ide-bin</code>.</p>

<h3 id="12-installing-the-boards-manager">1.2. Installing the boards manager</h3>

<p>Open Arduino IDE and go to File &gt; Preferences and in the field “Additional Board Manager URLs:” add:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://raw.githubusercontent.com/digistump/arduino-boards-index/master/package_digistump_index.json
</code></pre></div></div>
<p>If you have multiple additional board manager URLs you can separate them with a comma: <code class="language-plaintext highlighter-rouge">,</code></p>

<p>Then in Tools &gt; Board &gt; Boards Manager, search for “digispark” and install “Digistump AVR Boards”</p>

<h2 id="2-flashing-a-payload">2. Flashing a payload</h2>

<p>Select in Tools &gt; Board &gt; Digistump AVR Boards, the board “Digispark (Default - 16.5mhz)”<br />
Then create a sketch and write your bad USB payload, for example:</p>

<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"DigiKeyboard.h"</span><span class="cp">
</span>
<span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">// Give Linux time to enumerate the Digispark as a keyboard.</span>
  <span class="n">DigiKeyboard</span><span class="p">.</span><span class="n">delay</span><span class="p">(</span><span class="mi">1000</span><span class="p">);</span>

  <span class="c1">// Open a terminal.</span>
  <span class="n">DigiKeyboard</span><span class="p">.</span><span class="n">sendKeyStroke</span><span class="p">(</span><span class="n">KEY_T</span><span class="p">,</span> <span class="n">MOD_CONTROL_LEFT</span> <span class="o">|</span> <span class="n">MOD_ALT_LEFT</span><span class="p">);</span>
  <span class="n">DigiKeyboard</span><span class="p">.</span><span class="n">delay</span><span class="p">(</span><span class="mi">1000</span><span class="p">);</span>

  <span class="c1">// Launch Firefox.</span>
  <span class="n">DigiKeyboard</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"firefox https://hadow.fr"</span><span class="p">);</span>
  <span class="n">DigiKeyboard</span><span class="p">.</span><span class="n">sendKeyStroke</span><span class="p">(</span><span class="n">KEY_ENTER</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">loop</span><span class="p">()</span> <span class="p">{</span>
<span class="p">}</span>
</code></pre></div></div>

<p>(Please note that this payload will work only with a QWERTY layout)</p>

<p>You can also find payloads <a href="https://github.com/CedArctic/DigiSpark-Scripts">here</a> and <a href="https://github.com/MTK911/Attiny85">here</a>.</p>

<p>Then upload the sketch, in the output you should see:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sketch uses 2944 bytes (48%) of program storage space. Maximum is 6012 bytes.
Global variables use 162 bytes of dynamic memory.
Running Digispark Uploader...
Plug in device now... (will timeout in 60 seconds)
</code></pre></div></div>
<p>Plug in your device and you should see:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; Please plug in the device ... 
&gt; Press CTRL+C to terminate the program.
&gt; Device is found!
connecting: 16% complete
connecting: 22% complete
connecting: 28% complete
connecting: 33% complete
&gt; Device has firmware version 1.6
&gt; Available space for user applications: 6012 bytes
&gt; Suggested sleep time between sending pages: 8ms
&gt; Whole page count: 94  page size: 64
&gt; Erase function sleep duration: 752ms
parsing: 50% complete
&gt; Erasing the memory ...
erasing: 55% complete
erasing: 60% complete
erasing: 65% complete
&gt; Starting to upload ...
writing: 70% complete
writing: 75% complete
writing: 80% complete
&gt; Starting the user app ...
running: 100% complete
&gt;&gt; Micronucleus done. Thank you!
</code></pre></div></div>

<p>You can then unplug your device and plug it into a computer to execute the payload.<br />
<strong>Disclaimer:</strong> Only plug in this device in computers you are allowed to.</p>

<p>This short blog post also demonstrates why you should never pick up random USB sticks from the ground and plug them into your computer.</p>]]></content><author><name>Sam Hadow</name></author><category term="cybersecurity" /><category term="hardware" /><summary type="html"><![CDATA[This short blog post explains how to use a Digispark Attiny85 as a bad USB device.]]></summary></entry><entry><title type="html">First Steps With An Evilcrow Cable Wind</title><link href="https://hadow.fr/blog/first-steps-with-an-evilcrow-cable-wind.html" rel="alternate" type="text/html" title="First Steps With An Evilcrow Cable Wind" /><published>2026-08-29T00:00:00+02:00</published><updated>2026-08-29T00:00:00+02:00</updated><id>https://hadow.fr/blog/first-steps-with-an-evilcrow-cable-wind</id><content type="html" xml:base="https://hadow.fr/blog/first-steps-with-an-evilcrow-cable-wind.html"><![CDATA[<p>This blog post is a guide on how to flash an <a href="https://github.com/joelsernamoreno/EvilCrowCable-Wind">EvilCrow Cable Wind</a> and how to use it running your first payload. The model used in this blog post is the USB A to USB C version. This guide also assumes you’re using a linux distribution as your operating system, preferably Archlinux.</p>

<h1 id="steps">Steps</h1>

<h2 id="1-preparing-the-environment">1. Preparing the environment</h2>

<h3 id="11-installing-arduino-ide-and-the-required-libraries">1.1. Installing Arduino IDE and the required libraries</h3>

<p>First install <code class="language-plaintext highlighter-rouge">esptool</code> and <code class="language-plaintext highlighter-rouge">pyserial</code>, on Archlinux this can be done with the command <code class="language-plaintext highlighter-rouge">sudo pacman -S esptool python-pyserial</code><br />
Then install the <a href="https://github.com/arduino/arduino-ide">Arduino IDE</a>. On Archlinux it’s available in the AUR as the package <code class="language-plaintext highlighter-rouge">arduino-ide-bin</code>.</p>

<p>Once it’s installed, you’ll need to add 3 libraries in your Arduino library directory (usually located in ~/Arduino/libraries).</p>
<ul>
  <li>copy and unzip the USB library included in the <a href="https://github.com/joelsernamoreno/EvilCrowCable-Wind">repository</a> in EvilCrowCable-Wind/modified-libraries/</li>
  <li>copy and unzip the husarnet-esp32-arduino library included in the <a href="https://github.com/joelsernamoreno/EvilCrowCable-Wind">repository</a> in EvilCrowCable-Wind/modified-libraries/</li>
  <li>download the ArduinoJSON library with <code class="language-plaintext highlighter-rouge">git clone https://github.com/bblanchon/ArduinoJson.git</code></li>
</ul>

<h3 id="12-installing-the-boards-manager">1.2. Installing the boards manager</h3>

<p>Open Arduino IDE and go to File &gt; Preferences and in the field “Additional Board Manager URLs:” add</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://espressif.github.io/arduino-esp32/package_esp32_index.json
</code></pre></div></div>

<p><img src="/assets/img/2026-08-29-first-steps-with-an-evilcrow-cable-wind/1.png" alt="Picture 1" /></p>

<p>Then in Tools &gt; Board &gt; Boards Manager, search for “esp32” and install “esp 32 by Espressif Systems”</p>

<p><img src="/assets/img/2026-08-29-first-steps-with-an-evilcrow-cable-wind/2.png" alt="Picture 2" /></p>

<h2 id="2-flashing-the-firmware">2. Flashing the firmware</h2>

<h2 id="21-plugging-the-evilcrow-cable-wind-in-flash-mode">2.1. Plugging the EvilCrow Cable Wind in flash mode</h2>

<p>Place a magnet on the active end (USB A) of the EvilCrow Cable Wind and connect it to the computer, it should be recognized as “USB JTAG/serial debug unit” in dmesg (<code class="language-plaintext highlighter-rouge">sudo dmesg --follow</code>). Example output:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span> 4371.460527] usb 1-9: new full-speed USB device number 32 using xhci_hcd
<span class="o">[</span> 4371.585041] usb 1-9: New USB device found, <span class="nv">idVendor</span><span class="o">=</span>303a, <span class="nv">idProduct</span><span class="o">=</span>1001, <span class="nv">bcdDevice</span><span class="o">=</span> 1.01
<span class="o">[</span> 4371.585060] usb 1-9: New USB device strings: <span class="nv">Mfr</span><span class="o">=</span>1, <span class="nv">Product</span><span class="o">=</span>2, <span class="nv">SerialNumber</span><span class="o">=</span>3
<span class="o">[</span> 4371.585068] usb 1-9: Product: USB JTAG/serial debug unit
<span class="o">[</span> 4371.585075] usb 1-9: Manufacturer: Espressif
<span class="o">[</span> 4371.585080] usb 1-9: SerialNumber: 10:51:DB:38:6E:9C
<span class="o">[</span> 4371.589316] cdc_acm 1-9:1.0: ttyACM0: USB ACM device
</code></pre></div></div>

<p><img src="/assets/img/2026-08-29-first-steps-with-an-evilcrow-cable-wind/3.png" alt="Picture 3" /></p>

<h2 id="22-flashing-the-firmware">2.2. Flashing the firmware</h2>

<p>Open EvilCrowCable-Wind/firmware/firmware.ino in Arduino IDE from the <a href="https://github.com/joelsernamoreno/EvilCrowCable-Wind">repository</a>. You can modify in this file the password and SSID of the access point the ESP32 will try to connect to. By default it’s <code class="language-plaintext highlighter-rouge">Evil Crow Cable Wind</code> for the SSID and <code class="language-plaintext highlighter-rouge">123456789</code> for the password. I recommend changing these values.</p>

<p>Then in Tools, select your port and the board “ESP32S3 Dev Module”, in my case the port is ACM0, but it could be USB0 or another port. And configure all the parameters as follow:</p>

<p><img src="/assets/img/2026-08-29-first-steps-with-an-evilcrow-cable-wind/4.png" alt="Picture 4" /></p>

<p>Specifically:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: right">#</th>
      <th>Setting</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">1</td>
      <td>USB CDC On Boot</td>
      <td>Disabled</td>
    </tr>
    <tr>
      <td style="text-align: right">2</td>
      <td>CPU Frequency</td>
      <td>80MHz (Wi-Fi)</td>
    </tr>
    <tr>
      <td style="text-align: right">3</td>
      <td>Core Debug Level</td>
      <td>None</td>
    </tr>
    <tr>
      <td style="text-align: right">4</td>
      <td>USB DFU On Boot</td>
      <td>Disabled</td>
    </tr>
    <tr>
      <td style="text-align: right">5</td>
      <td>Erase All Flash Before Sketch Upload</td>
      <td>Disabled</td>
    </tr>
    <tr>
      <td style="text-align: right">6</td>
      <td>Events Run On</td>
      <td>Core 1</td>
    </tr>
    <tr>
      <td style="text-align: right">7</td>
      <td>Flash Mode</td>
      <td>QIO 80MHZ</td>
    </tr>
    <tr>
      <td style="text-align: right">8</td>
      <td>Flash Size</td>
      <td>8MB (64Mb)</td>
    </tr>
    <tr>
      <td style="text-align: right">9</td>
      <td>JTAG Adapter</td>
      <td>Disabled</td>
    </tr>
    <tr>
      <td style="text-align: right">10</td>
      <td>Arduino Runs On</td>
      <td>Core 1</td>
    </tr>
    <tr>
      <td style="text-align: right">11</td>
      <td>USB Firmware MSC On Boot</td>
      <td>Disabled</td>
    </tr>
    <tr>
      <td style="text-align: right">12</td>
      <td>Partition Scheme</td>
      <td>8M with SPIFFS (3MB APP/1.5MB SPIFFS)</td>
    </tr>
    <tr>
      <td style="text-align: right">13</td>
      <td>PSRAM</td>
      <td>Disabled</td>
    </tr>
    <tr>
      <td style="text-align: right">14</td>
      <td>Upload Mode</td>
      <td>UART0 / Hardware CDC</td>
    </tr>
    <tr>
      <td style="text-align: right">15</td>
      <td>Upload Speed</td>
      <td>921600</td>
    </tr>
    <tr>
      <td style="text-align: right">16</td>
      <td>USB Mode</td>
      <td>Hardware CDC and JTAG</td>
    </tr>
    <tr>
      <td style="text-align: right">17</td>
      <td>Zigbee Mode</td>
      <td>Disabled</td>
    </tr>
  </tbody>
</table>

<h1 id="3-using-the-evilcrow-cable-wind">3. Using the EvilCrow Cable Wind</h1>

<h2 id="31-connecting-to-the-cable">3.1. Connecting to the cable</h2>

<p>Upon start the EvilCrow Cable Wind will try to connect to a network with the SSID and password specified in the firmware, so first setup an access point with a phone or laptop and then plug in the active end (USB A) of the EvilCrow Cable in a computer.<br />
Theorically the cable should be accessible at <code class="language-plaintext highlighter-rouge">http://cable-wind.local</code> but it doesn’t work well with a phone.<br />
Instead you can connect a laptop (or use the laptop with the access point) to the access point and directly find the IP address of the cable.</p>
<ul>
  <li>If you have a laptop setup as an access point:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ip neighbor
</code></pre></div>    </div>
  </li>
  <li>If you connected a laptop to the access point:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ip route
</code></pre></div>    </div>
    <p>A possible output is:</p>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>default via 10.27.197.13 dev wlp2s0 proto dhcp src 10.27.197.114 metric 600 
10.27.197.0/24 dev wlp2s0 proto kernel scope <span class="nb">link </span>src 10.27.197.114 metric 600
</code></pre></div>    </div>
    <p>Then force the discovery of the devices on this subnetwork</p>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nmap <span class="nt">-sn</span> 10.27.197.0/24
</code></pre></div>    </div>
    <p>And find the host Espressif</p>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Nmap scan report <span class="k">for </span>10.27.197.126
Host is up <span class="o">(</span>0.21s latency<span class="o">)</span><span class="nb">.</span>
MAC Address: XX:XX:XX:XX:XX:XX <span class="o">(</span>Espressif<span class="o">)</span>
</code></pre></div>    </div>
    <p>You can then connect to the cable with this IP address.</p>
  </li>
</ul>

<h2 id="32-writing-your-first-payload">3.2. Writing your first payload</h2>

<p>For example to payload a Linux running a desktop with CTRL+ALT+T shortcut to open a terminal (Like Gnome, KDE Plasma, XFCE, etc…) and open my website in firefox:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Delay 1000
Press KEY_LEFT_CTRL
Press KEY_LEFT_ALT
PressRelease t
Delay 1000
PrintLine firefox https://hadow.fr
Delay 500
</code></pre></div></div>

<p><img src="/assets/img/2026-08-29-first-steps-with-an-evilcrow-cable-wind/5.png" alt="Picture 5" /></p>

<p>As you can imagine, you can run much more complex payloads targeting a wide range of environments. As such it should be treated as a serious security tool, not as a harmless gadget or toy.<br />
This is also why the paranoia around unknown USB cables isn’t entirely unjustified: you can’t always tell what’s hiding inside a cable. Use it only on systems you own or have explicit permission to payload.</p>]]></content><author><name>Sam Hadow</name></author><category term="cybersecurity" /><category term="hardware" /><summary type="html"><![CDATA[This blog post is a guide on how to flash an EvilCrow Cable Wind and how to use it running your first payload. The model used in this blog post is the USB A to USB C version. This guide also assumes you’re using a linux distribution as your operating system, preferably Archlinux.]]></summary></entry><entry><title type="html">Self Hosting A Minecraft Server On A Pi5</title><link href="https://hadow.fr/blog/self-hosting-a-minecraft-server-on-a-pi5.html" rel="alternate" type="text/html" title="Self Hosting A Minecraft Server On A Pi5" /><published>2026-08-23T00:00:00+02:00</published><updated>2026-08-23T00:00:00+02:00</updated><id>https://hadow.fr/blog/self-hosting-a-minecraft-server-on-a-pi5</id><content type="html" xml:base="https://hadow.fr/blog/self-hosting-a-minecraft-server-on-a-pi5.html"><![CDATA[<p>In this blog post, I’ll describe how to self host a minecraft server on a raspberry Pi 5. This guide assumes you already have a minimal install of debian 13 on your Pi5.</p>

<h1 id="steps">Steps</h1>

<h2 id="1-installing-the-required-packages">1. Installing the required packages</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>default-jre openjdk-25-jre
</code></pre></div></div>

<h2 id="2-running-the-server">2. Running the server</h2>

<h3 id="21-creating-a-dedicated-user">2.1) Creating a dedicated user:</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>useradd <span class="nt">-m</span> minecraft
</code></pre></div></div>

<h3 id="22-enabling-autologin-for-minecraft-on-tty2">2.2) Enabling autologin for minecraft on TTY2</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /etc/systemd/system/getty@tty2.service.d
</code></pre></div></div>
<p>and in this folder I created this file:</p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#/etc/systemd/system/getty@tty2.service.d/autologin.conf
</span><span class="nn">[Service]</span><span class="w">
</span><span class="py">ExecStart</span><span class="p">=</span>
<span class="py">ExecStart</span><span class="p">=</span><span class="s">-/sbin/agetty --autologin minecraft --noclear %I $TERM</span>
</code></pre></div></div>

<h3 id="23-enabling-lingering-for-minecraft-which-is-needed-to-start-user-systemd-services-even-without-a-shell-session">2.3) Enabling lingering for minecraft (which is needed to start user systemd services even without a shell session)</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>loginctl enable-linger minecraft
</code></pre></div></div>

<h3 id="24-downloading-the-minecraft-server">2.4) Downloading the minecraft server:</h3>

<p>Download the latest version of fabric minecraft server from <a href="https://fabricmc.net/use/server/">here</a><br />
For example:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>curl <span class="nt">-o</span> /home/minecraft/server.jar https://meta.fabricmc.net/v2/versions/loader/26.2/0.19.3/1.1.2/server/jar
</code></pre></div></div>

<h3 id="25-creating-the-systemd-service">2.5) Creating the systemd service</h3>
<p>First creating the folder:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /home/minecraft/.config/systemd/user
</code></pre></div></div>
<p>And then a service unit file:</p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#/home/minecraft/.config/systemd/user/minecraft.service
</span><span class="nn">[Unit]</span><span class="w">
</span><span class="py">Description</span><span class="p">=</span><span class="s">Minecraft Server</span>
<span class="py">After</span><span class="p">=</span><span class="s">network-online.target</span>
<span class="py">Wants</span><span class="p">=</span><span class="s">network-online.target</span>
<span class="w">
</span><span class="nn">[Service]</span><span class="w">
</span><span class="py">WorkingDirectory</span><span class="p">=</span><span class="s">/home/minecraft</span>
<span class="py">ExecStart</span><span class="p">=</span><span class="s">/usr/bin/java -Xmx4G -jar /home/minecraft/server.jar nogui</span>
<span class="w">
</span><span class="py">Restart</span><span class="p">=</span><span class="s">on-failure</span>
<span class="py">RestartSec</span><span class="p">=</span><span class="s">5</span>
<span class="w">
</span><span class="nn">[Install]</span><span class="w">
</span><span class="py">WantedBy</span><span class="p">=</span><span class="s">default.target</span>
</code></pre></div></div>

<p>After this make sure the ownership is correct:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo chown</span> <span class="nt">-R</span> minecraft:minecraft /home/minecraft
</code></pre></div></div>

<p>And then to start the service:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl <span class="nt">--user</span> <span class="nt">--machine</span><span class="o">=</span>minecraft@.host daemon-reload
<span class="nb">sudo </span>systemctl <span class="nt">--user</span> <span class="nt">--machine</span><span class="o">=</span>minecraft@.host start minecraft
</code></pre></div></div>

<p>after the first start you’ll need to accept the EULA:</p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#/home/minecraft/eula.txt
</span><span class="py">eula</span><span class="p">=</span><span class="s">TRUE</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl <span class="nt">--user</span> <span class="nt">--machine</span><span class="o">=</span>minecraft@.host restart minecraft
</code></pre></div></div>

<h2 id="3-checking-logs">3. Checking logs</h2>

<p>To check the minecraft server related logs, we can use the following command:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>journalctl <span class="nv">_UID</span><span class="o">=</span><span class="si">$(</span><span class="nb">id</span> <span class="nt">-u</span> minecraft<span class="si">)</span>
</code></pre></div></div>

<h2 id="4-additional-remarks">4. Additional remarks</h2>

<h3 id="41-server-properties">4.1) Server properties</h3>

<p>You can change the server properties in <code class="language-plaintext highlighter-rouge">/home/minecraft/server.properties</code>.<br />
I recommend enabling the whitelist if your server is exposed on the internet.</p>

<h3 id="42-executing-commands">4.2) Executing commands</h3>

<p>In <code class="language-plaintext highlighter-rouge">/home/minecraft/server.properties</code> enable RCON:</p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">enable-rcon</span><span class="p">=</span><span class="s">true</span>
<span class="py">rcon.port</span><span class="p">=</span><span class="s">25575</span>
<span class="py">rcon.password</span><span class="p">=</span><span class="s">CHANGE_ME</span>
</code></pre></div></div>

<p>Ideally you now want strict permissions on this file since it contains your RCON password in cleartext:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo chmod </span>600 /home/minecraft/server.properties
</code></pre></div></div>

<p>Then you can administer your server with a RCON client like mcrcon:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mcrcon <span class="nt">-H</span> 127.0.0.1 <span class="nt">-P</span> 25575 <span class="nt">-p</span> <span class="s1">'YOUR_RCON_PASSWORD'</span>
</code></pre></div></div>

<h3 id="43-mods">4.3) Mods</h3>

<p>Put your mods in <code class="language-plaintext highlighter-rouge">/home/minecraft/mods</code><br />
I recommend installing these mods:</p>
<ul>
  <li><a href="https://modrinth.com/mod/fabric-api">fabric api</a></li>
  <li><a href="https://modrinth.com/mod/carpet">carpet</a> : For technical minecraft players</li>
  <li><a href="https://modrinth.com/mod/lithium">lithium</a> : For optimizations</li>
</ul>]]></content><author><name>Sam Hadow</name></author><category term="games" /><category term="sysadmin" /><summary type="html"><![CDATA[In this blog post, I’ll describe how to self host a minecraft server on a raspberry Pi 5. This guide assumes you already have a minimal install of debian 13 on your Pi5.]]></summary></entry><entry><title type="html">Self Hosting Element Call</title><link href="https://hadow.fr/blog/self-hosting-element-call.html" rel="alternate" type="text/html" title="Self Hosting Element Call" /><published>2026-07-23T00:00:00+02:00</published><updated>2026-07-23T00:00:00+02:00</updated><id>https://hadow.fr/blog/self-hosting-element-call</id><content type="html" xml:base="https://hadow.fr/blog/self-hosting-element-call.html"><![CDATA[<p>In this blog post I’ll explain how to setup <a href="https://github.com/element-hq/element-call">element call</a> using synapse as a matrix homeserver. This blog post assumes you already followed <a href="https://hadow.fr/blog/self-hosting-a-matrix-instance-with-podman.html">this one</a>.</p>

<h1 id="steps">steps</h1>

<h2 id="1-enabling-experimental-features">1. enabling experimental features</h2>

<p>in the homeserver.yaml in your synapse homeserver add the following:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">experimental_features</span><span class="pi">:</span>
  <span class="c1"># MSC3266: Room summary API. Used for knocking over federation</span>
  <span class="na">msc3266_enabled</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="c1"># MSC4222 needed for syncv2 state_after. This allow clients to</span>
  <span class="c1"># correctly track the state of the room.</span>
  <span class="na">msc4222_enabled</span><span class="pi">:</span> <span class="kc">true</span>

<span class="c1"># The maximum allowed duration by which sent events can be delayed, as</span>
<span class="c1"># per MSC4140.</span>
<span class="na">max_event_delay_duration</span><span class="pi">:</span> <span class="s">24h</span>

<span class="na">rc_message</span><span class="pi">:</span>
  <span class="c1"># This needs to match at least e2ee key sharing frequency plus a bit of headroom</span>
  <span class="c1"># Note key sharing events are bursty</span>
  <span class="na">per_second</span><span class="pi">:</span> <span class="m">0.5</span>
  <span class="na">burst_count</span><span class="pi">:</span> <span class="m">30</span>

<span class="na">rc_delayed_event_mgmt</span><span class="pi">:</span>
  <span class="c1"># This needs to match at least the heart-beat frequency plus a bit of headroom</span>
  <span class="c1"># Currently the heart-beat is every 5 seconds which translates into a rate of 0.2Hz</span>
  <span class="na">per_second</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">burst_count</span><span class="pi">:</span> <span class="m">20</span>
</code></pre></div></div>

<h2 id="2-generating-keys-for-the-livekit-server">2. generating keys for the livekit server</h2>

<p>run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>podman run <span class="nt">--rm</span> livekit/livekit-server:latest generate-keys
</code></pre></div></div>

<p>And write down the API key and API secret.</p>

<h2 id="3-running-the-containers-for-the-livekit">3. running the containers for the livekit</h2>

<p>Don’t forget to replace <code class="language-plaintext highlighter-rouge">YOUR_PUBLIC_IP</code>, <code class="language-plaintext highlighter-rouge">YOUR_API_KEY</code> and <code class="language-plaintext highlighter-rouge">YOUR_API_SECRET</code></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> /home/data/podman/element-call
</code></pre></div></div>

<p>in <code class="language-plaintext highlighter-rouge">/home/data/podman/element-call/config-livekit.yaml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">port</span><span class="pi">:</span> <span class="m">7880</span>

<span class="na">bind_addresses</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">0.0.0.0"</span>

<span class="na">rtc</span><span class="pi">:</span>
  <span class="na">tcp_port</span><span class="pi">:</span> <span class="m">7881</span>
  <span class="na">port_range_start</span><span class="pi">:</span> <span class="m">50100</span>
  <span class="na">port_range_end</span><span class="pi">:</span> <span class="m">50200</span>

  <span class="na">node_ip</span><span class="pi">:</span> <span class="s">YOUR_PUBLIC_IP</span>

<span class="na">room</span><span class="pi">:</span>
  <span class="na">auto_create</span><span class="pi">:</span> <span class="kc">false</span>

<span class="na">logging</span><span class="pi">:</span>
  <span class="na">level</span><span class="pi">:</span> <span class="s">info</span>

<span class="na">turn</span><span class="pi">:</span>
  <span class="na">enabled</span><span class="pi">:</span> <span class="kc">false</span>

<span class="na">keys</span><span class="pi">:</span>
  <span class="na">YOUR_API_KEY</span><span class="pi">:</span> <span class="s">YOUR_API_SECRET</span>
</code></pre></div></div>

<p>For the containers replace <code class="language-plaintext highlighter-rouge">matrixrtc.hadow.fr</code>, <code class="language-plaintext highlighter-rouge">hadow.fr</code> and <code class="language-plaintext highlighter-rouge">synapse.hadow.fr</code> with your own domains.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>podman run <span class="nt">-d</span> <span class="se">\</span>
    <span class="nt">--pod</span><span class="o">=</span>synapse <span class="se">\</span>
    <span class="nt">--name</span><span class="o">=</span>element-call-jwt <span class="se">\</span>
    <span class="nt">--restart</span><span class="o">=</span>unless-stopped <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">LIVEKIT_JWT_BIND</span><span class="o">=</span>:8080 <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">LIVEKIT_URL</span><span class="o">=</span>wss://matrixrtc.hadow.fr/livekit/sfu <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">LIVEKIT_KEY</span><span class="o">=</span>YOUR_API_KEY <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">LIVEKIT_SECRET</span><span class="o">=</span>YOUR_API_SECRET <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">LIVEKIT_FULL_ACCESS_HOMESERVERS</span><span class="o">=</span>hadow.fr <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">MATRIX_HOMESERVER_URL</span><span class="o">=</span>https://synapse.hadow.fr <span class="se">\</span>
    ghcr.io/element-hq/lk-jwt-service:latest
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>podman run <span class="nt">-d</span> <span class="se">\</span>
    <span class="nt">--pod</span><span class="o">=</span>synapse <span class="se">\</span>
    <span class="nt">--name</span><span class="o">=</span>element-call-livekit <span class="se">\</span>
    <span class="nt">--restart</span><span class="o">=</span>unless-stopped <span class="se">\</span>
    <span class="nt">-v</span> /home/data/podman/element-call/config-livekit.yaml:/etc/livekit.yaml:ro,Z <span class="se">\</span>
    docker.io/livekit/livekit-server:latest <span class="se">\</span>
    <span class="nt">--config</span> /etc/livekit.yaml
</code></pre></div></div>

<p>You can now regenerate the files for the systemd services:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/.config/systemd/user/
podman generate systemd <span class="nt">--restart-policy</span><span class="o">=</span>on-failure <span class="nt">--files</span> <span class="nt">--new</span> <span class="nt">--name</span> synapse

systemctl <span class="nt">--user</span> daemon-reload
systemctl <span class="nt">--user</span> restart pod-synapse.service
</code></pre></div></div>

<p>Then complete in <code class="language-plaintext highlighter-rouge">pod-synapse.service</code> with the ports:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nt">-p</span> 8008:8008 <span class="se">\</span>
    <span class="nt">-p</span> 7880:7880 <span class="se">\</span>
    <span class="nt">-p</span> 7881:7881 <span class="se">\</span>
    <span class="nt">-p</span> 50100-50200:50100-50200/udp <span class="se">\</span>
    <span class="nt">-p</span> 8070:8080 <span class="se">\</span>
    <span class="nt">-m</span><span class="o">=</span>2048m
</code></pre></div></div>

<p>Don’t forget to open the UDP range 50100-50200 and the TCP port 7881 in your firewall. For example with nftables:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        #element call
        tcp dport 7881 accept
        udp dport 50100-50200 accept
</code></pre></div></div>

<h2 id="4-nginx-configuration-files">4. nginx configuration files</h2>

<p>In a nginx configuration file (for example in <code class="language-plaintext highlighter-rouge">/etc/nginx/sites-available/matrixrtc.conf</code>):</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span>
    <span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span><span class="p">;</span>
    <span class="kn">listen</span> <span class="s">[::]:443</span> <span class="s">ssl</span><span class="p">;</span>
    <span class="kn">server_name</span> <span class="s">matrixrtc.hadow.fr</span><span class="p">;</span>

    <span class="kn">ssl_certificate</span> <span class="n">/etc/letsencrypt/live/hadow.fr/fullchain.pem</span><span class="p">;</span>
    <span class="kn">ssl_certificate_key</span> <span class="n">/etc/letsencrypt/live/hadow.fr/privkey.pem</span><span class="p">;</span>

    <span class="kn">location</span> <span class="s">^~</span> <span class="n">/livekit/jwt/</span> <span class="p">{</span>
      <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$host</span><span class="p">;</span>
      <span class="kn">proxy_set_header</span> <span class="s">X-Real-IP</span> <span class="nv">$remote_addr</span><span class="p">;</span>
      <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$proxy_add_x_forwarded_for</span><span class="p">;</span>
      <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-Proto</span> <span class="nv">$scheme</span><span class="p">;</span>
      <span class="kn">proxy_pass</span> <span class="s">http://127.0.0.1:8070/</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kn">location</span> <span class="s">^~</span> <span class="n">/livekit/sfu/</span> <span class="p">{</span>
      <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$host</span><span class="p">;</span>
      <span class="kn">proxy_set_header</span> <span class="s">X-Real-IP</span> <span class="nv">$remote_addr</span><span class="p">;</span>
      <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$proxy_add_x_forwarded_for</span><span class="p">;</span>
      <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-Proto</span> <span class="nv">$scheme</span><span class="p">;</span>
      <span class="kn">proxy_send_timeout</span> <span class="mi">120</span><span class="p">;</span>
      <span class="kn">proxy_read_timeout</span> <span class="mi">120</span><span class="p">;</span>
      <span class="kn">proxy_buffering</span> <span class="no">off</span><span class="p">;</span>
      <span class="kn">proxy_set_header</span> <span class="s">Accept-Encoding</span> <span class="s">gzip</span><span class="p">;</span>
      <span class="kn">proxy_set_header</span> <span class="s">Upgrade</span> <span class="nv">$http_upgrade</span><span class="p">;</span>
      <span class="kn">proxy_set_header</span> <span class="s">Connection</span> <span class="s">"upgrade"</span><span class="p">;</span>
      <span class="kn">proxy_pass</span> <span class="s">http://127.0.0.1:7880/</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In your well known in <code class="language-plaintext highlighter-rouge">/etc/nginx/sites-available/http.conf</code> modify the snippet with the following:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">location</span> <span class="n">/.well-known/matrix/client</span> <span class="p">{</span>
        <span class="kn">return</span> <span class="mi">200</span> <span class="s">'</span><span class="p">{</span><span class="kn">"m.homeserver":</span> <span class="p">{</span><span class="kn">"base_url":</span> <span class="s">"https://synapse.hadow.fr"</span><span class="err">}</span><span class="s">,"m.identity_server":</span> <span class="p">{</span><span class="kn">"base_url":</span> <span class="s">"https://vector.im"</span><span class="err">}</span><span class="s">,"org.matrix.msc4143.rtc_foci":</span> <span class="s">[</span><span class="p">{</span><span class="kn">"type":</span> <span class="s">"livekit","livekit_service_url":</span> <span class="s">"https://matrixrtc.hadow.fr/livekit/jwt"</span><span class="err">}</span><span class="s">]</span><span class="err">}</span><span class="s">'</span><span class="p">;</span>
        <span class="kn">add_header</span> <span class="s">Content-Type</span> <span class="nc">application/json</span><span class="p">;</span>
        <span class="kn">add_header</span> <span class="s">"Access-Control-Allow-Origin"</span> <span class="s">*</span><span class="p">;</span>
    <span class="p">}</span>
</code></pre></div></div>]]></content><author><name>Sam Hadow</name></author><category term="messaging" /><category term="podman" /><category term="sysadmin" /><summary type="html"><![CDATA[In this blog post I’ll explain how to setup element call using synapse as a matrix homeserver. This blog post assumes you already followed this one.]]></summary></entry><entry><title type="html">Self Hosting A Matrix Instance With Podman</title><link href="https://hadow.fr/blog/self-hosting-a-matrix-instance-with-podman.html" rel="alternate" type="text/html" title="Self Hosting A Matrix Instance With Podman" /><published>2026-04-30T00:00:00+02:00</published><updated>2026-04-30T00:00:00+02:00</updated><id>https://hadow.fr/blog/self-hosting-a-matrix-instance-with-podman</id><content type="html" xml:base="https://hadow.fr/blog/self-hosting-a-matrix-instance-with-podman.html"><![CDATA[<p>In this blog post I’ll describe how to self host a <a href="https://matrix.org/">matrix.org</a> instance using podman. The homeserver used will be synapse.</p>

<h1 id="what-are-matrixorg-and-synapse">What are matrix.org and synapse?</h1>

<p>First a short introduction:<br />
matrix.org is an open source, secure and decentralized communication protocol. Secure because it defines how end to end encryption should be implemented in the clients.<br />
With matrix.org you have <a href="https://matrix.org/ecosystem/servers/">homeservers</a> and <a href="https://matrix.org/ecosystem/clients/">clients</a>. Homeservers are what the clients connect to and can federate with each other <em>(meaning someone with an account on homeserver A can talk to someone with an account on homeserver B)</em>.<br />
<a href="https://matrix.org/ecosystem/bridges/">Bridges</a> to other messaging services can also be hosted alongside a homeserver. Hosting your own homeserver allows you to host the bridges you want.<br />
In this blog post I explain how to self host a specific homeserver, which is maintained by the matrix.org foundation: synapse.</p>

<h1 id="self-hosting-synapse">self hosting synapse</h1>

<h2 id="initial-setup">initial setup</h2>

<h3 id="creating-secret-and-directories">creating secret and directories</h3>

<p>Create the database secret not to expose it in your configuration files:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="nt">-n</span> <span class="s2">"&lt;postgres-pass&gt;"</span> <span class="o">&gt;</span> /tmp/secret
podman secret create synapse_postgres_pass /tmp/secret
</code></pre></div></div>

<p>Adapt the path to your own path for the directories:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> /home/data/podman/synapse/<span class="o">{</span>db,config,media,logs<span class="o">}</span>
</code></pre></div></div>

<h3 id="generating-the-configuration-file">generating the configuration file</h3>

<p><em>(adapt your-domain to your own, for example: example.org)</em></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>podman pod create <span class="nt">--name</span> synapse <span class="nt">-p</span> 8008:8008 <span class="nt">-m</span><span class="o">=</span>2048m

podman run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
    <span class="nt">-v</span> /home/data/podman/synapse/config:/data:Z <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">SYNAPSE_SERVER_NAME</span><span class="o">=</span>&lt;your-domain&gt; <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">SYNAPSE_REPORT_STATS</span><span class="o">=</span>no <span class="se">\</span>
    docker.io/matrixdotorg/synapse:latest generate
</code></pre></div></div>

<h3 id="modifying-the-configuration-file">modifying the configuration file</h3>

<p>The previous command will generate a homeserver.yaml file. You’ll have to modify this file before using synapse. You’ll have to modify at least the following parts:</p>

<ul>
  <li>modify the database part <em>(replace the password)</em>
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">database</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">psycopg2</span>
<span class="na">txn_limit</span><span class="pi">:</span> <span class="m">10000</span>
<span class="na">args</span><span class="pi">:</span>
  <span class="na">user</span><span class="pi">:</span> <span class="s">synapse</span>
  <span class="na">password</span><span class="pi">:</span> <span class="s">&lt;POSTGRES_PASSWORD&gt;</span>
  <span class="na">dbname</span><span class="pi">:</span> <span class="s">synapse</span>
  <span class="na">host</span><span class="pi">:</span> <span class="s">127.0.0.1</span>
  <span class="na">port</span><span class="pi">:</span> <span class="m">5432</span>
  <span class="na">cp_min</span><span class="pi">:</span> <span class="m">5</span>
  <span class="na">cp_max</span><span class="pi">:</span> <span class="m">10</span>
</code></pre></div>    </div>
  </li>
  <li>set <code class="language-plaintext highlighter-rouge">enable_registration</code> to false unless you want users to register freely on your instance.</li>
  <li>set a shared secret file with <code class="language-plaintext highlighter-rouge">registration_shared_secret_path</code> to have access to an <a href="https://element-hq.github.io/synapse/latest/admin_api/register_api.html">API</a> to create users. Be sure to use a secure secret as anyone having this secret can register on your instance as an admin. Also keep in mind the path is relative to the container, not the host.</li>
</ul>

<h3 id="reverse-proxy">reverse proxy</h3>

<p>In this guide the port used is 8008, we’ll use nginx to server the synapse homeserver on the port 443.<br />
<em>(adapt your-domain to your own)</em><br />
This part is important for users to have the name user@your-domain while hosting synapse on a subdomain. And it’s also important for clients and other homeserver to recognize your server.</p>

<p>in sites-available:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># synapse.conf</span>
<span class="k">server</span> <span class="p">{</span>
    <span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span><span class="p">;</span>
    <span class="kn">listen</span> <span class="s">[::]:443</span> <span class="s">ssl</span><span class="p">;</span>

    <span class="kn">server_name</span> <span class="s">synapse.&lt;your-domain&gt;</span><span class="p">;</span>

    <span class="kn">ssl_certificate</span> <span class="n">/etc/letsencrypt/live/&lt;your-domain&gt;/fullchain.pem</span><span class="p">;</span>
    <span class="kn">ssl_certificate_key</span> <span class="n">/etc/letsencrypt/live/&lt;your-domain&gt;/privkey.pem</span><span class="p">;</span>

    <span class="kn">location</span> <span class="n">/.well-known/matrix/server</span> <span class="p">{</span>
      <span class="kn">return</span> <span class="mi">200</span> <span class="s">'</span><span class="p">{</span><span class="kn">"m.server":</span> <span class="s">"synapse.&lt;your-domain&gt;:443"</span><span class="err">}</span><span class="s">'</span><span class="p">;</span>
      <span class="kn">add_header</span> <span class="s">Content-Type</span> <span class="nc">application/json</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="kn">location</span> <span class="n">/.well-known/matrix/client</span> <span class="p">{</span>
      <span class="kn">return</span> <span class="mi">200</span> <span class="s">'</span><span class="p">{</span><span class="kn">"m.homeserver":</span> <span class="p">{</span><span class="kn">"base_url":</span> <span class="s">"https://synapse.&lt;your-domain&gt;"</span><span class="err">}</span><span class="s">,"m.identity_server":</span> <span class="p">{</span><span class="kn">"base_url":</span> <span class="s">"https://vector.im"</span><span class="err">}}</span><span class="s">'</span><span class="p">;</span>
      <span class="kn">add_header</span> <span class="s">Content-Type</span> <span class="nc">application/json</span><span class="p">;</span>
      <span class="kn">add_header</span> <span class="s">"Access-Control-Allow-Origin"</span> <span class="s">*</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
        <span class="kn">proxy_pass</span> <span class="s">http://127.0.0.1:8008</span><span class="p">;</span>
        <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$remote_addr</span><span class="p">;</span>
        <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-Proto</span> <span class="nv">$scheme</span><span class="p">;</span>
        <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$host</span><span class="p">;</span>

        <span class="c1"># Nginx by default only allows file uploads up to 1M in size</span>
        <span class="c1"># Increase client_max_body_size to match max_upload_size defined in homeserver.yaml</span>
        <span class="kn">client_max_body_size</span> <span class="mi">256M</span><span class="p">;</span>

        <span class="c1"># Synapse responses may be chunked, which is an HTTP/1.1 feature.</span>
        <span class="kn">proxy_http_version</span> <span class="mf">1.1</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kn">add_header</span> <span class="s">Strict-Transport-Security</span> <span class="s">"max-age=31536000</span><span class="p">;</span> <span class="kn">includeSubDomains"</span> <span class="s">always</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In your http.conf add this snippet in the https server block:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">location</span> <span class="n">/.well-known/matrix/server</span> <span class="p">{</span>
    <span class="kn">return</span> <span class="mi">200</span> <span class="s">'</span><span class="p">{</span><span class="kn">"m.server":</span> <span class="s">"synapse.&lt;your-domain&gt;:443"</span><span class="err">}</span><span class="s">'</span><span class="p">;</span>
    <span class="kn">add_header</span> <span class="s">Content-Type</span> <span class="nc">application/json</span><span class="p">;</span>
    <span class="kn">add_header</span> <span class="s">"Access-Control-Allow-Origin"</span> <span class="s">*</span><span class="p">;</span>
<span class="p">}</span>

<span class="kn">location</span> <span class="n">/.well-known/matrix/client</span> <span class="p">{</span>
    <span class="kn">return</span> <span class="mi">200</span> <span class="s">'</span><span class="p">{</span><span class="kn">"m.homeserver":</span> <span class="p">{</span><span class="kn">"base_url":</span> <span class="s">"https://synapse.&lt;your-domain&gt;"</span><span class="err">}</span><span class="s">,"m.identity_server":</span> <span class="p">{</span><span class="kn">"base_url":</span> <span class="s">"https://vector.im"</span><span class="err">}}</span><span class="s">'</span><span class="p">;</span>
    <span class="kn">add_header</span> <span class="s">Content-Type</span> <span class="nc">application/json</span><span class="p">;</span>
    <span class="kn">add_header</span> <span class="s">"Access-Control-Allow-Origin"</span> <span class="s">*</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="running-the-pod">running the pod</h2>

<h3 id="with-podman-command-line">with podman command line</h3>

<p>Again adapt the path:<br />
<em>The database version can be different but it needs to be pinned to a specific version to avoid issues with updates. You’ll have to manually update this container.</em></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>podman run <span class="nt">-d</span> <span class="nt">--pod</span><span class="o">=</span>synapse <span class="se">\</span>
    <span class="nt">--secret</span> synapse_postgres_pass,type<span class="o">=</span><span class="nb">env</span>,target<span class="o">=</span>POSTGRES_PASSWORD <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">POSTGRES_DB</span><span class="o">=</span><span class="s2">"synapse"</span> <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">POSTGRES_USER</span><span class="o">=</span><span class="s2">"synapse"</span> <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">POSTGRES_INITDB_ARGS</span><span class="o">=</span><span class="s2">"--encoding=UTF-8 --lc-collate=C --lc-ctype=C"</span> <span class="se">\</span>
    <span class="nt">-v</span> /home/data/podman/synapse/db:/var/lib/postgresql/data:Z <span class="se">\</span>
    <span class="nt">--name</span><span class="o">=</span>synapse-db <span class="se">\</span>
    docker.io/library/postgres:16

podman run <span class="nt">-d</span> <span class="nt">--pod</span><span class="o">=</span>synapse <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">SYNAPSE_CONFIG_PATH</span><span class="o">=</span>/data/homeserver.yaml <span class="se">\</span>
    <span class="nt">-v</span> /home/data/podman/synapse/config:/data:Z <span class="se">\</span>
    <span class="nt">-v</span> /home/data/podman/synapse/media:/data/media:z <span class="se">\</span>
    <span class="nt">-v</span> /home/data/podman/synapse/logs:/data/logs:Z <span class="se">\</span>
    <span class="nt">--name</span><span class="o">=</span>synapse-app <span class="se">\</span>
    <span class="nt">--label</span> io.containers.autoupdate<span class="o">=</span>registry docker.io/matrixdotorg/synapse:latest
</code></pre></div></div>

<p>Then you can generate the systemd services:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/.config/systemd/user/
podman generate systemd <span class="nt">--restart-policy</span><span class="o">=</span>on-failure <span class="nt">--files</span> <span class="nt">--new</span> <span class="nt">--name</span> synapse

systemctl <span class="nt">--user</span> daemon-reload
systemctl <span class="nt">--user</span> <span class="nb">enable</span> <span class="nt">--now</span> pod-synapse.service
</code></pre></div></div>

<h3 id="with-systemd-services-directly">with systemd services directly</h3>

<p>Create these files in <code class="language-plaintext highlighter-rouge">~/.config/systemd/user/</code>.</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># pod-synapse.service
</span><span class="w">
</span><span class="nn">[Unit]</span><span class="w">
</span><span class="py">Description</span><span class="p">=</span><span class="s">Podman pod-synapse.service</span>
<span class="py">Documentation</span><span class="p">=</span><span class="s">man:podman-generate-systemd(1)</span>
<span class="py">Wants</span><span class="p">=</span><span class="s">network-online.target</span>
<span class="py">After</span><span class="p">=</span><span class="s">network-online.target</span>
<span class="py">RequiresMountsFor</span><span class="p">=</span><span class="s">/run/user/1000/containers</span>
<span class="py">Wants</span><span class="p">=</span><span class="s">container-synapse-app.service container-synapse-db.service</span>
<span class="py">Before</span><span class="p">=</span><span class="s">container-synapse-app.service container-synapse-db.service</span>
<span class="w">
</span><span class="nn">[Service]</span><span class="w">
</span><span class="py">Environment</span><span class="p">=</span><span class="s">PODMAN_SYSTEMD_UNIT=%n</span>
<span class="py">Restart</span><span class="p">=</span><span class="s">on-failure</span>
<span class="py">TimeoutStopSec</span><span class="p">=</span><span class="s">70</span>
<span class="py">ExecStartPre</span><span class="p">=</span><span class="s">/usr/bin/podman pod create </span><span class="se">\
</span><span class="w">	</span><span class="s">--infra-conmon-pidfile %t/pod-synapse.pid </span><span class="se">\
</span><span class="w">	</span><span class="s">--pod-id-file %t/pod-synapse.pod-id </span><span class="se">\
</span><span class="w">	</span><span class="s">--exit-policy=stop </span><span class="se">\
</span><span class="w">	</span><span class="s">--name synapse </span><span class="se">\
</span><span class="w">	</span><span class="s">-p 8008:8008 </span><span class="se">\
</span><span class="w">	</span><span class="s">-m=2048m </span><span class="se">\
</span><span class="w">	</span><span class="s">--replace</span>
<span class="py">ExecStart</span><span class="p">=</span><span class="s">/usr/bin/podman pod start </span><span class="se">\
</span><span class="w">	</span><span class="s">--pod-id-file %t/pod-synapse.pod-id</span>
<span class="py">ExecStop</span><span class="p">=</span><span class="s">/usr/bin/podman pod stop </span><span class="se">\
</span><span class="w">	</span><span class="s">--ignore </span><span class="se">\
</span><span class="w">	</span><span class="s">--pod-id-file %t/pod-synapse.pod-id  </span><span class="se">\
</span><span class="w">	</span><span class="s">-t 10</span>
<span class="py">ExecStopPost</span><span class="p">=</span><span class="s">/usr/bin/podman pod rm </span><span class="se">\
</span><span class="w">	</span><span class="s">--ignore </span><span class="se">\
</span><span class="w">	</span><span class="s">-f </span><span class="se">\
</span><span class="w">	</span><span class="s">--pod-id-file %t/pod-synapse.pod-id</span>
<span class="py">PIDFile</span><span class="p">=</span><span class="s">%t/pod-synapse.pid</span>
<span class="py">Type</span><span class="p">=</span><span class="s">forking</span>
<span class="w">
</span><span class="nn">[Install]</span><span class="w">
</span><span class="py">WantedBy</span><span class="p">=</span><span class="s">default.target</span>
</code></pre></div></div>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># container-synapse-db.service
</span><span class="nn">[Unit]</span><span class="w">
</span><span class="py">Description</span><span class="p">=</span><span class="s">Podman container-synapse-db.service</span>
<span class="py">Documentation</span><span class="p">=</span><span class="s">man:podman-generate-systemd(1)</span>
<span class="py">Wants</span><span class="p">=</span><span class="s">network-online.target</span>
<span class="py">After</span><span class="p">=</span><span class="s">network-online.target</span>
<span class="py">RequiresMountsFor</span><span class="p">=</span><span class="s">%t/containers</span>
<span class="py">BindsTo</span><span class="p">=</span><span class="s">pod-synapse.service</span>
<span class="py">After</span><span class="p">=</span><span class="s">pod-synapse.service</span>
<span class="w">
</span><span class="nn">[Service]</span><span class="w">
</span><span class="py">Environment</span><span class="p">=</span><span class="s">PODMAN_SYSTEMD_UNIT=%n</span>
<span class="py">Restart</span><span class="p">=</span><span class="s">on-failure</span>
<span class="py">TimeoutStopSec</span><span class="p">=</span><span class="s">70</span>
<span class="py">ExecStart</span><span class="p">=</span><span class="s">/usr/bin/podman run </span><span class="se">\
</span><span class="w">	</span><span class="s">--cidfile=%t/%n.ctr-id </span><span class="se">\
</span><span class="w">	</span><span class="s">--cgroups=no-conmon </span><span class="se">\
</span><span class="w">	</span><span class="s">--rm </span><span class="se">\
</span><span class="w">	</span><span class="s">--pod-id-file %t/pod-synapse.pod-id </span><span class="se">\
</span><span class="w">	</span><span class="s">--sdnotify=conmon </span><span class="se">\
</span><span class="w">	</span><span class="s">--replace </span><span class="se">\
</span><span class="w">	</span><span class="s">-d </span><span class="se">\
</span><span class="w">	</span><span class="s">--secret synapse_postgres_pass,type=env,target=POSTGRES_PASSWORD </span><span class="se">\
</span><span class="w">	</span><span class="s">-e POSTGRES_DB=synapse </span><span class="se">\
</span><span class="w">	</span><span class="s">-e POSTGRES_USER=synapse </span><span class="se">\
</span><span class="w">	</span><span class="s">-e "POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C" </span><span class="se">\
</span><span class="w">	</span><span class="s">-v /home/data/podman/synapse/db:/var/lib/postgresql/data:Z </span><span class="se">\
</span><span class="w">	</span><span class="s">--name=synapse-db </span><span class="se">\
</span><span class="w">	</span><span class="s">docker.io/library/postgres:16</span>
<span class="py">ExecStop</span><span class="p">=</span><span class="s">/usr/bin/podman stop </span><span class="se">\
</span><span class="w">	</span><span class="s">--ignore -t 10 </span><span class="se">\
</span><span class="w">	</span><span class="s">--cidfile=%t/%n.ctr-id</span>
<span class="py">ExecStopPost</span><span class="p">=</span><span class="s">/usr/bin/podman rm </span><span class="se">\
</span><span class="w">	</span><span class="s">-f </span><span class="se">\
</span><span class="w">	</span><span class="s">--ignore -t 10 </span><span class="se">\
</span><span class="w">	</span><span class="s">--cidfile=%t/%n.ctr-id</span>
<span class="py">Type</span><span class="p">=</span><span class="s">notify</span>
<span class="py">NotifyAccess</span><span class="p">=</span><span class="s">all</span>
<span class="w">
</span><span class="nn">[Install]</span><span class="w">
</span><span class="py">WantedBy</span><span class="p">=</span><span class="s">default.target</span>
</code></pre></div></div>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># container-synapse-app.service
</span><span class="w">
</span><span class="nn">[Unit]</span><span class="w">
</span><span class="py">Description</span><span class="p">=</span><span class="s">Podman container-synapse-app.service</span>
<span class="py">Documentation</span><span class="p">=</span><span class="s">man:podman-generate-systemd(1)</span>
<span class="py">Wants</span><span class="p">=</span><span class="s">network-online.target</span>
<span class="py">After</span><span class="p">=</span><span class="s">network-online.target</span>
<span class="py">RequiresMountsFor</span><span class="p">=</span><span class="s">%t/containers</span>
<span class="py">BindsTo</span><span class="p">=</span><span class="s">pod-synapse.service</span>
<span class="py">After</span><span class="p">=</span><span class="s">pod-synapse.service</span>
<span class="w">
</span><span class="nn">[Service]</span><span class="w">
</span><span class="py">Environment</span><span class="p">=</span><span class="s">PODMAN_SYSTEMD_UNIT=%n</span>
<span class="py">Restart</span><span class="p">=</span><span class="s">on-failure</span>
<span class="py">TimeoutStopSec</span><span class="p">=</span><span class="s">70</span>
<span class="py">ExecStart</span><span class="p">=</span><span class="s">/usr/bin/podman run </span><span class="se">\
</span><span class="w">	</span><span class="s">--cidfile=%t/%n.ctr-id </span><span class="se">\
</span><span class="w">	</span><span class="s">--cgroups=no-conmon </span><span class="se">\
</span><span class="w">	</span><span class="s">--rm </span><span class="se">\
</span><span class="w">	</span><span class="s">--pod-id-file %t/pod-synapse.pod-id </span><span class="se">\
</span><span class="w">	</span><span class="s">--sdnotify=conmon </span><span class="se">\
</span><span class="w">	</span><span class="s">--replace </span><span class="se">\
</span><span class="w">	</span><span class="s">-d </span><span class="se">\
</span><span class="w">	</span><span class="s">-e SYNAPSE_CONFIG_PATH=/data/homeserver.yaml </span><span class="se">\
</span><span class="w">	</span><span class="s">-v /home/data/podman/synapse/config:/data:Z </span><span class="se">\
</span><span class="w">	</span><span class="s">-v /home/data/podman/synapse/media:/data/media:z </span><span class="se">\
</span><span class="w">	</span><span class="s">-v /home/data/podman/synapse/logs:/data/logs:Z </span><span class="se">\
</span><span class="w">	</span><span class="s">--name=synapse-app </span><span class="se">\
</span><span class="w">	</span><span class="s">--label io.containers.autoupdate=registry docker.io/matrixdotorg/synapse:latest</span>
<span class="py">ExecStop</span><span class="p">=</span><span class="s">/usr/bin/podman stop </span><span class="se">\
</span><span class="w">	</span><span class="s">--ignore -t 10 </span><span class="se">\
</span><span class="w">	</span><span class="s">--cidfile=%t/%n.ctr-id</span>
<span class="py">ExecStopPost</span><span class="p">=</span><span class="s">/usr/bin/podman rm </span><span class="se">\
</span><span class="w">	</span><span class="s">-f </span><span class="se">\
</span><span class="w">	</span><span class="s">--ignore -t 10 </span><span class="se">\
</span><span class="w">	</span><span class="s">--cidfile=%t/%n.ctr-id</span>
<span class="py">Type</span><span class="p">=</span><span class="s">notify</span>
<span class="py">NotifyAccess</span><span class="p">=</span><span class="s">all</span>
<span class="w">
</span><span class="nn">[Install]</span><span class="w">
</span><span class="py">WantedBy</span><span class="p">=</span><span class="s">default.target</span>
</code></pre></div></div>

<h2 id="managing-the-homeserver">managing the homeserver</h2>

<h3 id="registering-users">registering users</h3>

<p>With access to the server, creating the user interactively:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>podman <span class="nb">exec</span> <span class="nt">-it</span> synapse-app /bin/sh
register_new_matrix_user <span class="nt">--user</span> &lt;username&gt; <span class="nt">--config</span> /data/homeserver.yaml
</code></pre></div></div>

<p>Or with the registration API, you can use <a href="https://git.hadow.fr/sam.hadow/Useful-scripts/src/branch/main/create_matrix_account.sh">this script</a>.</p>

<h2 id="what-about-bridges">what about bridges?</h2>

<p>Bridges allow you to receive message from other services in matrix. In this section I’ll give an example on how to self host a bridge alongside your matrix instance with the discord bridge.</p>

<h3 id="discord-bridge-example">discord bridge example</h3>

<p>Creating the initial bridge files:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> /home/data/podman/synapse/mautrix-discord
podman run <span class="nt">--rm</span> <span class="nt">-v</span> /home/data/podman/synapse/mautrix-discord:/data:z dock.mau.dev/mautrix/discord:latest
</code></pre></div></div>

<p>Create the database: <em>(adapt the password)</em></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>podman <span class="nb">exec</span> <span class="nt">-it</span> synapse-db /bin/bash
psql <span class="nt">-U</span> synapse
CREATE USER discordmautrix WITH PASSWORD <span class="s1">'password'</span><span class="p">;</span>
CREATE DATABASE discordmautrix WITH OWNER <span class="o">=</span> <span class="s1">'discordmautrix'</span> <span class="p">;</span>
</code></pre></div></div>

<p>Then delete synapse container and recreate it with the bridge registration file: <em>(don’t forget to regenerate, or adapt, the systemd service)</em></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>podman run <span class="nt">-d</span> <span class="nt">--pod</span><span class="o">=</span>synapse <span class="se">\</span>
<span class="nt">-e</span> <span class="nv">SYNAPSE_CONFIG_PATH</span><span class="o">=</span>/data/homeserver.yaml <span class="se">\</span>
<span class="nt">-v</span> /home/data/podman/synapse/config:/data:Z <span class="se">\</span>
<span class="nt">-v</span> /home/data/podman/synapse/media:/data/media:z <span class="se">\</span>
<span class="nt">-v</span> /home/data/podman/synapse/logs:/data/logs:Z <span class="se">\</span>
<span class="nt">-v</span> /home/data/podman/synapse/mautrix-discord-registration.yaml:/data/mautrix-discord-registration.yaml:Z <span class="se">\</span>
<span class="nt">--name</span><span class="o">=</span>synapse-app <span class="se">\</span>
<span class="nt">--label</span> io.containers.autoupdate<span class="o">=</span>registry docker.io/matrixdotorg/synapse:latest
</code></pre></div></div>

<p>And for the bridge:</p>

<p>Adapt the file <code class="language-plaintext highlighter-rouge">/home/data/podman/synapse/mautrix-discord/config.yaml</code>:</p>

<p>for the database part:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="na">database</span><span class="pi">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">postgres</span>
        <span class="na">uri</span><span class="pi">:</span> <span class="s">postgres://discordmautrix:password:5432/discordmautrix?sslmode=disable</span>
</code></pre></div></div>

<p>Then run the bridge:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>podman run <span class="nt">-d</span> <span class="nt">--pod</span><span class="o">=</span>synapse <span class="se">\</span>
<span class="nt">-v</span> /home/data/podman/synapse/mautrix-discord:/data:z <span class="se">\</span>
<span class="nt">--name</span><span class="o">=</span>synapse-discord-mautrix <span class="se">\</span>
<span class="nt">--label</span> io.containers.autoupdate<span class="o">=</span>registry dock.mau.dev/mautrix/discord:latest
</code></pre></div></div>

<h4 id="using-the-bridge">using the bridge</h4>

<p>You’ll then have access to a user “discord bridge bot” on your instance where you can log in into your discord account and then bridge your messages.</p>]]></content><author><name>Sam Hadow</name></author><category term="messaging" /><category term="podman" /><category term="sysadmin" /><summary type="html"><![CDATA[In this blog post I’ll describe how to self host a matrix.org instance using podman. The homeserver used will be synapse.]]></summary></entry><entry><title type="html">3d Printed And Handwired 60% Keyboard</title><link href="https://hadow.fr/blog/3d-printed-and-handwired-60-keyboard.html" rel="alternate" type="text/html" title="3d Printed And Handwired 60% Keyboard" /><published>2026-03-31T00:00:00+02:00</published><updated>2026-03-31T00:00:00+02:00</updated><id>https://hadow.fr/blog/3d-printed-and-handwired-60%25-keyboard</id><content type="html" xml:base="https://hadow.fr/blog/3d-printed-and-handwired-60-keyboard.html"><![CDATA[<p>I’ll describe in this short blog post how I 3d printed (not including the keycaps this time) a 60% keyboard (61 keys). Then hanwired it, and installed a firmware with ZMK on it. It is similar to the <a href="/blog/3d-printed-and-handwired-numpad.html">numpad</a>, just a bigger project. Read the numpad post first if you want more detailed instructions on how to handwire a keyboard.</p>

<h1 id="steps">steps</h1>

<h2 id="3d-printing-parts">3d printing parts</h2>

<p>Just like for the numpad I designed the parts with <a href="https://openscad.org/">openscad</a>, the parts and bash scripts are in the same repository <a href="https://git.hadow.fr/sam.hadow/keyboard_scad">here</a>.<br />
The script <code class="language-plaintext highlighter-rouge">keyboard60.sh</code> can be used to generate the stl files for the backplate and case. They’re both cut in half to make them fit on my 3d printer plate.<br />
If you want to 3d print the keycaps, there is a script too. But I personally used keycaps I already had on the side instead, mostly to see the letters better on the keycaps. The side engraving on my 3d printed keycaps is not that visible.<br />
I printed the parts with my bambulab P1S 3d printer, I used a 0.4mm nozzle and a 0.16mm layer height, white PETG filament as the material.<br />
I used T8000 glue to glue the parts.</p>

<p>The backplate looks like this before gluing the two halves:</p>

<p><img src="/assets/img/2026-03-31-3d-printed-and-handwired-60-keyboard/1.png" alt="Picture 1" /></p>

<p>With the switches mounted (I used MMD holy panda switches this time too):</p>

<p><img src="/assets/img/2026-03-31-3d-printed-and-handwired-60-keyboard/2.png" alt="Picture 2" /></p>

<h2 id="handwiring-the-keyboard">handwiring the keyboard</h2>

<p>Just like with the numpad we first need to bend the diodes so that it’s easier to solder them to the switches. I then soldered the rows with the legs of the diodes and cut the extra length after the anode.<br />
The keyboard is handwired columns to rows, so the diodes are soldered with the anodes soldered to the switches pins (the red part here). I used 1N4148 diodes as they’re very cheap.</p>

<p><img src="/assets/img/2026-03-31-3d-printed-and-handwired-60-keyboard/3.png" alt="Picture 3" /></p>

<p>And then after a few hours of soldering, and gluing the two haves together:<br />
(I also mounted the stabilizers, this time I used cheap plated mounted stabilizers.)</p>

<p><img src="/assets/img/2026-03-31-3d-printed-and-handwired-60-keyboard/4.png" alt="Picture 4" /></p>

<p><img src="/assets/img/2026-03-31-3d-printed-and-handwired-60-keyboard/5.png" alt="Picture 5" /></p>

<p>I used yellow and green wires alternating them between the columns to have an easier time soldering them to the microcontroller and writing the firmware after. I also used a pen to mark where to strip the wire to solder it to the pins. It was more convenient than just using my thumb to hold the wire where I have to strip it.</p>

<p>And this time too I used a Pro Micro NRF52840.</p>

<h2 id="flashing-the-firmware">flashing the firmware</h2>

<p>I used ZMK to flash the firmware, you can find the ZMK configuration in <a href="https://git.hadow.fr/sam.hadow/zmk-config">this repository</a> and <a href="https://github.com/Sam-Hadow/zmk-config">this one</a> for github action to automate the build process. This keyboard is named <code class="language-plaintext highlighter-rouge">keeb60_3d</code>. Like in the previous blog post, I recommand flashing the firmware before clipping the backplate in the case as you might have to reset the microcontroller to flash a fixed firmware.</p>

<h2 id="assembling-the-keyboard">assembling the keyboard</h2>

<p>I glued the two halves of the keyboard, taped the battery on the side of the case, soldered it to the microcontroller and the power switch of the keyboard.<br />
Like last time the switch is a 15mm round switch like <a href="https://aliexpress.com/item/1005009714438836.html">this</a> one. And the battery is a 3.7V LiPo battery with a 320mAh capacity (for the bluetooth).</p>

<p><img src="/assets/img/2026-03-31-3d-printed-and-handwired-60-keyboard/6.png" alt="Picture 6" /></p>

<p>I then glued the microcontroller in its holder, clipped in the backplate to the holder and mounted the keycaps.</p>

<h1 id="final-result">final result</h1>

<p>The keyboard:</p>

<p><img src="/assets/img/2026-03-31-3d-printed-and-handwired-60-keyboard/7.png" alt="Picture 7" /></p>

<p>The power switch and USB C port in the back:</p>

<p><img src="/assets/img/2026-03-31-3d-printed-and-handwired-60-keyboard/8.png" alt="Picture 8" /></p>]]></content><author><name>Sam Hadow</name></author><category term="3dprinting" /><category term="hardware" /><category term="soldering" /><summary type="html"><![CDATA[I’ll describe in this short blog post how I 3d printed (not including the keycaps this time) a 60% keyboard (61 keys). Then hanwired it, and installed a firmware with ZMK on it. It is similar to the numpad, just a bigger project. Read the numpad post first if you want more detailed instructions on how to handwire a keyboard.]]></summary></entry><entry><title type="html">3d Printed And Handwired Numpad</title><link href="https://hadow.fr/blog/3d-printed-and-handwired-numpad.html" rel="alternate" type="text/html" title="3d Printed And Handwired Numpad" /><published>2026-03-16T00:00:00+01:00</published><updated>2026-03-16T00:00:00+01:00</updated><id>https://hadow.fr/blog/3d-printed-and-handwired-numpad</id><content type="html" xml:base="https://hadow.fr/blog/3d-printed-and-handwired-numpad.html"><![CDATA[<p>In this blog post I’ll describe how I (mostly) 3d printed a numpad (21 keys keyboard), hanwired it, and installed a firmware with ZMK on it.</p>

<h1 id="steps">steps</h1>

<h2 id="3d-printing-parts">3d printing parts</h2>

<p>I designed the parts with <a href="https://openscad.org/">openscad</a>, the repository is available <a href="https://git.hadow.fr/sam.hadow/keyboard_scad">here</a>.<br />
I used the script <code class="language-plaintext highlighter-rouge">keycaps_numpad.sh</code> and the openscad script <code class="language-plaintext highlighter-rouge">xda_keycap.scad</code> to generate the stl files for the keycaps with an engraving on the side for each keycap. Then generated the stl files for the backplate and case with the files <code class="language-plaintext highlighter-rouge">backplate_numpad.scad</code> and <code class="language-plaintext highlighter-rouge">case_numpad.scad</code>.<br />
I then printed them with my bambulab P1S 3d printer, I used a 0.4mm nozzle and a 0.08mm layer height. I used white PETG filament to print all the parts. All the parts took about 12 hours to print.<br />
Here is the result for the 3d printed parts:</p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/1.png" alt="Picture 1" /></p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/2.png" alt="Picture 2" /></p>

<p>You’ll notice the backplate and the case changed between the two pictures. I changed the backplate design to be able to fit stabilizers instead of printing them myself. And for the case I changed the design to have stronger pillars and also enabled supports generation in the slicer.<br />
You’ll also notice the keycaps were printed with the side, and not the bottom, facing the plate. It takes longer to print but gives a smoother top with a XDA keycap profile. The engraving was printed facing away from the plate to make the seam between the front and top of the key smoother.</p>

<h2 id="mounting-the-switches-and-stabilizers-on-the-backplate">mounting the switches and stabilizers on the backplate</h2>

<p>I used MMD holy panda switches and cheap plate mounted stabilizers that I glued on the backplate. Just be careful not to glue the metal part to the stabilizer and pay attention to the orientation of the metal parts so that they don’t collide with the pillars in the case.</p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/3.png" alt="Picture 3" /></p>

<p>You’ll notice some switches have 2 additional legs, they’re the same switches apart from this variation which doesn’t matter in this build. These two additional plastic legs are for PCB mount builds but are still adequate for plate mount builds. Just buy the cheapest version, or the one in stock with your supplier.</p>

<h2 id="handwiring-the-keyboard">handwiring the keyboard</h2>

<h3 id="bending-the-diodes">bending the diodes</h3>

<p>The diodes are just there to have the current flowing  in one direction (in my build from the columns to the rows) so you can use whichever diodes you have. I used 1N4148 diodes myself as they’re very cheap.<br />
We first need to bend the diode to have an easier time soldering them to the switches pins, I used the edge of my desk and then a small L shaped hex key to bend the diodes. For this build we need to prepare 21 diodes.<br />
Pay attention to the orientation of the diode, the anode (red part here) needs to be where the bend is while we keep the wire after the cathode (black part) straight</p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/4.png" alt="Picture 4" /></p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/5.png" alt="Picture 5" /></p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/6.png" alt="Picture 6" /></p>

<h3 id="soldering-the-diodes">soldering the diodes</h3>

<p>We then need to solder the diodes to one pin for each switch, you can pick the “top” or “bottom” pin, it doesn’t matter as long as you’re consistent with your rows and columns wiring later. You can stay consistent and always pick the same pin to have an easier time wiring the rows and columns but electrically it doesn’t matter. After soldering the diodes you can cut the extra wire after the anode.</p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/7.png" alt="Picture 7" /></p>

<h3 id="wiring-the-rows">wiring the rows</h3>

<p>I used the extra wire after the cathode on the diodes to wire the rows like this:</p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/8.png" alt="Picture 8" /></p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/9.png" alt="Picture 9" /></p>

<h3 id="wiring-the-columns-and-the-microcontroller">wiring the columns and the microcontroller</h3>

<p>After wiring the rows, wire the columns using the other pin on each switch. And then wire each row and column to a pin on your microcontroller. I used a pro micro nRF52840 as it support bluetooth, is supported by ZMK, and is pretty cheap. I also wired a 3.7V LiPo battery with a 320mAh capacity to the controller to be able to use the keyboard wirelessly. The capacity doesn’t matter much, above 300mAh is good to have a long lasting battery. At least if you don’t plan to use RGB leds.<br />
Also note to which pin you wire each row and column to adapt the ZMK layout later.</p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/10.png" alt="Picture 10" /></p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/11.png" alt="Picture 11" /></p>

<h2 id="flashing-the-firmware">flashing the firmware</h2>

<p>I recommend flashing the firmware before clipping the plate in the case so that you can easily access to reset pin to reset your microcontroller if you mess up the firmware and proceed with trial and error with ZMK.<br />
I have <a href="https://git.hadow.fr/sam.hadow/zmk-config">this repository</a> and <a href="https://github.com/Sam-Hadow/zmk-config">this one</a> on github as the build process is simplified with github action. The config for this keyboard is in <code class="language-plaintext highlighter-rouge">zmk-config/boards/shield/numpad3d</code>, in there you need to define the layout in <code class="language-plaintext highlighter-rouge">numpad3d.keymap</code> and the GPIO mapping for each row and column in <code class="language-plaintext highlighter-rouge">numpad3d.overlay</code>. You can also enable the bluetooth and tweak some features in <code class="language-plaintext highlighter-rouge">numpad3d.conf</code>. You then need to specify the keyboard to build and which microcontroller is used in <code class="language-plaintext highlighter-rouge">zmk-config/build.yaml</code>.<br />
To flash the firmware you plug in the microcontroller and copy the firmware to the detected disk device, the microcontroller will then automatically reset itself.</p>

<h2 id="assembling-the-keyboard">assembling the keyboard</h2>

<p>You can then assemble the keyboard. I used electrical tape to keep the microcontroller and the battery in place in the case. After that I clipped in the plate in the case and added the keycaps on the switches. Here is the final result:</p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/12.png" alt="Picture 12" /></p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/13.png" alt="Picture 13" /></p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/14.png" alt="Picture 14" /></p>

<h1 id="possible-improvements">possible improvements</h1>

<p>Overall 3d printing and handwiring this keyboard was a fun project but, as always, there are possible improvements, I’ll list them here in no particular order.</p>

<ul>
  <li>Adapting the github action automation to gitea action.</li>
  <li>Designing a screw in cover to hold the microcontroller and battery in place instead of using electrical tape.</li>
  <li>Multi filament printing and having a text in a different color instead of an engraving for the keycaps, though I don’t own an AMS yet. Currently the keycaps look okay during the day, but it’s harder to see the engraving in low light conditions.</li>
  <li><del>Adding a switch on one of the battery wire to be able to turn off the keyboard. Currently it’s constantly on.</del></li>
</ul>

<h1 id="update-24032026">Update 24/03/2026</h1>

<p>I modified the case a little and added a switch for the battery to turn off the keyboard. The switch I used is a 15mm round switch like <a href="https://aliexpress.com/item/1005009714438836.html">this</a> one (sanitized and non affiliated link).</p>

<p><img src="/assets/img/2026-03-16-3d-printed-and-handwired-numpad/15.png" alt="Picture 15" /></p>]]></content><author><name>Sam Hadow</name></author><category term="3dprinting" /><category term="hardware" /><category term="soldering" /><summary type="html"><![CDATA[In this blog post I’ll describe how I (mostly) 3d printed a numpad (21 keys keyboard), hanwired it, and installed a firmware with ZMK on it.]]></summary></entry><entry><title type="html">Archlinux Waydroid Installation Guide</title><link href="https://hadow.fr/blog/archlinux-waydroid-installation-guide.html" rel="alternate" type="text/html" title="Archlinux Waydroid Installation Guide" /><published>2026-02-15T00:00:00+01:00</published><updated>2026-02-15T00:00:00+01:00</updated><id>https://hadow.fr/blog/archlinux-waydroid-installation-guide</id><content type="html" xml:base="https://hadow.fr/blog/archlinux-waydroid-installation-guide.html"><![CDATA[<p>This blog post is a short guide to run and use <a href="https://github.com/waydroid/waydroid">waydroid</a> on archlinux.</p>

<h1 id="what-is-waydroid">What is waydroid?</h1>

<p>Waydroid is a container-based approach to boot a ful android system on a regular linux system with an x86 or ARM CPU, and so has less overhead than an android x86 virtual machine to run android apps on a linux system. Waydroid only works in a wayland session but it’s still possible to use a nested session if you use X11, which will be covered in this guide.</p>

<h1 id="installation-guide">Installation guide</h1>

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

<p>A kernel which comes with the <code class="language-plaintext highlighter-rouge">rust_binder</code> module is necessary to run waydroid, <code class="language-plaintext highlighter-rouge">linux-zen</code> kernel includes this module.</p>

<h3 id="other-kernels">Other kernels</h3>

<p>If you’re using another kernel you can add it via DKMS (note that you can use another <a href="https://wiki.archlinux.org/title/AUR_helpers">aur helper</a> than yay).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yay <span class="nt">-S</span> binder_linux-dkms
</code></pre></div></div>

<p>Then you can manually load it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>modprobe binder-linux <span class="nv">devices</span><span class="o">=</span>binder,hwbinder,vndbinder
</code></pre></div></div>

<p>Or load it automatically at boot:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"binder_linux"</span> <span class="o">&gt;</span> /etc/modules-load.d/binder_linux.conf
<span class="nb">echo</span> <span class="s2">"options binder_linux devices=binder,hwbinder,vndbinder"</span> <span class="o">&gt;</span> /etc/modprobe.d/binder_linux.conf
</code></pre></div></div>

<h2 id="installing-necessary-packages">Installing necessary packages:</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>pacman <span class="nt">-S</span> waydroid
</code></pre></div></div>

<h3 id="on-x11">[On X11]</h3>

<p>If using X11, you’ll need to run a nested wayland session, a simple solution is using cage. You could also use weston.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>pacman <span class="nt">-S</span> cage
</code></pre></div></div>

<h2 id="initializing-waydroid">Initializing waydroid:</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>waydroid init
</code></pre></div></div>

<p>Or with google apps support:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>waydroid init <span class="nt">-s</span> GAPPS
</code></pre></div></div>

<h2 id="running-waydroid">Running waydroid</h2>

<p>This command will automatically start the waydroid container and a session before showing the UI.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>waydroid show-full-ui
</code></pre></div></div>

<p>Otherwise if you want a CLI, you have to start the container and then a session:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl start waydroid-container.service
waydroid session start
</code></pre></div></div>

<h4 id="useful-commands">Useful commands</h4>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Command</th>
      <th style="text-align: left">Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">waydroid session start</code></td>
      <td style="text-align: left">Starting a session</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">waydroid session stop</code></td>
      <td style="text-align: left">Stopping a session</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">waydroid status</code></td>
      <td style="text-align: left">Checking Waydroid status</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">sudo waydroid upgrade</code></td>
      <td style="text-align: left">Upgrading the LineageOS image</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">waydroid app list</code></td>
      <td style="text-align: left">Get the list of installed apps</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">waydroid app install $path_to_apk</code></td>
      <td style="text-align: left">Install an APK</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">waydroid show-full-ui</code></td>
      <td style="text-align: left">Launch the GUI</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">waydroid app launch $package_name</code></td>
      <td style="text-align: left">Launch an app</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">sudo waydroid shell</code></td>
      <td style="text-align: left">Launch a shell</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">waydroid --help</code></td>
      <td style="text-align: left">Display the help message</td>
    </tr>
    <tr>
      <td style="text-align: left"> </td>
      <td style="text-align: left"> </td>
    </tr>
  </tbody>
</table>

<h3 id="on-x11-1">[On X11]</h3>

<p>On X11 waydroid container can be started but then all waydroid commands need to be run inside a nested wayland session.<br />
If you just need waydroid UI the simplest is with cage:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cage <span class="nt">--</span> waydroid show-full-ui
</code></pre></div></div>

<p>If you need the command line then you need to have a console running inside a wayland session. For example with Konsole:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cage <span class="nt">--</span> konsole
</code></pre></div></div>

<p>You can then type the commands the same as described above.</p>

<h2 id="firewall-rules-and-packet-forwarding">Firewall rules and packet forwarding</h2>

<p>You need some additional rules in your firewall if you want the network to work inside waydroid. For example with nftables you need these additional rules in your tables:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>table inet filter {
        chain input {
                # -------------------------------- waydroid
                iifname "waydroid0" accept comment "Allow incoming network traffic from WayDroid"

        }

        chain forward {
                # -------------------------------- waydroid
                iifname "waydroid0" accept comment "Allow incomming network traffic from WayDroid"
                oifname "waydroid0" accept comment "Allow outgoing network traffic from WayDroid"
        }
        chain output {
        }
}

</code></pre></div></div>

<p>You also need to enable packet forwarding. To check if it’s already enabled:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sysctl net.ipv4.ip_forward
sysctl net.ipv6.conf.all.forwarding
</code></pre></div></div>

<p>If it’s not enabled you can permanently enable it in the file <code class="language-plaintext highlighter-rouge">/etc/sysctl.conf</code> by uncommenting the lines <code class="language-plaintext highlighter-rouge">net.ipv4.ip_forward=1</code> for IPv4 and <code class="language-plaintext highlighter-rouge">net.ipv6.conf.all.forwarding=1</code> for IPv6. Please note that in most cases you can for now just enable the IPv4 packet forwarding and ignore the IPv6 one.<br />
And to reload the configuration:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>sysctl <span class="nt">-p</span> /etc/sysctl.conf
</code></pre></div></div>

<h2 id="additional-notes">Additional notes</h2>

<h4 id="1-clipboard-sharing">1) clipboard sharing</h4>

<p>If you want to share the clipboard between a wayland session and waydroid UI you need to install the packages <code class="language-plaintext highlighter-rouge">python-pyclip</code> and <code class="language-plaintext highlighter-rouge">wl-clipboard</code>.<br />
It however won’t work with X11 and nested wayland sessions.</p>

<h4 id="2-app-stores">2) app stores</h4>

<p>You might want to install <a href="https://gitlab.com/AuroraOSS/AuroraStore">aurora store</a>, an open source google play store client not requiring a google account. And an F-Droid client like <a href="https://f-droid.org/en/packages/com.looker.droidify/">droidify</a>.</p>

<h4 id="3-gpu">3) GPU</h4>

<p>If you have an Intel or AMD GPU it should work out of the box. But if you have a NVIDIA GPU you’ll need to enable software rendering. For that in <code class="language-plaintext highlighter-rouge">/var/lib/waydroid/waydroid.cfg</code> add the following:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[properties]
ro.hardware.gralloc=default
ro.hardware.egl=swiftshader
</code></pre></div></div>

<p>and then run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>waydroid upgrade <span class="nt">--offline</span>
<span class="nb">sudo </span>systemctl restart waydroid-container.service
</code></pre></div></div>

<h4 id="4-running-arm-apps">4) Running ARM apps</h4>

<p>ARM apps won’t work at first if you have a x86 CPU, it’ll say the app is incompatible for your device when trying to install it. To use arm apps you need to install a translation layer. It’s recommanded to use <code class="language-plaintext highlighter-rouge">libndk</code> on AMD CPUs and <code class="language-plaintext highlighter-rouge">libhoudini</code> on Intel CPUs. To do that:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yay <span class="nt">-S</span> waydroid-script-git

<span class="nb">sudo </span>waydroid-extras <span class="nb">install </span>libndk 
<span class="c"># or</span>
<span class="nb">sudo </span>waydroid-extras <span class="nb">install </span>libhoudini 

<span class="nb">sudo </span>systemctl restart waydroid-container.service
</code></pre></div></div>

<p>And if you’re interested in learning more about Intel houdini you can have a look at <a href="https://media.defcon.org/DEF%20CON%2029/DEF%20CON%2029%20presentations/Brian%20Hong%20-%20Sleight%20of%20ARM%20-%20%20Demystifying%20Intel%20Houdini.pdf">this presentation</a>, and in <a href="https://www.youtube.com/watch?v=kdd8dSifxvU">video</a></p>

<h4 id="5-disabling-on-screen-keyboard">5) Disabling on screen keyboard</h4>

<p>By default waydroid shows AOSP on screen keyboard, which is useless on a computer with a keyboard already, to disable it the setting is in <code class="language-plaintext highlighter-rouge">Settings &gt; System &gt; Languages &amp; input &gt; Physical keyboard &gt; Use on-screen keyboard</code></p>

<h4 id="6-selinux">6) SELinux</h4>

<p>This module is necessary for waydroid to work:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>module local-waydroid-nft 1.0;

require {
        type virtd_t;
        type iptables_t;
        class process { noatsecure rlimitinh siginh };
}

#============= virtd_t ==============
allow virtd_t iptables_t:process { noatsecure rlimitinh siginh };
</code></pre></div></div>

<p>You can compile it and load it with these commands (with the content above in a file <code class="language-plaintext highlighter-rouge">local-waydroid-nft.te</code>):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>checkmodule <span class="nt">-m</span> <span class="nt">-o</span> local-waydroid-nft.mod local-waydroid-nft.te
semodule_package <span class="nt">-o</span> local-waydroid-nft.pp <span class="nt">-m</span> local-waydroid-nft.mod
semodule <span class="nt">-i</span> local-waydroid-nft.pp
</code></pre></div></div>

<p>And this script makes starting waydroid more convenient:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nb">sudo</span> /usr/lib/waydroid/data/scripts/waydroid-net.sh start
cage <span class="nt">--</span> waydroid show-full-ui
</code></pre></div></div>

<h4 id="7-additional-troubleshooting">7) Additional troubleshooting</h4>

<p>Finally you might want to check the <a href="https://wiki.archlinux.org/title/Waydroid">archwiki</a> directly if having issues with waydroid.<br />
And although it’s quite old and contains some unecessary steps now, you can check <a href="https://forum.garudalinux.org/t/ultimate-guide-to-install-waydroid-in-any-arch-based-distro-especially-garuda/15902">this guide</a> too.</p>]]></content><author><name>Sam Hadow</name></author><category term="archlinux" /><category term="sysadmin" /><summary type="html"><![CDATA[This blog post is a short guide to run and use waydroid on archlinux.]]></summary></entry><entry><title type="html">Build Metasploitable 3 From Source With Qemu Kvm</title><link href="https://hadow.fr/blog/build-metasploitable-3-from-source-with-qemu-kvm.html" rel="alternate" type="text/html" title="Build Metasploitable 3 From Source With Qemu Kvm" /><published>2026-02-06T00:00:00+01:00</published><updated>2026-02-06T00:00:00+01:00</updated><id>https://hadow.fr/blog/build-metasploitable-3-from-source-with-qemu-kvm</id><content type="html" xml:base="https://hadow.fr/blog/build-metasploitable-3-from-source-with-qemu-kvm.html"><![CDATA[<p>This blog post is a short guide to build from source a metasploitable3 disk image for qemu kvm (qcow2 format), and then how to use the built image.</p>

<h2 id="pre-requisite-packer-and-its-plugins">Pre-requisite: packer and its plugins</h2>

<p>On archlinux:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>pacman <span class="nt">-S</span> packer
</code></pre></div></div>

<p>then independently of your distribution:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>packer plugins <span class="nb">install </span>github.com/hashicorp/qemu
packer plugins <span class="nb">install </span>github.com/hashicorp/chef
</code></pre></div></div>

<h2 id="steps">Steps:</h2>

<h3 id="1-clone-metasploitable3-repository">1) Clone metasploitable3 repository</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/rapid7/metasploitable3.git
<span class="nb">cd </span>metasploitable3
</code></pre></div></div>

<h3 id="2-disable-vagrant-post-processor">2) Disable Vagrant post-processor</h3>

<p>The default template packages the build in a .box Vagrant file which is unnecessary. Backup the template and then edit it.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cp </span>packer/templates/ubuntu_1404.json packer/templates/ubuntu_1404.json.bak
</code></pre></div></div>

<p>In <code class="language-plaintext highlighter-rouge">packer/templates/ubuntu_1404.json</code> remove the entire <code class="language-plaintext highlighter-rouge">post-processors</code> block.<br />
You can check if the JSON file is valid with this command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 <span class="nt">-m</span> json.tool packer/templates/ubuntu_1404.json <span class="o">&gt;</span>/dev/null <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"OK"</span>
</code></pre></div></div>

<p>If it doesn’t print <code class="language-plaintext highlighter-rouge">OK</code> the JSON is not valid</p>

<h3 id="3-docker-fix">3) Docker fix</h3>

<p>Modern docker is broken with metasploitable3.<br />
Backup the original file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cp </span>chef/cookbooks/metasploitable/recipes/flags.rb chef/cookbooks/metasploitable/recipes/flags.rb.bak
</code></pre></div></div>

<p>Then remove the docker part from it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sed</span> <span class="nt">-e</span> <span class="s2">"/^# 7 of Diamonds</span><span class="nv">$/</span><span class="s2">,/^end</span><span class="nv">$/</span><span class="s2">d"</span> <span class="se">\</span>
    <span class="nt">-e</span> <span class="s2">"/^include_recipe 'metasploitable::docker'/d"</span> <span class="se">\</span>
    <span class="nt">-e</span> <span class="s2">"/^directory '</span><span class="se">\/</span><span class="s2">opt</span><span class="se">\/</span><span class="s2">docker' do/,/^end</span><span class="nv">$/</span><span class="s2">d"</span> <span class="se">\</span>
    <span class="nt">-e</span> <span class="s2">"/^cookbook_file '</span><span class="se">\/</span><span class="s2">opt</span><span class="se">\/</span><span class="s2">docker</span><span class="se">\/</span><span class="s2">Dockerfile' do/,/^end</span><span class="nv">$/</span><span class="s2">d"</span> <span class="se">\</span>
    <span class="nt">-e</span> <span class="s2">"/^cookbook_file '</span><span class="se">\/</span><span class="s2">opt</span><span class="se">\/</span><span class="s2">docker</span><span class="se">\/</span><span class="s2">7_of_diamonds.zip' do/,/^end</span><span class="nv">$/</span><span class="s2">d"</span> <span class="se">\</span>
    <span class="nt">-e</span> <span class="s2">"/^docker_image '7_of_diamonds' do/,/^end</span><span class="nv">$/</span><span class="s2">d"</span> <span class="se">\</span>
    <span class="nt">-e</span> <span class="s2">"/^docker_container '7_of_diamonds' do/,/^end</span><span class="nv">$/</span><span class="s2">d"</span> <span class="se">\</span>
    <span class="nt">-e</span> <span class="s2">"/^file '</span><span class="se">\/</span><span class="s2">opt</span><span class="se">\/</span><span class="s2">docker</span><span class="se">\/</span><span class="s2">7_of_diamonds.zip' do/,/^end</span><span class="nv">$/</span><span class="s2">d"</span> <span class="se">\</span>
    chef/cookbooks/metasploitable/recipes/flags.rb <span class="o">&gt;</span> /tmp/flags.rb.<span class="nv">$$</span> <span class="o">&amp;&amp;</span> <span class="nb">mv</span> /tmp/flags.rb.<span class="nv">$$</span> chef/cookbooks/metasploitable/recipes/flags.rb
</code></pre></div></div>

<h3 id="4-build-the-image">4) Build the image</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>packer build <span class="nt">-only</span><span class="o">=</span>qemu packer/templates/ubuntu_1404.json
</code></pre></div></div>

<p>It will open a GUI and start the installation, in the console you should see the installation process. It will connect to the virtual machine in SSH to install the vulnerable services.</p>

<h3 id="5-using-the-built-image">5) Using the built image</h3>

<p>You’ll find the built image in qcow2 format in <code class="language-plaintext highlighter-rouge">output-qemu/</code>, for example mine is <code class="language-plaintext highlighter-rouge">output-qemu/metasploitable3-ub1404</code>.<br />
You can then import it in virt-manager.<br />
Please note that the disk device bus type should be SATA, not VirtIO or the boot will fail as the initramfs inside the image does not have VirtIO drivers. Similarly the virtual network device model should be e1000e and not virtio. The default user and password will be <code class="language-plaintext highlighter-rouge">vagrant</code>.<br />
Please also note that you should never connect this virtual machine to the internet as it’s intentionally made to have all sort of vulnerabilities. As such you should create an isolated network in virt-manager and connect it to this network only.<br />
You can then study the vulnerabilities from an another virtual machine like a Kali linux or from your host using metasploit or other similar tools.</p>]]></content><author><name>Sam Hadow</name></author><category term="sysadmin" /><category term="virtualization" /><category term="cybersecurity" /><summary type="html"><![CDATA[This blog post is a short guide to build from source a metasploitable3 disk image for qemu kvm (qcow2 format), and then how to use the built image.]]></summary></entry><entry><title type="html">Archlinux Uefi Lvm On Luks With Selinux Installation Guide</title><link href="https://hadow.fr/blog/archlinux-uefi-lvm-on-luks-with-selinux-installation-guide.html" rel="alternate" type="text/html" title="Archlinux Uefi Lvm On Luks With Selinux Installation Guide" /><published>2026-01-25T00:00:00+01:00</published><updated>2026-01-25T00:00:00+01:00</updated><id>https://hadow.fr/blog/archlinux-uefi-lvm-on-luks-with-selinux-installation-guide</id><content type="html" xml:base="https://hadow.fr/blog/archlinux-uefi-lvm-on-luks-with-selinux-installation-guide.html"><![CDATA[<p>This post is a guide to install archlinux in UEFI with full disk encryption (LVM on LUKS), SELinux enabled and optionally secure boot.<br />
<strong>Warning:</strong> apparmor is much easier to use on archlinux, SELinux on archlinux <strong>will</strong> require you to write some policies manually. Fedora is a better solution if you want SELinux working out of the box and don’t want to fight SELinux writing policies. This guide is mostly for advanced users.</p>

<h2 id="preparing-installation-media">preparing installation media</h2>

<p>Follow the initial steps from archlinux <a href="https://wiki.archlinux.org/title/Installation_guide">wiki</a> to prepare your installation media, the most convenient solution is to copy the ISO to a <a href="https://www.ventoy.net/en/index.html">ventoy</a> USB key.<br />
Then boot your archlinux ISO and first check you have internet access on your computer:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ping archlinux.org
</code></pre></div></div>

<p>If using WiFi, you’ll need to use <a href="https://wiki.archlinux.org/title/Iwd#iwctl">iwctl</a></p>

<h1 id="installation-steps">installation steps</h1>

<h2 id="1-setting-up-lvm-and-preparing-volumes">1) setting up LVM and preparing volumes</h2>

<h3 id="11-loading-the-required-kernel-modules">1.1) loading the required kernel modules</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>modprobe efivarfs
modprobe dm-crypt 
modprobe dm-mod 
</code></pre></div></div>

<h3 id="12-partition-table">1.2) partition table</h3>

<p>Find the disk you want to use to install your system with <code class="language-plaintext highlighter-rouge">lsblk</code>. For me it’s <code class="language-plaintext highlighter-rouge">vda</code> as I’m using a virtual machine to write this guide.<br />
<strong>important:</strong> Select a GPT parition table type and not MBR.<br />
Create a partition table with <code class="language-plaintext highlighter-rouge">cfdisk /dev/vda</code> (or your prefered tool) and create 2 partitions:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">/dev/vda1</code> as ef EFI (FAT-12/16/32) type, a size of 512MB is fine</li>
  <li><code class="language-plaintext highlighter-rouge">/dev/vda2</code> as 83 Linux filesystem type for the rest of the disk</li>
</ul>

<p>Write the partition table to the disk.</p>

<h3 id="13-setting-up-the-encrypted-volume">1.3) setting up the encrypted volume</h3>

<p>Create the encrypted volume, it will ask you for a confirmation and let you choose a passphrase. (Note that PBKDF2 key derivation algorithm is weaker than Argon2 but is required when using the default grub bootloader, the installation using systemd-boot will not be covered in this guide.)</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cryptsetup luksFormat <span class="nt">--type</span> luks2 <span class="nt">--pbkdf</span> pbkdf2 /dev/vda2
</code></pre></div></div>

<p>Then open it with the passphrase chosen.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cryptsetup luksOpen /dev/vda2 luks
</code></pre></div></div>

<h3 id="14-setting-up-lvm">1.4) setting up LVM</h3>

<p>We will create 3 volumes, one for the SWAP, one for the ROOT filesystem and one for the HOME filesystem. Adapt the sizes to what is appropriate for you, in my case I’m using a virtual machine with a 25GB disk so I won’t create a huge root volume.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pvcreate /dev/mapper/luks
vgcreate system /dev/mapper/luks
lvcreate <span class="nt">-L</span> 1G <span class="nt">-n</span> swap system
lvcreate <span class="nt">-L</span> 10G <span class="nt">-n</span> root system
lvcreate <span class="nt">-l</span> 100%FREE <span class="nt">-n</span> home system
</code></pre></div></div>

<h3 id="15-formating-and-mounting-the-volumes">1.5) formating and mounting the volumes</h3>

<p>I’ll use a BTRFS filesystem but use the filesystem you prefer.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#formatting</span>
mkfs.fat <span class="nt">-F32</span> /dev/vda1
fatlabel /dev/vda1 BOOT
mkswap <span class="nt">-L</span> SWAP /dev/mapper/system-swap
mkfs.btrfs <span class="nt">-L</span> ROOT /dev/mapper/system-root
mkfs.btrfs <span class="nt">-L</span> HOME /dev/mapper/system-home

<span class="c">#mounting</span>
swapon /dev/mapper/system-swap
mount <span class="nt">-o</span> <span class="nv">compress</span><span class="o">=</span>zstd /dev/mapper/system-root /mnt 
<span class="nb">mkdir</span> <span class="nt">-p</span> /mnt/<span class="o">{</span>home,boot,boot/efi,etc<span class="o">}</span>
mount <span class="nt">-o</span> <span class="nv">compress</span><span class="o">=</span>zstd /dev/mapper/system-home /mnt/home
mount /dev/vda1 /mnt/boot/efi
</code></pre></div></div>

<p>Then create the fstab.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>genfstab <span class="nt">-pU</span> /mnt <span class="o">&gt;&gt;</span> /mnt/etc/fstab
</code></pre></div></div>

<h2 id="2-base-installation">2) base installation</h2>

<h3 id="21-installing-required-packages">2.1) installing required packages</h3>

<p>We’ll use reflector to generate the mirrorlist. Adapt the country code to pick the closest mirrors.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>reflector <span class="nt">-c</span> FR <span class="nt">-p</span> https <span class="nt">-a</span> 12 <span class="nt">--sort</span> rate <span class="nt">--save</span> /etc/pacman.d/mirrorlist
</code></pre></div></div>

<p>Then install the required packages and some useful packages.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pacstrap <span class="nt">-i</span> /mnt base base-devel linux linux-firmware linux-headers pacman-contrib man-pages btrfs-progs vim git bash-completion
</code></pre></div></div>

<h3 id="22-chroot">2.2) chroot</h3>

<p>Chroot into the system to continue the installation.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>arch-chroot /mnt /bin/bash
</code></pre></div></div>

<h4 id="221-locales-hostname-clock-and-timezone">2.2.1) locales, hostname, clock and timezone</h4>

<p>In the file <code class="language-plaintext highlighter-rouge">/etc/locale.gen</code>, uncomment your locales (for example <code class="language-plaintext highlighter-rouge">en_US.UTF-8 UTF-8</code>)<br />
You can also set your preferences for the virtual console in <code class="language-plaintext highlighter-rouge">/etc/vconsole.conf</code>, for example for a QWERTY layout:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">KEYMAP</span><span class="p">=</span><span class="s">us</span>
<span class="py">FONT</span><span class="p">=</span><span class="s">lat9w-16</span>
</code></pre></div></div>

<p>And in <code class="language-plaintext highlighter-rouge">/etc/locale.conf</code> (LC_COLLATE=C for case sensitive sorting):</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">LANG</span><span class="p">=</span><span class="s">en_US.UTF-8</span>
<span class="py">LC_COLLATE</span><span class="p">=</span><span class="s">C </span>
</code></pre></div></div>

<p>And finally generate the locales:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>locale-gen 
</code></pre></div></div>

<p>You can set your hostname in <code class="language-plaintext highlighter-rouge">/etc/hostname</code>.</p>

<p>For your clock and timezone: (adapt to your timezone)</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">ln</span> <span class="nt">-sf</span> /usr/share/zoneinfo/Europe/Paris /etc/localtime
hwclock <span class="nt">--systohc</span> <span class="nt">--utc</span>
</code></pre></div></div>

<h4 id="222-root-password-and-user-creation">2.2.2) root password and user creation</h4>

<p>Set your root password with <code class="language-plaintext highlighter-rouge">passwd</code><br />
Then create a user (named sam here) in the wheel group (to use sudo) and set its password with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>useradd <span class="nt">-m</span> <span class="nt">-G</span> wheel sam
passwd sam
</code></pre></div></div>

<p>and uncomment the line containing <code class="language-plaintext highlighter-rouge">%wheel ALL=(ALL:ALL) ALL</code> in <code class="language-plaintext highlighter-rouge">/etc/sudoers</code></p>

<h4 id="223-selinux-installation-from-aur">2.2.3) SELinux installation (from AUR)</h4>

<p>Log in as your created user and install an AUR helper to make things easier:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>su sam
<span class="nb">cd
</span>git clone https://aur.archlinux.org/yay.git
<span class="nb">cd </span>yay
makepkg <span class="nt">-si</span>
<span class="nb">cd</span>
</code></pre></div></div>

<p>Then install the required packages for SELinux to work. It will ask you for replacements with conflicting packages, answer yes every time.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yay <span class="nt">-S</span> libsepol libselinux checkpolicy secilc setools libsemanage semodule-utils policycoreutils selinux-python python-ipy mcstrans restorecond
yay <span class="nt">-S</span> pam-selinux pambase-selinux coreutils-selinux findutils-selinux iproute2-selinux logrotate-selinux openssh-selinux psmisc-selinux shadow-selinux cronie-selinux
yay <span class="nt">-S</span> sudo-selinux
</code></pre></div></div>

<p>Note: If <code class="language-plaintext highlighter-rouge">sudo-selinux</code> fails to build, build it with <code class="language-plaintext highlighter-rouge">--nocheck</code> option:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/.cache/yay/sudo-selinux
makepkg <span class="nt">-si</span> <span class="nt">--nocheck</span>
</code></pre></div></div>

<p>Then exit your user, remodify <code class="language-plaintext highlighter-rouge">/etc/sudoers</code> to uncomment the line containing <code class="language-plaintext highlighter-rouge">%wheel ALL=(ALL:ALL) ALL</code> and relog as your user. After that:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>pacman <span class="nt">-S</span> less
yay <span class="nt">-S</span> systemd-selinux systemd-libs-selinux util-linux-selinux util-linux-libs-selinux
<span class="nb">cd</span> ~/.cache/yay/systemd-selinux
makepkg <span class="nt">-si</span> <span class="nt">--nocheck</span>
</code></pre></div></div>

<p>Due to a <a href="https://bugs.archlinux.org/task/39767">cyclic dependency which won’t be fixed</a> the complete build will fail at first, that’s why you need to manually build systemd-selinux again after. For some reason less is also required for the build but not a build dependency.<br />
Then more SELinux packages and a policy:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yay <span class="nt">-S</span> selinux-alpm-hook dbus-selinux selinux-refpolicy-arch
</code></pre></div></div>

<p>If the check fails for <code class="language-plaintext highlighter-rouge">dbus-selinux</code> build it with <code class="language-plaintext highlighter-rouge">--nocheck</code> option:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/.cache/yay/dbus-selinux
makepkg <span class="nt">-si</span> <span class="nt">--nocheck</span>
</code></pre></div></div>

<p>If you want to apply a fedora-style user context (as root):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>semanage login <span class="nt">-m</span> <span class="nt">-s</span> unconfined_u __default__
</code></pre></div></div>

<h4 id="224-necessary-packages">2.2.4) necessary packages</h4>

<p>Network for next boot:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pacman <span class="nt">-S</span> networkmanager
systemctl <span class="nb">enable </span>NetworkManager
</code></pre></div></div>

<p>grub and lvm2:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pacman <span class="nt">-S</span> grub efibootmgr lvm2
</code></pre></div></div>

<p>Secure boot:<br />
<em>Note: If you don’t want to bother with secure boot, do not run sbctl related command, and later when generating the boot image with grub, remove</em> <code class="language-plaintext highlighter-rouge">--modules="tpm" --disable-shim-lock</code> <em>from the command</em></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pacman <span class="nt">-S</span> sbctl
sbctl create-keys
sbctl enroll-keys <span class="nt">-m</span>
</code></pre></div></div>

<h5 id="linking-tpm-and-luks-key">linking TPM and LUKS key</h5>

<p>Without this step, the computer is still vulnerable to evild maid attacks and a software keylogger could be installed by disabling the secure boot (clear CMOS for example). To make sure the encrypted device is linked to the TPM, we need to run the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>systemd-cryptenroll <span class="nt">--tpm2-device</span><span class="o">=</span>auto <span class="nt">--tpm2-pcrs</span><span class="o">=</span>0+7 /dev/vda2 
</code></pre></div></div>

<p><a href="https://wiki.archlinux.org/title/Systemd-cryptenroll">systemd-cryptenroll</a> also support hardware security keys like FIDO2 tokens, in this case you can refer to the archwiki for the command syntax.<br />
systemd-boot is also the recommended option instead of using grub when linking the TPM and LUKS key, you can then only have a PIN and also be protected in more scenarios.</p>

<h4 id="225-optional-packages">2.2.5) optional packages</h4>

<p>If you have an intel CPU:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pacman <span class="nt">-S</span> intel-ucode
</code></pre></div></div>

<p>If you have a laptop and want battery optimization:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pacman <span class="nt">-S</span> tlp
systemctl <span class="nb">enable </span>tlp
</code></pre></div></div>

<p>If you need bluetooth:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pacman <span class="nt">-S</span> bluez
systemctl <span class="nb">enable </span>bluetooth
</code></pre></div></div>

<p>If you want 32bits apps (necessary for some packages), in <code class="language-plaintext highlighter-rouge">/etc/pacman.conf</code> uncomment these lines:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#[multilib]
#include = /etc/pacman.d/mirrorlist
</span></code></pre></div></div>

<h4 id="225-generating-boot-images">2.2.5) generating boot images</h4>

<p>in <code class="language-plaintext highlighter-rouge">/etc/mkinitcpio.conf</code> on the <code class="language-plaintext highlighter-rouge">MODULES=()</code> line, add <code class="language-plaintext highlighter-rouge">dm-mod</code> and in the <code class="language-plaintext highlighter-rouge">HOOKS=()</code> line you should have:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">HOOKS</span><span class="p">=</span><span class="s">(base udev autodetect modconf block keyboard encrypt lvm2 filesystems fsck)</span>
</code></pre></div></div>

<p>Then:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mkinitcpio <span class="nt">-p</span> linux
</code></pre></div></div>

<p>sbctl post-hook should automatically sign <code class="language-plaintext highlighter-rouge">/boot/vmlinuz-linux</code>. Otherwise run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sbctl sign <span class="nt">-s</span> /boot/vmlinuz-linux
</code></pre></div></div>

<p>Then in <code class="language-plaintext highlighter-rouge">/etc/default/grub</code></p>
<ul>
  <li>uncomment the line <code class="language-plaintext highlighter-rouge">GRUB_UNABLE_CRYPTODISK=y</code></li>
  <li>in <code class="language-plaintext highlighter-rouge">GRUB_CMDLINE_LINUX=""</code> add <code class="language-plaintext highlighter-rouge">cryptdevice=/dev/vda2:system root=/dev/mapper/system-root</code></li>
  <li>in <code class="language-plaintext highlighter-rouge">GRUB_CMDLINE_LINUX_DEFAULT=""</code> add <code class="language-plaintext highlighter-rouge">lsm=selinux,landlock,lockdown,yama,integrity,bpf</code></li>
</ul>

<p>Then:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>grub-install <span class="nt">--target</span><span class="o">=</span>x86_64-efi <span class="nt">--efi-directory</span><span class="o">=</span>/boot/efi <span class="nt">--bootloader-id</span><span class="o">=</span>arch_grub <span class="nt">--modules</span><span class="o">=</span><span class="s2">"tpm"</span> <span class="nt">--disable-shim-lock</span> <span class="nt">--recheck</span>
grub-mkconfig <span class="nt">-o</span> /boot/grub/grub.cfg
sbctl sign <span class="nt">-s</span> /boot/efi/EFI/arch_grub/grubx64.efi
</code></pre></div></div>

<p><em>Please note that in a virtual machine you’ll need an emulated TPM for the virtual machine to boot if you want secure boot enabled.</em></p>

<h3 id="3-exit-and-reboot">3) exit and reboot</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">exit</span>    <span class="c"># until you exit the chroot</span>
umount <span class="nt">-R</span> /mnt
swapoff <span class="nt">-a</span>
reboot
</code></pre></div></div>

<h3 id="4-optional-avoid-entering-the-same-password-twice-to-decrypt-the-disk-when-booting">4) (Optional) avoid entering the same password twice to decrypt the disk when booting</h3>

<p>GRUB unlocks /dev/vda2 so it can read /boot and load the kernel/initramfs.<br />
The initramfs then takes over and unlocks /dev/vda2 again for the actual root/LVM setup.<br />
But GRUB doesn’t pass the password it obtained to the initramfs requiring you to enter the password twice to decrypt the system.<br />
You can follow these steps to avoid this issue:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo install</span> <span class="nt">-d</span> <span class="nt">-m</span> 700 /etc/cryptsetup-keys.d

<span class="nb">sudo dd </span><span class="nv">bs</span><span class="o">=</span>512 <span class="nv">count</span><span class="o">=</span>4 <span class="k">if</span><span class="o">=</span>/dev/random <span class="nv">iflag</span><span class="o">=</span>fullblock <span class="se">\</span>
    | <span class="nb">sudo install</span> <span class="nt">-m</span> 600 /dev/stdin /etc/cryptsetup-keys.d/system.key

<span class="nb">sudo </span>cryptsetup luksAddKey /dev/vda2 /etc/cryptsetup-keys.d/system.key
</code></pre></div></div>
<p>Then add to <code class="language-plaintext highlighter-rouge">/etc/mkinitcpio.conf</code>:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">FILES</span><span class="o">=(</span>/etc/cryptsetup-keys.d/system.key<span class="o">)</span>
</code></pre></div></div>
<p>Then in <code class="language-plaintext highlighter-rouge">/etc/default/grub</code> in <code class="language-plaintext highlighter-rouge">GRUB_CMDLINE_LINUX=""</code> append:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> cryptkey=rootfs:/etc/cryptsetup-keys.d/system.key
</code></pre></div></div>
<p>Then rebuild:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>mkinitcpio <span class="nt">-P</span>
<span class="nb">sudo </span>grub-mkconfig <span class="nt">-o</span> /boot/grub/grub.cfg
</code></pre></div></div>

<h3 id="5-post-reboot-steps">5) post reboot steps</h3>

<h4 id="51-relabel-files">5.1) relabel files</h4>

<p>check SELinux status with <code class="language-plaintext highlighter-rouge">sestatus</code>, it should be in permissive with refpolicy-arch as the loaded policy.<br />
Relabel all the files correctly with the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>restorecon <span class="nt">-RFv</span> /
</code></pre></div></div>

<p>you might want to clean yay cache before doing that to have less files to relabel:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">rm</span> <span class="nt">-rf</span> ~/.cache/yay/<span class="k">*</span>
</code></pre></div></div>

<h4 id="52-enable-auditd">5.2) enable auditd</h4>

<p>enable auditd service to read AVC denials from SELinux later.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>systemctl <span class="nb">enable</span> <span class="nt">--now</span> auditd
</code></pre></div></div>

<h4 id="53-create-and-load-necessary-policy">5.3) create and load necessary policy</h4>

<p>Create a file <code class="language-plaintext highlighter-rouge">requiredmod.te</code> with the following content:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>module requiredmod 1.0;

require {
        type auditd_etc_t;
        type getty_t;
        type var_run_t;
        type tmpfs_t;
        type local_login_t;
        type systemd_tmpfiles_t;
        type init_runtime_t;
        type devpts_t;
        type kernel_t;
        type device_t;
        type udev_t;
        type hugetlbfs_t;
        type udev_tbl_t;
        type policy_config_t;
        type tmp_t;
        type unconfined_t;
        type var_lib_t;
        type systemd_userdbd_runtime_t;
        type systemd_user_runtime_dir_t;
        type systemd_sessions_t;
        type systemd_userdbd_t;
        type etc_runtime_t;
        type systemd_logind_t;
        type file_context_t;
        type semanage_t;
        type selinux_config_t;
        type initrc_runtime_t;
        type sshd_t;
        class dir { write add_name remove_name getattr open read search };
        class file { getattr open read write create getattr ioctl lock relabelfrom relabelto setattr unlink };
        class sock_file write;
        class unix_stream_socket { read write ioctl connectto};
        class capability2 block_suspend;
        class filesystem { associate quotaget quotamod };
        class key { link search };
        class process { noatsecure rlimitinh siginh transition };

}

#============= getty_t ==============
allow getty_t tmpfs_t:dir { getattr open read };
allow getty_t var_run_t:file { getattr open read };
allow getty_t initrc_runtime_t:dir { getattr open read };

#============= local_login_t ==============
allow local_login_t init_runtime_t:sock_file write;
allow local_login_t systemd_logind_t:unix_stream_socket connectto;
allow local_login_t var_lib_t:dir { add_name remove_name };
allow local_login_t var_lib_t:file { create getattr lock open read setattr unlink write };

#============= sshd_t ==============
allow sshd_t local_login_t:key { link search };
allow sshd_t systemd_logind_t:unix_stream_socket connectto;
allow sshd_t unconfined_t:process { noatsecure rlimitinh siginh };

#============= systemd_tmpfiles_t ==============
allow systemd_tmpfiles_t auditd_etc_t:dir search;
allow systemd_tmpfiles_t auditd_etc_t:file getattr;

#============= systemd_sessions_t ==============
allow systemd_sessions_t kernel_t:dir search;
allow systemd_sessions_t kernel_t:file { getattr ioctl open read };

#============= systemd_user_runtime_dir_t ==============
allow systemd_user_runtime_dir_t etc_runtime_t:file { open read };
allow systemd_user_runtime_dir_t kernel_t:dir search;
allow systemd_user_runtime_dir_t kernel_t:file { getattr ioctl open read };
allow systemd_user_runtime_dir_t systemd_userdbd_runtime_t:sock_file write;
allow systemd_user_runtime_dir_t systemd_userdbd_t:unix_stream_socket connectto;
allow systemd_user_runtime_dir_t tmp_t:dir read;
allow systemd_user_runtime_dir_t tmpfs_t:filesystem { quotaget quotamod };

#============= systemd_userdbd_t ==============
allow systemd_userdbd_t initrc_runtime_t:dir { getattr open read search };

#============= devpts_t ==============
allow devpts_t device_t:filesystem associate;

#============= hugetlbfs_t ==============
allow hugetlbfs_t device_t:filesystem associate;

#============= kernel_t ==============
allow kernel_t self:capability2 block_suspend;

#============= tmpfs_t ==============
allow tmpfs_t device_t:filesystem associate;

#============= udev_t ==============
allow udev_t kernel_t:unix_stream_socket { read write ioctl };
allow udev_t udev_tbl_t:dir { write add_name };
allow udev_t var_run_t:sock_file write;
</code></pre></div></div>

<p>Run the following command to compile and load the module:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>checkmodule <span class="nt">-m</span> <span class="nt">-o</span> requiredmod.mod requiredmod.te
semodule_package <span class="nt">-o</span> requiredmod.pp <span class="nt">-m</span> requiredmod.mod
semodule <span class="nt">-i</span> requiredmod.pp
</code></pre></div></div>

<p>And set a few booleans:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>setsebool <span class="nt">-P</span> allow_polyinstantiation on
setsebool <span class="nt">-P</span> systemd_tmpfiles_manage_all on
setsebool <span class="nt">-P</span> ssh_sysadm_login on
</code></pre></div></div>

<p>Then in <code class="language-plaintext highlighter-rouge">/etc/selinux/config</code>, set SELinux mode to enforcing instead of permissive and reboot.</p>

<p>And that’s it, you now have a minimal archlinux installation in UEFI with full disk encryption, secure boot, and SELinux in enforcing mode.</p>]]></content><author><name>Sam Hadow</name></author><category term="archlinux" /><category term="selinux" /><category term="sysadmin" /><summary type="html"><![CDATA[This post is a guide to install archlinux in UEFI with full disk encryption (LVM on LUKS), SELinux enabled and optionally secure boot. Warning: apparmor is much easier to use on archlinux, SELinux on archlinux will require you to write some policies manually. Fedora is a better solution if you want SELinux working out of the box and don’t want to fight SELinux writing policies. This guide is mostly for advanced users.]]></summary></entry></feed>