<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-04-04T00:59:22+00:00</updated><id>/feed.xml</id><title type="html">Myhro Blog</title><subtitle></subtitle><author><name>Tiago Ilieve</name></author><entry><title type="html">Full Disk Encryption on OVH UEFI Servers</title><link href="/2025/09/full-disk-encryption-on-ovh-uefi-servers" rel="alternate" type="text/html" title="Full Disk Encryption on OVH UEFI Servers" /><published>2025-09-22T00:00:00+00:00</published><updated>2025-09-22T00:00:00+00:00</updated><id>/2025/09/full-disk-encryption-on-ovh-uefi-servers</id><content type="html" xml:base="/2025/09/full-disk-encryption-on-ovh-uefi-servers"><![CDATA[<p>For a while, it has been possible to boot OVH servers in rescue mode and, using QEMU, install an OS in any way you want with its regular installation ISO. This includes unsupported modes such as a non-standard filesystem on <code class="language-plaintext highlighter-rouge">/</code> or using full disk encryption. The only requirement was to map the disks to regular QEMU devices, such as <code class="language-plaintext highlighter-rouge">-hda /dev/sda</code> arguments. I don’t remember exactly when I first did that, but there are references to this process <a href="https://community.ovhcloud.com/community/en/installing-operating-system-from-custom-image-on-ovh-vps-centos-8-tutorial">in their community forums</a> dating back to at least 2020. I might have seen someone mention it in a blog post a few years earlier.</p>

<p>Since I usually opt for their cheaper line of servers, from Kimsufi or SoYouStart, I’m used to old hardware. The server I’m currently replacing, for instance, is an <a href="https://www.intel.com/content/www/us/en/products/sku/65729/intel-xeon-processor-e31245-v2-8m-cache-3-40-ghz/specifications.html">Intel Xeon E3-1245 V2</a> from 2012 (!) with regular SATA SSDs, where the BIOS boots from the MBR. What happens is that the same custom OS installation procedure doesn’t work for newer servers, which use UEFI and are powered by NVMe disks. I tried multiple combinations of:</p>

<ul>
  <li>Regular installation as if it were a BIOS-based server with SSD drives.</li>
  <li>Passing them as NVMe drives using <code class="language-plaintext highlighter-rouge">-drive file=/dev/nvme0n1</code> and <code class="language-plaintext highlighter-rouge">-device nvme</code>.</li>
  <li>UEFI installation with <code class="language-plaintext highlighter-rouge">-bios /usr/share/ovmf/OVMF.fd</code>, using the <a href="https://packages.debian.org/trixie/ovmf">OVMF</a> firmware.</li>
  <li>Creating an EFI System Partition (ESP) both on and off a RAID-1 array.</li>
  <li>Using the default “entire disk” automatic partitioning from the Debian installer.</li>
  <li>Performing a regular installation with no RAID at all.</li>
  <li>Cloning this regular installation onto both disks to ensure either of them would boot.</li>
</ul>

<p>And probably a few other combinations I don’t recall. What’s important is that none of them worked. Every time I checked the boot logs via KVM/IPMI, I got an error like: <code class="language-plaintext highlighter-rouge">rEFInd - Chain on hard drive failed. Next</code>. I kept wondering what could be missing, but more important than doing the installation exactly as I wanted was achieving the final goal. I didn’t want to install an unsupported OS. I only wanted a Debian installation with full disk encryption, which isn’t supported by the OVH web-based OS installer. That’s when I started looking into how to encrypt an existing Linux installation.</p>

<p>There are guides like <a href="https://blog.williamdes.eu/Infrastructure/tutorials/encrypt-an-existing-debian-system-with-luks/">Encrypt an existing Debian 12 system with LUKS</a>, which aren’t exactly wrong but are overly complicated and contain unnecessary steps. Every time I see a complicated guide, I wonder how to simplify the process. Starting from that and with the <a href="https://wiki.archlinux.org/title/Dm-crypt/Device_encryption#Encrypt_an_existing_unencrypted_file_system">always-on-point instructions from the Arch Linux Wiki</a>, I was able to encrypt an existing Debian installation.</p>

<h2 id="encrypting-an-existing-debian-system">Encrypting an Existing Debian System</h2>

<p>The process goes like this:</p>

<p>Do a regular Debian 13 (Trixie) installation using the <a href="https://help.ovhcloud.com/csm/en-sg-dedicated-servers-getting-started-dedicated-server?id=kb_article_view&amp;sysparm_article=KB0043478">OVH web installer</a>. The most important part is to keep <code class="language-plaintext highlighter-rouge">/boot</code> separate from other partitions and on RAID-1 if you’re using RAID. It will also create the ESP partition separately. The goal is to never need to touch these two partitions.</p>

<p>After the installation is finished, log in to the new system and install the required packages to boot it. The <code class="language-plaintext highlighter-rouge">dropbear-initramfs</code> package is what allows us to unlock the encrypted root partition via SSH. For it to work, you need to set its own <code class="language-plaintext highlighter-rouge">authorized_keys</code> file, since it won’t have access to anything on disk before unlocking.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo apt install cryptsetup-initramfs dropbear-initramfs
(...)
$ sudo vim /etc/dropbear/initramfs/authorized_keys
</code></pre></div></div>

<p>Configure the server to boot in <a href="https://help.ovhcloud.com/csm/en-dedicated-servers-ovhcloud-rescue?id=kb_article_view&amp;sysparm_article=KB0043949">rescue mode</a> and restart it. After logging in again, check the current partitions to identify the data partition to be encrypted.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># lsblk | grep -v nbd
NAME        MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
nvme1n1     259:0    0 419.2G  0 disk
├─nvme1n1p1 259:1    0   511M  0 part
├─nvme1n1p2 259:2    0     1G  0 part
│ └─md2       9:2    0  1022M  0 raid1
└─nvme1n1p3 259:3    0 417.7G  0 part
  └─md3       9:3    0 835.1G  0 raid0
nvme0n1     259:4    0 419.2G  0 disk
├─nvme0n1p1 259:5    0   511M  0 part
├─nvme0n1p2 259:6    0     1G  0 part
│ └─md2       9:2    0  1022M  0 raid1
├─nvme0n1p3 259:7    0 417.7G  0 part
│ └─md3       9:3    0 835.1G  0 raid0
└─nvme0n1p4 259:8    0     2M  0 part
</code></pre></div></div>

<p>In this case, we are interested in <code class="language-plaintext highlighter-rouge">/dev/md3</code>, the root partition on top of RAID-0. Check the filesystem for errors so that other tools don’t complain about it later.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># e2fsck -f /dev/md3
e2fsck 1.47.0 (5-Feb-2023)
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
root: 31979/54730752 files (1.1% non-contiguous), 3972359/218920960 blocks
</code></pre></div></div>

<p>Now comes a very important part: shrink the filesystem but not the whole partition. The goal is to leave space for the LUKS header to be created at the end of the partition.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># resize2fs -p -M /dev/md3
resize2fs 1.47.0 (5-Feb-2023)
Resizing the filesystem on /dev/md3 to 957980 (4k) blocks.
Begin pass 2 (max = 512733)
Relocating blocks             XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Begin pass 3 (max = 6681)
Scanning inode table          XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Begin pass 4 (max = 3388)
Updating inode references     XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
The filesystem on /dev/md3 is now 957980 (4k) blocks long.
</code></pre></div></div>

<p>Next, perform the actual encryption. This is also where you set the passphrase to unlock it. This will take time, depending on the disk speed and partition size, as the process rewrites everything, including unused/free space. In this case, it took a little over 25 minutes to encrypt the 835GB partition.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># cryptsetup reencrypt --encrypt --reduce-device-size 32M /dev/md3

WARNING!
========
This will overwrite data on LUKS2-temp-03ea47d5-415b-4622-9510-5b7340d6c557.new irrevocably.

Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for LUKS2-temp-03ea47d5-415b-4622-9510-5b7340d6c557.new:
Verify passphrase:
Finished, time 25m20s,  835 GiB written, speed 562.6 MiB/s
</code></pre></div></div>

<p>When encryption finishes, open it as a regular device and expand the filesystem to fill the partition again. This will use all available space minus what was reserved for the LUKS header.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># cryptsetup open /dev/md3 md3_crypt
Enter passphrase for /dev/md3:
# resize2fs /dev/mapper/md3_crypt
resize2fs 1.47.0 (5-Feb-2023)
Resizing the filesystem on /dev/mapper/md3_crypt to 218916864 (4k) blocks.
The filesystem on /dev/mapper/md3_crypt is now 218916864 (4k) blocks long.
</code></pre></div></div>

<p>Mount the device and update both <code class="language-plaintext highlighter-rouge">/etc/crypttab</code> and <code class="language-plaintext highlighter-rouge">/etc/fstab</code>. The former should refer to the filesystem UUID, but the latter can just point to the <code class="language-plaintext highlighter-rouge">/dev/mapper/md3_crypt</code> device, since it will use the name defined in crypttab.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># mount /dev/mapper/md3_crypt /mnt/
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># blkid | grep /dev/md3
/dev/md3: UUID="a9c05676-6aa5-4a7b-acc9-a5eca5de4fed" TYPE="crypto_LUKS"
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># cat /mnt/etc/crypttab
# &lt;target name&gt; &lt;source device&gt;         &lt;key file&gt;      &lt;options&gt;
md3_crypt UUID=a9c05676-6aa5-4a7b-acc9-a5eca5de4fed none luks,discard
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># cat /mnt/etc/fstab
/dev/mapper/md3_crypt   /       ext4    defaults        0       1
UUID=5bc9cb1c-6607-429d-9219-3675ee773b12       /boot   ext4    defaults        0       0
LABEL=EFI_SYSPART       /boot/efi       vfat    defaults        0       1
</code></pre></div></div>

<p>The last step is to bind-mount the system directories, plus the actual <code class="language-plaintext highlighter-rouge">/boot</code>, and update the initramfs again in the chroot. This is required for two reasons:</p>

<ul>
  <li>It needs to grab the SSH <code class="language-plaintext highlighter-rouge">authorized_keys</code> file defined earlier in this process.</li>
  <li>It needs to be aware of the updated <code class="language-plaintext highlighter-rouge">crypttab</code> file, which tells it what to unlock during boot.</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># mount --bind /dev/ /mnt/dev/
# mount --bind /proc/ /mnt/proc/
# mount --bind /sys/ /mnt/sys/
# mount /dev/md2 /mnt/boot/
# chroot /mnt/
# update-initramfs -u -k all
update-initramfs: Generating /boot/initrd.img-6.12.43+deb13-amd64
</code></pre></div></div>

<p>After that, exit the chroot, reboot, and log in via SSH as <code class="language-plaintext highlighter-rouge">root</code> once your machine is online and responding to pings. Then, after running <code class="language-plaintext highlighter-rouge">cryptroot-unlock</code> and entering the proper passphrase, the machine will mount the encrypted device and proceed with the boot.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>To unlock root partition, and maybe others like swap, run `cryptroot-unlock`.


BusyBox v1.37.0 (Debian 1:1.37.0-6+b3) built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ # cryptroot-unlock
Please unlock disk md3_crypt:
cryptsetup: md3_crypt set up successfully
</code></pre></div></div>

<p>Finally, the machine should now be online and running with full disk encryption, except for <code class="language-plaintext highlighter-rouge">/boot</code> and the ESP partition.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo lsblk
NAME            MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
nvme1n1         259:0    0 419.2G  0 disk
├─nvme1n1p1     259:1    0   511M  0 part  /boot/efi
├─nvme1n1p2     259:2    0     1G  0 part
│ └─md2           9:2    0  1022M  0 raid1 /boot
└─nvme1n1p3     259:3    0 417.7G  0 part
  └─md3           9:3    0 835.1G  0 raid0
    └─md3_crypt 253:0    0 835.1G  0 crypt /
nvme0n1         259:4    0 419.2G  0 disk
├─nvme0n1p1     259:5    0   511M  0 part
├─nvme0n1p2     259:6    0     1G  0 part
│ └─md2           9:2    0  1022M  0 raid1 /boot
├─nvme0n1p3     259:7    0 417.7G  0 part
│ └─md3           9:3    0 835.1G  0 raid0
│   └─md3_crypt 253:0    0 835.1G  0 crypt /
└─nvme0n1p4     259:8    0     2M  0 part
</code></pre></div></div>

<p>The process may be a bit more involved than installing the OS from scratch using built-in encryption options. Still, it’s not too complex, provided you don’t skip any steps. It’s definitely better than running a machine outside of your physical reach that stores everything in plain text.</p>]]></content><author><name>Tiago Ilieve</name></author><summary type="html"><![CDATA[For a while, it has been possible to boot OVH servers in rescue mode and, using QEMU, install an OS in any way you want with its regular installation ISO. This includes unsupported modes such as a non-standard filesystem on / or using full disk encryption. The only requirement was to map the disks to regular QEMU devices, such as -hda /dev/sda arguments. I don’t remember exactly when I first did that, but there are references to this process in their community forums dating back to at least 2020. I might have seen someone mention it in a blog post a few years earlier.]]></summary></entry><entry><title type="html">Ad-blocking in a Manifest V3 world</title><link href="/2024/10/ad-blocking-in-a-manifest-v3-world" rel="alternate" type="text/html" title="Ad-blocking in a Manifest V3 world" /><published>2024-10-20T00:00:00+00:00</published><updated>2024-10-20T00:00:00+00:00</updated><id>/2024/10/ad-blocking-in-a-manifest-v3-world</id><content type="html" xml:base="/2024/10/ad-blocking-in-a-manifest-v3-world"><![CDATA[<p>I started receiving warnings on Chrome about <a href="https://github.com/gorhill/uBlock">uBlock Origin</a>, stating, “<a href="https://github.com/uBlockOrigin/uBlock-issues/wiki/About-Google-Chrome's-%22This-extension-may-soon-no-longer-be-supported%22">This extension may soon no longer be supported</a>”, after I set it up on a new computer nearly three months ago. These warnings are related to the <a href="https://developer.chrome.com/docs/extensions/develop/migrate/mv2-deprecation-timeline">ongoing deprecation of Manifest V2</a>, which has been going on for a few years and is expected to be finalized by June next year. I can’t even imagine using the internet without an ad blocker, so I decided to explore the current alternatives.</p>

<p>To be honest, the process was a bit daunting. <a href="https://www.osnews.com/story/140947/googles-ad-blocking-crackdown-underway/">Some articles suggest that there’s no hope</a> unless you switch to a browser supported by a company that doesn’t rely on ads - something that doesn’t really exist. In situations like this, I tend to try the simplest, most straightforward solution first before diving into more complex, over-the-top options. In this case, I decided to switch to the Manifest V3-based extension from the same developers: <a href="https://github.com/uBlockOrigin/uBOL-home">uBlock Origin Lite</a>.</p>

<p>The experience was actually better than I expected. Even in “Basic” mode, which doesn’t require permission to read or change data on all sites, it worked quite well with the default filters. I did notice some empty ad boxes, as the page layouts aren’t reworked, but that’s something I’m already used to when visiting sites on mobile with <a href="https://adguard.com/">AdGuard</a> (doing the <a href="https://adguard-dns.io/en/public-dns.html">block via DNS</a>). No need to change to the “Optimal” mode for now.</p>

<p>In fact, enabling the “Overlay Notices” and “Other Annoyances” filter lists made the experience even more pleasant than before. There are no more “please disable your ad-block” or “please donate to this site using Google” overlays to disrupt my browsing. So while the transition from V2 to V3 may have been a hassle for extension developers, as an end-user, I can’t say I’m unhappy with it. I’m still experiencing the (mostly) ad-free internet I was used to.</p>]]></content><author><name>Tiago Ilieve</name></author><summary type="html"><![CDATA[I started receiving warnings on Chrome about uBlock Origin, stating, “This extension may soon no longer be supported”, after I set it up on a new computer nearly three months ago. These warnings are related to the ongoing deprecation of Manifest V2, which has been going on for a few years and is expected to be finalized by June next year. I can’t even imagine using the internet without an ad blocker, so I decided to explore the current alternatives.]]></summary></entry><entry><title type="html">The cloud won, again</title><link href="/2024/08/the-cloud-won-again" rel="alternate" type="text/html" title="The cloud won, again" /><published>2024-08-18T00:00:00+00:00</published><updated>2024-08-18T00:00:00+00:00</updated><id>/2024/08/the-cloud-won-again</id><content type="html" xml:base="/2024/08/the-cloud-won-again"><![CDATA[<p><a href="/2018/03/how-i-finally-migrated-my-whole-website-to-the-cloud">Since 2018</a> (and <a href="/2020/01/the-cloud-computing-era-is-now">again in 2020</a>), I’ve been writing about how defaulting to cloud-based solutions instead of self-hosting everything has changed my life for the better. Even knowing that for years, I still made the wrong decision to self-host a service I needed and almost doubled down on doing it again. This was until I stopped and figured out an easier way to achieve the same goal.</p>

<p>I’ve been a <a href="https://www.dropbox.com/">Dropbox</a> user for nearly 15 years. It really simplified my approach to backups for personal stuff: a cloud-synced folder on my laptop where I put everything that’s not already on a cloud service. Accidentally deleted something? I can just go to their web app and restore it. The problem is that I don’t want it offering a read-write version of this folder on every device I have. Sometimes I just need a temporary folder to drop a screenshot from my Windows gaming machine so I can access it from my phone.</p>

<p><a href="https://www.resilio.com/sync/">Resilio Sync</a> (previously known as BitTorrent Sync) is what I was using for that. It has a few problems, including being super slow even after configuring everything possible to bypass its relay servers (spoiler: it doesn’t). Plus, it doesn’t have cloud-backed storage, so I ran an instance of it on a server to have an always-on copy of the files there. Not exactly a drop-in replacement for Dropbox, but it was still useful until I realized I was completely unhappy with its performance.</p>

<p>Things were reaching a point where I was considering self-hosting <a href="https://nextcloud.com/">Nextcloud</a> (fork of the original ownCloud) just for its file-syncing feature or even writing my own cloud-folder synchronization tool backed by S3-compatible storage. That’s when it clicked: I realized I don’t need real-time syncing for the simple use case of easily sharing single files from a computer to my phone. I just needed a way to access a Cloudflare R2 bucket from a mobile app.</p>

<p>After looking around, asking <a href="https://chatgpt.com/">ChatGPT</a> and <a href="https://www.perplexity.ai/">Perplexity</a>, I settled on <a href="https://apps.apple.com/us/app/s3-files-bucket-storage/id6447647340">S3 Files</a>. I can upload a file from Windows using <a href="https://winscp.net/">WinSCP</a> or from a macOS/Linux terminal using <a href="https://s3tools.org/s3cmd">s3cmd</a>. Each machine uses a fine-grained access key I can revoke if needed. The S3 API is ubiquitous. I just needed a mobile app to access it when I’m away from my computer. It offered me the ease, speed, availability, and robustness of the cloud, which are miles ahead compared to self-hosting anything.</p>]]></content><author><name>Tiago Ilieve</name></author><summary type="html"><![CDATA[Since 2018 (and again in 2020), I’ve been writing about how defaulting to cloud-based solutions instead of self-hosting everything has changed my life for the better. Even knowing that for years, I still made the wrong decision to self-host a service I needed and almost doubled down on doing it again. This was until I stopped and figured out an easier way to achieve the same goal.]]></summary></entry><entry><title type="html">Introducing Myhro Notes</title><link href="/2024/07/introducing-myhro-notes" rel="alternate" type="text/html" title="Introducing Myhro Notes" /><published>2024-07-24T00:00:00+00:00</published><updated>2024-07-24T00:00:00+00:00</updated><id>/2024/07/introducing-myhro-notes</id><content type="html" xml:base="/2024/07/introducing-myhro-notes"><![CDATA[<p>Writing is hard. I’ve been writing in this blog since 2011, when posts were still written in Portuguese - those have since been deleted. It hasn’t gotten any easier after nearly 15 years. Posts still take hours to be written, proofread and double-checked before being published. On top of that, much of the energy I have to write longer chunks of text is spent on project documentation, issues and pull request descriptions, both in and out of work-related duties. The result is that posts on this blog are becoming rare - this is the first one for the entire year.</p>

<p>But it doesn’t need to always be like that. Not every piece of advice or knowledge I’d like to share needs to be in a long blog article format. Sometimes I discover something useful, either through my own exploration or based on someone else’s experience, and I share it with close friends with a small comment of a few sentences. This happens on Slack workspaces, WhatsApp groups or even in direct messages. In the end, I thought: what if I write this for the wider internet and share the link with the same people, instead of writing directly to them?</p>

<p>Based on the concept of “blogmarks”, where small blog posts are used to share links that, in a distant past, would be bookmarked to <a href="https://en.wikipedia.org/wiki/Delicious_(website)">del.icio.us</a>, I started <a href="https://notes.myhro.info/">Myhro Notes</a>. There’s usually one or two short paragraphs adding context, explaining why the link is interesting. They are listed by date, like a blog, but only the title is visible on the home page. The posts themselves will eventually be available on search engines. I expect my future self to be one of its users looking for content posted there.</p>]]></content><author><name>Tiago Ilieve</name></author><summary type="html"><![CDATA[Writing is hard. I’ve been writing in this blog since 2011, when posts were still written in Portuguese - those have since been deleted. It hasn’t gotten any easier after nearly 15 years. Posts still take hours to be written, proofread and double-checked before being published. On top of that, much of the energy I have to write longer chunks of text is spent on project documentation, issues and pull request descriptions, both in and out of work-related duties. The result is that posts on this blog are becoming rare - this is the first one for the entire year.]]></summary></entry><entry><title type="html">Managing Podman containers with systemd</title><link href="/2023/12/managing-podman-containers-with-systemd" rel="alternate" type="text/html" title="Managing Podman containers with systemd" /><published>2023-12-23T00:00:00+00:00</published><updated>2023-12-23T00:00:00+00:00</updated><id>/2023/12/managing-podman-containers-with-systemd</id><content type="html" xml:base="/2023/12/managing-podman-containers-with-systemd"><![CDATA[<p>Since my early days in programming, I’ve always worried about isolated development environments. Of course, this wasn’t relevant when developing C applications with no external dependencies in <a href="https://www.codeblocks.org/">Code::Blocks</a>, but it soon became a necessity when I had to deal with Python packages through <a href="https://virtualenv.pypa.io/">virtualenv</a>. The same happened with Ruby versions using <a href="https://github.com/rbenv/rbenv">rbenv</a>. Later I settled on <a href="https://asdf-vm.com/">asdf</a> to do that with multiple Go/Node.js versions, which basically solved the problem for good for many programming languages and even some CLI tools that are sensible to versioning, like <code class="language-plaintext highlighter-rouge">kubectl</code>.</p>

<p>But dealing with multiple runtimes or packages is just one piece of the equation in the grand schema of cleanly handling dependencies. Sometimes you have to worry about the versions of the external services a project makes use of, like a database or cache system. This is also a solved problem since <a href="https://en.wikipedia.org/wiki/Docker_(software)">Docker</a> came into the picture over 10 years ago. I remember that the Docker Compose mantra when it launched (<a href="https://web.archive.org/web/20140802222736/http://www.fig.sh/">still called Fig</a>) was: “<em>no more installing Postgres on your laptop!</em>”. This is just a bit more complicated when you don’t use the original Docker implementation, but another container management system, like <a href="https://podman.io/">Podman</a>.</p>

<p>Podman offers several advantages over Docker. It can run containers without requiring <code class="language-plaintext highlighter-rouge">root</code> access; it doesn’t depend on a service daemon running all the time; and it doesn’t require <a href="https://www.docker.com/pricing/">a paid subscription depending on the usage</a>. It’s a simpler tool, with an almost 1:1 compatible UI overall. The main difference is that it doesn’t seamlessly handle containers with <code class="language-plaintext highlighter-rouge">--restart</code> flags. I mean, of course it does restart containers when their processes are interrupted, but they won’t be brought up after a host reboot - which tends to happen from time to time in a workstation.</p>

<p>When looking into how to solve this problem, I realised that the <code class="language-plaintext highlighter-rouge">podman generate</code> command can create <a href="https://systemd.io/">systemd</a> service unit files. So instead of tinkering about how to integrate the two tools, figuring the file syntax and functionality, it’s possible to just create a new systemd service of the desired container as if it were any other program/process. And the best part is that we can still do that without <code class="language-plaintext highlighter-rouge">root</code>, thanks to <a href="https://wiki.archlinux.org/title/systemd/User">systemd user services</a>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ podman create --name redis -p 6379:6379 docker.io/redis:7
Trying to pull docker.io/library/redis:7...
(...)
$ mkdir -p ~/.config/systemd/user/
$ podman generate systemd --name redis &gt; ~/.config/systemd/user/redis.service
</code></pre></div></div>

<p>To increase the service’s reliability, it’s preferable to drop the <code class="language-plaintext highlighter-rouge">PIDFile</code> line from the configuration file. It typically looks like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PIDFile=/run/user/1000/containers/vfs-containers/c1b1c3e5dba5368c29ada52a638378e5fec74e1a62e913919528b9c3846c14bb/userdata/conmon.pid
</code></pre></div></div>

<p>This ensures that even if the container is recreated, like when updating its image, systemd won’t be referencing its older ID, as it will only care about its name. This can be done programmatically with <code class="language-plaintext highlighter-rouge">sed</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sed -i '/PIDFile/d' ~/.config/systemd/user/redis.service
</code></pre></div></div>

<p>The generated file should be similar to:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># container-redis.service
# autogenerated by Podman 4.3.1
# Sat Dec 23 17:18:01 -03 2023
</span>
<span class="nn">[Unit]</span>
<span class="py">Description</span><span class="p">=</span><span class="s">Podman container-redis.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="nn">[Service]</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 start redis</span>
<span class="py">ExecStop</span><span class="p">=</span><span class="s">/usr/bin/podman stop  </span><span class="se">\
</span>        <span class="s">-t 10 redis</span>
<span class="py">ExecStopPost</span><span class="p">=</span><span class="s">/usr/bin/podman stop  </span><span class="se">\
</span>        <span class="s">-t 10 redis</span>
<span class="py">Type</span><span class="p">=</span><span class="s">forking</span>

<span class="nn">[Install]</span>
<span class="py">WantedBy</span><span class="p">=</span><span class="s">default.target</span>
</code></pre></div></div>

<p>The final step consists in starting the service and enabling it to launch on boot:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ systemctl --user start redis
$ systemctl --user enable redis
Created symlink /home/myhro/.config/systemd/user/default.target.wants/redis.service → /home/myhro/.config/systemd/user/redis.service.
$ systemctl --user status redis
● redis.service - Podman container-redis.service
     Loaded: loaded (/home/myhro/.config/systemd/user/redis.service; enabled; preset: enabled)
     Active: active (running) since Sat 2023-12-23 17:25:40 -03; 13s ago
       Docs: man:podman-generate-systemd(1)
      Tasks: 17 (limit: 37077)
     Memory: 11.1M
        CPU: 60ms
     CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/redis.service
             ├─46851 /usr/bin/slirp4netns --disable-host-loopback --mtu=65520 --enable-sandbox --enable-seccomp --enable-ipv6 -c -e 3 -r 4 --netns-type=path /run/user/1000/netns/netns-102ff957-157c-adcb-bd4a-45e7e0d2a50f tap0
             ├─46853 rootlessport
             ├─46859 rootlessport-child
             └─46869 /usr/bin/conmon --api-version 1 -c c1b1c3e5dba5368c29ada52a638378e5fec74e1a62e913919528b9c3846c14bb -u c1b1c3e5dba5368c29ada52a638378e5fec74e1a62e913919528b9c3846c14bb -r /usr/bin/crun -b /home/myhro/.local/share/(...)

Dec 23 17:25:40 leptok redis[46869]: 1:C 23 Dec 2023 20:25:40.071 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
Dec 23 17:25:40 leptok redis[46869]: 1:M 23 Dec 2023 20:25:40.072 * monotonic clock: POSIX clock_gettime
Dec 23 17:25:40 leptok redis[46869]: 1:M 23 Dec 2023 20:25:40.072 * Running mode=standalone, port=6379.
Dec 23 17:25:40 leptok redis[46869]: 1:M 23 Dec 2023 20:25:40.072 * Server initialized
Dec 23 17:25:40 leptok redis[46869]: 1:M 23 Dec 2023 20:25:40.072 * Loading RDB produced by version 7.2.3
Dec 23 17:25:40 leptok redis[46869]: 1:M 23 Dec 2023 20:25:40.072 * RDB age 18 seconds
Dec 23 17:25:40 leptok redis[46869]: 1:M 23 Dec 2023 20:25:40.072 * RDB memory usage when created 0.83 Mb
Dec 23 17:25:40 leptok redis[46869]: 1:M 23 Dec 2023 20:25:40.072 * Done loading RDB, keys loaded: 0, keys expired: 0.
Dec 23 17:25:40 leptok redis[46869]: 1:M 23 Dec 2023 20:25:40.072 * DB loaded from disk: 0.000 seconds
Dec 23 17:25:40 leptok redis[46869]: 1:M 23 Dec 2023 20:25:40.072 * Ready to accept connections tcp
</code></pre></div></div>

<p>In summary, I quite liked how easy it was to leverage the strengths of both the Podman and systemd in their integration. Being able to do that in a rootless way is definitely a huge plus. Before doing that, I always believed that managing Linux services was a root-only thing. But now that I think about it, I realize that when Docker was the only game in town, managing containers also required elevated privileges. I’m glad that we are moving away from this idea, piece by piece.</p>]]></content><author><name>Tiago Ilieve</name></author><summary type="html"><![CDATA[Since my early days in programming, I’ve always worried about isolated development environments. Of course, this wasn’t relevant when developing C applications with no external dependencies in Code::Blocks, but it soon became a necessity when I had to deal with Python packages through virtualenv. The same happened with Ruby versions using rbenv. Later I settled on asdf to do that with multiple Go/Node.js versions, which basically solved the problem for good for many programming languages and even some CLI tools that are sensible to versioning, like kubectl.]]></summary></entry><entry><title type="html">MikroTik hAP ac3 Review</title><link href="/2023/05/mikrotik-hap-ac3-review" rel="alternate" type="text/html" title="MikroTik hAP ac3 Review" /><published>2023-05-04T00:00:00+00:00</published><updated>2023-05-04T00:00:00+00:00</updated><id>/2023/05/mikrotik-hap-ac3-review</id><content type="html" xml:base="/2023/05/mikrotik-hap-ac3-review"><![CDATA[<p>As I <a href="/2022/01/mikrotik-hex-rb750gr3-review">mentioned in the previous review</a>, my experience with the MikroTik router that only supported wired networking encouraged me to look for a Wi-Fi one. After browsing the available models, the slogan of a particular one, the <a href="https://mikrotik.com/product/hap_ac3">hAP ac3</a>, caught my attention:</p>

<blockquote>
  <p>Forget about endless searching for the perfect router and scrolling through an eternity of reviews and specifications! We have created a single affordable home access point that has all the features you might need for years to come.</p>
</blockquote>

<p>A highly configurable router, with gigabit wired networking and dual-band 2.4 and 5 GHz Wi-Fi at an affordable price (abroad, where it costs US$ 99, not here where it costs R$ 800)? It seemed like exactly what I was looking for.</p>

<h1 id="findings">Findings</h1>

<p>After spending 3 hours configuring the wired router, I thought to myself: “Ah, now that I know how MikroTik works, it’ll be easy. 15 or 20 minutes and everything will be working.” What a mistake. The interface is literally the same with an additional <code class="language-plaintext highlighter-rouge">Wireless</code> option in the menu, but even setting a password on the unprotected Wi-Fi network was challenging. I really scratched my head trying to understand how things worked and spent another 2 hours configuring it in the way I wanted.</p>

<p>Configuring the 5 GHz Wi-Fi transmission, in particular, was quite difficult. It has a “radar detection” system to use higher frequencies (5.5-5.6 GHz) that takes literally 10 minutes (!) on each boot to decide which one to use, time in which the wireless network remains unavailable during this process. To avoid frustration, I manually chose a lower frequency option (5.1-5.2 GHz).</p>

<p>After everything was configured, I noticed that the 5 GHz Wi-Fi signal was weaker in other rooms than it used to be with my TP-Link router. Weak enough for iOS to automatically fall back to 4G. Along with the weaker signal came a drop in connection speed. On my MacBook, it fell from 400 to 200 Mbps, and on my iPhone from 200 to 100 Mbps, both measured at <a href="https://fast.com/">Fast.com</a> in other rooms, compared to the TP-Link router I intended to replace. Although that would be sufficient bandwidth to cover most of my use cases, it seemed unacceptable to downgrade the speed I was used to, given the price and quality I expected from the device.</p>

<p>The solution was to go back to a setup identical to the wired MikroTik: connecting the TP-Link router to the new MikroTik and using only the Wi-Fi from the former. In router mode, the speed loss was the same. In access point mode, I achieved the same speed as before when connecting the TP-Link directly to the modem or the wired MikroTik.</p>

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

<p>It wasn’t a very wise decision to buy a more expensive model because of Wi-Fi and ultimately not use it, but the experience was valuable. It still solves my Dual WAN support issue, albeit in a less than ideal way, and I could return the borrowed MikroTik. I couldn’t exactly pinpoint why its 5 GHz network was so much slower than the TP-Link, but I’ve encountered similar situations caused by software (as the same has happened to me with DD-WRT) in a not-so distant past. It’s not what I expected from MikroTik, a device whose software is precisely its selling point, but who knows. Today, if I were to set up the same system without having the TP-Link router available, I would get a simpler wired MikroTik and connect a Unifi AP to it. It would be the best of both worlds and the cost would be virtually the same.</p>]]></content><author><name>Tiago Ilieve</name></author><summary type="html"><![CDATA[As I mentioned in the previous review, my experience with the MikroTik router that only supported wired networking encouraged me to look for a Wi-Fi one. After browsing the available models, the slogan of a particular one, the hAP ac3, caught my attention:]]></summary></entry><entry><title type="html">MikroTik hEX (RB750Gr3) Review</title><link href="/2022/01/mikrotik-hex-rb750gr3-review" rel="alternate" type="text/html" title="MikroTik hEX (RB750Gr3) Review" /><published>2022-01-01T00:00:00+00:00</published><updated>2022-01-01T00:00:00+00:00</updated><id>/2022/01/mikrotik-hex-rb750gr3-review</id><content type="html" xml:base="/2022/01/mikrotik-hex-rb750gr3-review"><![CDATA[<p>The <a href="https://mikrotik.com/product/RB750Gr3">MikroTik hEX (RB750Gr3)</a> is a simple wired router - one of the cheapest from the Latvian company - that surely does its job. It’s really flexible, which is simultaneously both a pro and a con. It’s the router with the largest amount of configuration options that I ever seen, including the possibility of making any sort of LAN/WAN combination from the five ports available.</p>

<h1 id="impressions">Impressions</h1>

<ul>
  <li>One of MikroTik’s RouterOS biggest features are the countless configuration options in its GUI, both though web and via the native application <a href="https://wiki.mikrotik.com/wiki/Manual:Winbox">WinBox</a>. The problem is that a considerable chunk of its documentation, both in the official channels and from random tips scattered around the Internet, is focused on its CLI (called just <code class="language-plaintext highlighter-rouge">Terminal</code>).</li>
  <li>It has a non-standard factory reset process. One has to hold its reset button, which is super thin and can’t be reached with a pen, as soon as the device is connected to the power supply. It’s not enough to just hold the reset button anytime.</li>
  <li>Its configuration options are flexible, incredibly flexible, to the point that it doesn’t prevent the user from doing a catastrophic and irreversible change. In the first time I was setting up the LAN bridge options, I somehow removed the port in which the router IP (<code class="language-plaintext highlighter-rouge">192.168.88.1</code>) was associated with. This made it completely lose connectivity and there was no way to access its web UI ever again. Had to reset it to restore access after that.</li>
  <li>The wizard configuration system called <code class="language-plaintext highlighter-rouge">Quick Set</code> offers very few options for people who want to configure the router as quick as possible. And the same time, it does too much magic under the hood, resulting in possible headaches in the future. I don’t recommend using it.</li>
  <li>After resetting the device a couple times and configure everything by hand in the <code class="language-plaintext highlighter-rouge">WebFig</code>, its web GUI, I scratched my head to understand how to access this interface after connecting through a Wi-Fi router, which I had to use given it only offers wired connections.
    <ul>
      <li>As the Wi-Fi router was giving me an IP in the <code class="language-plaintext highlighter-rouge">192.168.0.0/24</code> range, I wasn’t able to access the router at <code class="language-plaintext highlighter-rouge">192.168.88.1</code>, even though the Wi-Fi router could reach it. I was only able to access it after manually adding the <code class="language-plaintext highlighter-rouge">192.168.0.0/16</code> range as allowed to the <code class="language-plaintext highlighter-rouge">admin</code> user. When using <code class="language-plaintext highlighter-rouge">Quick Set</code> this isn’t needed, as this configuration is done without ever informing the user.</li>
    </ul>
  </li>
  <li>Some options aren’t available in the <code class="language-plaintext highlighter-rouge">WebFig</code> interface, like changing the MAC address of the ethernet ports directly. At the same time the <code class="language-plaintext highlighter-rouge">Quick Set</code> does this (maybe via CLI in the background), suggesting that this is indeed possible. A workaround for that is to create a single-port bridge and change the MAC address of this virtual interface.</li>
  <li>It’s super easy to update the device, considering both the RouterOS and its firmware itself, which are two separate processes. Given that internet access is properly configured, all that is required are a couple clicks in the interface and a reboot to perform each one of them.</li>
</ul>

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

<p>It’s not a router that I would recommend for the faint of heart nor people who are not ready to face a few frustrations. Even I, being someone used to configure routers even before I was 15 years old, scratched my head to understand how a few things work and spent at least 3 hours to leave it as close as possible from what I wanted. Even though, I wasn’t able to configure the Dual-WAN option with a stand-by connection that is automatically activated when the main one goes down. Via the web UI this didn’t work right and via CLI it looked like too much of a hassle.</p>

<p>In the end I was able to manage both connections manually, accessing the <code class="language-plaintext highlighter-rouge">WebFig</code> and deactivating one while re-activating the other. This is still better than physically switching the cable from one modem to the other, and still having to access the configuration page to switch between DHCP and PPPoE in the Wi-Fi router solely WAN port.</p>

<p>I’m considering trying out a MikroTik Wi-Fi router, given that having fewer devices involved might simplify the setup. Having one less device connected to the uninterruptible power supply will also probably improve its autonomy.</p>]]></content><author><name>Tiago Ilieve</name></author><summary type="html"><![CDATA[The MikroTik hEX (RB750Gr3) is a simple wired router - one of the cheapest from the Latvian company - that surely does its job. It’s really flexible, which is simultaneously both a pro and a con. It’s the router with the largest amount of configuration options that I ever seen, including the possibility of making any sort of LAN/WAN combination from the five ports available.]]></summary></entry><entry><title type="html">Importing CSV files with SQLite</title><link href="/2021/12/importing-csv-files-with-sqlite" rel="alternate" type="text/html" title="Importing CSV files with SQLite" /><published>2021-12-23T00:00:00+00:00</published><updated>2021-12-23T00:00:00+00:00</updated><id>/2021/12/importing-csv-files-with-sqlite</id><content type="html" xml:base="/2021/12/importing-csv-files-with-sqlite"><![CDATA[<p>GitHub offers a very superficial view of how GitHub Actions runners are spending their minutes on private repositories. Currently, the only way to get detailed information about it is via the <code class="language-plaintext highlighter-rouge">Get usage report</code> button in the <a href="https://docs.github.com/en/billing/managing-billing-for-github-actions/viewing-your-github-actions-usage">project/organization billing page</a>. The only problem is that the generated report is a CSV file, shifting the responsibility of filtering and visualizing data to the user. While it’s true that most of the users of this report are used to deal with CSV files, be them developers or accountants experts in handling spreadsheets, this is definitely not the most user-friendly way of offering insights into billing data.</p>

<p>When facing this issue, at first I thought about using <a href="https://harelba.github.io/q/">harelba/q</a> to query the CSV files directly in the command line. The problem is that <code class="language-plaintext highlighter-rouge">q</code> isn’t that straightforward to install, as apparently it is not available via <code class="language-plaintext highlighter-rouge">apt</code> nor <code class="language-plaintext highlighter-rouge">pip</code>, nor one is able to easily change the data once it’s imported, like in a regular database. In the first time I resorted to create a database on PostgreSQL and import the CSV file into it, but after that I never remember the CSV import syntax and it still requires a daemon running just for that. I kept thinking that there should be a simpler way: what if I use SQLite for that?</p>

<p>In order to not have to <code class="language-plaintext highlighter-rouge">CAST()</code> each <code class="language-plaintext highlighter-rouge">TEXT</code> column whenever working with dates or numbers, the following <code class="language-plaintext highlighter-rouge">schema.sql</code> can be used:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">billing</span> <span class="p">(</span>
  <span class="nb">date</span> <span class="nb">DATE</span><span class="p">,</span>
  <span class="n">product</span> <span class="nb">TEXT</span><span class="p">,</span>
  <span class="n">repository</span> <span class="nb">TEXT</span><span class="p">,</span>
  <span class="n">quantity</span> <span class="nb">NUMERIC</span><span class="p">,</span>
  <span class="n">unity</span> <span class="nb">TEXT</span><span class="p">,</span>
  <span class="n">price</span> <span class="nb">NUMERIC</span><span class="p">,</span>
  <span class="n">workflow</span> <span class="nb">TEXT</span><span class="p">,</span>
  <span class="n">notes</span> <span class="nb">TEXT</span>
<span class="p">);</span>
</code></pre></div></div>

<p>After that, it’s possible to import the CSV file with the <code class="language-plaintext highlighter-rouge">sqlite3</code> CLI tool. The <code class="language-plaintext highlighter-rouge">--skip 1</code> argument to the <code class="language-plaintext highlighter-rouge">.import</code> command <a href="https://www.sqlite.org/cli.html#importing_csv_files">is needed to avoid importing the CSV header as data</a>, given that SQLite considers it to be a regular row when the table already exists:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sqlite3 github.db
SQLite version 3.36.0 2021-06-18 18:58:49
Enter ".help" for usage hints.
sqlite&gt; .read schema.sql
sqlite&gt; .mode csv
sqlite&gt; .import --skip 1 c2860a05_2021-12-23_01.csv billing
sqlite&gt; SELECT COUNT(*) FROM billing;
1834
</code></pre></div></div>

<p>Now it’s easy to dig into the billing data. In order to have a better presentation, <code class="language-plaintext highlighter-rouge">.mode column</code> can be enabled to both show the column names and align their output. We can, for instance, find out which workflows consumed most minutes in the last week and their respective repositories:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sqlite&gt; .mode column
sqlite&gt; SELECT date, repository, workflow, quantity FROM billing WHERE date &gt; date('now', '-7 days') AND product = 'actions' ORDER BY quantity DESC LIMIT 5;
date        repository         workflow                              quantity
----------  -----------------  ------------------------------------  --------
2021-12-21  contoso/api        .github/workflows/main.yml            392
2021-12-18  contoso/terraform  .github/workflows/staging-images.yml  361
2021-12-22  contoso/api        .github/workflows/main.yml            226
2021-12-21  contoso/api        .github/workflows/qa.yml              185
2021-12-20  contoso/api        .github/workflows/main.yml            140
</code></pre></div></div>

<p>Another important example of the data that can be fetched is the cost per repository in the last week, summing the cost of all their workflows. An <code class="language-plaintext highlighter-rouge">UPDATE</code> statement is required to apply a small data fix, given that the CSV contains a dollar sign <code class="language-plaintext highlighter-rouge">$</code> in the rows of the <code class="language-plaintext highlighter-rouge">price</code> column that needs to be dropped:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sqlite&gt; UPDATE billing SET price = REPLACE(price, '$', '');
sqlite&gt; SELECT repository, SUM(quantity) * price AS amount FROM billing WHERE date &gt; date('now', '-7 days') AND product = 'actions' GROUP BY repository;
repository          amount
------------------  ------
contoso/api         11.68
contoso/public-web  0.128
contoso/status      1.184
contoso/terraform   2.92
contoso/webapp      0.6
</code></pre></div></div>

<p>Not intuitive as a web page where one can just click around to filter and sort a report, but definitely doable. As a side note, one cool aspect of SQLite is that it doesn’t require a file database do be used. If started as <code class="language-plaintext highlighter-rouge">sqlite3</code>, with no arguments, all of it’s storage needs are handled entirely in memory. This makes it even more interesting for data exploration cases like these, offering all of its queries capabilities without ever persisting data to disk.</p>]]></content><author><name>Tiago Ilieve</name></author><summary type="html"><![CDATA[GitHub offers a very superficial view of how GitHub Actions runners are spending their minutes on private repositories. Currently, the only way to get detailed information about it is via the Get usage report button in the project/organization billing page. The only problem is that the generated report is a CSV file, shifting the responsibility of filtering and visualizing data to the user. While it’s true that most of the users of this report are used to deal with CSV files, be them developers or accountants experts in handling spreadsheets, this is definitely not the most user-friendly way of offering insights into billing data.]]></summary></entry><entry><title type="html">Configuring firewalld on Debian Bullseye</title><link href="/2021/12/configuring-firewalld-on-debian-bullseye" rel="alternate" type="text/html" title="Configuring firewalld on Debian Bullseye" /><published>2021-12-19T00:00:00+00:00</published><updated>2021-12-19T00:00:00+00:00</updated><id>/2021/12/configuring-firewalld-on-debian-bullseye</id><content type="html" xml:base="/2021/12/configuring-firewalld-on-debian-bullseye"><![CDATA[<p>After doing a clean Debian 11 (Bullseye) installation on a new machine, the next step after installing basic CLI tools and disabling SSH root/password logins was to configure its firewall. It’s easy to imagine how big was my surprise when I found out that the <code class="language-plaintext highlighter-rouge">iptables</code> command wasn’t available. While <a href="https://developers.redhat.com/blog/2016/10/28/what-comes-after-iptables-its-successor-of-course-nftables">it’s known for at least 5 years</a> that this was going to happen, it still took me some time to let the idea of its deprecation sink and actually digest the situation. I scratched my head a bit wondering if the day I would be obliged to learn how to use <a href="https://netfilter.org/projects/nftables/">nftables</a> had finally came.</p>

<p>While looking for some guidance on what are the best practices to manage firewall rules these days, I found the article “<a href="https://ral-arturo.org/2019/10/14/debian-netfilter.html">What to expect in Debian 11 Bullseye for nftables/iptables</a>”, which explains the situation in a straightforward way. The article ends up suggesting that <a href="https://firewalld.org/">firewalld</a> is supposed to be the default firewall rules wrapper/manager - something that is news to me. I never met the author while actively working on Debian, but I do know he’s the maintainer of multiple firewall-related packages in the distribution and also works on the <a href="https://www.netfilter.org/">netfilter project</a> itself. Based on these credentials, I took the advice knowing it came from someone who knows what they are doing.</p>

<p>A fun fact is that the <code class="language-plaintext highlighter-rouge">iptables</code> package <a href="https://packages.debian.org/bullseye/firewalld">is actually a dependency</a> for <code class="language-plaintext highlighter-rouge">firewalld</code> on Debian Bullseye. This should not be the case on future releases. After installing it, I went for the simplest goal ever: block all incoming connections while allowing SSH (and preferably <a href="https://mosh.org/">Mosh</a>, if possible). Before doing any changes, I tried to familiarize myself with the basic commands. I won’t repeat what multiple other sources say, so I suggest this <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-using-firewalld-on-centos-8">Digital Ocean article that explains firewalld concepts, like zones and rules persistency</a>.</p>

<p>In summary, what one needs to understand is that there are multiple “zones” within firewalld. Each one can have different sets of rules. In order to simplify the setup, I checked what was the default zone, added the network interface adapter to it and defined the needed rules there. No need for further granularity in this use case. Here, the default zone is the one named <code class="language-plaintext highlighter-rouge">public</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo firewall-cmd --get-default-zone
public
$ sudo firewall-cmd --list-all
public
  target: default
  icmp-block-inversion: no
  interfaces:
  sources:
  services: dhcpv6-client ssh
  ports:
  protocols:
  forward: no
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
</code></pre></div></div>

<p>Knowing that, it was quite simple to associate the internet-connected network interface to it and update the list of allowed services. <code class="language-plaintext highlighter-rouge">dhcpv6-client</code> is going to be removed because this machine isn’t on an IPv6-enabled network:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo firewall-cmd --change-interface eth0
success
$ sudo firewall-cmd --add-service mosh
success
$ sudo firewall-cmd --remove-service dhcpv6-client
success
</code></pre></div></div>

<p>It’s important to execute <code class="language-plaintext highlighter-rouge">sudo firewall-cmd --runtime-to-permanent</code> after confirming the rules where defined as expected, otherwise they would be lost on service/machine restarts:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: mosh ssh
  ports:
  protocols:
  forward: no
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
$ sudo firewall-cmd --runtime-to-permanent
success
</code></pre></div></div>

<p>A side effect of the <code class="language-plaintext highlighter-rouge">target: default</code> setting is that it <code class="language-plaintext highlighter-rouge">REJECT</code>s packets by default, instead of <code class="language-plaintext highlighter-rouge">DROP</code>ing them. This basically informs the client that any connections were actively rejected instead of silently dropping the packets - the latter which might be preferable. It’s confusing why it’s called <code class="language-plaintext highlighter-rouge">default</code> instead of <code class="language-plaintext highlighter-rouge">REJECT</code>, and also not clear <a href="https://www.putorius.net/introduction-to-firewalld-basics.html#firewalld-targets">if it’s actually possible to change the default behavior</a>. In any case, it’s possible to explicitly change it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo firewall-cmd --set-target DROP --permanent
success
$ sudo firewall-cmd --reload
success
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">--set-target</code> option requires the <code class="language-plaintext highlighter-rouge">--permanent</code> flag, but it doesn’t apply the changes instantly, requiring them to be reloaded.</p>

<p>An implication of dropping everything is that ICMP packets are blocked as well, preventing the machine from answering <code class="language-plaintext highlighter-rouge">ping</code> requests. The way this can be configured is a bit confusing, given that the logic is flipped. There’s a need to enable <code class="language-plaintext highlighter-rouge">icmp-block-inversion</code> and add (which in practice would be removing it) an ICMP block for <code class="language-plaintext highlighter-rouge">echo-request</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo firewall-cmd --add-icmp-block-inversion
success
$ sudo firewall-cmd --add-icmp-block echo-request
success
</code></pre></div></div>

<p>The result will look like this, always remembering to persist the changes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo firewall-cmd --list-all
public (active)
  target: DROP
  icmp-block-inversion: yes
  interfaces: eth0
  sources:
  services: mosh ssh
  ports:
  protocols:
  forward: no
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks: echo-request
  rich rules:
$ sudo firewall-cmd --runtime-to-permanent
success
</code></pre></div></div>

<p>For someone who hadn’t used <code class="language-plaintext highlighter-rouge">firewalld</code> before, I can say it was OK to use it in this simple use case. There was no need to learn the syntax for <code class="language-plaintext highlighter-rouge">nft</code> commands nor the one for <code class="language-plaintext highlighter-rouge">nftables</code> rules and it worked quite well in the end. The process of unblocking ICMP <code class="language-plaintext highlighter-rouge">ping</code> requests is a bit cumbersome with the flipped logic, and could have been made simpler, but it’s still doable. All-in-all I’m happy with the solution and will look forward how to use it, for instance, in a <a href="https://docs.ansible.com/ansible/latest/collections/ansible/posix/firewalld_module.html">non-interactive way with Ansible</a>.</p>]]></content><author><name>Tiago Ilieve</name></author><summary type="html"><![CDATA[After doing a clean Debian 11 (Bullseye) installation on a new machine, the next step after installing basic CLI tools and disabling SSH root/password logins was to configure its firewall. It’s easy to imagine how big was my surprise when I found out that the iptables command wasn’t available. While it’s known for at least 5 years that this was going to happen, it still took me some time to let the idea of its deprecation sink and actually digest the situation. I scratched my head a bit wondering if the day I would be obliged to learn how to use nftables had finally came.]]></summary></entry><entry><title type="html">Exporting Prometheus metrics from Go</title><link href="/2021/08/exporting-prometheus-metrics-from-golang" rel="alternate" type="text/html" title="Exporting Prometheus metrics from Go" /><published>2021-08-14T00:00:00+00:00</published><updated>2021-08-14T00:00:00+00:00</updated><id>/2021/08/exporting-prometheus-metrics-from-golang</id><content type="html" xml:base="/2021/08/exporting-prometheus-metrics-from-golang"><![CDATA[<p>Exporting <a href="https://prometheus.io/">Prometheus</a> metrics is quite straightforward, specially from a Go application - it is a Go project after all, as long as you know the basics of the process. The first step is to understand that Prometheus is not just a monitoring system, but also a time series database. So in order to collect metrics with it, there are three components involved: an application exporting its metrics in Prometheus format, a Prometheus scraper that will grab these metrics in pre-defined intervals and a time series database that will store them for later consumption - usually Prometheus itself, but it’s possible to use <a href="https://prometheus.io/docs/operating/integrations/#remote-endpoints-and-storage">other storage backends</a>. The focus here is the first component, the metrics export process.</p>

<p>The first step is to decide which type is more suitable for the metric to be exported. The Prometheus documentation gives <a href="https://prometheus.io/docs/concepts/metric_types/">a nice explanation about the four types (Counter, Gauge, Histogram and Summary) offered</a>. What’s important to understand is that they are basically a metric name (like <code class="language-plaintext highlighter-rouge">job_queue_size</code>), possibly associated with labels (like <code class="language-plaintext highlighter-rouge">{type="email"}</code>) that will have a numeric value associated with it (like <code class="language-plaintext highlighter-rouge">10</code>). When scraped, these will be associated with the collection time, which makes it possible, for instance, to later plot these values in a graph. Different types of metrics will offer different facilities to collect the data.</p>

<p>Next, there’s a need to decide when metrics will be observed. The short answer is “<a href="https://prometheus.io/docs/instrumenting/writing_exporters/#scheduling">synchronously, at collection time</a>”. The application shouldn’t worry about observing metrics in the background and give the last collected values when scraped. The scrape request itself should trigger the metrics observation - it doesn’t matter if this process isn’t instant. The long answer is that it depends, as when monitoring events, like HTTP requests or jobs processed in a queue, metrics will be observed at event time to be later collected when scraped.</p>

<p>The following example will illustrate how metrics can be observed at event time:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
  <span class="s">"io"</span>
  <span class="s">"log"</span>
  <span class="s">"net/http"</span>

  <span class="s">"github.com/gorilla/mux"</span>
  <span class="s">"github.com/prometheus/client_golang/prometheus"</span>
  <span class="s">"github.com/prometheus/client_golang/prometheus/promhttp"</span>
<span class="p">)</span>

<span class="k">var</span> <span class="n">httpRequestsTotal</span> <span class="o">=</span> <span class="n">prometheus</span><span class="o">.</span><span class="n">NewCounter</span><span class="p">(</span>
  <span class="n">prometheus</span><span class="o">.</span><span class="n">CounterOpts</span><span class="p">{</span>
    <span class="n">Name</span><span class="o">:</span>        <span class="s">"http_requests_total"</span><span class="p">,</span>
    <span class="n">Help</span><span class="o">:</span>        <span class="s">"Total number of HTTP requests"</span><span class="p">,</span>
    <span class="n">ConstLabels</span><span class="o">:</span> <span class="n">prometheus</span><span class="o">.</span><span class="n">Labels</span><span class="p">{</span><span class="s">"server"</span><span class="o">:</span> <span class="s">"api"</span><span class="p">},</span>
  <span class="p">},</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">HealthCheck</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">httpRequestsTotal</span><span class="o">.</span><span class="n">Inc</span><span class="p">()</span>
  <span class="n">w</span><span class="o">.</span><span class="n">WriteHeader</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusOK</span><span class="p">)</span>
  <span class="n">io</span><span class="o">.</span><span class="n">WriteString</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="s">"OK"</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">prometheus</span><span class="o">.</span><span class="n">MustRegister</span><span class="p">(</span><span class="n">httpRequestsTotal</span><span class="p">)</span>

  <span class="n">r</span> <span class="o">:=</span> <span class="n">mux</span><span class="o">.</span><span class="n">NewRouter</span><span class="p">()</span>
  <span class="n">r</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"/healthcheck"</span><span class="p">,</span> <span class="n">HealthCheck</span><span class="p">)</span>
  <span class="n">r</span><span class="o">.</span><span class="n">Handle</span><span class="p">(</span><span class="s">"/metrics"</span><span class="p">,</span> <span class="n">promhttp</span><span class="o">.</span><span class="n">Handler</span><span class="p">())</span>

  <span class="n">addr</span> <span class="o">:=</span> <span class="s">":8080"</span>
  <span class="n">srv</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">http</span><span class="o">.</span><span class="n">Server</span><span class="p">{</span>
    <span class="n">Addr</span><span class="o">:</span>    <span class="n">addr</span><span class="p">,</span>
    <span class="n">Handler</span><span class="o">:</span> <span class="n">r</span><span class="p">,</span>
  <span class="p">}</span>
  <span class="n">log</span><span class="o">.</span><span class="n">Print</span><span class="p">(</span><span class="s">"Starting server at "</span><span class="p">,</span> <span class="n">addr</span><span class="p">)</span>
  <span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">srv</span><span class="o">.</span><span class="n">ListenAndServe</span><span class="p">())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>There’s a single Counter metric called <code class="language-plaintext highlighter-rouge">http_requests_total</code> (the “total” suffix is a <a href="https://prometheus.io/docs/practices/naming/">naming convention</a>) with a constant label <code class="language-plaintext highlighter-rouge">{server="api"}</code>. The <code class="language-plaintext highlighter-rouge">HealthCheck()</code> HTTP handler itself will call the <code class="language-plaintext highlighter-rouge">Inc()</code> method responsible for incrementing this counter, but in a real-life application that would <a href="https://github.com/gorilla/mux#middleware">preferable be done in a HTTP middleware</a>. It’s important to not forget to register the metrics variable within the <code class="language-plaintext highlighter-rouge">prometheus</code> library itself, otherwise it won’t show up in the collection.</p>

<p>Let’s see how they work using the <a href="https://github.com/ducaale/xh"><code class="language-plaintext highlighter-rouge">xh</code> HTTPie Rust clone</a>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ xh localhost:8080/metrics | grep http_requests_total
# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{server="api"} 0
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ xh localhost:8080/healthcheck
HTTP/1.1 200 OK
content-length: 2
content-type: text/plain; charset=utf-8
date: Sat, 14 Aug 2021 12:26:03 GMT

OK
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ xh localhost:8080/metrics | grep http_requests_total
# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{server="api"} 1
</code></pre></div></div>

<p>This is cool, but as the metric relies on constant labels, the measurement isn’t that granular. With a small modification we can use dynamic labels to store this counter per route and HTTP method:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">diff --git a/main.go b/main.go
index 5d6079a..53249b1 100644
</span><span class="gd">--- a/main.go
</span><span class="gi">+++ b/main.go
</span><span class="p">@@ -10,16 +10,17 @@</span> import (
        "github.com/prometheus/client_golang/prometheus/promhttp"
 )

-var httpRequestsTotal = prometheus.NewCounter(
<span class="gi">+var httpRequestsTotal = prometheus.NewCounterVec(
</span>        prometheus.CounterOpts{
                Name:        "http_requests_total",
                Help:        "Total number of HTTP requests",
                ConstLabels: prometheus.Labels{"server": "api"},
        },
<span class="gi">+       []string{"route", "method"},
</span> )

 func HealthCheck(w http.ResponseWriter, r *http.Request) {
<span class="gd">-       httpRequestsTotal.Inc()
</span><span class="gi">+       httpRequestsTotal.WithLabelValues("/healthcheck", r.Method).Inc()
</span>        w.WriteHeader(http.StatusOK)
        io.WriteString(w, "OK")
 }
</code></pre></div></div>

<p>Again, in a real-life application it’s better to <a href="https://pkg.go.dev/github.com/gorilla/mux#CurrentRoute">let the route be auto-discovered in runtime</a> instead of hard-coding its value within the handler. The result will look like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ xh localhost:8080/metrics | grep http_requests_total
# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{route="/healthcheck",method="GET",server="api"} 1
</code></pre></div></div>

<p>The key here is to understand that the counter vector doesn’t that mean multiple values will be stored in the same metric. What it does is to use different label values to create a multi-dimensional metric, where each label combination is an element of the vector.</p>]]></content><author><name>Tiago Ilieve</name></author><summary type="html"><![CDATA[Exporting Prometheus metrics is quite straightforward, specially from a Go application - it is a Go project after all, as long as you know the basics of the process. The first step is to understand that Prometheus is not just a monitoring system, but also a time series database. So in order to collect metrics with it, there are three components involved: an application exporting its metrics in Prometheus format, a Prometheus scraper that will grab these metrics in pre-defined intervals and a time series database that will store them for later consumption - usually Prometheus itself, but it’s possible to use other storage backends. The focus here is the first component, the metrics export process.]]></summary></entry></feed>