<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"><channel xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><title>IT Notes - tutorial</title><link>https://it-notes.dragas.net/categories/tutorial/</link><description>Articles in category tutorial</description><language>en</language><lastBuildDate>Thu, 07 May 2026 10:45:00 +0000</lastBuildDate><atom:link href="https://it-notes.dragas.net/categories/tutorial/feed.xml" rel="self" type="application/rss+xml"></atom:link><item><title>Monitor your devices with LibreNMS on FreeBSD</title><link>https://it-notes.dragas.net/2026/05/07/monitor-your-services-with-librenms-on-freebsd/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/watchdog.webp" alt="Monitor your devices with LibreNMS on FreeBSD"&gt;&lt;/p&gt;&lt;p&gt;&lt;a href="https://www.librenms.org"&gt;LibreNMS&lt;/a&gt; has been a faithful companion for years now. It quietly handles the monitoring of my servers, devices, and services without demanding much in return - exactly what you want from a tool whose job is to watch over everything else. It's a solid alternative to heavier solutions like Zabbix, and it gives you alerts, data, and graphs on virtually anything reachable over SNMP.&lt;/p&gt;
&lt;p&gt;I usually install it on a host that is &lt;em&gt;not&lt;/em&gt; reachable from the outside, then let it poll all the devices through a VPN: a single observation point, clean perimeter. The ability to create multiple dashboards - and to filter them by user - has also let me give clients a transparent window onto their own servers. Transparency, in my experience, is always the better long-term bet.&lt;/p&gt;
&lt;p&gt;Together with &lt;a href="https://it-notes.dragas.net/2024/07/22/install-uptime-kuma-freebsd-jail/"&gt;Uptime-Kuma&lt;/a&gt; (and the good old Nagios/Munin pair), LibreNMS lives in a FreeBSD jail on my monitoring servers and just does its job.&lt;/p&gt;
&lt;p&gt;This post walks through a plain installation of LibreNMS on FreeBSD: package-based, no reverse proxy, no HTTPS, no fancy hardening. The goal is to get to a working setup you can build on top of.&lt;/p&gt;
&lt;h2&gt;Assumptions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;FreeBSD 15.0-RELEASE, in a jail or on a dedicated VM/host&lt;/li&gt;
&lt;li&gt;nginx + php-fpm + MySQL 8.4&lt;/li&gt;
&lt;li&gt;LibreNMS installed from the official package — not via &lt;code&gt;git clone&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One note before we start: in this guide I use plain HTTP just to reach the first-time setup. If your LibreNMS instance won't stay confined to a private network or behind a VPN, configuring HTTPS is mandatory, not optional.&lt;/p&gt;
&lt;h2&gt;Installation&lt;/h2&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;pkg install librenms mysql84-server python3 nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;LibreNMS currently depends on PHP 8.4. If you want to speed PHP up, install OPcache too:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;pkg install php84-opcache
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;MySQL&lt;/h2&gt;
&lt;p&gt;Two settings need to be in place &lt;em&gt;before&lt;/em&gt; MySQL starts for the first time. After the first start they cannot be changed without reinitializing the data directory, so it's worth getting them right now.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;cd /usr/local/etc/mysql
cp my.cnf.sample my.cnf
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In the &lt;code&gt;[mysqld]&lt;/code&gt; section, add:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;innodb_file_per_table=1
lower_case_table_names=0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now start MySQL:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;service mysql-server enable
service mysql-server start
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;On a fresh FreeBSD install, the local &lt;code&gt;root&lt;/code&gt; user can connect to MySQL without a password from the command line. Connect and create the database and user. I'm using &lt;code&gt;password&lt;/code&gt; here as a placeholder - don't.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;mysql
&lt;/code&gt;&lt;/pre&gt;

&lt;pre class="highlight"&gt;&lt;code class="language-sql"&gt;CREATE DATABASE librenms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'librenms'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON librenms.* TO 'librenms'@'localhost';
exit
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;php-fpm&lt;/h2&gt;
&lt;p&gt;Edit &lt;code&gt;/usr/local/etc/php-fpm.d/www.conf&lt;/code&gt; and adjust the listen directives:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;listen = /var/run/php-fpm-librenms.sock
listen.owner = www
listen.group = www
listen.mode = 0660
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then create &lt;code&gt;php.ini&lt;/code&gt; from the production sample:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;cd /usr/local/etc
cp php.ini-production php.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And set the timezone in &lt;code&gt;php.ini&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;date.timezone = Europe/Rome
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;nginx&lt;/h2&gt;
&lt;p&gt;Since this jail (or host) is dedicated to LibreNMS, we can rewrite the &lt;code&gt;server&lt;/code&gt; block in &lt;code&gt;/usr/local/etc/nginx/nginx.conf&lt;/code&gt; directly:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-nginx"&gt;server {
    listen      80;
    #server_name yourServerName
    root        /usr/local/www/librenms/html;
    index       index.php;

    charset utf-8;
    gzip on;
    gzip_types text/css application/javascript text/javascript application/x-javascript image/svg+xml text/plain text/xsd text/xsl text/xml image/x-icon;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location /api/v0 {
        try_files $uri $uri/ /api_v0.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        set $path_info $fastcgi_path_info;
        try_files $fastcgi_script_name =404;
        include fastcgi_params;
        fastcgi_param SERVER_SOFTWARE &amp;quot;&amp;quot;;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $path_info;
        fastcgi_index index.php;
        fastcgi_pass unix:/var/run/php-fpm-librenms.sock;
        fastcgi_buffers 256 4k;
        fastcgi_intercept_errors on;
        fastcgi_read_timeout 14400;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now start nginx and php-fpm:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;service nginx enable
service nginx start

service php_fpm enable
service php_fpm start
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;LibreNMS configuration&lt;/h2&gt;
&lt;p&gt;Copy the default config:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;cp /usr/local/www/librenms/config.php.default /usr/local/www/librenms/config.php
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Because we installed from the package, this file already has the right commands and paths for FreeBSD - no need to hunt down &lt;code&gt;mtr&lt;/code&gt;, &lt;code&gt;fping&lt;/code&gt;, &lt;code&gt;snmpwalk&lt;/code&gt; and friends one by one.&lt;/p&gt;
&lt;p&gt;Create the directory for RRD graphs and set ownership:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;mkdir -p /var/db/librenms/rrd
chown -R www:www /var/db/librenms
chmod 775 /var/db/librenms/rrd
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then the &lt;code&gt;.env&lt;/code&gt; file:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;cd /usr/local/www/librenms
cp .env.example .env
chown www .env
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Edit &lt;code&gt;.env&lt;/code&gt; and set at least:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DB_DATABASE&lt;/code&gt; - &lt;code&gt;librenms&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DB_USERNAME&lt;/code&gt; - &lt;code&gt;librenms&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DB_PASSWORD&lt;/code&gt; - the one you actually used (not &lt;code&gt;password&lt;/code&gt;, please)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then add this line, which tells LibreNMS we still need to run the web installer:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;INSTALL=true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A note on permissions. The official LibreNMS documentation suggests &lt;code&gt;chown -R www:www&lt;/code&gt; over the entire application tree, but on FreeBSD the package already lays down sane ownership, with &lt;code&gt;storage/&lt;/code&gt; and &lt;code&gt;bootstrap/cache/&lt;/code&gt; writable by &lt;code&gt;www&lt;/code&gt;. There's no reason to widen the rest of the codebase. If &lt;code&gt;validate.php&lt;/code&gt; complains later about something write-related, the first place to check is:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;ls -la /usr/local/www/librenms/storage /usr/local/www/librenms/bootstrap/cache
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now generate the app key as &lt;code&gt;www&lt;/code&gt;, since the file is owned by &lt;code&gt;www&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;su -m www -c &amp;quot;php artisan key:generate&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And tighten &lt;code&gt;.env&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;chmod 600 .env
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Refresh the configuration cache:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;su -m www -c &amp;quot;lnms config:clear&amp;quot;
su -m www -c &amp;quot;lnms config:cache&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Web installer&lt;/h2&gt;
&lt;p&gt;Open &lt;code&gt;http://host/install&lt;/code&gt; and follow the steps. The validation process may fail. Refreshing the cache picks up the values written to &lt;code&gt;config.php&lt;/code&gt; during the install:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;su -m www -c &amp;quot;lnms config:clear&amp;quot;
su -m www -c &amp;quot;lnms config:cache&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When the web installer is done, edit .env again and remove the INSTALL=true line if it's still there. Leaving it in place re-exposes the installer to anyone who can reach the URL.&lt;/p&gt;
&lt;h2&gt;Polling service&lt;/h2&gt;
&lt;p&gt;LibreNMS needs something to actually run the polls. On FreeBSD, the package ships an rc service that runs the LibreNMS dispatcher, so there's no need to manage cron entries by hand the way most Linux guides assume.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;service librenms enable
service librenms start
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Validate&lt;/h2&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;cd /usr/local/www/librenms
su -m www -c './validate.php'
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You may see a couple of complaints right after starting the service - usually scheduler-related and self-resolving within a few minutes. Re-run &lt;code&gt;validate.php&lt;/code&gt; once the dispatcher has had time to settle. Anything still red after that is worth investigating.&lt;/p&gt;
&lt;h2&gt;Next steps&lt;/h2&gt;
&lt;p&gt;At this point you can log into the web interface and start adding devices, configuring SNMP, and building dashboards. For that, the &lt;a href="https://docs.librenms.org/"&gt;official LibreNMS documentation&lt;/a&gt; is excellent, and there's no point in me paraphrasing it here.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Thu, 07 May 2026 10:45:00 +0000</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2026/05/07/monitor-your-services-with-librenms-on-freebsd/</guid><category>freebsd</category><category>monitoring</category><category>server</category><category>networking</category><category>hosting</category><category>tutorial</category><category>security</category><category>jail</category><category>ownyourdata</category></item><item><title>Time Machine inside a FreeBSD jail</title><link>https://it-notes.dragas.net/2026/01/28/time-machine-freebsd-jail/</link><description>&lt;p&gt;&lt;img src="https://unsplash.com/photos/W32yvc0JJjw/download?force=true&amp;w=640" alt="Time Machine inside a FreeBSD jail"&gt;&lt;/p&gt;&lt;p&gt;Many of my clients do not use Microsoft systems on their desktops; they use Linux-based systems or, in some cases, FreeBSD. Many use Apple systems - macOS - and are generally satisfied with them.
While I wash my hands of it when it comes to Microsoft systems (telling them they have to manage their desktops autonomously), I am often able to lend a hand with macOS. And one of the main requests they make is to manage the backups of their individual workstations.&lt;/p&gt;
&lt;p&gt;macOS, thanks to its Unix base, offers good native tools. Time Machine is transparent and effective, allowing a certain freedom of management. APFS, Apple's current file system, supports snapshots, so the backup will be effectively made on a snapshot. It also supports multiple receiving devices, so you can even have a certain redundancy of the backup itself.&lt;/p&gt;
&lt;p&gt;Having many FreeBSD servers, I am often asked to use their resources and storage. To build, in practice, a Time Machine inside one of the servers. And it is a simple and practical operation, quick and "painless". There are many guides, including the excellent one by &lt;a href="https://freebsdfoundation.org/our-work/journal/browser-based-edition/storage-and-filesystems/samba-based-time-machine-backups/"&gt;Benedict Reuschling&lt;/a&gt; from which I took inspiration for this one, and I will describe the steps I usually follow to set it all up in just a few minutes.&lt;/p&gt;
&lt;p&gt;I usually use &lt;a href="https://bastillebsd.org"&gt;BastilleBSD&lt;/a&gt; to manage my jails, so the first step is to create a new jail dedicated to the purpose. Here you have to decide on the approach: I suggest using a VNET jail or an "inherit" jail - meaning one that attaches to the host's network stack. On one hand, the inherit approach is less secure but, as often happens, it depends on the complexity of the situation. If, for example, we are using a Raspberry PI dedicated to the purpose, there is no reason to complicate things with bridges, etc., but we can attach directly to the network card with a creation command like:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;bastille create tmjail 15.0-RELEASE inherit igb0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Where &lt;code&gt;igb0&lt;/code&gt; is the network interface we want to attach to.&lt;/p&gt;
&lt;p&gt;In case we want to attach to the interface but in the form of a bridge, we should use this syntax:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;bastille create -V tmjail 15.0-RELEASE 192.168.0.42/24 igb0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Or, if our server already has a bridge (in this case it's &lt;code&gt;bridge0&lt;/code&gt;, but yours might be named differently):&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;bastille create -B tmjail 15.0-RELEASE 192.168.0.42/24 bridge0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point, you can choose: do we want to keep the backups inside the jail or in a separate dataset - which can even be on another pool? In some cases, this can be extremely useful: often I have jails running on fast disks (SSD or NVMe) but abundant storage on slower devices. In this example, therefore, I will create an external dataset for the backups (directly from the host) and mount it in the jail. You could also delegate the entire management of the dataset to the jail, which is a different approach.&lt;/p&gt;
&lt;p&gt;Let's create a space of 600 GB - already reserved - on the chosen pool. 600 GB is a small space, but it's ok for an example:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zfs create -o quota=600G -o reservation=600G bigpool/tmdata
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We can also create separate datasets inside for each user and assign a specific space:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zfs create -o refquota=500g -o refreservation=500g bigpool/tmdata/stefano
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We can enter the jail and install what we need, remembering also to create the "mountpoint" for the dataset we just created:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;bastille console tmjail 

pkg install -y samba419
mkdir /tmdata
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Exit the jail and instruct Bastille to mount the dataset inside the jail every time it is launched:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;exit
bastille mount tmjail /bigpool/tmdata /tmdata nullfs rw 0 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let's go back into the jail and start with the actual configuration. First, for each Time Machine user, we will create a system user. In my example, I will create the user "stefano", giving him &lt;code&gt;/var/empty&lt;/code&gt; as the home directory - this will give an error since we created a Bastille thin jail, but it's not a problem. It happens because in a thin jail some system paths are read-only or not manageable as they are on a full base system, but the user is only needed for ownership and Samba login.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-text"&gt;root@tmjail:~ # adduser
Username: stefano
Full name: Stefano
Uid (Leave empty for default):
Login group [stefano]:
Login group is stefano. Invite stefano into other groups? []:
Login class [default]:
Shell (sh csh tcsh nologin) [sh]: nologin
Home directory [/home/stefano]: /var/empty
Home directory permissions (Leave empty for default):
Use password-based authentication? [yes]: no
Lock out the account after creation? [no]:
Username    : stefano
Password    : &amp;lt;disabled&amp;gt;
Full Name   : Stefano
Uid         : 1001
Class       :
Groups      : stefano
Home        : /var/empty
Home Mode   :
Shell       : /usr/sbin/nologin
Locked      : no
OK? (yes/no) [yes]: yes
pw: chmod(var/empty): Operation not permitted
pw: chown(var/empty): Operation not permitted
adduser: INFO: Successfully added (stefano) to the user database.
Add another user? (yes/no) [no]: no
Goodbye!
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Give the correct permissions to the user:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# If you've not created specific datasets for the users, you'd better create their home directories now
mkdir /tmdata/stefano
chown -R stefano /tmdata/stefano/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we configure Samba for Time Machine. The file to create/modify is &lt;code&gt;/usr/local/etc/smb4.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-ini"&gt;[global]
workgroup = WORKGROUP
security = user
passdb backend = tdbsam
fruit:aapl = yes
fruit:model = MacSamba
fruit:advertise_fullsync = true
fruit:metadata = stream
fruit:veto_appledouble = no
fruit:nfs_aces = no
fruit:wipe_intentionally_left_blank_rfork = yes
fruit:delete_empty_adfiles = yes

[TimeMachine]
path = /tmdata/%U
valid users = %U
browseable = yes
writeable = yes
vfs objects = catia fruit streams_xattr zfsacl
fruit:time machine = yes
create mask = 0600
directory mask = 0700
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We have set up Time Machine to support all the necessary features of macOS and to show itself as "Time Machine". Having set &lt;code&gt;path = /tmdata/%U&lt;/code&gt;, each user will only see their own path.&lt;/p&gt;
&lt;p&gt;At this point, we create the Samba user (meaning the one we will have to type on macOS when we configure the Time Machine):&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;smbpasswd -a stefano
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The Time Machine is seen by macOS because it announces itself via mDNS on the network. This type of service is performed by Avahi, which we are now going to configure. Although not strictly necessary (we can always find the Time Machine by connecting directly to its IP and macOS will remember everything), seeing it announced will help other non-expert users and ourselves when we have to configure another Mac in the future.&lt;/p&gt;
&lt;p&gt;Recent Samba releases won't need any specific avahi configuration, so we can skip this step.&lt;/p&gt;
&lt;p&gt;We are now ready to enable everything.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;service dbus enable
service dbus start
service avahi-daemon enable
service avahi-daemon start
service samba_server enable
service samba_server start
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Et voilà. If everything went according to plan, the Time Machine will announce itself on your network (if you have different networks, remember to configure the mDNS proxy on your router) and you will be able to log in (with the smb user you created) and start your first backup.&lt;/p&gt;
&lt;p&gt;I suggest encrypting the backups for maximum security and observing, from time to time, your Mac as it silently makes its backups to your trusted FreeBSD server.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Wed, 28 Jan 2026 08:52:00 +0000</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2026/01/28/time-machine-freebsd-jail/</guid><category>freebsd</category><category>timemachine</category><category>apple</category><category>backup</category><category>data</category><category>zfs</category><category>server</category><category>tutorial</category><category>ownyourdata</category></item><item><title>Installing Void Linux on ZFS with Hibernation Support</title><link>https://it-notes.dragas.net/2025/12/22/void-linux-zfs-hibernation-guide/</link><description>&lt;p&gt;&lt;img src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/02/Void_Linux_logo.svg/960px-Void_Linux_logo.svg.png" alt="Installing Void Linux on ZFS with Hibernation Support"&gt;&lt;/p&gt;&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;FreeBSD continues to make strides in desktop support, but Linux still holds an advantage in hardware compatibility. After running openSUSE Tumbleweed on my mini PC for several months, I decided it was time to switch to a solution I could control more closely. Not because Tumbleweed doesn't work well - it works great! - but I prefer having direct control over what happens on my machine. And I want native ZFS, because I prefer it over btrfs and it allows me to manage snapshots, backups, and rollbacks just as I do on FreeBSD, using the same tools and procedures.&lt;/p&gt;
&lt;p&gt;The choice of &lt;a href="https://voidlinux.org/"&gt;Void Linux&lt;/a&gt; comes from its BSD-like approach: modular and free of unnecessary complexity. This makes it an excellent solution for this type of setup.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.zfsbootmenu.org/"&gt;ZFSBootMenu&lt;/a&gt; is an extremely powerful tool. It provides an experience similar to FreeBSD's boot loader and natively supports ZFS. I strongly recommend reading the documentation and exploring its features, as some of them - like the built-in SSH daemon - can be genuine lifesavers in recovery scenarios.&lt;/p&gt;
&lt;h2&gt;Prerequisites and Audience&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;This guide is not for absolute beginners.&lt;/strong&gt; If you're new to Linux or Unix-like operating systems, you'd be better served by a ready-to-use distribution like &lt;a href="https://www.opensuse.org/"&gt;openSUSE&lt;/a&gt; Leap (or Tumbleweed for a rolling distribution), &lt;a href="https://linuxmint.com/"&gt;Linux Mint&lt;/a&gt;, &lt;a href="https://www.debian.org/"&gt;Debian&lt;/a&gt;, &lt;a href="https://ubuntu.com/"&gt;Ubuntu&lt;/a&gt;, or &lt;a href="https://manjaro.org/"&gt;Manjaro&lt;/a&gt;. The purpose of this article is to demonstrate a stable, upgradeable, and reasonably secure base setup for users already comfortable with system administration. It uses the &lt;strong&gt;glibc&lt;/strong&gt; variant of Void Linux. The &lt;em&gt;&lt;a href="https://docs.voidlinux.org/installation/musl.html"&gt;musl&lt;/a&gt;&lt;/em&gt; version requires different commands, for example for locale generation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use at your own risk.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This guide synthesizes instructions from several sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.zfsbootmenu.org/en/latest/guides/void-linux/uefi.html"&gt;Void Linux (UEFI) from ZFSBootMenu&lt;/a&gt; - which doesn't address swap. Using a zvol for swap (not the best solution) prevents hibernation and resume. Our approach uses a separate encrypted swap partition that enables proper resume.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.voidlinux.org/installation/guides/fde.html"&gt;Void Linux Full Disk Encryption&lt;/a&gt; - excellent for btrfs or ext4, but we want ZFS. We'll borrow the swap configuration approach from here.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://compactbunker.org/p/install-void-linux/"&gt;Install Void Linux with a desktop environment + Flatpaks&lt;/a&gt; - for the desktop portion.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your setup differs from what's described here (NVMe disk, UEFI boot, Secure Boot disabled), consult the linked guides for explanations and variations.&lt;/p&gt;
&lt;h3&gt;Installation Script (Optional)&lt;/h3&gt;
&lt;p&gt;If you want to reproduce this setup quickly, I maintain a script that automates the procedure described in this guide: disk partitioning, ZFS pool and dataset creation, encrypted swap for hibernation resume, dracut configuration, and ZFSBootMenu EFI setup. An optional KDE Plasma desktop installation is also supported.&lt;/p&gt;
&lt;p&gt;The script is interactive and will ask for the required parameters (target disk, timezone and keymap, passphrases, desktop options). &lt;a href="https://brew.bsd.cafe/stefano/void-zfs-hibernation"&gt;Requirements, usage instructions, and known limitations are documented in the repository README&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That said, I still recommend going through the manual process at least once. Understanding each step is part of the value of this setup, especially when troubleshooting or adapting it to different hardware.&lt;/p&gt;
&lt;h2&gt;Boot Environment&lt;/h2&gt;
&lt;p&gt;Since ZFS isn't supported by the base Void Linux image, we'll use &lt;a href="https://github.com/leahneukirchen/hrmpf/releases"&gt;hrmpf&lt;/a&gt;, an excellent rescue system based on Void Linux that includes ZFS support out of the box.&lt;/p&gt;
&lt;p&gt;After booting, you can either proceed directly or SSH into the machine to continue remotely. I generally prefer SSH since it makes copy-paste operations much easier - especially when dealing with UUIDs and long commands. To enable SSH access, set a root password and allow root login:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;passwd
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Edit &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; and enable:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;PermitRootLogin yes
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Restart the SSH daemon:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;sv restart sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Find the machine's IP address:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;ip addr
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can now connect via SSH from another device.&lt;/p&gt;
&lt;h2&gt;Initial Setup&lt;/h2&gt;
&lt;p&gt;Set up the environment variables and generate a host ID - we need it for ZFS:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;source /etc/os-release
export ID

zgenhostid -f 0x00bab10c
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Disk Configuration&lt;/h2&gt;
&lt;p&gt;Identify your target disk and set up the partition variables. This approach keeps everything consistent and reduces errors:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# Set the base disk - adjust this to match your system
export DISK=&amp;quot;/dev/nvme0n1&amp;quot;

# For NVMe disks, partitions are named like nvme0n1p1, nvme0n1p2, etc.
# For SATA/SAS disks (sda, sdb), partitions are named sda1, sda2, etc.
# Set the partition separator accordingly:
export PART_SEP=&amp;quot;p&amp;quot;  # Use &amp;quot;p&amp;quot; for NVMe, empty string &amp;quot;&amp;quot; for SATA/SAS

# Define partition numbers
export BOOT_PART=&amp;quot;1&amp;quot;
export SWAP_PART=&amp;quot;2&amp;quot;
export POOL_PART=&amp;quot;3&amp;quot;

# Build full device paths
export BOOT_DEVICE=&amp;quot;${DISK}${PART_SEP}${BOOT_PART}&amp;quot;
export SWAP_DEVICE=&amp;quot;${DISK}${PART_SEP}${SWAP_PART}&amp;quot;
export POOL_DEVICE=&amp;quot;${DISK}${PART_SEP}${POOL_PART}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Verify your configuration before proceeding:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;echo &amp;quot;Boot device: $BOOT_DEVICE&amp;quot;
echo &amp;quot;Swap device: $SWAP_DEVICE&amp;quot;
echo &amp;quot;Pool device: $POOL_DEVICE&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Wipe the Disk&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Warning: This operation will irreversibly destroy all data on the selected disk. Double-check that you've selected the correct disk and be sure to have a complete backup of your system!&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zpool labelclear -f &amp;quot;$DISK&amp;quot;

wipefs -a &amp;quot;$DISK&amp;quot;
sgdisk --zap-all &amp;quot;$DISK&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Create Partitions&lt;/h2&gt;
&lt;h3&gt;EFI System Partition&lt;/h3&gt;
&lt;p&gt;If you're not using UEFI boot, adapt this procedure following the appropriate guide linked at the beginning of this post:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;sgdisk -n &amp;quot;${BOOT_PART}:1m:+512m&amp;quot; -t &amp;quot;${BOOT_PART}:ef00&amp;quot; &amp;quot;$DISK&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Swap Partition&lt;/h3&gt;
&lt;p&gt;The swap partition should be slightly larger than your RAM to support hibernation. When you hibernate, the entire contents of RAM are written to swap, so you need enough space to hold it all plus some overhead. In this example, I have 16 GB of RAM, so I'm creating an 18 GB swap partition:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;sgdisk -n &amp;quot;${SWAP_PART}:0:+18g&amp;quot; -t &amp;quot;${SWAP_PART}:8200&amp;quot; &amp;quot;$DISK&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;ZFS Pool Partition&lt;/h3&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;sgdisk -n &amp;quot;${POOL_PART}:0:-10m&amp;quot; -t &amp;quot;${POOL_PART}:bf00&amp;quot; &amp;quot;$DISK&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Set Up ZFS Encryption&lt;/h2&gt;
&lt;p&gt;Encrypting the disk is strongly recommended, especially for laptops. Replace &lt;code&gt;SomeKeyphrase&lt;/code&gt; with a strong passphrase that's easy to type. Keep in mind that during early boot, the keyboard layout might default to US, so choose a passphrase that's easy to type on a US keyboard layout:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;echo 'SomeKeyphrase' &amp;gt; /etc/zfs/zroot.key
chmod 000 /etc/zfs/zroot.key
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Create the ZFS Pool&lt;/h2&gt;
&lt;p&gt;Create the pool with conservative, well-tested options:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zpool create -f -o ashift=12 \
 -O compression=lz4 \
 -O acltype=posixacl \
 -O xattr=sa \
 -O relatime=on \
 -O encryption=aes-256-gcm \
 -O keylocation=file:///etc/zfs/zroot.key \
 -O keyformat=passphrase \
 -o autotrim=on \
 -o compatibility=openzfs-2.2-linux \
 -m none zroot &amp;quot;$POOL_DEVICE&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Create ZFS Datasets&lt;/h2&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zfs create -o mountpoint=none zroot/ROOT
zfs create -o mountpoint=/ -o canmount=noauto zroot/ROOT/${ID}
zfs create -o mountpoint=/home zroot/home

zpool set bootfs=zroot/ROOT/${ID} zroot
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Export and Reimport for Installation&lt;/h2&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zpool export zroot
zpool import -N -R /mnt zroot
zfs load-key -L prompt zroot

zfs mount zroot/ROOT/${ID}
zfs mount zroot/home

udevadm trigger
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Install the Base System&lt;/h2&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;XBPS_ARCH=x86_64 xbps-install \
  -S -R https://mirrors.servercentral.com/voidlinux/current \
  -r /mnt base-system
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Copy Host Configuration&lt;/h2&gt;
&lt;p&gt;Copy the files we generated earlier to the new system:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;cp /etc/hostid /mnt/etc
mkdir -p /mnt/etc/zfs
cp /etc/zfs/zroot.key /mnt/etc/zfs
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Configure Encrypted Swap&lt;/h2&gt;
&lt;p&gt;Now we'll set up the encrypted swap partition. This is where the hibernation magic happens - by using a separate LUKS-encrypted partition instead of a ZFS zvol, we can properly resume from hibernation.&lt;/p&gt;
&lt;p&gt;Format the swap partition with LUKS:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;cryptsetup luksFormat --type luks1 &amp;quot;$SWAP_DEVICE&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Open the encrypted partition, create the swap filesystem, and activate it:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;cryptsetup luksOpen &amp;quot;$SWAP_DEVICE&amp;quot; cryptswap
mkswap /dev/mapper/cryptswap
swapon /dev/mapper/cryptswap
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Preserve Variables for Chroot&lt;/h2&gt;
&lt;p&gt;Before entering the chroot, save the disk variables so they remain available inside the new environment:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;cat &amp;lt;&amp;lt; EOF &amp;gt; /mnt/root/disk-vars.sh
export DISK=&amp;quot;$DISK&amp;quot;
export PART_SEP=&amp;quot;$PART_SEP&amp;quot;
export BOOT_PART=&amp;quot;$BOOT_PART&amp;quot;
export SWAP_PART=&amp;quot;$SWAP_PART&amp;quot;
export POOL_PART=&amp;quot;$POOL_PART&amp;quot;
export BOOT_DEVICE=&amp;quot;$BOOT_DEVICE&amp;quot;
export SWAP_DEVICE=&amp;quot;$SWAP_DEVICE&amp;quot;
export POOL_DEVICE=&amp;quot;$POOL_DEVICE&amp;quot;
export ID=&amp;quot;$ID&amp;quot;
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Enter the Chroot Environment&lt;/h2&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;xchroot /mnt
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;From this point forward, all commands are executed inside the new system.&lt;/p&gt;
&lt;p&gt;First, load the saved variables:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;source /root/disk-vars.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Configure fstab&lt;/h2&gt;
&lt;p&gt;Add the swap entry to &lt;code&gt;/etc/fstab&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;/dev/mapper/cryptswap   none            swap            defaults        0 0
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Set Up Automatic Swap Unlock&lt;/h2&gt;
&lt;p&gt;To avoid entering the swap password separately after unlocking the ZFS pool, we'll create a keyfile stored on the encrypted ZFS dataset. This is secure because the keyfile only becomes accessible after the ZFS pool is unlocked.&lt;/p&gt;
&lt;p&gt;First, install cryptsetup in the new system:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;xbps-install -S cryptsetup
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Generate a random keyfile and add it to the LUKS partition:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;dd bs=1 count=64 if=/dev/urandom of=/boot/volume.key

cryptsetup luksAddKey &amp;quot;$SWAP_DEVICE&amp;quot; /boot/volume.key

chmod 000 /boot/volume.key
chmod -R g-rwx,o-rwx /boot
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Add the keyfile to &lt;code&gt;/etc/crypttab&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;echo &amp;quot;cryptswap   $SWAP_DEVICE   /boot/volume.key   luks&amp;quot; &amp;gt;&amp;gt; /etc/crypttab
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Include the keyfile and crypttab in the initramfs. Create &lt;code&gt;/etc/dracut.conf.d/10-crypt.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;install_items+=&amp;quot; /boot/volume.key /etc/crypttab &amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Basic System Configuration&lt;/h2&gt;
&lt;p&gt;Configure keyboard layout and hardware clock. Adjust the keymap and timezone to match your location:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;cat &amp;lt;&amp;lt; EOF &amp;gt;&amp;gt; /etc/rc.conf
KEYMAP=&amp;quot;us&amp;quot;
HARDWARECLOCK=&amp;quot;UTC&amp;quot;
EOF

ln -sf /usr/share/zoneinfo/Europe/Rome /etc/localtime
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Configure locales:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;cat &amp;lt;&amp;lt; EOF &amp;gt;&amp;gt; /etc/default/libc-locales
en_US.UTF-8 UTF-8
en_US ISO-8859-1
EOF

echo &amp;quot;LANG=en_US.UTF-8&amp;quot; &amp;gt; /etc/locale.conf

xbps-reconfigure -f glibc-locales
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Set the root password:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;passwd
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Configure ZFS Boot Support&lt;/h2&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;cat &amp;lt;&amp;lt; EOF &amp;gt; /etc/dracut.conf.d/zol.conf
nofsck=&amp;quot;yes&amp;quot;
add_dracutmodules+=&amp;quot; zfs &amp;quot;
omit_dracutmodules+=&amp;quot; btrfs &amp;quot;
install_items+=&amp;quot; /etc/zfs/zroot.key &amp;quot;
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Install ZFS:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;xbps-install -S zfs
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Configure ZFSBootMenu&lt;/h2&gt;
&lt;p&gt;Set the basic boot properties:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zfs set org.zfsbootmenu:commandline=&amp;quot;quiet&amp;quot; zroot/ROOT
zfs set org.zfsbootmenu:keysource=&amp;quot;zroot/ROOT/${ID}&amp;quot; zroot
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;The Critical Step: Hibernation Support&lt;/h3&gt;
&lt;p&gt;Now we need to configure hibernation resume. This is the key insight that makes this setup work: normally, the encrypted ZFS root mounts first, and then it unlocks the swap partition. But when resuming from hibernation, the kernel needs to read the hibernation image from swap &lt;em&gt;before&lt;/em&gt; mounting the root filesystem - otherwise, the saved state would be lost.&lt;/p&gt;
&lt;p&gt;To solve this, we tell ZFSBootMenu to unlock the swap partition early, before mounting ZFS, by specifying its LUKS UUID.&lt;/p&gt;
&lt;p&gt;Get the UUID of your swap partition:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;blkid &amp;quot;$SWAP_DEVICE&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You'll see output like:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;/dev/...: UUID=&amp;quot;xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&amp;quot; TYPE=&amp;quot;crypto_LUKS&amp;quot; PARTUUID=&amp;quot;...&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Store the UUID in a variable for the next step:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;SWAP_UUID=$(blkid -s UUID -o value &amp;quot;$SWAP_DEVICE&amp;quot;)
echo &amp;quot;Swap UUID: $SWAP_UUID&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now set the boot parameters using the captured UUID:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zfs set org.zfsbootmenu:commandline=&amp;quot;rd.luks.uuid=$SWAP_UUID resume=/dev/mapper/cryptswap&amp;quot; zroot/ROOT/${ID}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Set Up EFI Boot&lt;/h2&gt;
&lt;p&gt;Create and mount the EFI partition:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;mkfs.vfat -F32 &amp;quot;$BOOT_DEVICE&amp;quot;

mkdir -p /boot/efi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Add the EFI partition to &lt;code&gt;/etc/fstab&lt;/code&gt; using its UUID:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;BOOT_UUID=$(blkid -s UUID -o value &amp;quot;$BOOT_DEVICE&amp;quot;)
echo &amp;quot;UUID=$BOOT_UUID    /boot/efi    vfat    defaults    0 0&amp;quot; &amp;gt;&amp;gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Mount it:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;mount /boot/efi
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Install ZFSBootMenu&lt;/h2&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;xbps-install -S curl

mkdir -p /boot/efi/EFI/ZBM
curl -o /boot/efi/EFI/ZBM/VMLINUZ.EFI -L https://get.zfsbootmenu.org/efi
cp /boot/efi/EFI/ZBM/VMLINUZ.EFI /boot/efi/EFI/ZBM/VMLINUZ-BACKUP.EFI
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Configure the EFI boot entries:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;xbps-install -S efibootmgr

efibootmgr -c -d &amp;quot;$DISK&amp;quot; -p &amp;quot;$BOOT_PART&amp;quot; \
  -L &amp;quot;ZFSBootMenu (Backup)&amp;quot; \
  -l '\EFI\ZBM\VMLINUZ-BACKUP.EFI'

efibootmgr -c -d &amp;quot;$DISK&amp;quot; -p &amp;quot;$BOOT_PART&amp;quot; \
  -L &amp;quot;ZFSBootMenu&amp;quot; \
  -l '\EFI\ZBM\VMLINUZ.EFI'
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Microcode updates&lt;/h3&gt;
&lt;p&gt;Void Linux is modular, so you may need to install additional packages for your specific hardware. For the Intel microcode, you need the non-free repo:
For example:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# For Intel CPUs
xbps-install -S void-repo-nonfree 
xbps-install -S intel-ucode

# For AMD CPUs/GPUs
xbps-install -S linux-firmware-amd
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After installing microcode updates, regenerate the boot images and exit:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;xbps-reconfigure -fa
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Desktop Installation (Optional)&lt;/h2&gt;
&lt;p&gt;If all you need is a minimal system or a server, you're done and ready to reboot. For a complete desktop environment, continue with the following steps.&lt;/p&gt;
&lt;h3&gt;Install Core Desktop Packages&lt;/h3&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;xbps-install -S vim nano dbus elogind polkit xorg xorg-fonts xorg-video-drivers xorg-input-drivers dejavu-fonts-ttf terminus-font NetworkManager pipewire alsa-pipewire wireplumber xdg-user-dirs unzip gzip xz 7zip
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Install KDE Plasma&lt;/h3&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;xbps-install -S kde-plasma dolphin konsole firefox kdegraphics-thumbnailers ffmpegthumbs vlc ark kwrite discover kf6-purpose
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Enable Services&lt;/h3&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;ln -s /etc/sv/NetworkManager /etc/runit/runsvdir/default/
ln -s /etc/sv/dbus /etc/runit/runsvdir/default/
ln -s /etc/sv/udevd /etc/runit/runsvdir/default/
ln -s /etc/sv/polkitd /etc/runit/runsvdir/default/
ln -s /etc/sv/sddm /etc/runit/runsvdir/default/
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Configure PipeWire Audio&lt;/h3&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;mkdir -p /etc/xdg/autostart
ln -sf /usr/share/applications/pipewire.desktop /etc/xdg/autostart/

mkdir -p /etc/pipewire/pipewire.conf.d
ln -sf /usr/share/examples/wireplumber/10-wireplumber.conf /etc/pipewire/pipewire.conf.d/
ln -sf /usr/share/examples/pipewire/20-pipewire-pulse.conf /etc/pipewire/pipewire.conf.d/

mkdir -p /etc/alsa/conf.d
ln -sf /usr/share/alsa/alsa.conf.d/50-pipewire.conf /etc/alsa/conf.d
ln -sf /usr/share/alsa/alsa.conf.d/99-pipewire-default.conf /etc/alsa/conf.d
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Enable Additional Repositories and Flatpak (Optional)&lt;/h3&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;xbps-install -S void-repo-nonfree void-repo-multilib void-repo-multilib-nonfree

xbps-install -S flatpak
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Create a Regular User and exit&lt;/h3&gt;
&lt;p&gt;For desktop use, create a non-root user with appropriate group memberships.
Replace &lt;code&gt;username&lt;/code&gt; with your desired username.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;useradd -m username
passwd username
usermod username -G video,wheel,plugdev,kvm,audio,network
exit
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Fix for NetworkManager&lt;/h3&gt;
&lt;p&gt;xchroot will bind mount /etc/resolv.conf and leave an empty file. Network Manager won't like it. So let's clean it up:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;umount -l /mnt/etc/resolv.conf 2&amp;gt;/dev/null || true

rm -f /mnt/etc/resolv.conf
ln -s /run/NetworkManager/resolv.conf /mnt/etc/resolv.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Exit and Reboot&lt;/h2&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;umount -n -R /mnt
zpool export zroot
reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Post-Installation&lt;/h2&gt;
&lt;p&gt;If everything went well, after entering your ZFS encryption password, you'll be greeted by the SDDM login screen.&lt;/p&gt;
&lt;h2&gt;Testing Hibernation&lt;/h2&gt;
&lt;p&gt;To verify that hibernation works correctly, you can clock the "Hibernate" button or:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;loginctl hibernate
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The system should power off. When you turn it back on, ZFSBootMenu will prompt for the password, unlock the swap partition, detect the hibernation image, and resume your session exactly where you left off.&lt;/p&gt;
&lt;p&gt;If resume fails, check that:
1. The LUKS UUID in the ZFS commandline property matches your swap partition
2. The swap partition is large enough for your RAM
3. The dracut configuration includes the crypttab and keyfile&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;You now have a fully functional Void Linux system with native ZFS, full disk encryption, and working hibernation. The system is rolling, lightweight, and easy to maintain. Enjoy!&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Mon, 22 Dec 2025 08:43:02 +0000</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2025/12/22/void-linux-zfs-hibernation-guide/</guid><category>linux</category><category>desktop</category><category>zfs</category><category>server</category><category>tutorial</category><category>ownyourdata</category><category>voidlinux</category></item><item><title>Self-hosting your Mastodon media with SeaweedFS</title><link>https://it-notes.dragas.net/2025/11/06/self-hosting-your-mastodon-media-with-seaweedfs/</link><description>&lt;p&gt;&lt;img src="https://images.unsplash.com/photo-1611926653458-09294b3142bf?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDR8fHNvY2lhbCUyMG5ldHdvcmt8ZW58MHx8fHwxNjY5MTI0MzQ5&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Self-hosting your Mastodon media with SeaweedFS"&gt;&lt;/p&gt;&lt;p&gt;&lt;a href="https://joinmastodon.org/"&gt;Mastodon&lt;/a&gt; 4.5.0 is here, and with it come some interesting changes that, in my opinion, might encourage more people to consider it for self-hosting their Fediverse community.&lt;/p&gt;
&lt;p&gt;While it may not be as lightweight and simple as other solutions (like &lt;a href="https://codeberg.org/grunfink/snac2"&gt;snac&lt;/a&gt; or &lt;a href="https://gotosocial.org/"&gt;GoToSocial&lt;/a&gt; or many others), I believe it remains one of the best platforms for managing a medium-sized Fediverse community, thanks in part to the direct feedback that many admins have provided to the developers.&lt;/p&gt;
&lt;p&gt;I have previously written about how to &lt;a href="https://it-notes.dragas.net/2022/11/23/installing-mastodon-on-a-freebsd-jail/"&gt;install Mastodon in a FreeBSD jail&lt;/a&gt; and how to &lt;a href="https://it-notes.dragas.net/2024/10/09/2024-modifying-limits-in-mastodon-4-3/"&gt;modify its character and poll limits&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One of the most critical initial decisions (which can be changed later, but with extra work) is where to store your media files. Mastodon downloads and re-processes all media it encounters from other instances for three main reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local Caching:&lt;/strong&gt; Your users connect to your media server, reducing the load on the original instance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security:&lt;/strong&gt; Re-processing media helps to remove any potential "impurities" before they reach the user's device.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy:&lt;/strong&gt; It prevents disclosing your users' IP addresses to other instances. A user will only connect to their own instance to fetch all data, including remote content.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At least initially, media files will be the largest part of your instance's storage footprint. It is therefore essential to plan where to store them and to add a regular cleanup script; otherwise, their growth will be exponential.&lt;/p&gt;
&lt;p&gt;Mastodon supports uploading media to external S3-compatible solutions, and many admins use the usual commercial providers, paying for data uploads and transfers.&lt;/p&gt;
&lt;p&gt;I am a firm believer in "Own Your Data", so I have always used my own self-hosted S3 servers. I initially started with Minio, but over time, I realized that, by design, it doesn't perform well with a multitude of small files (performance degrades). After running some tests, I decided to switch to &lt;a href="https://github.com/seaweedfs/seaweedfs"&gt;SeaweedFS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;SeaweedFS "is a fast distributed storage system for blobs, objects, files, and data lake, for billions of files! Blob store has O(1) disk seek..." - this, combined with the fact that it is a mature and proven piece of software, was enough for me to give it a try. The result? Excellent. The I/O and CPU load on my media server dropped drastically, making SeaweedFS an incredibly suitable solution. Furthermore, some of its features (like the ability to run a &lt;a href="https://github.com/seaweedfs/seaweedfs/wiki/Filer-Active-Active-cross-cluster-continuous-synchronization"&gt;filer.sync&lt;/a&gt;) allow for efficient and fast replication to other storage, another host, or... anything else.&lt;/p&gt;
&lt;p&gt;SeaweedFS works perfectly with Mastodon, and I will explain the steps to get it into production.&lt;/p&gt;
&lt;p&gt;I will install SeaweedFS in a dedicated jail and use a dedicated subdomain. This ensures that the media server can be moved to another host at any time without reconfiguring everything or changing domains. SeaweedFS has its own FreeBSD package, installable via &lt;code&gt;pkg&lt;/code&gt;, or can be downloaded directly from the project's website.&lt;/p&gt;
&lt;p&gt;In either case, I will describe a "test" setup - which can also be used in production without issues. However, I highly recommend diving deeper into the tool, as it is incredibly powerful and flexible and can solve many more problems than one might imagine.&lt;/p&gt;
&lt;h3&gt;Setting up the SeaweedFS Jail&lt;/h3&gt;
&lt;p&gt;First, let's create a dedicated jail with BastilleBSD:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;bastille create media 14.3-RELEASE 10.0.0.66 bastille0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now, let's enter the jail and install SeaweedFS (and tmux, which can be useful):&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;bastille console media
pkg install -y tmux seaweedfs
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I suggest launching SeaweedFS in a tmux session so you can monitor its output. Later, you should configure an automatic startup method, such as using the included rc.d file or any other method you prefer.&lt;/p&gt;
&lt;p&gt;Create a directory for the data and start SeaweedFS as the "seaweedfs" user:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;mkdir -p /seaweedfs/data
chown -R seaweedfs /seaweedfs
su -m seaweedfs
cd /seaweedfs/
/usr/local/bin/weed server -dir /seaweedfs/data -s3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point, SeaweedFS will start and create everything it needs to function, including the S3 server.&lt;/p&gt;
&lt;h3&gt;Configuring Buckets and Users&lt;/h3&gt;
&lt;p&gt;Now, let's open the weed shell to create the necessary bucket and users:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;weed shell
s3.bucket.create -name mastomedia
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Still in the weed shell, create a user for Mastodon and grant read permissions for unauthenticated users (which is necessary to serve media to the world):&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;s3.configure -access_key=mastomedia -secret_key=CHANGEME -buckets=mastomedia -user=mastodon -actions=Read,Write,List,Tagging,Admin -apply
s3.configure -buckets=mastomedia -user=anonymous -actions=Read -apply
s3.configure -buckets=mastomedia -actions=Read -apply
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Security Tip:&lt;/strong&gt; For the &lt;code&gt;-secret_key&lt;/code&gt;, avoid using a simple password. You can generate a strong, random key directly from your shell with a command like &lt;code&gt;openssl rand -base64 32&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Done. SeaweedFS is now ready to receive (and serve) media. The next step is to set up a reverse proxy to serve everything over HTTPS. My preferred approach is to configure the system as if it were external, even if the services are in adjacent jails. This might use slightly more resources, but the time and trouble it saves in the future are well worth it.&lt;/p&gt;
&lt;h3&gt;Nginx Reverse Proxy Configuration&lt;/h3&gt;
&lt;p&gt;The reverse proxy can be configured something like this:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;[...]

server {
   server_name  media.mastodon.example.com;

   ignore_invalid_headers off;
   client_max_body_size 0; # Allow large file uploads without Nginx limits

   location / {
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;

      proxy_connect_timeout 300;
      proxy_http_version 1.1;
      proxy_set_header Connection &amp;quot;&amp;quot;;
      chunked_transfer_encoding off;

      expires 1y;
      add_header Cache-Control public;

      add_header X-Cache-Status $upstream_cache_status;
      add_header X-Content-Type-Options nosniff;

      proxy_pass http://10.0.0.66:8333;
   }

# ... other server configurations like SSL ...

}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Mastodon Configuration&lt;/h3&gt;
&lt;p&gt;Now let's configure Mastodon. If you are running the setup wizard for the first time, here is a summary of the options:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;[...]
Do you want to store uploaded files on the cloud? yes
Provider Minio
Minio endpoint URL: https://media.mastodon.example.com
Minio bucket name: mastomedia
Minio access key: mastomedia
Minio secret key: CHANGEME
Do you want to access the uploaded files from your own domain? Yes
Domain for uploaded files: media.mastodon.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If Mastodon is already active, or once the setup is complete, the options in your .env.prod file should be modified to be consistent with what SeaweedFS expects:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;S3_ENABLED=true
S3_PROTOCOL=https
S3_REGION=us-east-1
S3_ENDPOINT=https://media.mastodon.example.com
S3_HOSTNAME=media.mastodon.example.com
S3_BUCKET=mastomedia
AWS_ACCESS_KEY_ID=mastomedia
AWS_SECRET_ACCESS_KEY=CHANGEME
S3_FORCE_SINGLE_REQUEST=true
# remove the S3_ALIAS_HOST if it is set
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;IMPORTANT NOTE:&lt;/strong&gt; If both services are in jails on the same host (i.e., SeaweedFS is on the same host as Mastodon), you should ensure that the Mastodon jail can reach the SeaweedFS jail through the reverse proxy and not via the external IP. To do this, add the following line to the /etc/hosts file of the &lt;strong&gt;Mastodon jail&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;10.0.0.1        media.mastodon.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In this example, the reverse proxy is at 10.0.0.1. If you are not using a separate reverse proxy but are exposing Nginx directly from the jail (as described in my Mastodon installation article), use the IP of the Mastodon jail itself instead (e.g., 10.0.0.42).&lt;/p&gt;
&lt;p&gt;With this setup, Mastodon will be able to upload media to the SeaweedFS server and generate the correct links for other instances, public visitors, and users of your own instance.&lt;/p&gt;
&lt;p&gt;Have fun with SeaweedFS!&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Thu, 06 Nov 2025 11:30:02 +0000</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2025/11/06/self-hosting-your-mastodon-media-with-seaweedfs/</guid><category>freebsd</category><category>container</category><category>hosting</category><category>jail</category><category>networking</category><category>server</category><category>tutorial</category><category>web</category><category>fediverse</category><category>mastodon</category><category>ownyourdata</category><category>seaweedfs</category></item><item><title>Make Your Own Backup System – Part 2: Forging the FreeBSD Backup Stronghold</title><link>https://it-notes.dragas.net/2025/07/29/make-your-own-backup-system-part-2-forging-the-freebsd-backup-stronghold/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/hard_disk.webp" alt="A hard disk - ready to host our backups"&gt;&lt;/p&gt;&lt;p&gt;With the &lt;a href="https://it-notes.dragas.net/2025/07/18/make-your-own-backup-system-part-1-strategy-before-scripts/"&gt;primary backup strategies and methodologies introduced&lt;/a&gt;, we've reached the point where we can get specific: the Backup Server configuration.&lt;/p&gt;
&lt;p&gt;When choosing the type of backup server to use, I tend to favor specific setups: either I trust a professional backup service provider (like Colin Percival's &lt;a href="https://www.tarsnap.com/"&gt;Tarsnap&lt;/a&gt;), or I want full control over the disks where the backups will be hosted. In both cases, for the past twenty years, my operating system of choice for backup servers has been FreeBSD. With a few rare exceptions for clients with special requests, it covers all my needs. When I require Linux-based solutions, such as the &lt;a href="https://www.proxmox.com/en/products/proxmox-backup-server/overview"&gt;Proxmox Backup Server&lt;/a&gt;, I create a VM and manage it within.&lt;/p&gt;
&lt;p&gt;I typically use both IPv4 and IPv6. For IPv4, I "play" with NAT and port forwarding. For IPv6, I tend to assign a public IPv6 address to each jail or VM, which is then filtered by the physical server's firewall. Unfortunately, every provider, server, and setup has a different approach to IPv6, making it impossible to cover them all in this article. When a provider allows for routed setups, I use this approach: &lt;a href="https://it-notes.dragas.net/2023/09/23/make-your-own-vpn-freebsd-wireguard-ipv6-and-ad-blocking-included/"&gt;Make your own VPN: FreeBSD, WireGuard, IPv6, and ad-blocking included&lt;/a&gt; - assigning a /72 to the bridge for the jails and VMs.&lt;/p&gt;
&lt;p&gt;In my opinion, FreeBSD is a perfect all-rounder for backups, thanks to its ability to completely partition services. You can separate backup services (or specific servers/clients) into different jails or even VMs. Furthermore, using ZFS greatly enhances both flexibility and the range of tools you can use.&lt;/p&gt;
&lt;p&gt;The main distinction is usually between local backup servers (physically accessible, though not always attended, and in locations deemed secure) and remote ones, such as leased external servers. I personally use a combination of both. If the services I need to back up are external, in a datacenter, and need to be quickly restorable, I prefer to always have a copy on another server in a different datacenter with good outbound connectivity. This guarantees good bandwidth for restores, which isn't always available from a local connection to the outside world. However, an internal, nearby, and accessible backup server (even a Raspberry Pi or a mini PC) ensures physical access to the data. Whenever possible, I maintain both an external and an internal copy - and they are autonomous, meaning the internal copy is &lt;em&gt;not&lt;/em&gt; a replica of the external one, but an additional, independent backup. This ensures that if a problem occurs with the external backup, it won't automatically propagate to the internal one. In any case, the backup must always be in a different datacenter from the one containing the production data. When &lt;a href="https://www.reuters.com/article/idUSKBN2B20NT/"&gt;the fire at the OVH datacenter in Strasbourg&lt;/a&gt; caused the entire complex to shut down, many people found themselves in trouble because their backups were in the same, now unreachable, location. I had a copy with another provider, in a different datacenter and country, as well as a local copy.&lt;/p&gt;
&lt;p&gt;Despite it being "just" a backup server, I almost always use some form of disk redundancy. If I have two disks, I set up a mirror. With three or more, I use RaidZ1 or RaidZ2. This is because, in my view, backups are nearly as important as production data. The inability to recover data from a backup means it's lost forever. And it happens often, very often, that someone contacts me to recover a file (or a database, etc.) days or weeks after its accidental loss or deletion. Usually, pulling out a file from a two-month-old backup generates a mix of disbelief, admiration, but above all, a sense of security in the person requesting it. And that is what our work should instill in the people we collaborate with.&lt;/p&gt;
&lt;p&gt;The backup server should be hardened. If possible, it should be protected and unreachable from the outside. My best backup servers are those accessible only via VPN, capable of pulling the data on their own. If they are on a LAN, it's even better if they are completely disconnected from the Internet.&lt;/p&gt;
&lt;p&gt;For this very reason, &lt;strong&gt;backups must always be encrypted&lt;/strong&gt;. Having a backup means having full access to the data, and the backup server is the prime target for being breached or stolen if the goal is to get your hands on that data. I've seen healthcare facilities' backup servers being targeted (in a rather trivial way, to be honest) by journalists looking for health details of important figures. It is therefore critical that the backup server be as secure as possible.&lt;/p&gt;
&lt;p&gt;Based on the type of access, I use two types of encryption:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;If the server is local&lt;/strong&gt; (especially if the ZFS pool is on external disks), I usually install FreeBSD on UFS in read-only mode, as &lt;a href="https://it-notes.dragas.net/2024/05/31/freebsd-tips-and-tricks-native-ro-rootfs/"&gt;I've described in a previous article&lt;/a&gt;, and encrypt the backup disks with &lt;a href="https://man.freebsd.org/cgi/man.cgi?geli(8)"&gt;GELI&lt;/a&gt;. This ensures that in the event of a "dirty" shutdown (more likely in unattended environments), I can reconnect to the host and then reactivate the ZFS pool. This approach makes it nearly impossible to retrieve even the pool's metadata if the disks are stolen, as GELI performs a full-device encryption. For example, an employee of a company I work with stole one of the secondary backup disks (which was located at a different, unmonitored company site) to steal information. He got nothing but a criminal complaint. With this approach, it's also not necessary to further encrypt the datasets, which avoids some issues (which I'll discuss later, in a future post).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If the server is remote&lt;/strong&gt;, in a datacenter, I usually use ZFS native encryption, encrypting the main backup dataset (and &lt;a href="https://bastillebsd.org/"&gt;BastilleBSD&lt;/a&gt;'s, if applicable). Consequently, all child datasets containing backups will also be encrypted. In this case as well, a password will be required after a reboot to unlock those datasets, ensuring that the data cannot be extracted if control of the disks is lost.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is an example of how to use GELI to encrypt an entire partition and then create a ZFS pool on it (in the example, the disk is &lt;code&gt;da1&lt;/code&gt; - do not follow these commands blindly, or you will erase all content on the &lt;code&gt;da1&lt;/code&gt; device!):&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# WARNING: This destroys the existing partition table on disk da1
gpart destroy -F da1

# Create a new GPT partition table
gpart create -s gpt da1

# Add a freebsd-zfs partition that spans the entire disk
# The -a 1m flag ensures proper alignment
gpart add -t freebsd-zfs -a 1m da1

# Initialize GELI encryption on the new partition (da1p1)
# We use AES-XTS with 256-bit keys and a 4k sector size
# The -b flag means &amp;quot;boot,&amp;quot; prompting for the passphrase at boot time
geli init -b -l 256 -s 4096 da1p1
# You will be prompted for a passphrase: choose a strong one and save it!

# Attach the encrypted partition. A new device /dev/da1p1.eli will be created.
# You will be prompted for the passphrase you just set
geli attach da1p1

# (Optional) Check the status of the encrypted device
geli status da1p1

# Create the ZFS pool &amp;quot;bckpool&amp;quot; on the encrypted device
# We enable zstd compression (an excellent compromise) and disable atime
zpool create -O compression=zstd -O atime=off bckpool da1p1.eli
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In this setup, the reference pool for everything related to backups will be &lt;code&gt;bckpool&lt;/code&gt; - and you'll need to keep this in mind for the next steps. Additionally, after every server reboot, you'll need to "unlock" the disk and import the pool:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# Enter the passphrase when prompted
geli attach da1p1

# Import the ZFS pool, which is now visible
zpool import bckpool
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With this method, it's not necessary to encrypt the ZFS datasets, as the underlying disk (or, more precisely, the partition containing the ZFS pool) is already encrypted.&lt;/p&gt;
&lt;p&gt;If, instead, you choose to encrypt the ZFS dataset (for example, if you install FreeBSD on the same disks that will hold the data and don't want to use a multi-partition approach), you should create a base encrypted dataset. Inside it, you can create the various backup datasets, VMs, and the BastilleBSD mountpoint. Due to property inheritance, they will all be encrypted as well.&lt;/p&gt;
&lt;p&gt;To create an encrypted dataset, a command like this will suffice:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# Creates a new dataset with encryption enabled.
# keylocation=prompt will ask for a passphrase every time it's mounted.
# keyformat=passphrase specifies the key type.
zfs create -o encryption=on -o keylocation=prompt -o keyformat=passphrase zfspool/dataset
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In this case, after every reboot, you will need to load the key and mount the dataset:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zfs load-key zfspool/dataset
zfs mount zfspool/dataset
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Keep in mind the setup you choose, as many of the subsequent choices and commands will depend on it.&lt;/p&gt;
&lt;h2&gt;Base System Setup&lt;/h2&gt;
&lt;p&gt;I'll install BastilleBSD - a useful tool for separating services into jails. It will be helpful for isolating our backup services:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;pkg install -y bastille
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you used ZFS for the root filesystem, you can proceed directly with the setup. Otherwise (i.e., ZFS on other disks), you'll need to edit the &lt;code&gt;/usr/local/etc/bastille/bastille.conf&lt;/code&gt; file and specify the correct dataset on which to install the jails. Then run:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;bastille setup
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once the automatic setup is complete, check the &lt;code&gt;/etc/pf.conf&lt;/code&gt; file - it will be automatically configured to only accept SSH connections. Ensure the network interface is set correctly. When you activate &lt;code&gt;pf&lt;/code&gt;, you will be kicked out of the server, but you can then reconnect.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;service pf start
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let's bootstrap a FreeBSD release for the jails - this will be useful later.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;bastille bootstrap 14.3-RELEASE update
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now, we create a local bridge. Jails and VMs can be attached to it, making them fully autonomous. Using VNET jails, for example, will allow the creation of VPNs or &lt;code&gt;tun&lt;/code&gt; interfaces inside them, simplifying potential future setups (and increasing security by using a dedicated network stack).&lt;/p&gt;
&lt;p&gt;Modify the &lt;code&gt;/etc/rc.conf&lt;/code&gt; file and add:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# Add lo1 and bridge0 to the list of cloned interfaces
cloned_interfaces=&amp;quot;lo1 bridge0&amp;quot;
# Assign an IP address and netmask to the bridge
ifconfig_bridge0=&amp;quot;inet 192.168.0.1 netmask 255.255.255.0&amp;quot;
# Enable gateway functionality for routing
gateway_enable=&amp;quot;yes&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let's also modify &lt;code&gt;/etc/pf.conf&lt;/code&gt; to allow the &lt;code&gt;192.168.0.0/24&lt;/code&gt; subnet to access the Internet via NAT. We will skip packet filtering on &lt;code&gt;bridge0&lt;/code&gt; and enable NAT. This isn't the most secure setup, but it's sufficient to get started:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;#...
# Skip PF processing on the internal bridge interface
set skip on bridge0
#...
# NAT traffic from our internal network to the outside world
nat on $ext_if from 192.168.0.0/24 to any -&amp;gt; ($ext_if:0)
#...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To ensure the new settings are correct, it's a good idea to test with a reboot.&lt;/p&gt;
&lt;p&gt;Since I often configure &lt;a href="https://github.com/freebsd/vm-bhyve"&gt;vm-bhyve&lt;/a&gt; in my setups, I prefer to install it right away, creating the dataset that will contain the VMs and installation templates. Remember that &lt;code&gt;zroot&lt;/code&gt; is only valid if you installed the entire system on ZFS; otherwise, you'll need to change it to your own dataset:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# Install required packages
pkg install vm-bhyve grub2-bhyve bhyve-firmware
# Create a dataset to store VMs
zfs create zroot/VMs
# Enable the vm service at boot
sysrc vm_enable=&amp;quot;YES&amp;quot;
# Set the directory for VMs, using the ZFS dataset
sysrc vm_dir=&amp;quot;zfs:zroot/VMs&amp;quot;
# Initialize vm-bhyve
vm init
# Copy the example templates
cp /usr/local/share/examples/vm-bhyve/* /zroot/VMs/.templates/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point, I usually enable the console via &lt;code&gt;tmux&lt;/code&gt;. This means that when a VM is launched, it won't open a VNC port by default, but a &lt;code&gt;tmux&lt;/code&gt; session connected to the VM's serial port. Let's install and configure &lt;code&gt;tmux&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;pkg install -y tmux
vm set console=tmux
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let's also attach the switch we created (&lt;code&gt;bridge0&lt;/code&gt;) to &lt;code&gt;vm-bhyve&lt;/code&gt; so we can use it:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;vm switch create -t manual -b bridge0 public
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now, &lt;code&gt;vm-bhyve&lt;/code&gt; is ready.&lt;/p&gt;
&lt;p&gt;The basic infrastructure is complete. We now have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ZFS&lt;/strong&gt; to ensure data integrity, which will also handle redundancy, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BastilleBSD&lt;/strong&gt; to manage jails, useful for backing up Linux, NetBSD, OpenBSD, and non-ZFS FreeBSD machines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;vm-bhyve&lt;/strong&gt; to install specific systems (like Proxmox Backup Server).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Backup Strategies&lt;/h2&gt;
&lt;p&gt;I use various backup tools, too many to list in this article. So I'll make a broad distinction, describing how to use this server to achieve our goal: securing data.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For &lt;strong&gt;FreeBSD servers with ZFS&lt;/strong&gt; (hosts, VMs, jails, hypervisors, and their respective VMs), I use an extremely useful, efficient, and reliable tool: &lt;a href="https://github.com/psy0rz/zfs_autobackup"&gt;zfs-autobackup&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;Linux servers (without ZFS), NetBSD, OpenBSD&lt;/strong&gt;, etc. (any non-ZFS OS), I usually use &lt;a href="https://www.borgbackup.org/"&gt;BorgBackup&lt;/a&gt;. There are other fantastic tools like &lt;a href="https://restic.net/"&gt;restic&lt;/a&gt;, &lt;a href="https://kopia.io/"&gt;Kopia&lt;/a&gt;, etc., but BorgBackup has never let me down and has served me well even on low-power devices and after incredibly complex disasters.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;Proxmox servers&lt;/strong&gt; (a solution I've used with satisfaction in production since 2013, although I'm recently migrating to FreeBSD/bhyve where possible), I use two possible alternatives (often both at the same time): if the storage is ZFS, I use the &lt;code&gt;zfs-autobackup&lt;/code&gt; approach. In either case, the most practical solution is the Proxmox Backup Server. And the Proxmox Backup Server is one of the reasons I proposed installing &lt;code&gt;vm-bhyve&lt;/code&gt;: running it in a VM and storing the data on the FreeBSD host gives you the best of both worlds. Some time ago, I tried running it in a FreeBSD jail (via Linuxulator), but it didn't work.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Backups using zfs-autobackup&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;zfs-autobackup&lt;/code&gt; is an extremely useful and effective tool. It allows for "pull" type backups, as well as having an intermediary host that connects to both the source and destination, which is useful if you don't want direct contact between the source and destination. I won't describe the latter setup, but the documentation is clear, and I have several of them in production, ensuring that the production server and its backup server cannot communicate with each other.&lt;/p&gt;
&lt;p&gt;I usually create a dataset for each server and instruct &lt;code&gt;zfs-autobackup&lt;/code&gt; to keep that server's backups in that dataset. The snapshots taken and transferred will all be from the same instant, so as not to create a time skew (some tools of this kind snapshot a dataset, then transfer it, which can result in minutes of difference between two different datasets from the same server).&lt;/p&gt;
&lt;p&gt;I've described in detail how I perform this type of backup in a &lt;a href="https://it-notes.dragas.net/2022/05/30/how-we-are-migrating-many-of-our-servers-from-linux-to-freebsd-part-2/"&gt;previous post&lt;/a&gt;, so I suggest reading that post for reference.&lt;/p&gt;
&lt;p&gt;Let's install zfs-autobackup on the FreeBSD server:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;pkg install py311-zfs-autobackup mbuffer
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Backups for other servers using BorgBackup&lt;/h3&gt;
&lt;p&gt;When I don't have ZFS available or need to perform a file-based backup (all or partial), I use a different technique. &lt;code&gt;BorgBackup&lt;/code&gt; backups are primarily "push" based, meaning the client will connect to the backup server. This is not optimal or the most secure approach, as the backup server should, in theory, be hardened. Even when protecting everything via VPN, the risk remains that a compromised server could connect to its backup server and alter or delete the backups. I have seen this happen in ransomware cases (especially in the Microsoft world), and so I try to be careful to minimize this type of problem, mainly through snapshots of the backup server (an operation that will be described later).&lt;/p&gt;
&lt;p&gt;To ensure the highest possible security, I create a FreeBSD jail on the backup server for each server I need to back up. The advantage of this approach is the complete separation of all servers from each other. By using a regular user inside a jail, a compromised server that connects to its backup server would only be able to reach its own backups, as it would be confined to a user account and, even if it managed to escalate privileges, still be inside a jail.&lt;/p&gt;
&lt;p&gt;Let's say, for example, we want to back up a server called "ServerA" (great imagination, I know). We create a dedicated jail on the backup server:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# Create a new VNET jail named &amp;quot;servera&amp;quot; attached to our bridge
bastille create -B servera 14.3-RELEASE 192.168.0.101/24 bridge0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;BastilleBSD will automatically set the host's gateway for the jail. In our case, this is incorrect, so we need to modify it and set the jail's gateway to &lt;code&gt;192.168.0.1&lt;/code&gt; in the &lt;code&gt;/usr/local/bastille/jails/servera/root/etc/rc.conf&lt;/code&gt; file:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# ...
defaultrouter=&amp;quot;192.168.0.1&amp;quot;
# ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Restart the jail and connect to it:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;bastille restart servera
bastille console servera
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now, inside the jail, we install &lt;code&gt;borgbackup&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;pkg install py311-borgbackup
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;BorgBackup doesn't run a daemon; it's launched by the remote server (which sends its data to the backup server), so it's important that the installed version is compatible with the one on the remote host.&lt;/p&gt;
&lt;p&gt;Since we'll be using SSH, let's enable it:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;service sshd enable
service sshd start
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And create a non-privileged user for this purpose:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# The 'adduser' utility provides an interactive way to create a user.
root@servera:~ # adduser
Username: servera
Full name: Server A
Uid (Leave empty for default): 
Login group [servera]: 
Login group is servera. Invite servera into other groups? []: 
Login class [default]: 
Shell (sh csh tcsh nologin) [sh]: 
Home directory [/home/servera]: 
Home directory permissions (Leave empty for default): 
Use password-based authentication? [yes]: 
Use an empty password? (yes/no) [no]: 
Use a random password? (yes/no) [no]: yes
Lock out the account after creation? [no]: 
Username    : servera
Password    : &amp;lt;random&amp;gt;
Full Name   : Server A
Uid         : 1001
Class       : 
Groups      : servera 
Home        : /home/servera
Home Mode   : 
Shell       : /bin/sh
Locked      : no
OK? (yes/no) [yes]: yes
adduser: INFO: Successfully added (servera) to the user database.
adduser: INFO: Password for (servera) is: JIkdq8Ex
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The user is created and can receive SSH connections. After setting everything up, I suggest disabling password-based login in the jail's SSH configuration, using only public key authentication.&lt;/p&gt;
&lt;p&gt;As mentioned, the biggest risk of a "push" backup is that a compromised client could access the backup server and delete or encrypt the backup history, rendering it useless.&lt;/p&gt;
&lt;p&gt;To drastically mitigate this risk, we can configure SSH to force the client to operate in a special Borg mode called &lt;strong&gt;append-only&lt;/strong&gt;. In this mode, the SSH key used by the client will only have permission to create new archives, not to read or delete old ones. However, this approach could complicate some client-side operations (like &lt;code&gt;mount&lt;/code&gt;, &lt;code&gt;prune&lt;/code&gt;, etc.), forcing them to be done on the server. For this reason, I won't describe it in this setup, "limiting" myself to taking snapshots of the repositories. It can be a very good practice, so I recommend considering it.&lt;/p&gt;
&lt;p&gt;Let's initialize the BorgBackup repository. In this example, for simplicity, I won't set up repository encryption. If the jails are on an encrypted dataset or GELI-encrypted disks, there will still be data encryption on the disks, but there will be no protection against someone who could physically access the server while the disks are mounted. As usual, security is like an onion: every layer helps. Personally, I suggest enabling and using it ALWAYS.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# Switch to the new user
su -l servera
# Initialize a new Borg repo named &amp;quot;servera&amp;quot; with no encryption (for this example)
borg init -e none servera
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The jail is ready, but it's unreachable from the outside. There are two ways to make it accessible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Install a VPN system inside the jail itself.&lt;/strong&gt; Using tools like Zerotier or Tailscale (which don't need to expose ports) will immediately create the conditions to connect to the jail, which will remain inaccessible from the outside. As the jail is a VNET jail, we're free to choose any of the supported VPN technologies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expose a port on the backup server&lt;/strong&gt;, i.e., on the host, to allow external connections. Many advise against this path as they consider it less secure. It is, but sometimes we don't have the luxury of installing whatever we want on the server we're backing up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To expose the port, go back to the host and modify the &lt;code&gt;/etc/pf.conf&lt;/code&gt; file, creating the &lt;code&gt;rdr&lt;/code&gt; and &lt;code&gt;pass&lt;/code&gt; rules to let packets in:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# ...
# Redirect incoming traffic on port 1122 to the jail's SSH port (22)
rdr on $ext_if inet proto tcp from any to any port = 1122 -&amp;gt; 192.168.0.101 port 22
# ...
# Allow incoming traffic on port 1122
pass in inet proto tcp from any to any port 1122 flags S/SA keep state
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Reload the &lt;code&gt;pf&lt;/code&gt; configuration:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;service pf reload
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The jail will now be reachable on the server's public IP, on port 1122. Obviously, this port number is for illustrative purposes, and I used &lt;code&gt;from any&lt;/code&gt;, but for better security, you should replace &lt;code&gt;any&lt;/code&gt; with the IP address of the server that will be connecting to perform the backup.&lt;/p&gt;
&lt;p&gt;By repeating this process for each server and creating a separate jail for each, you can have isolated jails in separate datasets with their backups, potentially setting space limits using ZFS quotas.&lt;/p&gt;
&lt;p&gt;It's important to remember that backing up a live filesystem (i.e., without a snapshot or dumps) has a very high probability of being impossible to restore completely. Databases hate this approach because files will change while being copied and tend to get corrupted. Of course, it depends on the nature of the data (a backup of a static website will have no issues, but a WordPress database probably will), but it's crucial to think about a technique to snapshot the filesystem before proceeding. For example, I have already written about how to create snapshots on FreeBSD with UFS in a previous article: &lt;a href="https://it-notes.dragas.net/2024/06/04/freebsd-tips-and-tricks-creating-snapshots-with-ufs/"&gt;FreeBSD tips and tricks: creating snapshots with UFS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I will cover other operating systems in a future, dedicated post.&lt;/p&gt;
&lt;h3&gt;Proxmox Backup Server in a Dedicated VM&lt;/h3&gt;
&lt;p&gt;Starting with version 4.0 (which is still in beta at the time of this writing), Proxmox Backup Server (PBS) supports storing its data in an S3 bucket. This is excellent news as it decouples the server from the data. There are great open-source S3 implementations, like &lt;a href="https://min.io/"&gt;Minio&lt;/a&gt; or &lt;a href="https://github.com/seaweedfs/seaweedfs"&gt;SeaweedFS&lt;/a&gt;, which allow for clustering, replication, etc. In this "simple" case, we will install Proxmox Backup Server in a small VM, while for the data, we'll install Minio in a native FreeBSD jail. The advantage is undeniable: the VM will only serve as an "intermediary", but the data will rest directly on the FreeBSD host's dataset, natively. It will also be possible to specify other external endpoints, other repositories, etc.&lt;/p&gt;
&lt;p&gt;As a philosophy, I tend not to use external providers unless for specific needs, so installing Minio in a jail is a perfect solution to manage this situation.&lt;/p&gt;
&lt;p&gt;Let's install PBS by downloading the ISO from their website (https://enterprise.proxmox.com/iso/) - at this moment, the version that supports this setup is 4.0 Beta.&lt;/p&gt;
&lt;p&gt;The directory to download to is the &lt;code&gt;vm-bhyve&lt;/code&gt; ISOs directory. It's not strictly necessary, but it's useful for not "losing" it somewhere. So, go to the directory and download it:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;cd /zroot/VMs/.iso
fetch https://enterprise.proxmox.com/iso/proxmox-backup-server_4.0-BETA-1.iso
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now let's create a VM with &lt;code&gt;vm-bhyve&lt;/code&gt;. We can start from the Debian template, but we'll make some modifications to optimize performance. In this example, I'm giving it 30 GB of disk space, 2 GB of RAM, and 2 cores.&lt;/p&gt;
&lt;p&gt;If you want to store all backups inside the VM, you'll need to size the virtual disk correctly (or create and attach another one). In this case, I will focus on the "clean" VM that will store its data on a dedicated jail with Minio.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;vm create -t debian -s 30G -m 2G -c 2 pbs
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once the empty VM is created, let's modify its options:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;vm configure pbs
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We will modify the VM to be UEFI and to use the NVME disk driver - bhyve &lt;a href="https://it-notes.dragas.net/2024/06/10/proxmox-vs-freebsd-which-virtualization-host-performs-better/"&gt;performs significantly better on NVME than virtio, as previously tested&lt;/a&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;loader=&amp;quot;uefi&amp;quot;
cpu=&amp;quot;2&amp;quot;
memory=&amp;quot;2G&amp;quot;
network0_type=&amp;quot;virtio-net&amp;quot;
network0_switch=&amp;quot;public&amp;quot;
disk0_type=&amp;quot;nvme&amp;quot;
disk0_name=&amp;quot;disk0.img&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Fortunately, the Proxmox team has provided for the installation of the Backup Server on devices without a graphical interface, so the boot menu will allow installation via serial console. Let's launch the installation and connect to the virtual serial console:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;cd /zroot/VMs/.iso
vm install pbs proxmox-backup-server_4.0-BETA-1.iso
vm console pbs
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Select the installation via &lt;strong&gt;Terminal UI (serial console)&lt;/strong&gt; and proceed normally as if it were a physical host, assigning an IPv4 address from the &lt;code&gt;192.168.0.x&lt;/code&gt; range (in this example, I'll use &lt;code&gt;192.168.0.3&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;This way, the Proxmox Backup Server will run in a VM, with the ability to take snapshots before updates, etc., without any worries.&lt;/p&gt;
&lt;p&gt;Once the installation is complete, PBS will reboot and listen on port 8007 of its IP. Again, as with the jails, we have two options: install a VPN system within the VM itself (thus exposing it automatically only on that VPN - generally a more secure operation) or expose port 8007 on the server's public IP.&lt;/p&gt;
&lt;p&gt;In the latter case, add the relevant lines to the &lt;code&gt;/etc/pf.conf&lt;/code&gt; file on the FreeBSD backup server:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# ...
# Redirect incoming traffic on port 8007 to the PBS VM's web interface
rdr on $ext_if inet proto tcp from any to any port = 8007 -&amp;gt; 192.168.0.3 port 8007
# ...
# Allow that traffic to pass
pass in inet proto tcp from any to any port 8007 flags S/SA keep state
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Reload the &lt;code&gt;pf&lt;/code&gt; configuration:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;service pf reload
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The PBS VM configuration is complete. If you chose to use the PBS's internal disk as a repository, no further operations are necessary (other than the normal repository creation, etc., within PBS).&lt;/p&gt;
&lt;p&gt;In this case, however, we will use a different approach.&lt;/p&gt;
&lt;h4&gt;Creating a Minio Jail as a Data Repository for PBS&lt;/h4&gt;
&lt;p&gt;This approach, in my opinion, has a number of important advantages. The first is that Minio will run in a dedicated jail on the host, at full performance, and will store the data directly on the physical ZFS datapool, thus removing any other layer in between. This jail could potentially be moved to other hosts (by connecting PBS and the jail via VPN or public IP), made redundant thanks to all of Minio's features, etc. Another solution I am successfully testing (in other setups) is SeaweedFS.&lt;/p&gt;
&lt;p&gt;Let's create a dedicated jail with Minio and put it on the bridge, so that PBS can access it on the LAN.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;bastille create -B minio 14.3-RELEASE 192.168.0.11/24 bridge0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If not configured directly, BastilleBSD will use the host's gateway for the jail, which is incorrect in this case. So let's go modify it and restart the jail. Enter the jail with:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;bastille console minio
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And modify the &lt;code&gt;/etc/rc.conf&lt;/code&gt; file to have the correct gateway (following the example addresses):&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# ...
ifconfig_vnet0=&amp;quot; inet 192.168.0.11/24 &amp;quot;
defaultrouter=&amp;quot;192.168.0.1&amp;quot;
# ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Exit the jail and restart it:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;bastille restart minio
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Enter the jail and install Minio:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;bastille console minio
pkg install -y minio
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Minio is already able to start, but PBS, even on the LAN, wants an encrypted connection. Fortunately, there's a handy tool that can generate the certificates for us:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# Download the certgen tool
fetch https://github.com/minio/certgen/releases/latest/download/certgen-freebsd-amd64

# Make it executable and run it for our jail's IP
chmod a+rx certgen-freebsd-amd64
./certgen-freebsd-amd64  -host &amp;quot;192.168.0.11&amp;quot;

# Create the necessary directories and set permissions
mkdir -p /usr/local/etc/minio/certs
cp private.key public.crt /usr/local/etc/minio/certs/
chown -R minio:minio /usr/local/etc/minio/certs/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let's view the certificate's fingerprint. Since it's self-signed, we'll need it for PBS later. For security reasons, PBS will ask for the fingerprint of non-directly verifiable certificates. Run the following command and take note of the result:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;openssl x509 -in /usr/local/etc/minio/certs/public.crt -noout -fingerprint -sha256
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point, enable and configure Minio in &lt;code&gt;/etc/rc.conf&lt;/code&gt;. 
&lt;strong&gt;WARNING&lt;/strong&gt;: The username and password (access key and secret) used in this example are insecure and for testing purposes only. It is strongly recommended to use different values:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# Enable Minio service
minio_enable=&amp;quot;YES&amp;quot;
# Set the address for the Minio console
minio_console_address=&amp;quot;:8751&amp;quot;
# Set the root user and password as environment variables
minio_env=&amp;quot;MINIO_ROOT_USER=testaccess MINIO_ROOT_PASSWORD=testsecret&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Start Minio:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;service minio start
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If everything went correctly, Minio is now running (with its certificates) and ready to receive connections.&lt;/p&gt;
&lt;p&gt;It's now time to create the bucket(s) that PBS will use. There are several ways to do this, but to test that everything is working and to configure PBS, I suggest connecting via an SSH tunnel.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;# Create an SSH tunnel from your local machine to the backup server
# Port 8007 is forwarded to the PBS web UI
# Port 8751 is forwarded to the Minio console
ssh user@backupServerIP -L8007:192.168.0.3:8007 -L8751:192.168.0.11:8751
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This way, we'll create a tunnel between the FreeBSD backup server and our workstation, mapping &lt;code&gt;127.0.0.1:8007&lt;/code&gt; to &lt;code&gt;192.168.0.3:8007&lt;/code&gt; (the PBS web interface) and &lt;code&gt;127.0.0.1:8751&lt;/code&gt; to &lt;code&gt;192.168.0.11:8751&lt;/code&gt; (the Minio console port).&lt;/p&gt;
&lt;p&gt;Now, connect to &lt;code&gt;https://127.0.0.1:8751&lt;/code&gt;, enter the credentials specified in &lt;code&gt;/etc/rc.conf&lt;/code&gt;, and create a bucket.&lt;/p&gt;
&lt;p&gt;Once the bucket is created, you can configure PBS to use it. Connect to PBS via &lt;code&gt;https://127.0.0.1:8007&lt;/code&gt; and go to &lt;strong&gt;S3 Endpoints&lt;/strong&gt;. Set a name, use &lt;code&gt;192.168.0.11&lt;/code&gt; as the IP and &lt;code&gt;9000&lt;/code&gt; as the port, enter the access and secret keys, and the certificate fingerprint we generated earlier. &lt;strong&gt;Enable "Path Style"&lt;/strong&gt; or it will not work.&lt;/p&gt;
&lt;p&gt;Then go to &lt;strong&gt;Datastores&lt;/strong&gt; and add it, as you would for any other S3 datastore, by specifying the created bucket and a local directory where the system will keep its cache.&lt;/p&gt;
&lt;p&gt;If everything was set up correctly, PBS will create its structure in the bucket, and from that moment on, you can use it. Always keep in mind that this is still a "technology preview", so there may be issues, but from my tests, it is sufficiently reliable.&lt;/p&gt;
&lt;h3&gt;Taking Local Snapshots of Backups&lt;/h3&gt;
&lt;p&gt;One of the most common techniques used in ransomware attacks is to also delete or encrypt backups. They often use automated methods, relying on the fact that many (too many!) consider a "backup" to be a simple copy of files to a network share. However, it's not impossible that, in specific cases, they might compromise the machine and connect to the backup server. This is nearly impossible with a "pull" type backup (like the one managed by &lt;code&gt;zfs-autobackup&lt;/code&gt;) but is still possible with the "push" approach, which involves using BorgBackup or similar tools.&lt;/p&gt;
&lt;p&gt;This happened to one of my clients once - in that case, the problem originated internally, from an employee who wanted to cover up his mistake, inadvertently creating a disaster - but that will be material for another post.&lt;/p&gt;
&lt;p&gt;Fortunately, the client had a system that solved the problem: thanks to ZFS, we can have local snapshots on the backup server, which are invisible and inaccessible to the production server. Since we have already installed &lt;code&gt;zfs-autobackup&lt;/code&gt;, it's easy to use it for this purpose as well. I've already talked about this in a &lt;a href="https://it-notes.dragas.net/2024/08/21/automating-zfs-snapshots-for-peace-of-mind/"&gt;previous article&lt;/a&gt; and won't rewrite the steps here. Just consult that article, keeping in mind that in this case, it's not advisable to snapshot all the datasets on the backup server (the space would grow exponentially), but only those at risk. In the cases analyzed in this post, this applies only to the &lt;code&gt;push&lt;/code&gt; part, as PBS will also be accessible only from the Proxmox servers and not from the VMs they contain. If, in this case too, you don't trust those who manage the Proxmox servers, just set up snapshots for the Minio jail as well.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;This long post aims to analyze, in a general way, how I believe one can manage reasonably secure backups of their data. Obviously, there are many variables, additional precautions, possible optimizations, hardening, etc., that must be studied on a case-by-case basis. There are old rules, new rules, old and new philosophies. Recently, many people who have embraced the cloud have often stopped thinking about backups, only to realize it when something happens and the data has, indeed, vanished... into the clouds.&lt;/p&gt;
&lt;p&gt;In this post, I have generically covered the setup of the backup server, and this demonstrates how FreeBSD, thanks to its features, can be considered an ideal platform for this type of task.&lt;/p&gt;
&lt;p&gt;In the next articles in this series, I will examine the client side, i.e., how to structure them for a sufficiently reliable backup, and how to monitor everything - because I've seen this too: people resting easy because the backup was supposedly running every night, but in fact, the backup had been failing every night for more than 4 years.&lt;/p&gt;
&lt;p&gt;Stay Tuned and stay...backupped!&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Tue, 29 Jul 2025 08:00:00 +0200</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2025/07/29/make-your-own-backup-system-part-2-forging-the-freebsd-backup-stronghold/</guid><category>backup</category><category>freebsd</category><category>jail</category><category>bhyve</category><category>borg</category><category>data</category><category>server</category><category>vps</category><category>filesystems</category><category>proxmox</category><category>snapshots</category><category>sysadmin</category><category>virtualization</category><category>ownyourdata</category><category>zfs</category><category>series</category><category>tutorial</category></item><item><title>New Article on BSD Cafe Journal: WordPress on FreeBSD with BastilleBSD</title><link>https://it-notes.dragas.net/2025/07/21/new-article-wordpress-on-freebsd-bastillebsd-on-bsd-cafe-journal/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/web_text.webp" alt="Web Text - a terminal"&gt;&lt;/p&gt;&lt;h2&gt;New Article Published&lt;/h2&gt;
&lt;p&gt;I'm excited to announce that I have published a new, in-depth article on the &lt;strong&gt;&lt;a href="https://journal.bsd.cafe/"&gt;BSD Cafe Journal&lt;/a&gt;&lt;/strong&gt;: "&lt;a href="https://journal.bsd.cafe/2025/07/21/wordpress-on-freebsd-with-bastillebsd-a-secure-alternative-to-linux-docker/"&gt;WordPress on FreeBSD with BastilleBSD: A Secure Alternative to Linux/Docker&lt;/a&gt;".&lt;/p&gt;
&lt;p&gt;This piece explores how to create a robust and secure WordPress installation on FreeBSD using BastilleBSD, leveraging the power and isolation of FreeBSD &lt;a href="https://it-notes.dragas.net/categories/jail/"&gt;jails&lt;/a&gt; as a compelling alternative to the more common Linux and Docker stack.&lt;/p&gt;
&lt;h2&gt;Future Technical Content&lt;/h2&gt;
&lt;p&gt;I'm excited to announce that I'm expanding my writing to a new platform! From now on, some of my more technical, long-form articles and tutorials will be published on &lt;a href="https://journal.bsd.cafe"&gt;The BSD Cafe Journal&lt;/a&gt;, a fantastic hub for BSD-related content that I'm happy to now contribute to.&lt;/p&gt;
&lt;p&gt;This new collaboration complements the work I do here. My personal blog will continue to be my home base, and you won't miss a thing! I'll still be posting my own articles and announcements right here, and I'll always include a direct link to any new content I publish elsewhere. This space will remain as active as ever.&lt;/p&gt;
&lt;p&gt;Thank you for reading&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Mon, 21 Jul 2025 09:30:00 +0200</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2025/07/21/new-article-wordpress-on-freebsd-bastillebsd-on-bsd-cafe-journal/</guid><category>freebsd</category><category>container</category><category>sysadmin</category><category>hosting</category><category>jail</category><category>ownyourdata</category><category>server</category><category>tutorial</category><category>web</category><category>blogging</category></item><item><title>Make Your Own Backup System – Part 1: Strategy Before Scripts</title><link>https://it-notes.dragas.net/2025/07/18/make-your-own-backup-system-part-1-strategy-before-scripts/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/hard_disk.webp" alt="A hard disk - ready to host our backups"&gt;&lt;/p&gt;&lt;h3&gt;Backup: Beyond the Simple Copy&lt;/h3&gt;
&lt;p&gt;For as long as I can remember, backup is something that has been underestimated by far too many people. Between flawed techniques, "Schrödinger's backups" (i.e., never tested, thus both valid and invalid at the same time), and conceptual errors about what they are and how they work (RAID is not a backup!), too much data has been lost due to deficiencies in this area.&lt;/p&gt;
&lt;p&gt;Nowadays, backup is often an afterthought. Many rely entirely on "the cloud" without ever asking how - or if - their data is actually protected. It's a detail many overlook, but even major cloud providers operate on a shared responsibility model. Their terms often clarify that while they secure the infrastructure, the ultimate responsibility for protecting and backing up your data lies with you. By putting everything "in the cloud", on clusters owned by other companies, or on distributed Kubernetes systems, backup seems unnecessary. When I sometimes ask developers or colleagues how they handle backups for all this, they look at me as if I'm speaking an archaic, unknown, and indecipherable language. The thought has simply never crossed their minds. But data is not ephemeral; it must be preserved in every way possible.&lt;/p&gt;
&lt;p&gt;I've always had a philosophy: data must always be restorable (and as quickly as possible), in an open format (meaning you shouldn't have to buy anything to restore or consult it), and consistent. These points may seem obvious, but they are not.&lt;/p&gt;
&lt;p&gt;I have encountered various scenarios of data loss:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.reuters.com/article/idUSKBN2B20NT/"&gt;Datacenters destroyed by fire&lt;/a&gt; – I had 142 servers there, but they were all restored in just a few hours.&lt;/li&gt;
&lt;li&gt;Server rooms flooded.&lt;/li&gt;
&lt;li&gt;Servers destroyed in earthquakes, often due to collapsing walls.&lt;/li&gt;
&lt;li&gt;Increasing incidents of various ransomware attacks.&lt;/li&gt;
&lt;li&gt;Intentional damage by entities seeking to create problems.&lt;/li&gt;
&lt;li&gt;Mistakes made by administrators, which can happen to anyone.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The risk escalates for servers connected to the internet, like e-commerce and email servers. Here, not only is data integrity crucial, but so is the uninterrupted operation of services. This series of posts will revisit some of my old articles to explain my core ideas on the subject and describe, at least in part, my primary techniques.&lt;/p&gt;
&lt;p&gt;Many consider a backup to be a simple copy. I often hear people say they have backups because they "copy the data", but this is often wrong and extremely dangerous, providing a false sense of security. Copying the files of a live database is an almost useless operation, as the result will nearly always be impossible to restore. It is essential to at least perform a proper dump and then transfer that file. Yet, many people do this and will only realize their mistake when they face an emergency and need to restore.&lt;/p&gt;
&lt;h3&gt;The Backup Plan: Asking the Right Questions&lt;/h3&gt;
&lt;p&gt;Before touching a single file, you must start with a plan, and that plan starts with asking the right questions:&lt;/p&gt;
&lt;p&gt;"How much risk am I willing to take? What data do I need to protect? What downtime can I tolerate in case of data loss? What type and amount of storage space do I have available?"&lt;/p&gt;
&lt;p&gt;The first question is particularly critical. A common but risky approach is to store a backup on the same machine that requires backing up. While convenient, this method fails in the event of a machine failure. Even relying on a classic USB drive for daily backups is not foolproof, as these devices are as susceptible to failure as any other hardware component. And contrary to popular belief, even high-end uninterruptible power supplies (UPS) are not immune to catastrophic failures.&lt;/p&gt;
&lt;p&gt;Thus, the initial step is to establish a management plan, balancing security and cost. The safest backup is often the one stored farthest from the source machine. However, this approach introduces challenges related to space and bandwidth. While local area network (LAN) backups are relatively straightforward, off-network backups involve additional connectivity considerations. This might lead to a compromise on the amount of data backed up to maintain operational speed during both backup and recovery processes.&lt;/p&gt;
&lt;p&gt;Safety doesn't always equate to practicality. For instance, with a 200 MBit/sec connection and 2 TB of backup data, the recovery time could be significant. However, if the goal is not rapid restorability but simply ensuring the data is available, the safest backup is often the one closest to us. That is, a backup we can "touch", disconnect, and consult even when offline.&lt;/p&gt;
&lt;p&gt;Therefore, it is essential to develop a backup policy tailored to specific needs, keeping in mind that no 'perfect' solution exists.&lt;/p&gt;
&lt;h3&gt;The Core Decision: Full Disk vs. Individual Files&lt;/h3&gt;
&lt;p&gt;When planning a backup strategy, one key decision is whether to back up the entire disk or just individual files. Or both of them. Each approach has its pros and cons.&lt;/p&gt;
&lt;h4&gt;Entire Disk (or Storage) Backup&lt;/h4&gt;
&lt;p&gt;Advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Complete Recovery: Restoring a full disk backup can quickly revert a system to its exact previous state, boot loader included.&lt;/li&gt;
&lt;li&gt;Integration in Virtualization Systems: If your VM is a single file on a filesystem like ZFS or btrfs, you can simply copy that file (after taking a snapshot) to get a complete backup of the VM. Solutions like Proxmox offer easy management of full disk backups, accessible via command line or web interface.&lt;/li&gt;
&lt;li&gt;Flexibility in Virtual Environments: Products like the Proxmox Backup Server offer the ability to recover individual files from a full backup, combining the benefits of both approaches.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Disadvantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Downtime for Physical Machines: Often, it's necessary to shut down the machine to create a full disk backup, leading to operational interruptions. A hybrid approach, if the physical host is running FreeBSD for example, is to take a snapshot and copy all the host's datasets. The restore process, however, will require some manual operations.&lt;/li&gt;
&lt;li&gt;Large Space Requirements: Full disk backups can consume substantial space, including unnecessary data.&lt;/li&gt;
&lt;li&gt;Potential Slowdowns and Compatibility Issues: The backup process can be slow and may encounter issues with non-standard file system configurations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Individual File Backup&lt;/h4&gt;
&lt;p&gt;While it might seem simpler, backing up individual files can get complicated.&lt;/p&gt;
&lt;p&gt;Advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Basic Utility Use: Possible with standard system utilities like tar, cp, rsync, etc.&lt;/li&gt;
&lt;li&gt;Granular Backups: Allows for backing up specific files and comparing them to previous versions.&lt;/li&gt;
&lt;li&gt;Delta Copying: Only modified parts of the files are backed up, saving space and reducing data transfer.&lt;/li&gt;
&lt;li&gt;Portability and Partial Recovery: Files can be moved individually and partially restored as needed.&lt;/li&gt;
&lt;li&gt;Compression and Deduplication: These features are often available at the file or block level.&lt;/li&gt;
&lt;li&gt;Operational Continuity: Can be done without shutting down the machine.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Disadvantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Storage Space Requirements: Simple copy solutions might require significant storage.&lt;/li&gt;
&lt;li&gt;Need for File System Snapshot: For efficient and consistent backups, a snapshot (like native ZFS snapshots, btrfs, LVM Volume snapshots, or Microsoft's VSS) is highly recommended before copying.&lt;/li&gt;
&lt;li&gt;Hidden Pitfalls: Issues may not become apparent until a backup is needed. And by then, it may be too late.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Key to Consistency: The Power of Snapshots&lt;/h3&gt;
&lt;p&gt;Backing up a "live" file system involves a "start" and an "end" moment. During this time, the data can change, leading to fatal inconsistencies. I've encountered such issues in the past: a large MySQL database was compromised, and I was tasked with its recovery. I confidently took the client's last file-based backup and restored various files (not a native dump). Unsurprisingly, the database failed to restart. The large data file had changed too much between the start and end of the copy, rendering it inconsistent. Fortunately, I also had a proper dump, so I managed to recover from that.&lt;/p&gt;
&lt;p&gt;The issue is evident: backing up a live file system is risky. An open database, even a basic one like a browser's history, is highly likely to get corrupted, making the backup useless.&lt;/p&gt;
&lt;p&gt;The solution is to create a snapshot of the entire file system before beginning the backup. This approach freezes a consistent "point-in-time" view of the data. To date, using snapshots, I have managed to recover everything.&lt;/p&gt;
&lt;p&gt;Here are the methods I've explored over the years:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Native File System Snapshot (e.g., &lt;a href="https://it-notes.dragas.net/2020/06/28/btrfs-automatic-snapshots-and-remote-backups/"&gt;BTRFS&lt;/a&gt; or &lt;a href="https://it-notes.dragas.net/2022/05/30/how-we-are-migrating-many-of-our-servers-from-linux-to-freebsd-part-2/"&gt;ZFS&lt;/a&gt;): If your file system inherently supports snapshots, it's wise to use this feature. It's likely to be the most efficient and technically sound option.&lt;/li&gt;
&lt;li&gt;LVM Snapshot: For those using LVM, creating a snapshot of the logical volume is a viable approach. This method can lead to some space wastage and, while I still use it, has occasionally caused the file system to freeze during the snapshot's destruction, necessitating a reboot. This has been a rare but recurring issue across different hardware setups, especially under high I/O load.&lt;/li&gt;
&lt;li&gt;DattoBD: I've tracked this tool since its inception. I used it extensively in the past, but I sometimes faced stability issues (kernel panics or the inability to delete snapshots, forcing a reboot). For snapshots with Datto, I often use UrBackup scripts, which are convenient and efficient.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Architecture: Push or Pull?&lt;/h3&gt;
&lt;p&gt;A longstanding debate among experts is whether backups should be initiated by the client (push) or requested by the server (pull). In my view, it depends.&lt;/p&gt;
&lt;p&gt;Generally, I prefer centralized backup systems on dedicated servers, maintained in highly secure environments with minimal services running. Therefore, I lean towards the "pull" method, where the server connects to the client to initiate the backup.&lt;/p&gt;
&lt;p&gt;Ideally, the backup server should not be reachable from the outside. It should be protected, hardened, and only be able to reach the setups it needs to back up. The goal is to minimize the possibility that the backup data could be compromised or deleted in case the client machine itself is attacked (which, unfortunately, is not so rare).&lt;/p&gt;
&lt;p&gt;This is not always possible, but there are ways to mitigate this problem. One way is to ensure that machines that must be backed up via "push" (i.e., by contacting the backup server themselves) can only access their own space. More importantly, the backup server, for security reasons, should maintain its own filesystem snapshots for a certain period. In this way, even in the worst-case scenario (workload compromised -&amp;gt; connection to backup server -&amp;gt; deletion of backups to demand a ransom), the backup server has its own snapshots. These server-side snapshots should not be accessible from the client host and should be kept long enough to ensure any compromise can be detected in time.&lt;/p&gt;
&lt;h3&gt;My Guiding Principles for a Good Backup System&lt;/h3&gt;
&lt;p&gt;Over the years, I've favored having granular control over backups, often finding the need to recover specific files or emails accidentally deleted by clients. A good backup system, in my opinion, should possess these key features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instant Recovery and Speed: The system should enable quick recovery and operate at a high processing speed.&lt;/li&gt;
&lt;li&gt;External Storage: Backups must be stored externally, not on the same system being backed up. Still, local snapshots are a good idea for immediate rollbacks.&lt;/li&gt;
&lt;li&gt;Security: I avoid using mainstream cloud storage services like Dropbox or Google Drive for primary backups. Own your data! &lt;/li&gt;
&lt;li&gt;Efficient Space Management: This includes features like compression and deduplication.&lt;/li&gt;
&lt;li&gt;Minimal Invasiveness: The system should require minimal additional components to function.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Conclusion: What's Next&lt;/h3&gt;
&lt;p&gt;The approach to backup is varied, and in this series, I will describe the main scenarios I usually face. I will start with the backup servers and their primary configurations, then move on to the various software and techniques I use.&lt;/p&gt;
&lt;p&gt;But that will begin with &lt;a href="https://it-notes.dragas.net/2025/07/29/make-your-own-backup-system-part-2-forging-the-freebsd-backup-stronghold/"&gt;the next post, where I'm talk about building the backup server which, of course, will be powered by FreeBSD&lt;/a&gt; - like all my backup servers for the last 20 years.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Fri, 18 Jul 2025 09:00:00 +0200</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2025/07/18/make-your-own-backup-system-part-1-strategy-before-scripts/</guid><category>backup</category><category>data</category><category>server</category><category>vps</category><category>filesystems</category><category>ownyourdata</category><category>series</category><category>tutorial</category></item><item><title>How to install FreeBSD on providers that don't support it with mfsBSD</title><link>https://it-notes.dragas.net/2025/07/02/install_freebsd_providers_mfsbsd/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/server_rack.webp" alt="FreeBSD installation on a server"&gt;&lt;/p&gt;&lt;p&gt;FreeBSD is an extremely powerful operating system. The ability to isolate services in jails and, thanks to ZFS, the simplicity with which you can create snapshots (both local and remote) make it a perfect system for increasing peace of mind, especially when running many workloads.&lt;/p&gt;
&lt;p&gt;Many providers, blinded by the success and large numbers achieved by Linux distributions, have decided to no longer support FreeBSD in their installers. While understandable (they might not have staff experienced with systems other than Linux), this can cause problems for those who want to try using something different. And yes, &lt;a href="https://it-notes.dragas.net/2025/03/23/osday-2025-why-choose-bsd-in-2025/"&gt;it makes perfect sense, even just to avoid IT monocultures&lt;/a&gt;, which are extremely harmful even in the medium term.&lt;/p&gt;
&lt;p&gt;There's an extremely powerful tool that, in my opinion, deserves much more attention than it gets. The tool is called &lt;a href="https://mfsbsd.vx.sk/"&gt;mfsBSD&lt;/a&gt;. Using the author's words: "This is a set of scripts that generates a bootable image (and/or ISO file), that creates a working minimal installation of FreeBSD (mfsBSD) or Linux (mfslinux). It is completely loaded into memory."&lt;/p&gt;
&lt;p&gt;mfsBSD works intelligently: it can be launched both via UEFI and via "traditional" boot since it has both boot modes enabled. It gets an IP address via DHCP (an operation that works with most providers) and opens an SSH server (with a preset password - so it's advisable to connect immediately and change it, to prevent someone else from doing it for you).&lt;/p&gt;
&lt;p&gt;This means that mfsBSD can be used both when you have a console available and, in many cases, without a console, since you'll just need to connect via SSH and start with the traditional installation.&lt;/p&gt;
&lt;p&gt;To install FreeBSD using mfsBSD, you just need to follow some very simple steps: all providers, in fact, offer the ability to boot in Linux "rescue mode", generally based on a sufficiently recent version. Set your server (whether physical or VPS, it doesn't matter) in rescue mode and reboot. Once active, connect via SSH (or open a console) to the server in rescue mode.&lt;/p&gt;
&lt;p&gt;Now it's sufficient to download the mfsBSD image (for example, from here: &lt;a href="https://mfsbsd.vx.sk/files/images/"&gt;https://mfsbsd.vx.sk/files/images/&lt;/a&gt; - in my case, I usually choose the normal image, which at the time of writing this post is https://mfsbsd.vx.sk/files/images/14/amd64/mfsbsd-14.2-RELEASE-amd64.img). At this point, it needs to be written with &lt;strong&gt;dd&lt;/strong&gt; directly to the server's disk (or, if in doubt, disks). For example:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;dd if=mfsbsd-14.2-RELEASE-amd64.img of=/dev/sda bs=5M conv=sync&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this case I wrote "sda", but if you had one or more NVMe drives, the correct device would be &lt;code&gt;/dev/nvme0n1&lt;/code&gt; for the first disk, &lt;code&gt;/dev/nvme1n1&lt;/code&gt; for the second, etc.&lt;/p&gt;
&lt;p&gt;Reboot. If you have a console, you'll see mfsBSD boot up, enable the SSH server, and position itself at the login. Otherwise, ping the server's IP until it starts responding. At that point, it's sufficient to connect via SSH as the root user.&lt;/p&gt;
&lt;p&gt;The root password is &lt;strong&gt;mfsroot&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;As the first thing, change the root password with the &lt;code&gt;passwd&lt;/code&gt; command&lt;/strong&gt;. This is to prevent someone from entering and compromising the machine during the installation time.&lt;/p&gt;
&lt;p&gt;Now launch the &lt;strong&gt;&lt;code&gt;bsdinstall&lt;/code&gt;&lt;/strong&gt; command and proceed with the normal FreeBSD installation (also setting up mirrors, RAID, etc., if desired), keeping in mind that mfsBSD is running in RAM, so you can overwrite the disk it was installed on without problems.&lt;/p&gt;
&lt;p&gt;After the reboot, you'll have your FreeBSD system installed and running.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Wed, 02 Jul 2025 09:30:00 +0200</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2025/07/02/install_freebsd_providers_mfsbsd/</guid><category>freebsd</category><category>hosting</category><category>server</category><category>vps</category><category>tutorial</category></item><item><title>Make Your Own Internet Presence with NetBSD and a 1 euro VPS – Part 1: Your Blog</title><link>https://it-notes.dragas.net/2025/04/22/make-your-own-internet-presence-with-netbsd-and-a-1-euro-vps-part-1-your-blog/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/terminal_htop.webp" alt="Photo: Terminal screen with htop"&gt;&lt;/p&gt;&lt;h2&gt;Why NetBSD?&lt;/h2&gt;
&lt;p&gt;For many years, I've been using (and appreciating) &lt;a href="https://www.netbsd.org/"&gt;NetBSD&lt;/a&gt; because it's stable, efficient, and reliable. The codebase has proven its reliability, &lt;a href="https://it-notes.dragas.net/2023/08/27/that-old-netbsd-server-running-since-2010/"&gt;running without reboots for years without issues&lt;/a&gt;. It supports ZFS (though differently than FreeBSD), LVM (useful for those accustomed to it on Linux), the ability to take filesystem snapshots (UFS2, making ZFS less crucial), and it's an &lt;a href="https://www.netbsd.org/docs/guide/en/chap-virt.html"&gt;excellent virtualization platform&lt;/a&gt;. Installation and updates are easy (including via &lt;a href="https://www.netbsd.org/docs/guide/en/chap-upgrading.html#using-sysupgrade"&gt;sysupgrade&lt;/a&gt; - which I'll cover in a future article). Since it focuses on portable and optimized code (running on ancient architectures requires cleanliness and correctness), it's particularly efficient on low-power devices, like embedded systems or cheap VMs. Therefore, it's one of the best solutions for a small personal setup that can still deliver excellent results and simple management.&lt;/p&gt;
&lt;p&gt;Indeed, the market offers very cheap VPS, often with just a single core and little RAM. But a modern single core packs power that a multi-core from just a few years ago could only dream of, and often, the I/O of these machines (a bottleneck for many services) is still decent. I personally use 1 euro per month VPS (VAT included - for those not subject to it, that's less than one euro per month!) with a public IPv4 address and (often) a /64 IPv6 block, ensuring full reachability across the entire network.
I'm not providing direct links as I have no affiliations, but netcup's "piko" VPS are among the types I use most often (&lt;a href="https://it-notes.dragas.net/2025/02/26/fedimeteo-how-a-tiny-freebsd-vps-became-a-global-weather-service-for-thousands/"&gt;a 4 euro/month netcup VM handles the entire FediMeteo project&lt;/a&gt;), and this type of VM is ideal for our purpose because some providers (like netcup) allow you to upload your own ISO and install your preferred operating system. On VPS like these, I've installed everything - including &lt;a href="https://omnios.org/"&gt;OmniOS&lt;/a&gt; and &lt;a href="https://www.tritondatacenter.com/smartos"&gt;SmartOS&lt;/a&gt; - without problems. And even such a small VPS, with an efficient operating system, can be extremely satisfying.&lt;/p&gt;
&lt;h2&gt;Why BSSG?&lt;/h2&gt;
&lt;p&gt;In this article, I'll describe how to create and publish a blog using &lt;a href="https://bssg.dragas.net"&gt;BSSG&lt;/a&gt; as it exemplifies my concept of portability and minimalism. BSSG on NetBSD currently doesn't leverage parallelism provided by tools like GNU Parallel, but for small to medium-sized blogs, this won't be an issue, especially considering these small VMs only have 1 core. Obviously, you can use any Static Site Generator (SSG) (like Hugo, Nikola, 11ty, Pelican, Zola, etc.) - the important thing is to have a static site served by a simple web server.&lt;/p&gt;
&lt;h2&gt;Let's Start with the Installation&lt;/h2&gt;
&lt;p&gt;Installing NetBSD is quite straightforward and is clearly covered, complete with explanatory screenshots, in the &lt;a href="https://www.netbsd.org/docs/guide/en/chap-exinst.html"&gt;excellent official NetBSD documentation&lt;/a&gt;, which I recommend using as a reference during the process, especially if it's your first time.&lt;/p&gt;
&lt;p&gt;In my case, I made sure to use the proposed disk geometry, use the standard automatic partitioning, but &lt;strong&gt;enable the "log" and "noatime" options for the filesystem&lt;/strong&gt;.
Both these options will provide a huge advantage in I/O operations, especially with BSSG, as the first enables journaling and the second prevents updating file metadata on every access. BSSG is more I/O bound than CPU bound, so any optimization is beneficial.&lt;/p&gt;
&lt;p&gt;Moving forward, I also recommend configuring the network (although installation can be done from packages on the installation ISO). For netcup, you can use DHCPv4 (even though it's a bit slow and sometimes seems to fail, the DHCP client will continue running in the background and eventually work).&lt;/p&gt;
&lt;p&gt;For IPv6, I usually configure it manually later, so I'll describe that further down.&lt;/p&gt;
&lt;p&gt;I also recommend enabling SSH, adding a regular user (and adding them to the &lt;code&gt;wheel&lt;/code&gt; group so they can gain root privileges) - in this case, I'll call the user &lt;em&gt;blog&lt;/em&gt;. Also, enable the installation of binary packages, as it will be convenient later to use &lt;code&gt;pkgin&lt;/code&gt; to install and update all necessary packages. All these steps are described clearly and in detail in the &lt;a href="https://www.netbsd.org/docs/guide/en/chap-exinst.html"&gt;guide&lt;/a&gt;, so I won't detail them here. But they are simple and logical, like all operations on BSD systems.&lt;/p&gt;
&lt;p&gt;After installation, reboot. If everything went correctly, you should be able to log in via console or SSH using the "blog" user (or whatever you named it).&lt;/p&gt;
&lt;p&gt;First, I suggest configuring the IPv6 address and installing the necessary packages.&lt;/p&gt;
&lt;p&gt;For IPv6, in the case of netcup, simply add one of the assigned addresses to the interface. In NetBSD, &lt;a href="https://www.netbsd.org/docs/guide/en/chap-net-practice.html"&gt;network interface configurations are stored (similar to OpenBSD) in specific files&lt;/a&gt;. For the first virtio interface, the file will be &lt;code&gt;/etc/ifconfig.vioif0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You need to elevate your privileges to root, open that file with your preferred editor, and add the configuration to the file itself:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ su -l
nb1euro# vi /etc/ifconfig.vioif0

inet6 your-ipv6-addr/64
up
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To test everything, perform a reboot and try pinging an IPv6 address (I often use &lt;code&gt;ping6 google.com&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;If all goes well, after a few seconds, you should see ping replies, confirming everything is configured correctly.&lt;/p&gt;
&lt;p&gt;Regarding packages, the only two strictly necessary ones are &lt;code&gt;bash&lt;/code&gt; and a markdown processor (by default, BSSG will use &lt;code&gt;commonmark&lt;/code&gt;; otherwise, it can be configured to use &lt;code&gt;pandoc&lt;/code&gt; or &lt;code&gt;Markdown.pl&lt;/code&gt;). &lt;code&gt;rsync&lt;/code&gt; can be useful for deployment. &lt;code&gt;sudo&lt;/code&gt; (or &lt;code&gt;doas&lt;/code&gt;) can be useful for elevating privileges for certain operations, at least at this stage.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ su -l
nb1euro# pkgin in bash cmark rsync sudo
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you're used to Linux, you can also install the "nano" editor:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro# pkgin in nano
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If &lt;code&gt;sudo&lt;/code&gt; was installed, it's now appropriate to grant users in the "wheel" group (like the regular user created during installation) the ability to elevate privileges. Edit the &lt;code&gt;sudoers&lt;/code&gt; file (I suggest using the &lt;code&gt;visudo&lt;/code&gt; command) and uncomment this line:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;## Uncomment to allow members of group wheel to execute any command
%wheel ALL=(ALL:ALL) ALL
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point, you can switch back to operating as the regular user, downloading and unpacking BSSG:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ ftp https://brew.bsd.cafe/stefano/BSSG/archive/0.15.1.tar.gz
nb1euro$ tar zxfv 0.15.1.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now that BSSG is ready, just initialize a directory with the structure for the new site:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ cd bssg
nb1euro$ ./bssg.sh init /home/blog/myblog
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Everything is set to start generating your blog. I recommend reading BSSG's &lt;code&gt;README.md&lt;/code&gt;. There are many options, themes, etc., but to get started, you just need to set the site's public URL. For example, if the site will be published as &lt;em&gt;myblog.example.com&lt;/em&gt; - just create a file at &lt;code&gt;/home/blog/myblog/config.sh.local&lt;/code&gt; (the path defined by the init command) and set the public URL:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;SITE_URL=&amp;quot;https://myblog.example.com&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This way, all URLs will be absolute URLs, which is necessary to ensure the correct functioning of RSS feeds, sitemaps, etc. This setting assumes HTTPS - if you just want to test the site over HTTP, simply use &lt;code&gt;http&lt;/code&gt; and then, optionally, change it to &lt;code&gt;https&lt;/code&gt; and regenerate the site later.&lt;/p&gt;
&lt;p&gt;You can already create your first test post, directly from the BSSG directory:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ ./bssg.sh post
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The system will use &lt;code&gt;nano&lt;/code&gt; if it's installed, otherwise it will use &lt;code&gt;vi&lt;/code&gt;. Don't worry, in the latter case, BSSG will write the procedure for exiting &lt;code&gt;vi&lt;/code&gt; as the post's text 🙂&lt;/p&gt;
&lt;p&gt;Once you save the post, BSSG will automatically generate the site. If everything went well, the &lt;code&gt;/home/blog/myblog/output&lt;/code&gt; directory will contain the final result. We are therefore ready for the first deployment, which can be done in many different ways. I will cover three:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Using &lt;a href="http://www.eterna23.net/bozohttpd/"&gt;bozohttpd&lt;/a&gt;, present &lt;a href="https://man.netbsd.org/httpd.8"&gt;by default in NetBSD's base system&lt;/a&gt;. It can be used via &lt;code&gt;inetd&lt;/code&gt; (launching an httpd process for each connection) or as a daemon. I'll describe the first option, showing in the final benchmarks how, even when used as a daemon, it remains a less performant solution.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Using nginx&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Using Caddy&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;First, it's advisable to obtain a certificate to configure and use HTTPS. If you only want to test using HTTP, this part can be safely bypassed. For solutions 1 and 2, I'll use &lt;code&gt;certbot&lt;/code&gt;, which is well-known to many users with Linux experience. Caddy, on the other hand, manages certificates automatically, so there's no need for other solutions and thus no need to install &lt;code&gt;certbot&lt;/code&gt;.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ sudo pkgin in py313-certbot
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To use &lt;code&gt;bozohttpd&lt;/code&gt;, no further installation is necessary. At this point, the options diverge.&lt;/p&gt;
&lt;h2&gt;Using NetBSD's Integrated httpd&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;bozohttpd&lt;/code&gt; is integrated into NetBSD and, by default, can be launched directly via &lt;code&gt;inetd&lt;/code&gt;. This solution, while not extremely efficient or scalable, is simple and requires few resources. It's fine if you expect only a few visits per day, but when used via &lt;code&gt;inetd&lt;/code&gt;, the initial latency for each connection is tangible. It can still be useful for some tests or small deployments.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;/etc/inetd.conf&lt;/code&gt; file already contains the options to handle this situation:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;#http           stream  tcp     nowait:600      _httpd  /usr/libexec/httpd      httpd /var/www
#http           stream  tcp6    nowait:600      _httpd  /usr/libexec/httpd      httpd /var/www
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;By uncommenting these two lines and restarting &lt;code&gt;inetd&lt;/code&gt; (&lt;code&gt;service inetd restart&lt;/code&gt;), the server will start responding to HTTP requests on both IPv4 and IPv6.&lt;/p&gt;
&lt;p&gt;If you want to add HTTPS support, no problem. Just request a certificate via &lt;code&gt;certbot&lt;/code&gt; and specify the webroot.&lt;/p&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ sudo certbot-3.13 certonly
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Choose option 2 - the one where you specify the webroot - enter the domain, and when prompted, provide &lt;code&gt;/var/www/&lt;/code&gt; as the webroot.&lt;/p&gt;
&lt;p&gt;The certificate will be created. Then, modify the &lt;code&gt;/etc/inetd.conf&lt;/code&gt; file to also include support for HTTPS, adding two lines similar to these (obviously, change the certificate paths):&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;https            stream  tcp     nowait:600      _httpd  /usr/libexec/httpd      httpd -Z /usr/pkg/etc/letsencrypt/live/myblog.example.com/fullchain.pem /usr/pkg/etc/letsencrypt/live/myblog.example.com/privkey.pem /var/www
https            stream  tcp6    nowait:600      _httpd  /usr/libexec/httpd      httpd -Z /usr/pkg/etc/letsencrypt/live/myblog.example.com/fullchain.pem /usr/pkg/etc/letsencrypt/live/myblog.example.com/privkey.pem /var/www
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: &lt;code&gt;httpd&lt;/code&gt; will run with the permissions of the &lt;code&gt;_httpd&lt;/code&gt; user, so make sure all certificates are readable by that user:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro# chown -R _httpd /usr/pkg/etc/letsencrypt/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Restart &lt;code&gt;inetd&lt;/code&gt;, and the server will also respond over HTTPS.&lt;/p&gt;
&lt;p&gt;To make your blog public, simply copy the files from the site's output directory to &lt;code&gt;/var/www/&lt;/code&gt; - this time using &lt;code&gt;sudo&lt;/code&gt; to bypass permission issues:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ sudo rsync -avhHPx /home/blog/myblog/output/ /var/www/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The site will be immediately visible.&lt;/p&gt;
&lt;h2&gt;Using nginx&lt;/h2&gt;
&lt;p&gt;Nginx is fast and efficient, and the performance difference is noticeable (some benchmarks follow below). For an efficient setup ready for a high number of visits, it's advisable to use a web server suited for the purpose, just like nginx.&lt;/p&gt;
&lt;p&gt;First, install nginx and the certbot plugin for nginx. This will simplify the installation and renewal of certificates:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ sudo pkgin in py313-certbot-nginx nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Copy the startup script to &lt;code&gt;/etc/rc.d&lt;/code&gt; - as indicated by the post-installation message. In NetBSD, this operation must be done manually, but it's always pointed out:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ sudo cp /usr/pkg/share/examples/rc.d/nginx /etc/rc.d
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: If you previously used &lt;code&gt;httpd&lt;/code&gt; from &lt;code&gt;inetd&lt;/code&gt; following the previous solution, you must disable it in &lt;code&gt;inetd.conf&lt;/code&gt; and restart &lt;code&gt;inetd&lt;/code&gt; to free up ports 80 and 443.&lt;/p&gt;
&lt;p&gt;Now you can create a virtual host for our new site.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ sudo vi /usr/pkg/etc/nginx/nginx.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and add, at the end of the file and before the final closing curly brace:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;server {
        listen 80;
        # If you also have configured IPv6 support
        listen [::]:80;

        root /var/www;
        index index.html index.htm;

        server_name myblog.example.com;

        # If you want a long cache for media and css - be careful, this means that if you change to a new theme, it might not be visible immediately as the browser might still use the old cached one
        location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
            expires 30d;
            add_header Cache-Control &amp;quot;public, no-transform&amp;quot;;
        }

        location / {
                try_files $uri $uri/ =404;
        }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now, it's time to configure the system to enable nginx. Just edit &lt;code&gt;/etc/rc.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ sudo vi /etc/rc.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and add:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nginx=YES
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now, you can start nginx:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ sudo service nginx start
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Nginx will start listening on port 80. Generating and installing the certificate is very simple:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ sudo certbot-3.13 --nginx -d myblog.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This command will request the certificate and install it, so nginx will already be configured to use it.&lt;/p&gt;
&lt;p&gt;As with the previous method, to make your blog public, simply copy the files from the site's output directory to &lt;code&gt;/var/www/&lt;/code&gt; - using &lt;code&gt;sudo&lt;/code&gt; to bypass permission issues:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ sudo rsync -avhHPx /home/blog/myblog/output/ /var/www/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The site will be immediately visible.&lt;/p&gt;
&lt;h2&gt;Using Caddy&lt;/h2&gt;
&lt;p&gt;Caddy is a convenient and all-in-one solution, efficient and fast. It's packaged for NetBSD and allows you to go online in a flash. I won't delve into the configuration because there are many tutorials (&lt;a href="https://caddyserver.com/docs/getting-started"&gt;including the official ones&lt;/a&gt;), but you just need to install it and run it:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ sudo pkgin in caddy
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once installed, go to the directory you want to serve (e.g., &lt;code&gt;/var/www&lt;/code&gt; or directly &lt;code&gt;/home/blog/myblog/output&lt;/code&gt;) and run:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;nb1euro$ sudo caddy file-server --domain myblog.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Caddy will start, request the certificate, and begin serving your blog over HTTPS as well. To install Caddy as a service (i.e., with a configuration file, etc.), you can proceed similarly to how it's done on Linux. The NetBSD Caddy package doesn't include the &lt;code&gt;rc.d&lt;/code&gt; script, but you can copy and paste one (into &lt;code&gt;/etc/rc.d/caddy&lt;/code&gt;) from &lt;a href="https://www.unitedbsd.com/d/1406-caddy-service/4"&gt;a thread posted on UnitedBSD&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Performance Comparison&lt;/h2&gt;
&lt;p&gt;I performed some performance tests on these solutions. Here are the results, on a single-core 1 euro/month VPS, from my home connection (which also has its own limitations):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NetBSD httpd via inetd:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;Running 10s test @ https://myblog.example.com/
  4 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   213.52ms  173.10ms   1.11s    76.01%
    Req/Sec    12.92      9.19    50.00     75.91%
  371 requests in 10.10s, 1.39MB read
Requests/sec:     36.72
Transfer/sec:    140.65KB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;These numbers are quite poor, linked to high latency caused by having to launch &lt;code&gt;bozohttpd&lt;/code&gt; for each incoming connection.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NetBSD httpd as a daemon:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;Running 10s test @ https://myblog.example.com/
  4 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    35.74ms    6.96ms 108.80ms   81.36%
    Req/Sec    18.29      9.45    50.00     70.88%
  676 requests in 10.10s, 2.53MB read
Requests/sec:     66.92
Transfer/sec:    256.32KB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here the situation is decidedly better, but not exceptional. &lt;code&gt;httpd&lt;/code&gt; isn't designed for high loads or performance.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nginx as a daemon, 1 worker:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;Running 10s test @ https://myblog.example.com/
  4 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    30.69ms    4.87ms  64.14ms   66.01%
    Req/Sec   379.39     65.94   464.00     90.91%
  15026 requests in 10.04s, 56.50MB read
Requests/sec:   1496.65
Transfer/sec:      5.63MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here we are on another level, showing truly solid performance. This type of result can handle significantly high loads without particular difficulty. The efficiency of both NetBSD and nginx pays off.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Caddy:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-plaintext"&gt;Running 10s test @ https://myblog.example.net/
  4 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    32.10ms    5.75ms  95.04ms   87.44%
    Req/Sec   362.74     64.29   434.00     91.67%
  14374 requests in 10.05s, 54.63MB read
Requests/sec:   1430.82
Transfer/sec:      5.44MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Caddy shows results comparable to nginx, so the choice between them depends solely on the type of configuration you want to achieve and the experience each person has with the specific platforms.&lt;/p&gt;
&lt;h2&gt;Conclusion: Efficient Minimalism&lt;/h2&gt;
&lt;p&gt;We've seen how it's possible to create a personal, professional, and performant online presence with minimal investment. This solution, based on NetBSD and a 1€/month VPS, offers several advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Negligible Cost&lt;/strong&gt;: For 12€ per year, you can have a website (and more!) completely under your control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Surprising Performance&lt;/strong&gt;: As demonstrated by the benchmarks, excellent performance can be achieved even with limited resources (up to 1400-1500 requests/second with nginx or Caddy).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security and Stability&lt;/strong&gt;: NetBSD is renowned for its reliability and security, fundamental characteristics for any online service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total Control&lt;/strong&gt;: Unlike free blogging platforms, you have full control over every aspect of your site.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learning Experience&lt;/strong&gt;: Managing a BSD system allows you to acquire valuable system administration skills.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This minimalist configuration demonstrates that you don't need to invest in expensive cloud solutions or oversized VPS to have a quality online presence. In an era where the tendency is to think "moooar powaaaar = better results", NetBSD reminds us that efficiency and good design can yield excellent results even with limited resources.&lt;/p&gt;
&lt;p&gt;After all, you don't need a thousand-node cloud to write something worth reading.&lt;/p&gt;
&lt;p&gt;In the upcoming articles in this series, we will explore how to expand this basic installation with other useful services and how to keep the system updated and secure over time.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Tue, 22 Apr 2025 07:30:36 +0200</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2025/04/22/make-your-own-internet-presence-with-netbsd-and-a-1-euro-vps-part-1-your-blog/</guid><category>netbsd</category><category>bssg</category><category>ssg</category><category>ownyourdata</category><category>server</category><category>web</category><category>blogging</category><category>tutorial</category><category>series</category></item><item><title>Caching snac Proxied Media with Nginx</title><link>https://it-notes.dragas.net/2025/02/08/caching-snac-proxied-media-with-nginx/</link><description>&lt;p&gt;&lt;img src="https://unsplash.com/photos/9Xf-jxvfpW8/download?ixid=M3wxMjA3fDB8MXxhbGx8MXx8fHx8fHx8MTc0MTU5MTUzNXw&amp;force=true&amp;w=1920" alt="Photo by &amp;lt;a href=&amp;quot;https://unsplash.com/it/@elenarossini&amp;quot;&amp;gt;Elena Rossini&amp;lt;/a&amp;gt; on Unsplash"&gt;&lt;/p&gt;&lt;p&gt;One of the useful yet resource-intensive features of platforms like Mastodon is that they reprocess (and store locally) all multimedia files from other instances.&lt;/p&gt;
&lt;p&gt;This behavior is implemented for three valid reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;To ensure files do not contain malicious code by reprocessing them locally.&lt;/li&gt;
&lt;li&gt;To prevent all users from all instances from overloading the original instance hosting the media by requesting it repeatedly.&lt;/li&gt;
&lt;li&gt;To hide the individual IP addresses of users from the original instance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While this approach has benefits, it also requires significant disk space - often many gigabytes per day.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/grunfink/snac2"&gt;snac&lt;/a&gt;, on the other hand, does not locally process or cache media by default. Instead, media URLs remain unchanged, meaning users will fetch content directly from its original source. This behavior is perfectly fine for many setups (such as mobile devices), but it may not be suitable for everyone.&lt;/p&gt;
&lt;p&gt;I suggested a small improvement to snac’s developer, who immediately recognized the benefit for users and implemented an interesting new feature: media proxying via the instance itself.&lt;/p&gt;
&lt;h3&gt;Enabling Media Proxying in snac&lt;/h3&gt;
&lt;p&gt;To enable this feature, simply add the following line to your &lt;code&gt;server.json&lt;/code&gt; configuration file:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-json"&gt;&amp;quot;proxy_media&amp;quot;: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once enabled, snac will rewrite all media URLs to pass through its own instance. This ensures that original instances will no longer see the IP addresses of individual users, as they will only see the IP of the snac instance itself.&lt;/p&gt;
&lt;p&gt;However, this also increases the load and bandwidth consumption of the instance since it must download and forward media every time a user accesses it.&lt;/p&gt;
&lt;h3&gt;Caching Proxied Media with nginx&lt;/h3&gt;
&lt;p&gt;To optimize performance, we can configure nginx to cache these proxied files transparently. This way, if multiple users from the snac instance (or the same user at different times) request the same media file, nginx will serve it from the local cache instead of fetching it again. This setup has two key advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reduces traffic&lt;/strong&gt; to the original instances.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lowers the load&lt;/strong&gt; on the snac instance, as it won’t need to download the same file repeatedly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To achieve this, create a cache storage area in nginx by adding the following to the &lt;code&gt;http&lt;/code&gt; section:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-conf"&gt;proxy_cache_path /var/cache/nginx/snac_media levels=1:2 keys_zone=snac_media:10m max_size=1g 
                 inactive=1d use_temp_path=off;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This defines a cache directory at &lt;code&gt;/var/cache/nginx/snac_media&lt;/code&gt;, with 10 MB allocated for metadata and a maximum cache size of 1 GB. Cached content will be invalidated and removed after one day, ensuring frequently accessed content (like profile avatars) remains cached while rarely used files get replaced over time.&lt;/p&gt;
&lt;p&gt;Next, add the following rule to your snac instance's virtual host configuration:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-conf"&gt;location ~ ^/.+/(x|y)/ {
    proxy_cache snac_media;
    proxy_pass http://snac-ip:8001;
    proxy_set_header Host $host;
    proxy_cache_valid 200 1d;
    proxy_cache_valid 404 1h;
    proxy_ignore_headers &amp;quot;Cache-Control&amp;quot; &amp;quot;Expires&amp;quot;;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    proxy_cache_lock on;
    add_header X-Proxy-Cache $upstream_cache_status;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After reloading nginx, the cache will start populating as users request media files. Unrequested content will never enter the cache, ensuring efficient storage usage without unnecessary clutter.&lt;/p&gt;
&lt;p&gt;By combining snac’s new media proxying feature with nginx caching, we can achieve a more balanced setup—reducing load on both our instance and external ones, improving privacy, and enhancing performance.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Sat, 08 Feb 2025 16:00:00 +0100</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2025/02/08/caching-snac-proxied-media-with-nginx/</guid><category>snac</category><category>snac2</category><category>fediverse</category><category>nginx</category><category>data</category><category>hosting</category><category>server</category><category>tutorial</category><category>ownyourdata</category><category>networking</category><category>web</category><category>tipsandtricks</category><category>social</category></item><item><title>Improving snac Performance with Nginx Proxy Cache</title><link>https://it-notes.dragas.net/2025/01/29/improving-snac-performance-with-nginx-proxy-cache/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/web_text.webp" alt="Improving snac Performance with Nginx Proxy Cache"&gt;&lt;/p&gt;&lt;p&gt;Some days ago, I migrated my personal Fediverse instance from &lt;a href="https://akkoma.social/"&gt;Akkoma&lt;/a&gt; to &lt;a href="https://codeberg.org/grunfink/snac2"&gt;snac&lt;/a&gt;. I appreciate snac a lot and believe it is the best solution available for many use cases.&lt;/p&gt;
&lt;p&gt;Akkoma is an excellent tool, but I noticed that even for a small instance like mine, the database grows exponentially, and the database activity remains constant. Despite low load, my disks are continuously "flashing" - which isn't a problem in itself but clearly indicates ongoing activity. In my case, this activity seems unnecessary since it's just a single-user instance.&lt;/p&gt;
&lt;p&gt;snac has shown excellent capabilities for managing the &lt;a href="https://fedimeteo.com"&gt;FediMeteo&lt;/a&gt; project (which I'll write about in detail soon), is lightweight, and has very few dependencies. Moreover, a dedicated snac instance handles sending updates from this blog to the Fediverse.&lt;/p&gt;
&lt;p&gt;After successfully transferring my followers from Akkoma to snac without major issues, I started using the new instance. However, as soon as I posted a photo (approximately 4MB), something happened that I somewhat expected but in a different form. My home internet connection (upload speed: 20 Mbit/sec) became saturated, but I also noticed that new connections and smaller entities were resulting in 499 errors - meaning nginx couldn't open new connections to snac. After some investigation, I realized the reason: for every remote instance, Nginx was requesting the multimedia file from snac. Due to saturated connections (snac allows setting the maximum number of active threads), it took several seconds, leading to thread exhaustion in snac. Consequently, subsequent nginx requests resulted in 499 errors as snac could no longer allocate a thread.&lt;/p&gt;
&lt;p&gt;To resolve this, I decided to implement direct caching using nginx. My reverse proxy (running in a different FreeBSD jail but this doesn't change the outcome) can cache multimedia files - storing them on first request and serving them directly to everyone who requests them without needing to ask snac every time. This approach is similar to &lt;a href="https://it-notes.dragas.net/2024/08/26/building-a-self-hosted-cdn-for-bsd-cafe-media/"&gt;what I use for the media in the BSD Cafe's Mastodon instance&lt;/a&gt;, where I employ &lt;a href="https://varnish-cache.org/"&gt;Varnish&lt;/a&gt; to keep everything in RAM.&lt;/p&gt;
&lt;p&gt;In snac, images and multimedia files are served from a specific path, such as: https://example.com/user/s/filename.png&lt;/p&gt;
&lt;p&gt;The key here is the &lt;em&gt;/s/&lt;/em&gt; segment. By instructing nginx to cache all files containing /s/ in their URL, I can offload some of the work from snac.&lt;/p&gt;
&lt;p&gt;I modified my &lt;code&gt;nginx.conf&lt;/code&gt; file to include a caching setup:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-conf"&gt;# Caching configuration for snac
proxy_cache_path /var/cache/nginx/snac_cache levels=1:2 keys_zone=snac:10m max_size=1g inactive=1440m use_temp_path=off;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This creates a section for &lt;code&gt;/var/cache/nginx&lt;/code&gt; where I define the caching parameters. It will allocate 10 MB of RAM for metadata, with a maximum cache size of 1 GB (useful if you decide to post some videos). Cached content will be considered invalid and removed after 1440 minutes (24 hours). This ensures that frequently accessed content (like profile avatars and banners) remains in the cache while less frequently accessed content can be replaced with newer content. The goal isn't to have all content cached but rather for nginx to serve files independently during peak times, preventing multiple remote instances from overwhelming snac simultaneously.&lt;/p&gt;
&lt;p&gt;In the virtual host configuration for snac, I added this specific override for multimedia content:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-conf"&gt;# Caching rules for /s/ path
location ~ ^/.+/s/ {
    proxy_cache snac;
    proxy_pass http://snac-jail-ip:8001;
    proxy_set_header Host $host;
    proxy_cache_valid 200 1d;
    proxy_cache_valid 404 1h;
    proxy_ignore_headers &amp;quot;Cache-Control&amp;quot; &amp;quot;Expires&amp;quot;;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    proxy_cache_lock on;
    add_header X-Proxy-Cache $upstream_cache_status;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After restarting nginx, the multimedia files will be cached on their first access and served by nginx, leaving snac's threads free to handle everything else. This setup ensures smoother performance and prevents resource exhaustion during periods of high activity or when new content is shared.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Wed, 29 Jan 2025 09:00:00 +0100</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2025/01/29/improving-snac-performance-with-nginx-proxy-cache/</guid><category>snac</category><category>snac2</category><category>fediverse</category><category>nginx</category><category>data</category><category>hosting</category><category>server</category><category>freebsd</category><category>tutorial</category><category>ownyourdata</category><category>networking</category><category>web</category><category>tipsandtricks</category><category>social</category></item><item><title>Managing ZFS Full Pool Issues with Reserved Space</title><link>https://it-notes.dragas.net/2024/11/28/managing-zfs-full-pool-issues-with-reserved-space/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/hard_disk.webp" alt="Managing ZFS Full Pool Issues with Reserved Space"&gt;&lt;/p&gt;&lt;p&gt;Yesterday morning, I received a panicked call from a developer:&lt;br /&gt;
"I accidentally filled up the storage, and now I can't perform any operations! My ZFS pool is full!"&lt;/p&gt;
&lt;p&gt;I immediately reassured them because I had anticipated this kind of issue. One of the things I almost always do when managing ZFS file systems is to reserve space in a specially created dataset.&lt;/p&gt;
&lt;p&gt;This is because ZFS, like all CoW (Copy-on-Write) file systems, can find itself unable to free up space when completely full. By using reserved space, I can always free it up and delete other data, restoring the system to normal operations.&lt;/p&gt;
&lt;p&gt;To reserve space, simply create a dataset and assign it a reserved size. Of course, this dataset should not be used for anything else; otherwise, the entire purpose would be defeated.&lt;/p&gt;
&lt;p&gt;To create it and reserve space, you only need two simple commands. For example:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zfs create zroot/reserved
zfs set reservation=5G zroot/reserved
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This creates the dataset and assigns it 5 GB of reserved space.&lt;/p&gt;
&lt;p&gt;Here’s the situation before the operation:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zfs list zroot
NAME    USED  AVAIL  REFER  MOUNTPOINT
zroot  3.02G   109G    96K  /zroot
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And here’s the situation after:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zfs list zroot
NAME    USED  AVAIL  REFER  MOUNTPOINT
zroot  8.02G   104G    96K  /zroot
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;As you can see, the 5 GB are removed from the available space and marked as used, but they are actually empty.&lt;/p&gt;
&lt;p&gt;In case of a full file system, you can delete this dataset (or reduce its size) to return to normal file system operation.&lt;/p&gt;
&lt;p&gt;Even with this technique, I still recommend not filling ZFS pools beyond 80% of their capacity, as performance degrades significantly past that point.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Thu, 28 Nov 2024 20:25:00 +0100</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2024/11/28/managing-zfs-full-pool-issues-with-reserved-space/</guid><category>zfs</category><category>freebsd</category><category>linux</category><category>data</category><category>filesystems</category><category>recovery</category><category>tipsandtricks</category><category>tutorial</category><category>series</category></item><item><title>Moving an entire FreeBSD installation to a new host or VM in a few easy steps</title><link>https://it-notes.dragas.net/2024/09/16/moving-freebsd-installation-new-host-vm/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/hard_disk.webp" alt="Moving an entire FreeBSD installation to a new host or VM in a few easy steps"&gt;&lt;/p&gt;&lt;p&gt;FreeBSD, especially when installed on ZFS, is incredibly simple to manage, &lt;a href="https://it-notes.dragas.net/2022/05/30/how-we-are-migrating-many-of-our-servers-from-linux-to-freebsd-part-2/"&gt;back up&lt;/a&gt;, and move to a different system.&lt;/p&gt;
&lt;p&gt;I often find myself having to move an entire system from one host to another, from a physical host to a VM, or vice versa, from a VM to a physical host.&lt;/p&gt;
&lt;p&gt;By following the approach of keeping the operating system clean and running all services within jails, this operation is usually quite simple: install the operating system on the new host, perform the few necessary configurations (like pf and network interfaces), transfer the jail datasets, restart the jails on the new host, and update the DNS.&lt;/p&gt;
&lt;p&gt;However, sometimes I need to move the entire host, including the original operating system. This article will describe this operation when the FreeBSD system uses ZFS as its root file system. This process isn't much more difficult, but there are a few details to keep in mind. Below is a breakdown of the two bootloader scenarios:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UEFI Bootloader&lt;/li&gt;
&lt;li&gt;BIOS Bootloader (non-UEFI)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;UEFI Bootloader&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Start from an ISO or image of &lt;a href="https://mfsbsd.vx.sk/"&gt;mfsbsd&lt;/a&gt;. Partition the disk according to the layout of the source system. If the source system is a standard FreeBSD (ZFS) installation, partition the destination disk (assumed to be da0) as follows. &lt;strong&gt;WARNING&lt;/strong&gt;: The first command will destroy any existing partition table on the disk.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;gpart destroy -F /dev/da0
gpart create -s gpt da0
gpart add -t efi -s 200M -l efiboot0 da0
newfs_msdos -F 32 -c 1 /dev/da0p1
mount -t msdosfs /dev/da0p1 /mnt
mkdir -p /mnt/EFI/BOOT
cp /boot/loader.efi /mnt/EFI/BOOT/BOOTX64.efi
umount /mnt
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;Create additional partitions for swap and ZFS:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;gpart add -a 1m -t freebsd-swap -s 2g -l swap0 da0
gpart add -a 1m -t freebsd-zfs -l zfs0 da0
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;Create the ZFS pool:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zpool create -O compression=zstd -O atime=off zroot /dev/gpt/zfs0
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;On the source system, create a snapshot of all datasets:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zfs snapshot -r zroot@snap01
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; You will need to connect to the source system using a user with privileges over the entire filesystem. For simplicity, I'll use &lt;code&gt;root&lt;/code&gt;, but it's not recommended to leave &lt;code&gt;root&lt;/code&gt; accessible via SSH. Limit access to private keys only. To allow root login over SSH, edit &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; and set &lt;code&gt;PermitRootLogin yes&lt;/code&gt;. After moving the server, &lt;strong&gt;remember to disable root access&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If &lt;a href="https://www.maier-komor.de/mbuffer.html"&gt;mbuffer&lt;/a&gt; is installed on the source system, it can help improve the performance of the transfer. If not, remove it from the next pipe command, which should be run from the destination system. The &lt;em&gt;-s&lt;/em&gt; will set the block-size, the &lt;em&gt;-m&lt;/em&gt; will set the buffer size (128MB, here):&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;ssh root@sourceip &amp;quot;zfs send -RLv zroot@snap01 | mbuffer -s 128k -m 128M&amp;quot; | zfs receive -F zroot
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;You have two options now:&lt;/li&gt;
&lt;li&gt;If the source server was idle and no further synchronization is required, proceed to the next step.&lt;/li&gt;
&lt;li&gt;If the transfer took a long time and services on the source server need to be stopped (e.g., databases), stop what you can, take another snapshot (e.g., &lt;code&gt;zfs snapshot -r zroot@snap02&lt;/code&gt;), and synchronize the differences by running another incremental send-receive from the destination server:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;ssh root@sourceip &amp;quot;zfs send -RLv -i zroot@snap01 zroot@snap02 | mbuffer -s 128k -m 128M&amp;quot; | zfs receive -F zroot
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;Set the boot filesystem:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zpool set bootfs=zroot/ROOT/default zroot
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;BIOS Bootloader&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Start from an ISO or image of &lt;a href="https://mfsbsd.vx.sk/"&gt;mfsbsd&lt;/a&gt;. Partition the disk according to the layout of the source system. If the source system is a standard FreeBSD (ZFS) installation, partition the destination disk (assumed to be &lt;code&gt;da0&lt;/code&gt;) as follows. &lt;strong&gt;WARNING:&lt;/strong&gt; The first command will destroy any existing partition table on the disk.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;gpart destroy -F /dev/da0
gpart create -s gpt da0
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;Create the necessary partitions:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;gpart add -a 1m -t freebsd-boot -s 512k -l boot da0
gpart add -a 1m -t freebsd-swap -s 2g -l swap0 da0
gpart add -a 1m -t freebsd-zfs -l zfs0 da0
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;Create the ZFS pool:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zpool create -O compression=zstd -O atime=off zroot /dev/gpt/zfs0
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;On the source system, create a snapshot of all datasets:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zfs snapshot -r zroot@snap01
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; You will need to connect to the source system using a user with privileges over the entire filesystem. For simplicity, I'll use &lt;code&gt;root&lt;/code&gt;, but it's not recommended to leave &lt;code&gt;root&lt;/code&gt; accessible via SSH. Limit access to private keys only. To allow root login over SSH, edit &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; and set &lt;code&gt;PermitRootLogin yes&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If &lt;code&gt;mbuffer&lt;/code&gt; is installed on the source system, it can help improve the performance of the transfer. If not, remove it from the next pipe command, which should be run from the destination system:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;ssh root@sourceip &amp;quot;zfs send -RLv zroot@snap01 | mbuffer -s 128k -m 128M&amp;quot; | zfs receive -F zroot
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;You have two options now:&lt;/li&gt;
&lt;li&gt;If the source server was idle and no further synchronization is required, proceed to the next step.&lt;/li&gt;
&lt;li&gt;If the transfer took a long time and services on the source server need to be stopped (e.g., databases), stop what you can, take another snapshot (e.g., &lt;code&gt;zfs snapshot -r zroot@snap02&lt;/code&gt;), and synchronize the differences by running another incremental send-receive from the destination server:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;ssh root@sourceip &amp;quot;zfs send -RLv -i zroot@snap01 zroot@snap02 | mbuffer -s 128k -m 128M&amp;quot; | zfs receive -F zroot
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;Install the bootloader on the destination server’s disk:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;gpart bootcode -b /boot/pmbr -p /boot/gptzfsboot -i 1 da0
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;Set the boot filesystem:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;zpool set bootfs=zroot/ROOT/default zroot
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point, everything should be ready for boot. Reboot and verify that the various mountpoints (e.g., swap, etc.) and network interface settings are correct. If everything works fine, the server will be an exact copy of the original.&lt;/p&gt;
&lt;p&gt;Moving or duplicating an entire FreeBSD system is simple, fast, and reliable. The ability to use ZFS incremental replication is an excellent method for minimizing downtime, especially for large systems. The initial copy will happen "live" while the source system continues to run without issues. Subsequent incrementals will be much faster, making the switch-over process seamless and efficient.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Mon, 16 Sep 2024 09:41:00 +0200</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2024/09/16/moving-freebsd-installation-new-host-vm/</guid><category>freebsd</category><category>server</category><category>zfs</category><category>tutorial</category><category>hosting</category><category>ownyourdata</category><category>series</category><category>tipsandtricks</category></item><item><title>Make your own Read-Only Device with NetBSD</title><link>https://it-notes.dragas.net/2024/09/10/make-your-own-readonly-device-with-netbsd/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/embedded2.webp" alt="Make your own Read-Only Device with NetBSD"&gt;&lt;/p&gt;&lt;p&gt;One detail that is often overlooked when dealing with embedded (or remote) devices is a key point of vulnerability: the file system.&lt;/p&gt;
&lt;p&gt;For non-COW file systems (like ext4 on Linux, FFS, etc.), there are situations where a crash or a power outage could cause corruption, requiring manual intervention. This risk is especially present when using root on SD cards or similar storage media in embedded systems. Over time, these media are destined to fail due to numerous writes.&lt;/p&gt;
&lt;p&gt;For certain use cases, it's advisable to set up a read-only root file system, which ensures better reliability in case of system issues. Think of scenarios like a router (critical for network access) or a caching reverse-proxy, such as the one described in my series &lt;a href="https://it-notes.dragas.net/2024/09/03/make-your-own-cdn-netbsd/"&gt;"Make your own CDN"&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;While &lt;a href="https://it-notes.dragas.net/2024/05/31/freebsd-tips-and-tricks-native-ro-rootfs/"&gt;FreeBSD natively supports this configuration&lt;/a&gt; and some Linux distributions offer custom solutions (e.g., &lt;a href="https://wiki.alpinelinux.org/wiki/Alpine_local_backup"&gt;Alpine Linux&lt;/a&gt;), &lt;strong&gt;NetBSD&lt;/strong&gt; stands out as an excellent choice for such devices. It supports nearly all embedded devices, is lightweight, and its stability minimizes the need for frequent updates.&lt;/p&gt;
&lt;h3&gt;The Key Idea&lt;/h3&gt;
&lt;p&gt;Although NetBSD doesn't provide native read-only support, it's flexible enough to allow for this configuration. Many years ago, I followed &lt;a href="https://www.netbsd.org/~jschauma/netbsd-solidstate.html"&gt;a howto&lt;/a&gt; to set up a read-only system, and the idea remains simple and effective: &lt;strong&gt;NetBSD writes primarily to specific locations&lt;/strong&gt;, unlike some Linux distributions that attempt to write to various parts of the file system. On NetBSD, the main write targets are the &lt;code&gt;/tmp&lt;/code&gt; directory and &lt;code&gt;/var&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;With this in mind, we can configure these directories to reside in memory file systems (mfs) and ensure that &lt;code&gt;/var&lt;/code&gt; contains everything necessary for the system to function correctly.&lt;/p&gt;
&lt;h3&gt;1. Prepare the Environment&lt;/h3&gt;
&lt;p&gt;Start with a basic NetBSD installation (I'll skip the installation steps as there are many tutorials available, and it's straightforward).&lt;/p&gt;
&lt;p&gt;As a first step, clean up the &lt;code&gt;/var&lt;/code&gt; directory. After each reboot, its contents will be extracted into the mfs and occupy RAM.&lt;/p&gt;
&lt;h4&gt;Disable &lt;code&gt;man.db&lt;/code&gt; generation and swap usage:&lt;/h4&gt;
&lt;p&gt;Edit the following configuration files:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;# /etc/rc.conf
makemandb=NO
no_swap=YES
&lt;/code&gt;&lt;/pre&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;# /etc/daily.conf
run_makemandb=NO
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then, remove the current &lt;code&gt;man.db&lt;/code&gt; file:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;rm /var/db/man.db
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once this is done, create a compressed archive of the current &lt;code&gt;/var&lt;/code&gt; contents to be replicated at every boot:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;cd /
tar -cvzf var-image.tar.gz var
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;2. Create a Custom Startup Script&lt;/h3&gt;
&lt;p&gt;Next, create a file called &lt;code&gt;/etc/rc.d/mount_mfs_fs&lt;/code&gt; with the following content:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;#!/bin/sh
#
# mount_mfs_fs: mount memory file system for /var
# by roby, 23 jun 2003 - adapted by Stefano - 01 Sep 2024

# PROVIDE: mount_mfs_fs
# REQUIRE: root

. /etc/rc.subr

name=&amp;quot;mount_mfs_fs&amp;quot;
start_cmd=&amp;quot;mount_mfs_fs_start&amp;quot;
stop_cmd=&amp;quot;:&amp;quot;

mount_mfs_fs_start()
{
    # Check if the /var entry is present and uncommented in /etc/fstab
    if grep -q '^tmpfs[[:space:]]\+/var[[:space:]]\+tmpfs[[:space:]]\+rw,-m1777,-sram%25' /etc/fstab; then
        echo &amp;quot;Mounting memory file system: /var&amp;quot;

        # Mount the file system for /var
        mount /var

        # Extract the contents of the tar file into /var
        tar -xvzpf /var-image.tar.gz -C /

        echo &amp;quot;Mounting memory file systems: Done.&amp;quot;
    else
        echo &amp;quot;The tmpfs entry for /var is not present or is commented out in /etc/fstab. Skipping mount and extraction.&amp;quot;
    fi
    sleep 5
}

load_rc_config $name
run_rc_command &amp;quot;$1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Make the script executable:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;chmod a+rx /etc/rc.d/mount_mfs_fs
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;3. Modify Boot Process&lt;/h3&gt;
&lt;p&gt;Now, modify the &lt;code&gt;/etc/rc.d/mountcritlocal&lt;/code&gt; file to require this script to run before its execution:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;#!/bin/sh
#
# $NetBSD: mountcritlocal,v 1.17 2022/02/20 14:42:07 alnsn Exp $
#

# PROVIDE: mountcritlocal
# REQUIRE: mount_mfs_fs

$_rc_subr_loaded . /etc/rc.subr

name=&amp;quot;mountcritlocal&amp;quot;
start_cmd=&amp;quot;mountcritlocal_start&amp;quot;
stop_cmd=&amp;quot;:&amp;quot;

mountcritlocal_start()
{
    #       Mount critical file systems that are `local'
    #       (as specified in $critical_filesystems_local)
    #       This usually includes /var.
    #
    mount_critical_filesystems local || return $?
    if checkyesno zfs; then
        mount_critical_filesystems_zfs || return $?
    fi
    return 0
}

load_rc_config $name
load_rc_config_var zfs zfs
run_rc_command &amp;quot;$1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;4. Configure &lt;code&gt;/etc/fstab&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Edit the &lt;code&gt;/etc/fstab&lt;/code&gt; file to mount &lt;code&gt;/tmp&lt;/code&gt; and &lt;code&gt;/var&lt;/code&gt; in memory:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;tmpfs           /tmp        tmpfs   rw,-m1777,-sram%25
tmpfs           /var        tmpfs   rw,-m1777,-sram%25
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;5. Test the Configuration&lt;/h3&gt;
&lt;p&gt;You can now reboot the system and check if everything works correctly. Once logged in, the &lt;code&gt;df&lt;/code&gt; command should show something like this:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;Filesystem      1K-blocks         Used        Avail %Cap Mounted on
/dev/ld0a        18298254       393938     16989404   2% /
tmpfs              524192         4224       519968   0% /var
kernfs                  1            1            0 100% /kern
ptyfs                   1            1            0 100% /dev/pts
procfs                  4            4            0 100% /proc
tmpfs              524192            4       524188   0% /var/shm
tmpfs              524192            4       524188   0% /tmp
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;6. Set Root to Read-Only&lt;/h3&gt;
&lt;p&gt;Currently, the system is still running in read-write mode, but &lt;code&gt;/var&lt;/code&gt; and &lt;code&gt;/tmp&lt;/code&gt; are in memory disks. To switch the root file system to read-only, edit the &lt;code&gt;/etc/fstab&lt;/code&gt; file like this:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;/dev/ld0a               /       ffs     ro               1 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;On the next reboot, the system will be in pure read-only mode.&lt;/p&gt;
&lt;h3&gt;7. Perform Updates&lt;/h3&gt;
&lt;p&gt;To install packages, update the system, or make any changes, you'll need to temporarily switch back to read-write mode. Comment out &lt;code&gt;/var&lt;/code&gt; in mfs, change the root file system back to read-write, and reboot:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;mount -uw /
vi /etc/fstab
/dev/ld0a               /       ffs     rw               1 1
[...]
#tmpfs           /var    tmpfs   rw,-m1777,-sram%25
[...]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After making the necessary changes, regenerate the &lt;code&gt;/var&lt;/code&gt; tarball as done in step 1.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Tue, 10 Sep 2024 01:41:00 +0200</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2024/09/10/make-your-own-readonly-device-with-netbsd/</guid><category>netbsd</category><category>server</category><category>hosting</category><category>tutorial</category><category>ownyourdata</category></item><item><title>Make Your Own CDN with NetBSD</title><link>https://it-notes.dragas.net/2024/09/03/make-your-own-cdn-netbsd/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/embedded.jpg" alt="Make Your Own CDN with NetBSD"&gt;&lt;/p&gt;&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;This article is a spin-off from &lt;a href="https://it-notes.dragas.net/2024/08/29/make-your-own-cdn-openbsd/"&gt;a previous post on how to create a self-hosted CDN&lt;/a&gt;, based on OpenBSD, but this time we'll focus on using &lt;a href="https://www.netbsd.org/"&gt;NetBSD&lt;/a&gt;. The idea is to &lt;a href="https://it-notes.dragas.net/2024/08/26/building-a-self-hosted-cdn-for-bsd-cafe-media/"&gt;create reverse proxies with local caching&lt;/a&gt;. These proxies would cache the content on the first request and serve it directly afterward. The proxies would be distributed across different regions, and the DNS would route requests to the nearest proxy based on the caller’s location. All this is achieved without relying on external CDNs, using self-managed tools instead. &lt;/p&gt;
&lt;p&gt;NetBSD is a lightweight, stable, and secure operating system that supports a wide range of hardware, making it an excellent choice for a caching reverse proxy. Devices that other operating systems may soon abandon, such as early Raspberry Pi models or i386 architecture, are still fully supported by NetBSD and will continue to be so. Additionally, NetBSD is an outstanding platform for virtualization (using &lt;a href="https://wiki.netbsd.org/ports/xen/howto/"&gt;Xen&lt;/a&gt; or &lt;a href="https://www.netbsd.org/docs/guide/en/chap-virt.html"&gt;qemu/nvmm&lt;/a&gt;) and deserves more attention than it currently receives.&lt;/p&gt;
&lt;p&gt;The choice of Varnish is based on several factors, with the main ones being the ability to keep the cache in RAM (which means it can run on read-only systems) and the ability to flush the cache remotely. For example, with each change to my blog, I can choose whether to perform an immediate flush (such as for a new article or an error) or wait for the cache's "natural" expiration (such as for a typo or minor, non-critical changes).&lt;/p&gt;
&lt;p&gt;While I won't detail the installation process for NetBSD, as it depends heavily on the hardware you have available, I will guide you through setting up a self-hosted CDN using NetBSD, Varnish, nginx, and the acme.sh or lego tool for SSL certificate management.&lt;/p&gt;
&lt;h2&gt;Installation&lt;/h2&gt;
&lt;p&gt;During the installation of NetBSD, ensure that you enable support for binary package management. This will install &lt;code&gt;pkgin&lt;/code&gt;, &lt;a href="https://pkgin.net/"&gt;a tool that simplifies package management&lt;/a&gt; on NetBSD. If you skip this step during installation, you can still install pkgin later, but it's easier to let the installer handle it.&lt;/p&gt;
&lt;p&gt;Once your system is up and running, use pkgin to install the necessary packages: Varnish, nginx, and, depending on your preference, either acme.sh or Go (if you plan to compile lego). Although lego is not available as a precompiled package, you can easily compile it locally using Go, but for simplicity, I recommend using acme.sh.&lt;/p&gt;
&lt;p&gt;For this setup, I will present two methods for generating and renewing certificates: using acme.sh or compiling lego - to reach the same final outcome as the &lt;a href="https://it-notes.dragas.net/2024/08/29/make-your-own-cdn-openbsd/"&gt;OpenBSD article&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Option 1: Using acme.sh (Recommended)&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://github.com/acmesh-official/acme.sh"&gt;acme.sh&lt;/a&gt; is a simple, yet powerful, shell script that handles certificate generation and renewal with ease. To install acme.sh:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;pkgin in acmesh varnish nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once acme.sh is installed, you can proceed to configure it for certificate management. It supports DNS authentication and integrates with many DNS providers, making it a flexible choice.&lt;/p&gt;
&lt;h3&gt;Option 2: Compiling Lego&lt;/h3&gt;
&lt;p&gt;If you prefer to use lego, you will need to compile it manually, as it is not available as a precompiled package for NetBSD. First, install Go and other necessary packages:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;pkgin in go varnish nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To compile lego, you’ll need some disk space. Since the &lt;code&gt;/tmp&lt;/code&gt; directory in NetBSD is often mounted as &lt;code&gt;tmpfs&lt;/code&gt; (using RAM), you may run out of space during compilation if your system has limited memory. You can temporarily disable &lt;code&gt;tmpfs&lt;/code&gt; by editing &lt;code&gt;/etc/fstab&lt;/code&gt; and commenting out the relevant line:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;#tmpfs           /tmp    tmpfs   rw,-m=1777,-s=ram%25
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After rebooting, compile lego:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;export GO111MODULE=on
go122 install github.com/go-acme/lego/v4/cmd/lego@latest

cp go/bin/lego /usr/pkg/bin/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once lego is compiled and installed, you can uncomment the &lt;code&gt;/tmp&lt;/code&gt; line in &lt;code&gt;/etc/fstab&lt;/code&gt; and reboot again.&lt;/p&gt;
&lt;h2&gt;Configuring Varnish and nginx&lt;/h2&gt;
&lt;p&gt;First, copy the necessary &lt;code&gt;rc.d&lt;/code&gt; scripts for nginx and Varnish:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;cp /usr/pkg/share/examples/rc.d/nginx /etc/rc.d/
cp /usr/pkg/share/examples/rc.d/varnishd /etc/rc.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then, add the following to &lt;code&gt;/etc/rc.conf&lt;/code&gt; to enable and configure nginx and Varnish:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;nginx=YES
varnishd=YES
varnishd_flags=&amp;quot;-f /usr/pkg/etc/varnish/default.vcl -T localhost:9999 -a &amp;quot;/var/run/varnish.sock&amp;quot;,user=nginx,group=varnish,mode=660 -s default,500m&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In this configuration, Varnish listens on a Unix socket, and nginx connects to it. This approach is more efficient and helps avoid some issues that may arise when exposing Varnish over an IP/port.&lt;/p&gt;
&lt;h3&gt;Creating the Varnish VCL Configuration&lt;/h3&gt;
&lt;p&gt;Next, create the VCL configuration file for Varnish at &lt;code&gt;/usr/pkg/etc/varnish/default.vcl&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;vcl 4.1;
import std;

# Backend - it-notes.dragas.net
backend it_notes {
    .host = &amp;quot;myBackendIP&amp;quot;;
    .port = &amp;quot;80&amp;quot;;
}

# ACL - purge - it-notes.dragas.net
acl purge_it_notes {
    &amp;quot;allowedToPurge_IP&amp;quot;;
}

sub vcl_recv {
    # it-notes.dragas.net
    if (req.http.Host == &amp;quot;it-notes.dragas.net&amp;quot;) {
        set req.backend_hint = it_notes;
        set req.http.Host = &amp;quot;it-notes.dragas.net&amp;quot;;

        # PURGE - it-notes.dragas.net
        if (req.method == &amp;quot;PURGE&amp;quot;) {
            std.log(&amp;quot;Purge request received for &amp;quot; + req.url);

            if (!std.ip(req.http.X-Forwarded-For, &amp;quot;0.0.0.0&amp;quot;) ~ purge_it_notes) {
                return (synth(405, &amp;quot;Not allowed.&amp;quot;));
            }

            if (req.url == &amp;quot;/&amp;quot; || req.url == &amp;quot;/*&amp;quot;) {
                ban(&amp;quot;req.http.host == &amp;quot; + req.http.host);
                return(synth(200, &amp;quot;Entire cache has been cleared.&amp;quot;));
            }
            return (purge);
        }

    } else {
        # Other domains - 404
        return (synth(404, &amp;quot;Domain not found&amp;quot;));
    }

    if (req.method != &amp;quot;GET&amp;quot; &amp;amp;&amp;amp; req.method != &amp;quot;HEAD&amp;quot;) {
        return (pipe);
    }

    return (hash);
}

sub vcl_backend_response {
    # TTL - it-notes.dragas.net
    if (bereq.http.host == &amp;quot;it-notes.dragas.net&amp;quot;) {
        if (bereq.url ~ &amp;quot;\.(gif|jpg|jpeg|png|webp|ico|css|js)$&amp;quot;) {
            set beresp.ttl = 1w;
            set beresp.grace = 1d;
            set beresp.keep = 7d;
            unset beresp.http.Set-Cookie;
            unset beresp.http.Cache-Control;
            set beresp.http.Cache-Control = &amp;quot;public, max-age=604800&amp;quot;;
        } else {
            set beresp.ttl = 15m;
            set beresp.grace = 48h;
            set beresp.keep = 7d;
        }
    }

    # Remove some headers
    unset beresp.http.Server;
    unset beresp.http.X-Powered-By;
    unset beresp.http.Via;

    return (deliver);
}

sub vcl_deliver {
    # Add X-Cache header
    if (obj.hits &amp;gt; 0) {
        set resp.http.X-Cache = &amp;quot;HIT&amp;quot;;
    } else {
        set resp.http.X-Cache = &amp;quot;MISS&amp;quot;;
    }

    std.log(&amp;quot;Delivering content for &amp;quot; + req.url + &amp;quot; - Cache: &amp;quot; + resp.http.X-Cache);

    # Remove Varnish headers
    unset resp.http.Via;
    unset resp.http.X-Varnish;

    return (deliver);
}

sub vcl_hash {
    hash_data(req.url);
    if (req.http.host) {
        hash_data(req.http.host);
    } else {
        hash_data(server.ip);
    }
    return (lookup);
}

sub vcl_hit {
    return (deliver);
}

sub vcl_miss {
    return (fetch);
}

sub vcl_purge {
    std.log(&amp;quot;Purge executed for &amp;quot; + req.url);
    return (synth(200, &amp;quot;Purge successful&amp;quot;));
}

sub vcl_synth {
    set resp.http.Content-Type = &amp;quot;text/html; charset=utf-8&amp;quot;;
    set resp.http.Retry-After = &amp;quot;5&amp;quot;;
    synthetic({&amp;quot;&amp;lt;!DOCTYPE html&amp;gt;
        &amp;lt;html&amp;gt;
            &amp;lt;head&amp;gt;
                &amp;lt;title&amp;gt;&amp;quot;} + resp.status + &amp;quot; &amp;quot; + resp.reason + {&amp;quot;&amp;lt;/title&amp;gt;
            &amp;lt;/head&amp;gt;
            &amp;lt;body&amp;gt;
                &amp;lt;h1&amp;gt;Status &amp;quot;} + resp.status + &amp;quot; &amp;quot; + resp.reason + {&amp;quot;&amp;lt;/h1&amp;gt;
                &amp;lt;p&amp;gt;&amp;quot;} + resp.reason + {&amp;quot;&amp;lt;/p&amp;gt;
                &amp;lt;h3&amp;gt;Guru Meditation:&amp;lt;/h3&amp;gt;
                &amp;lt;p&amp;gt;XID: &amp;quot;} + req.xid + {&amp;quot;&amp;lt;/p&amp;gt;
                &amp;lt;hr&amp;gt;
                &amp;lt;p&amp;gt;Varnish cache server&amp;lt;/p&amp;gt;
            &amp;lt;/body&amp;gt;
        &amp;lt;/html&amp;gt;
    &amp;quot;});
    return (deliver);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Configuring nginx&lt;/h3&gt;
&lt;p&gt;Now, modify the nginx configuration file at &lt;code&gt;/usr/pkg/etc/nginx/nginx.conf&lt;/code&gt;. Set the number of worker processes to "auto" to take advantage of all server cores, and configure the reverse proxy for your site(s). Here's an example configuration:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;[...]
worker_processes  auto;
[...]

server {
    server_name it-notes.dragas.net;

    location / {
        proxy_method $request_method;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_pass http://unix:/var/run/varnish.sock;
    }

    access_log /var/log/nginx/access.it-notes.dragas.net.log;
    error_log /var/log/nginx/error.it-notes.dragas.net.log;

    listen [::]:443 ssl;
    listen 443 ssl;
    http2 on;
    # If you're using acme.sh, just change the location of the certificates
    ssl_certificate /root/.lego/certificates/it-notes.dragas.net.crt;
    ssl_certificate_key /root/.lego/certificates/it-notes.dragas.net.key;
}

server {
    if ($host = it-notes.dragas.net) {
        return 301 https://$host$request_uri;
    }
    server_name it-notes.dragas.net;
    listen 80;
    listen [::]:80;
    return 404;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Starting Varnish and nginx&lt;/h3&gt;
&lt;p&gt;Finally, start the Varnish and nginx services:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;service varnishd start
service nginx start
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If everything is configured correctly, both Varnish and nginx will be up and running, ready to handle incoming connections.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Congratulations, you have successfully set up your own CDN on NetBSD. This solution is lightweight, stable, and fully under your control, allowing you to break free from the constraints of major service providers. With NetBSD's broad hardware support and minimal overhead, this setup can run on a wide variety of devices, making it a versatile choice for self-hosted solutions.&lt;/p&gt;
&lt;p&gt;I'm using it as a test and as a read-only root filesystem with a RAM-only local cache for my blog on a &lt;a href="https://www.raspberrypi.com/products/raspberry-pi-zero-w/"&gt;Raspberry Pi Zero W (first edition)&lt;/a&gt;, and as soon as I get the new FTTH, I'll probably make it accessible via IPv6 for Italy, putting it physically into production.&lt;/p&gt;
&lt;p&gt;If your goal is geo-replication, you can use DNS providers that offer location-based routing or set up your own DNS infrastructure to manage and resolve requests according to the user’s location. With multiple reverse proxies, separate DNS servers, and a well-configured cache, you can achieve a highly resilient system with minimal risk of a single point of failure.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Tue, 03 Sep 2024 01:41:00 +0200</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2024/09/03/make-your-own-cdn-netbsd/</guid><category>netbsd</category><category>server</category><category>hosting</category><category>tutorial</category><category>ownyourdata</category><category>vpn</category><category>ha</category><category>wireguard</category><category>web</category><category>cdn</category><category>bsdcafe</category><category>varnish</category><category>series</category></item><item><title>Make Your Own CDN with OpenBSD Base and Just 2 Packages</title><link>https://it-notes.dragas.net/2024/08/29/make-your-own-cdn-openbsd/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/web_text.webp" alt="Make Your Own CDN with OpenBSD Base and Just 2 Packages"&gt;&lt;/p&gt;&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;This article is a "spin-off" from the previous post "&lt;a href="https://it-notes.dragas.net/2024/08/26/building-a-self-hosted-cdn-for-bsd-cafe-media/"&gt;Building a Self-Hosted CDN for BSD Cafe Media&lt;/a&gt;," which I recommend reading, based on FreeBSD. In that article, I showed how I addressed the issue of geo-replication and geo-distribution of BSD Cafe media content. If you prefer a NetBSD-based setup, &lt;a href="https://it-notes.dragas.net/2024/09/03/make-your-own-cdn-netbsd/"&gt;there's an article that describes how to do it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The internet today relies TOO MUCH on just a few big players. When one of them stops working, half the world is impacted because too many services, in my opinion, depend on them. “Too big to fail,” some might say. 
“Single Point of Failure,” I respond."&lt;/p&gt;
&lt;p&gt;The strength of the internet has always been its extreme decentralization, which is now less evident due to this phenomenon.&lt;/p&gt;
&lt;p&gt;In this article, I want to show how easy it is to create a self-hosted CDN using OpenBSD and just two external packages: &lt;a href="https://varnish-cache.org/"&gt;Varnish&lt;/a&gt; and &lt;a href="https://github.com/go-acme/lego"&gt;Lego&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Actually, only one package is truly needed (Varnish), and SSL certificates could be generated using the built-in &lt;a href="https://man.openbsd.org/acme-client.1"&gt;acme-client&lt;/a&gt; in OpenBSD. However, this might be limiting since acme-client handles certificate generation via traditional methods (using a file in &lt;code&gt;.well-known&lt;/code&gt;), but when dealing with a CDN and several reverse proxies listening, you don’t have perfect control over which one will receive the certificate generation validation request.&lt;/p&gt;
&lt;p&gt;The choice of Varnish is based on several factors, with the main ones being the ability to keep the cache in RAM (which means it can run on read-only systems) and the ability to flush the cache remotely. For example, with each change to my blog, I can choose whether to perform an immediate flush (such as for a new article or an error) or wait for the cache's "natural" expiration (such as for a typo or minor, non-critical changes).&lt;/p&gt;
&lt;p&gt;For convenience and practicality, I’ll use the excellent Lego tool—a Go application that supports many DNS authentication methods, including PowerDNS.&lt;/p&gt;
&lt;h2&gt;Installation&lt;/h2&gt;
&lt;p&gt;The steps are quite simple. After installing and updating OpenBSD (using the &lt;a href="https://man.openbsd.org/syspatch"&gt;syspatch&lt;/a&gt; command), start by installing the two packages:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;obcdn# pkg_add lego varnish
quirks-7.14:updatedb-0p0: ok
quirks-7.14 signed on 2024-08-24T13:35:23Z
quirks-7.14: ok
lego-4.16.1: ok
varnish-7.4.2:bzip2-1.0.8p0: ok
varnish-7.4.2:pcre2-10.37p2: ok
useradd: Warning: home directory `/var/varnish' doesn't exist, and -m was not specified
varnish-7.4.2: ok
The following new rcscripts were installed: /etc/rc.d/varnishd
See rcctl(8) for details.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The next step is to enable Varnish:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;rcctl enable varnishd
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Varnish has a generic startup script, but it's best to customize the startup options. To do this, modify the &lt;code&gt;/etc/rc.conf.local&lt;/code&gt; file:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;varnishd_flags=&amp;quot;-j unix,user=_varnish,ccgroup=_varnish -f /etc/varnish/default.vcl -T localhost:9999 -a localhost:8080 -s default,500m&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This configuration sets Varnish with a 500 MB cache and listens on localhost, port 8080.&lt;/p&gt;
&lt;p&gt;Next, rename the default VCL file to prepare for your own content:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;mv /etc/varnish/default.vcl /etc/varnish/default.vcl.distrib
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Create a new &lt;code&gt;default.vcl&lt;/code&gt; file. Below is an example based on this blog (at the time of writing). You’ll need to adapt it according to your needs, especially if there are cookies or other dynamic content. Note that Varnish will fetch data from a specific backend accessed in http. If privacy is needed, consider creating a VPN between the backend and Varnish, &lt;a href="https://it-notes.dragas.net/2024/08/26/building-a-self-hosted-cdn-for-bsd-cafe-media/"&gt;as briefly mentioned in the previous article&lt;/a&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-vcl"&gt;vcl 4.1;
import std;

# Backend - it-notes.dragas.net
backend it_notes {
    .host = &amp;quot;myOriginalServer&amp;quot;;
    .port = &amp;quot;80&amp;quot;;
}

# ACL - purge - it-notes.dragas.net
acl purge_it_notes {
    &amp;quot;authorizedIPForCachePurge&amp;quot;;
}

sub vcl_recv {
    # it-notes.dragas.net
    if (req.http.Host == &amp;quot;it-notes.dragas.net&amp;quot;) {
        set req.backend_hint = it_notes;
        set req.http.Host = &amp;quot;it-notes.dragas.net&amp;quot;;

        # PURGE - it-notes.dragas.net
        if (req.method == &amp;quot;PURGE&amp;quot;) {
            std.log(&amp;quot;Purge request received for &amp;quot; + req.url);

            if (!std.ip(req.http.X-Forwarded-For, &amp;quot;0.0.0.0&amp;quot;) ~ purge_it_notes) {
                return (synth(405, &amp;quot;Not allowed.&amp;quot;));
            }

            if (req.url == &amp;quot;/&amp;quot; || req.url == &amp;quot;/*&amp;quot;) {
                ban(&amp;quot;req.http.host == &amp;quot; + req.http.host);
                return(synth(200, &amp;quot;Entire cache has been cleared.&amp;quot;));
            }
            return (purge);
        }

    } else {
        # Other domains - 404
        return (synth(404, &amp;quot;Domain not found&amp;quot;));
    }

    if (req.method != &amp;quot;GET&amp;quot; &amp;amp;&amp;amp; req.method != &amp;quot;HEAD&amp;quot;) {
        return (pipe);
    }

    return (hash);
}

sub vcl_backend_response {
    # TTL - it-notes.dragas.net
    if (bereq.http.host == &amp;quot;it-notes.dragas.net&amp;quot;) {
        if (bereq.url ~ &amp;quot;\.(gif|jpg|jpeg|png|webp|ico|css|js)$&amp;quot;) {
            set beresp.ttl = 1w;
            set beresp.grace = 1d;
            set beresp.keep = 7d;
            unset beresp.http.Set-Cookie;
            unset beresp.http.Cache-Control;
            set beresp.http.Cache-Control = &amp;quot;public, max-age=604800&amp;quot;;
        } else {
            set beresp.ttl = 15m;
            set beresp.grace = 48h;
            set beresp.keep = 7d;
        }
    }

    # Remove some headers
    unset beresp.http.Server;
    unset beresp.http.X-Powered-By;
    unset beresp.http.Via;

    return (deliver);
}

sub vcl_deliver {
    # Add X-Cache header
    if (obj.hits &amp;gt; 0) {
        set resp.http.X-Cache = &amp;quot;HIT&amp;quot;;
    } else {
        set resp.http.X-Cache = &amp;quot;MISS&amp;quot;;
    }

    std.log(&amp;quot;Delivering content for &amp;quot; + req.url + &amp;quot; - Cache: &amp;quot; + resp.http.X-Cache);

    # Remove Varnish headers
    unset resp.http.Via;
    unset resp.http.X-Varnish;

    return (deliver);
}

sub vcl_hash {
    hash_data(req.url);
    if (req.http.host) {
        hash_data(req.http.host);
    } else {
        hash_data(server.ip);
    }
    return (lookup);
}

sub vcl_hit {
    return (deliver);
}

sub vcl_miss {
    return (fetch);
}

sub vcl_purge {
    std.log(&amp;quot;Purge executed for &amp;quot; + req.url);
    return (synth(200, &amp;quot;Purge successful&amp;quot;));
}

sub vcl_synth {
    set resp.http.Content-Type = &amp;quot;text/html; charset=utf-8&amp;quot;;
    set resp.http.Retry-After = &amp;quot;5&amp;quot;;
    synthetic({&amp;quot;&amp;lt;!DOCTYPE html&amp;gt;
        &amp;lt;html&amp;gt;
            &amp;lt;head&amp;gt;
                &amp;lt;title&amp;gt;&amp;quot;} + resp.status + &amp;quot; &amp;quot; + resp.reason + {&amp;quot;&amp;lt;/title&amp;gt;
            &amp;lt;/head&amp;gt;
            &amp;lt;body&amp;gt;
                &amp;lt;h1&amp;gt;Status &amp;quot;} + resp.status + &amp;quot; &amp;quot; + resp.reason + {&amp;quot;&amp;lt;/h1&amp;gt;
                &amp;lt;p&amp;gt;&amp;quot;} + resp.reason + {&amp;quot;&amp;lt;/p&amp;gt;
                &amp;lt;h3&amp;gt;Guru Meditation:&amp;lt;/h3&amp;gt;
                &amp;lt;p&amp;gt;XID: &amp;quot;} + req.xid + {&amp;quot;&amp;lt;/p&amp;gt;
                &amp;lt;hr&amp;gt;
                &amp;lt;p&amp;gt;Varnish cache server&amp;lt;/p&amp;gt;
            &amp;lt;/body&amp;gt;
        &amp;lt;/html&amp;gt;
    &amp;quot;});
    return (deliver);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Start Varnish and check if it starts correctly:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;rcctl start varnishd
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Generating SSL Certificates&lt;/h2&gt;
&lt;p&gt;Before configuring &lt;a href="https://man.openbsd.org/relayd.conf.5"&gt;relayd&lt;/a&gt;, you’ll need to generate SSL certificates. Lego supports many DNS providers and provides clear and comprehensive examples, so I suggest reading its &lt;a href="https://github.com/go-acme/lego?tab=readme-ov-file"&gt;README file&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Certificates generated by Lego are not directly compatible with relayd, so you must generate them in the correct format. Add the &lt;code&gt;-k rsa4096&lt;/code&gt; flag to the Lego command to obtain certificates compatible with relayd.&lt;/p&gt;
&lt;p&gt;Once generated, the certificates will be in a subdirectory of the directory from which the command was launched. For example, if the command is run as root (which is unnecessary, but just for the example), the certificates will be in &lt;code&gt;/root/.lego/certificates/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;relayd expects certificates in a specific location. Copy them to the appropriate directories. In my example:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;obcdn# cp /root/.lego/certificates/it-notes.dragas.net.crt /etc/ssl/
obcdn# cp /root/.lego/certificates/it-notes.dragas.net.key /etc/ssl/private/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Remember to copy the files to the correct directories when renewing. You can also create symbolic links, this will make your life easier but it’s less secure.&lt;/p&gt;
&lt;h2&gt;Configuring relayd&lt;/h2&gt;
&lt;p&gt;It’s time to configure relayd. The file is &lt;code&gt;/etc/relayd.conf&lt;/code&gt;, and here’s an example configuration:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;log state changes
prefork 10

table &amp;lt;itnotes&amp;gt; { 127.0.0.1 }

http protocol &amp;quot;http&amp;quot; {
    match request header append &amp;quot;X-Forwarded-For&amp;quot; value &amp;quot;$REMOTE_ADDR&amp;quot;
    match request header append &amp;quot;X-Forwarded-By&amp;quot; value &amp;quot;$SERVER_ADDR:$SERVER_PORT&amp;quot;

    match request path &amp;quot;/*.atom&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.css&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.gif&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.html&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.ico&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.jpg&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.webp&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.js&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.png&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.rss&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.svg&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.xml&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.ttf&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.woff2&amp;quot; tag &amp;quot;CACHE&amp;quot;

    match response tagged &amp;quot;CACHE&amp;quot; header set &amp;quot;Cache-Control&amp;quot; value &amp;quot;public, max-age=604800&amp;quot;

    pass request header &amp;quot;Host&amp;quot; value &amp;quot;it-notes.dragas.net&amp;quot; forward to &amp;lt;itnotes&amp;gt;
}

http protocol &amp;quot;https&amp;quot; {
    match request header append &amp;quot;X-Forwarded-For&amp;quot; value &amp;quot;$REMOTE_ADDR&amp;quot;
    match request header append &amp;quot;X-Forwarded-By&amp;quot; value &amp;quot;$SERVER_ADDR:$SERVER_PORT&amp;quot;

    match response header set &amp;quot;Referrer-Policy&amp;quot; value &amp;quot;no-referrer&amp;quot;
    match response header set &amp;quot;X-Content-Type-Options&amp;quot; value &amp;quot;nosniff&amp;quot;
    match response header set &amp;quot;X-Download-Options&amp;quot; value &amp;quot;noopen&amp;quot;
    match response header set &amp;quot;X-Frame-Options&amp;quot; value &amp;quot;SAMEORIGIN&amp;quot;
    match response header set &amp;quot;X-Permitted-Cross-Domain-Policies&amp;quot; value &amp;quot;none&amp;quot;
    match response header set &amp;quot;X-XSS-Protection&amp;quot; value &amp;quot;1; mode=block&amp;quot;
    match response header set &amp;quot;Strict-Transport-Security&amp;quot; value &amp;quot;max-age=15552000; includeSubDomains; preload&amp;quot;

    match request path &amp;quot;/*.atom&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.css&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.gif&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.html&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.ico&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.jpg&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.webp&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.js&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.png&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.rss&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.svg&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.xml&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.ttf&amp;quot; tag &amp;quot;CACHE&amp;quot;
    match request path &amp;quot;/*.woff2&amp;quot; tag &amp;quot;CACHE&amp;quot;

    match response tagged &amp;quot;CACHE&amp;quot; header set &amp;quot;Cache-Control&amp;quot; value &amp;quot;public, max-age=604800&amp;quot;
    tcp { nodelay, sack, socket buffer 65536, backlog 100 }

    tls { keypair &amp;quot;it-notes.dragas.net&amp;quot; }

    pass request header &amp;quot;Host&amp;quot; value &amp;quot;it-notes.dragas.net&amp;quot; forward to &amp;lt;itnotes&amp;gt;
}

relay &amp;quot;http&amp;quot; {
    listen on vio0 port 80
    protocol &amp;quot;http&amp;quot;

    forward to &amp;lt;itnotes&amp;gt; port 8080
}

relay &amp;quot;https&amp;quot; {
    listen on vio0 port 443 tls
    protocol &amp;quot;https&amp;quot;

    forward to &amp;lt;itnotes&amp;gt; port 8080
}

relay &amp;quot;https6&amp;quot; {
    listen on my:ip:v6:address::1 port 443 tls
    protocol &amp;quot;https&amp;quot;

    forward to &amp;lt;itnotes&amp;gt; port 8080
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The content of the file is quite self-explanatory. Note that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vio0&lt;/code&gt; is the interface name—it should be modified based on the interface where relayd needs to listen.&lt;/li&gt;
&lt;li&gt;I’ve configured relayd to listen on port 80 as well.&lt;/li&gt;
&lt;li&gt;IPv4 and IPv6 listeners are separated. If IPv6 is not configured, simply comment out that part. Please, if you can, use IPv6. In a fairer world, everyone would have the right to at least one class of public addresses without having to pay exorbitant fees.&lt;/li&gt;
&lt;li&gt;The "keypair" must correspond to the certificate and key names in &lt;code&gt;/etc/ssl&lt;/code&gt; and &lt;code&gt;/etc/ssl/private&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Test the configuration, enable, and start relayd:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;obcdn# relayd -n
configuration OK
obcdn# rcctl enable relayd
obcdn# rcctl start relayd
relayd(ok)
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Final Checks&lt;/h2&gt;
&lt;p&gt;The stack is ready. A &lt;code&gt;ps&lt;/code&gt; command will show the process status:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;obcdn# ps aux
USER       PID %CPU %MEM   VSZ   RSS TT  STAT   STARTED       TIME COMMAND
root         1  0.0  0.0   920   276 ??  I       7:10PM    0:00.01 /sbin/init
[...]
_varnish 44999  0.0  0.2  2256  2424 ??  S       8:27PM    0:00.05 varnishd: Varnish-Mgt -i obcdn.my.domain (varnishd)
_varnish 54481  0.0  8.8 34500 88876 ??  S       8:27PM    0:00.60 varnishd: Varnish-Child -i obcdn.my.domain (varnishd)
root     57753  0.0  0.5  4080  4820 ??  IU      8:32PM    0:00.03 /usr/sbin/relayd
_relayd  95940  0.0  0.4  2152  3892 ??  Spc     8:32PM    0:00.01 relayd: pfe (relayd)
_relayd  81032  0.0  0.4  2156  3716 ??  Spc     8:32PM    0:00.01 relayd: hce (relayd)
_relayd  80312  0.0  0.5  2748  5280 ??  Ipc     8:32PM    0:00.03 relayd: relay (relayd)
_relayd  88919  0.0  0.5  2748  5268 ??  Ipc     8:32PM    0:00.04 relayd: relay (relayd)
_relayd  90186  0.0  0.5  2752  5272 ??  Ipc     8:32PM    0:00.03 relayd: relay (relayd)
_relayd  32851  0.0  0.5  2736  5284 ??  Ipc     8:32PM    0:00.03 relayd: relay (relayd)
_relayd  20270  0.0  0.5  2744  5252 ??  Ipc     8:32PM    0:00.03 relayd: relay (relayd)
_relayd  98826  0.0  0.5  2752  5264 ??  Ipc     8:32PM    0:00.04 relayd: relay (relayd)
_relayd   6454  0.0  0.5  2744  5264 ??  Ipc     8:32PM    0:00.03 relayd: relay (relayd)
_relayd  90102  0.0  0.5  2740  5276 ??  Ipc     8:32PM    0:00.03 relayd: relay (relayd)
_relayd  60314  0.0  0.5  2748  5336 ??  Ipc     8:32PM    0:00.03 relayd: relay (relayd)
_relayd  27246  0.0  0.5  2744  5284 ??  Ipc     8:32PM    0:00.03 relayd: relay (relayd)
_relayd  87082  0.0  0.4  2100  4508 ??  Ipc     8:32PM    0:00.02 relayd: ca (relayd)
_relayd  53308  0.0  0.4  2096  4496 ??  Ipc     8:32PM    0:00.02 relayd: ca (relayd)
_relayd  11697  0.0  0.4  2092  4492 ??  Ipc     8:32PM    0:00.02 relayd: ca (relayd)
_relayd  83636  0.0  0.4  2092  4500 ??  Ipc     8:32PM    0:00.02 relayd: ca (relayd)
_relayd  74563  0.0  0.4  2096  4484 ??  Ipc     8:32PM    0:00.02 relayd: ca (relayd)
_relayd  35910  0.0  0.4  2100  4508 ??  Ipc     8:32PM    0:00.02 relayd: ca (relayd)
_relayd  24911  0.0  0.4  2096  4504 ??  Ipc     8:32PM    0:00.02 relayd: ca (relayd)
_relayd  78649  0.0  0.4  2096  4496 ??  Ipc     8:32PM    0:00.02 relayd: ca (relayd)
_relayd  89586  0.0  0.4  2096  4500 ??  Ipc     8:32PM    0:00.02 relayd: ca (relayd)
_relayd  11989  0.0  0.4  2100  4500 ??  Ipc     8:32PM    0:00.02 relayd: ca (relayd)
[...]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In this case, both relayd and Varnish are running correctly.&lt;/p&gt;
&lt;h2&gt;Automating Certificate Renewal&lt;/h2&gt;
&lt;p&gt;As a final step, remember to create a script to renew the certificates, copy them to &lt;code&gt;/etc/ssl&lt;/code&gt; and &lt;code&gt;/etc/ssl/private&lt;/code&gt;, and restart relayd.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Congratulations, you now have your own free CDN, with your data, fully under your control. Portable, extendable, controllable, and outside of the big providers' grip.&lt;/p&gt;
&lt;p&gt;If your goal is geo-replication, you should use one of the available methods. Some DNS providers allow selection based on the caller's location (like Bunny.net), or, as I prefer, install your own DNS and use tools to manage operations and resolutions, &lt;a href="https://it-notes.dragas.net/2024/08/26/building-a-self-hosted-cdn-for-bsd-cafe-media/"&gt;as briefly described in the previous article&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With multiple separate reverse proxies, separate DNS servers (on different providers and possibly different countries or continents) capable of checking if the reverse proxies are operational, you can achieve an extremely low likelihood of encountering a Single Point of Failure, as all components, once the cache is filled, will be nearly autonomous even in the event of a (temporary) backend outage - i.e., the original node.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Thu, 29 Aug 2024 01:41:00 +0200</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2024/08/29/make-your-own-cdn-openbsd/</guid><category>openbsd</category><category>server</category><category>hosting</category><category>tutorial</category><category>ownyourdata</category><category>vpn</category><category>ha</category><category>wireguard</category><category>web</category><category>cdn</category><category>bsdcafe</category><category>varnish</category><category>series</category></item><item><title>Building a Self-Hosted CDN for BSD Cafe Media</title><link>https://it-notes.dragas.net/2024/08/26/building-a-self-hosted-cdn-for-bsd-cafe-media/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/network_lights.webp" alt="Building a Self-Hosted CDN for BSD Cafe Media"&gt;&lt;/p&gt;&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;For just over a year, &lt;a href="https://wiki.bsd.cafe"&gt;BSD Cafe&lt;/a&gt;'s media was hosted on a FreeBSD physical server jail with an outgoing bandwidth of 250 Mbit/sec. To mitigate bandwidth congestion, I initially integrated Cloudflare with a tunnel to serve media (and only media) through Cloudflare. The goal was to georeplicate the media and reduce the load on my server. To do this, the media server had to be on a separate domain managed by Cloudflare since the DNS for the primary bsd.cafe domain was managed by Bunny.net.&lt;/p&gt;
&lt;p&gt;The first step (mainly because I discovered that Bunny's DNS does not support IPv6) was to bring the DNS back in-house using two FreeBSD jails (running on different VPS providers), both powered by PowerDNS. PowerDNS supports LUA records, which would come in handy later.&lt;/p&gt;
&lt;p&gt;In line with the principles of &lt;a href="https://it-notes.dragas.net/tags/ownyourdata"&gt;self-hosting and data ownership&lt;/a&gt;, I decided to remove Cloudflare. I created a dedicated subdomain (media.bsd.cafe) and configured the reverse proxy in front of the jail running Minio to respond to that domain. I also reconfigured Mastodon for the new address, and after some fine-tuning, everything worked seamlessly. However, this led to some bandwidth congestion when media was posted, resulting in slower download speeds for users, especially during peak times. This is because, once content is published and federated servers are notified, they all attempt to download the newly published content - media included - almost simultaneously.&lt;/p&gt;
&lt;p&gt;Not wanting to abandon my media server (a dedicated jail with spinning disks, offering 4 TB of storage), I opted for a different approach that I’ll describe here, as it might be useful for similar setups.&lt;/p&gt;
&lt;p&gt;While this setup was implemented on FreeBSD, the configuration and tools - Nginx, Varnish, WireGuard and PowerDNS - are compatible with many operating systems, including Linux, with only minor adjustments required.&lt;/p&gt;
&lt;h2&gt;The Approach: Building a Self-Hosted CDN&lt;/h2&gt;
&lt;p&gt;The idea is to create reverse proxies with local caching. These proxies would cache the content on the first request and serve it directly afterward. The proxies would be distributed across different regions, and the DNS would route requests to the nearest proxy based on the caller’s location. All this is achieved without relying on external CDNs, using self-managed tools instead.&lt;/p&gt;
&lt;p&gt;To establish a direct connection between Minio and the reverse proxies, I configured WireGuard inside the jail. The reverse proxies connect via WireGuard, allowing them to access Minio securely as if they were on the same LAN.&lt;/p&gt;
&lt;p&gt;No further changes were needed on the media jail itself.&lt;/p&gt;
&lt;h2&gt;Setting Up the Reverse Proxies&lt;/h2&gt;
&lt;p&gt;I began configuring the reverse proxies (also running FreeBSD jails, OpenBSD (&lt;a href="https://it-notes.dragas.net/2024/08/29/make-your-own-cdn-openbsd/"&gt;setup described in another post&lt;/a&gt;) and NetBSD (also &lt;a href="https://it-notes.dragas.net/2024/09/03/make-your-own-cdn-netbsd/"&gt;described in another post&lt;/a&gt;), hosted on different providers). The choice of Varnish is based on several factors, with the main ones being the ability to keep the cache in RAM (which means it can run on read-only systems) and the ability to flush the cache remotely. For example, with each change to my blog, I can choose whether to perform an immediate flush (such as for a new article or an error) or wait for the cache's "natural" expiration (such as for a typo or minor, non-critical changes). &lt;/p&gt;
&lt;p&gt;First, I connected them via WireGuard to the Minio jail (I won’t detail the steps here; I’ve &lt;a href="https://it-notes.dragas.net/tags/wireguard"&gt;covered similar setups in other posts&lt;/a&gt;). Then, I installed Nginx and Varnish. A more granular setup would have Varnish on a separate jail, but this way, I can move the reverse proxy jails to different hosts with minimal hassle. Currently, these reverse proxies also serve this blog.&lt;/p&gt;
&lt;p&gt;Next, I installed and configured Varnish inside the jail:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;pkg install varnish7
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I created the directory &lt;code&gt;/usr/local/etc/varnish&lt;/code&gt; and wrote a custom VCL file to manage this setup, named &lt;code&gt;default.vcl&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;vcl 4.1;
import std;

# Backend - it-notes.dragas.net
backend it_notes {
    .host = &amp;quot;itnotesip&amp;quot;;
    .port = &amp;quot;itnotesport&amp;quot;;
}

# Backend - media.bsd.cafe
backend media_bsd {
    .host = &amp;quot;minioWGip&amp;quot;;
    .port = &amp;quot;minioport&amp;quot;;
}

# ACL - IPs allowed to purge - it-notes.dragas.net
acl purge_it_notes {
    &amp;quot;a.b.c.d&amp;quot;;
}

# ACL - IPs allowed to purge - media.bsd.cafe
acl purge_media_bsd {
    &amp;quot;e.f.g.h&amp;quot;;
}

sub vcl_recv {

    # it-notes.dragas.net
    if (req.http.Host == &amp;quot;it-notes.dragas.net&amp;quot;) {
        set req.backend_hint = it_notes;
        set req.http.Host = &amp;quot;it-notes.dragas.net&amp;quot;;

        # PURGE - it-notes.dragas.net
        if (req.method == &amp;quot;PURGE&amp;quot;) {

            std.log(&amp;quot;Purge request received for &amp;quot; + req.url);

            if (!std.ip(req.http.X-Real-IP, &amp;quot;0.0.0.0&amp;quot;) ~ purge_it_notes) {
                return (synth(405, &amp;quot;Not allowed.&amp;quot;));
            }

        if (req.url == &amp;quot;/&amp;quot; || req.url == &amp;quot;/*&amp;quot;) {
                ban(&amp;quot;req.http.host == &amp;quot; + req.http.host);
                return(synth(200, &amp;quot;Entire cache has been cleared.&amp;quot;));
        }
            return (purge);
        }

    # media.bsd.cafe
    } elsif (req.http.Host == &amp;quot;media.bsd.cafe&amp;quot;) {
        set req.backend_hint = media_bsd;
        set req.http.Host = &amp;quot;media.bsd.cafe&amp;quot;;

        # PURGE - media.bsd.cafe
        if (req.method == &amp;quot;PURGE&amp;quot;) {
            if (!std.ip(req.http.X-Real-IP, &amp;quot;0.0.0.0&amp;quot;) ~ purge_media_bsd) {
                return (synth(405, &amp;quot;Not allowed.&amp;quot;));
            }
            if (req.url == &amp;quot;/&amp;quot; || req.url == &amp;quot;/*&amp;quot;) {
                ban(&amp;quot;req.http.host == &amp;quot; + req.http.host);
                return(synth(200, &amp;quot;Entire cache has been cleared.&amp;quot;));
            }
            return (purge);
        }

    } else {
        # Other domains - 404
        return (synth(404, &amp;quot;Domain not found&amp;quot;));
    }

    if (req.method != &amp;quot;GET&amp;quot; &amp;amp;&amp;amp; req.method != &amp;quot;HEAD&amp;quot;) {
        return (pipe);
    }

    return (hash);
}

sub vcl_backend_response {
    # TTL - it-notes.dragas.net
    if (bereq.http.host == &amp;quot;it-notes.dragas.net&amp;quot;) {
        if (bereq.url ~ &amp;quot;\.(gif|jpg|jpeg|png|ico|css|js)$&amp;quot;) {
            set beresp.ttl = 1w;
            set beresp.grace = 1d;
            set beresp.keep = 7d;
            unset beresp.http.Set-Cookie;
            unset beresp.http.Cache-Control;
            set beresp.http.Cache-Control = &amp;quot;public, max-age=604800&amp;quot;;
        } else {
            set beresp.ttl = 15m;
            set beresp.grace = 48h;
            set beresp.keep = 7d;
        }

    # TTL - media.bsd.cafe
    } elsif (bereq.http.host == &amp;quot;media.bsd.cafe&amp;quot;) {
        if (bereq.url ~ &amp;quot;\.(mp4|mp3|wav|flac|ogg)$&amp;quot;) {
            set beresp.ttl = 1d;
            set beresp.grace = 6h;
            set beresp.keep = 3d;
            unset beresp.http.Set-Cookie;
            unset beresp.http.Cache-Control;
            set beresp.http.Cache-Control = &amp;quot;public, max-age=86400&amp;quot;;
        } else {
            set beresp.ttl = 30m;
            set beresp.grace = 12h;
            set beresp.keep = 3d;
        }
    }

    # Remove some headers
    unset beresp.http.Server;
    unset beresp.http.X-Powered-By;
    unset beresp.http.Via;

    return (deliver);
}

sub vcl_deliver {
    # ADD header X-Cache
    if (obj.hits &amp;gt; 0) {
        set resp.http.X-Cache = &amp;quot;HIT&amp;quot;;
    } else {
        set resp.http.X-Cache = &amp;quot;MISS&amp;quot;;
    }

  std.log(&amp;quot;Delivering content for &amp;quot; + req.url + &amp;quot; - Cache: &amp;quot; + resp.http.X-Cache);


    # Remove Varnish headers
    unset resp.http.Via;
    unset resp.http.X-Varnish;

    return (deliver);
}

sub vcl_hash {
    hash_data(req.url);
    if (req.http.host) {
        hash_data(req.http.host);
    } else {
        hash_data(server.ip);
    }
    return (lookup);
}

sub vcl_hit {
    return (deliver);
}

sub vcl_miss {
    return (fetch);
}

sub vcl_purge {
    std.log(&amp;quot;Purge executed for &amp;quot; + req.url);
    return (synth(200, &amp;quot;Purge successful&amp;quot;));
}

sub vcl_synth {
    set resp.http.Content-Type = &amp;quot;text/html; charset=utf-8&amp;quot;;
    set resp.http.Retry-After = &amp;quot;5&amp;quot;;
    synthetic( {&amp;quot;&amp;lt;!DOCTYPE html&amp;gt;
        &amp;lt;html&amp;gt;
            &amp;lt;head&amp;gt;
                &amp;lt;title&amp;gt;&amp;quot;} + resp.status + &amp;quot; &amp;quot; + resp.reason + {&amp;quot;&amp;lt;/title&amp;gt;
            &amp;lt;/head&amp;gt;
            &amp;lt;body&amp;gt;
                &amp;lt;h1&amp;gt;Status &amp;quot;} + resp.status + &amp;quot; &amp;quot; + resp.reason + {&amp;quot;&amp;lt;/h1&amp;gt;
                &amp;lt;p&amp;gt;&amp;quot;} + resp.reason + {&amp;quot;&amp;lt;/p&amp;gt;
                &amp;lt;h3&amp;gt;Guru Meditation:&amp;lt;/h3&amp;gt;
                &amp;lt;p&amp;gt;XID: &amp;quot;} + req.xid + {&amp;quot;&amp;lt;/p&amp;gt;
                &amp;lt;hr&amp;gt;
                &amp;lt;p&amp;gt;Varnish cache server&amp;lt;/p&amp;gt;
            &amp;lt;/body&amp;gt;
        &amp;lt;/html&amp;gt;
    &amp;quot;} );
    return (deliver);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This setup allows Varnish to handle both domains with distinct configurations but within the same cache.&lt;/p&gt;
&lt;p&gt;To enable Varnish, I updated the &lt;code&gt;/etc/rc.conf&lt;/code&gt; file with the following lines, setting a maximum cache size of 2GB:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;varnishd_enable=&amp;quot;YES&amp;quot;
varnishd_listen=&amp;quot;127.0.0.1:8080&amp;quot;
varnishd_config=&amp;quot;/usr/local/etc/varnish/default.vcl&amp;quot;
varnishd_storage=&amp;quot;default,2000M&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can now start Varnish:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;service varnishd start
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The next step is to create two virtual hosts on Nginx (one for it-notes.dragas.net and one for media.bsd.cafe) that will listen on both IPv4 and IPv6 for HTTP and HTTPS. HTTP connections will be redirected to HTTPS, and incoming HTTPS traffic will be passed to Varnish, which will either return cached data or fetch it from the original server (Minio via WireGuard, for media.bsd.cafe). Let's see the media.bsd.cafe part:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;server {
   server_name  media.bsd.cafe;

   [...]

   location / {
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_connect_timeout 300;
    proxy_http_version 1.1;
    proxy_set_header Connection &amp;quot;&amp;quot;;
    chunked_transfer_encoding off;

    expires 12h;
    add_header Cache-Control public;

    add_header X-Cache-Status $upstream_cache_status;
    add_header X-Content-Type-Options nosniff;

    add_header Strict-Transport-Security &amp;quot;max-age=31536000; includeSubDomains; preload&amp;quot; always;
    add_header Referrer-Policy &amp;quot;no-referrer-when-downgrade&amp;quot;;
    add_header Permissions-Policy &amp;quot;geolocation=(), microphone=(), camera=()&amp;quot;;

    proxy_pass http://127.0.0.1:8080;

    [...]

}

[...]

}

server {
    if ($host = media.bsd.cafe) {
        return 301 https://$host$request_uri;
    }
   listen       80;
   listen  [::]:80;
   server_name  media.bsd.cafe;
    return 404;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This configuration isn’t complete, but it provides a good idea of how to set up Nginx - each setup will vary. TTL and caching sizes will also differ based on the characteristics of each reverse proxy. For example, one of the proxies has an 8GB cache since I have ample resources there.&lt;/p&gt;
&lt;p&gt;Generating certificates is another important aspect. In this case, as the reverse proxies are distributed, they all need to respond to the same addresses. One approach is to generate the certificate on one proxy and distribute it to the others. In my case, I opted to use &lt;a href="https://github.com/go-acme/lego"&gt;lego&lt;/a&gt;, which, through PowerDNS’s API, adds a DNS record for validation. This way, each reverse proxy can independently generate and renew its certificates when needed.&lt;/p&gt;
&lt;h2&gt;Configuring DNS for Optimal Routing&lt;/h2&gt;
&lt;p&gt;Once everything is set up, it’s important to ensure that DNS responds correctly. In my case, I implemented a strategy like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Track reverse proxies that respond on port 443 (further refinements are possible and will be done later).&lt;/li&gt;
&lt;li&gt;Return the closest reverse proxy based on the client’s IP address.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unfortunately, PowerDNS on FreeBSD does not include GeoIP support by default, but I have my poudriere ready to compile and install the necessary packages. Alternatively, you could compile it within the jail using the port system.&lt;/p&gt;
&lt;p&gt;After that, I installed the &lt;code&gt;geoipupdate&lt;/code&gt; package (which requires a free license from MaxMind), updated the IP list, and configured PowerDNS to use the GeoIP database. I added the GeoIP backend alongside the existing SQLite3 backend and specified the database to use:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-code"&gt;launch=gsqlite3,geoip
geoip-database-files=mmdb:/usr/local/share/GeoIP/GeoLite2-City.mmdb
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally, I created a LUA record to return the correct address:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;testclosest.bsd.cafe   60      IN      LUA     A &amp;quot;ifportup(443, {'proxy1ip', 'proxy2ip','proxy3ip'}&amp;quot; &amp;quot;, {selector='pickclosest'})&amp;quot;
testclosest.bsd.cafe   60      IN      LUA     AAAA &amp;quot;ifportup(443, {'proxy1ip6', 'proxy2ip6','proxy3ip6'}&amp;quot; &amp;quot;, {selector='pickclosest'})&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And voilà! We now have a small, self-hosted CDN, keeping full control and ownership of our data. Adding a new reverse proxy is straightforward — simply clone an existing proxy, update the WireGuard configuration (adding a peer on the Minio jail and changing the keys on the new proxy), and add it to the DNS.&lt;/p&gt;
&lt;p&gt;Happy caching!&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Mon, 26 Aug 2024 08:41:00 +0200</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2024/08/26/building-a-self-hosted-cdn-for-bsd-cafe-media/</guid><category>freebsd</category><category>server</category><category>hosting</category><category>tutorial</category><category>jail</category><category>ownyourdata</category><category>vpn</category><category>ha</category><category>wireguard</category><category>web</category><category>cdn</category><category>bsdcafe</category><category>varnish</category><category>series</category></item><item><title>Installing Uptime-Kuma in a FreeBSD Jail</title><link>https://it-notes.dragas.net/2024/07/22/install-uptime-kuma-freebsd-jail/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/watchdog.webp" alt="Installing Uptime-Kuma in a FreeBSD Jail"&gt;&lt;/p&gt;&lt;p&gt;&lt;a href="https://github.com/louislam/uptime-kuma"&gt;Uptime-Kuma&lt;/a&gt; is a very useful tool. Besides being able to monitor whether services, websites, or ports are responding, it can also send notifications using many different services, even combined, and check the expiration of certificates. Additionally, it can create informative dashboards about the status of services.&lt;/p&gt;
&lt;p&gt;I have been using it extensively for years, &lt;a href="https://status.bsd.cafe"&gt;including for BSD Cafe&lt;/a&gt;. My initial deployments were on Docker containers, but recently, I have &lt;a href="https://it-notes.dragas.net/2022/02/05/how-we-are-migrating-many-of-our-servers-from-linux-to-freebsd-part-1-system-and-jails-setup/"&gt;moved everything to FreeBSD jails&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When proceeding with the standard installation described in their documentation, the process will halt because one of the third-party libraries (&lt;a href="https://playwright.dev/"&gt;Playwright&lt;/a&gt;) is not officially compatible with FreeBSD. However, since this library is not necessary for the type of checks I want to perform (I am not interested in checking web page components, just their response), it is possible to continue and run everything using a small hack.&lt;/p&gt;
&lt;p&gt;It can be installed directly on FreeBSD but, in my case, I prefer using jails. Note that if using traditional jails (i.e., not VNet), "ping" is disabled. Therefore, if using traditional jails and wanting to check hosts via ping, it is advisable to add the line &lt;code&gt;allow.raw_sockets;&lt;/code&gt; to the respective jail.conf.&lt;/p&gt;
&lt;p&gt;The installation can proceed as follows:&lt;/p&gt;
&lt;p&gt;The first step is to install the necessary dependencies:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;pkg install node20 npm-node20 git-lite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once the dependencies are installed, it is advisable to create a user to run everything and use that user for the next steps:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;pw add user ukuma -m
echo 'export LC_ALL=&amp;quot;en_US.UTF-8&amp;quot;' &amp;gt;&amp;gt; /home/ukuma/.profile
su -l ukuma
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point, clone the Uptime-Kuma repository and start installing the dependencies:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;git clone https://github.com/louislam/uptime-kuma.git
cd uptime-kuma/
npm run setup
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The operation will seem to complete correctly. However, running the program will result in a fatal error preventing it from functioning:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-bash"&gt;ukuma@ukuma:~/uptime-kuma $ node server/server.js
[...]
/home/ukuma/uptime-kuma/node_modules/playwright-core/lib/server/registry/index.js:258
    if (process.platform === 'linux') cacheDirectory = process.env.XDG_CACHE_HOME || _path.default.join(os.homedir(), '.cache'); else if (process.platform === 'darwin') cacheDirectory = _path.default.join(os.homedir(), 'Library', 'Caches'); else if (process.platform === 'win32') cacheDirectory = process.env.LOCALAPPDATA || _path.default.join(os.homedir(), 'AppData', 'Local'); else throw new Error('Unsupported platform: ' + process.platform);
                                                                                                                                                                                                                                                                                                                                                                                             ^

Error: Unsupported platform: freebsd
    at /home/ukuma/uptime-kuma/node_modules/playwright-core/lib/server/registry/index.js:258:388
    at Object.&amp;lt;anonymous&amp;gt; (/home/ukuma/uptime-kuma/node_modules/playwright-core/lib/server/registry/index.js:270:3)
    at Module._compile (node:internal/modules/cjs/loader:1369:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1427:10)
    at Module.load (node:internal/modules/cjs/loader:1206:32)
    at Module._load (node:internal/modules/cjs/loader:1022:12)
    at Module.require (node:internal/modules/cjs/loader:1231:19)
    at require (node:internal/modules/helpers:179:18)
    at Object.&amp;lt;anonymous&amp;gt; (/home/ukuma/uptime-kuma/node_modules/playwright-core/lib/server/index.js:84:17)
    at Module._compile (node:internal/modules/cjs/loader:1369:14)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The issue is clear: Playwright does not support FreeBSD, causing the operation to fail. At this point, there are two options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hope that Microsoft decides to support FreeBSD, which is unlikely in the short term.&lt;/li&gt;
&lt;li&gt;Trick Playwright into thinking FreeBSD is supported, which may break some related functionality, but it is a small price to pay to have all other features of Uptime-Kuma.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I will use the second method. Open the indicated file (in this case, &lt;code&gt;/home/ukuma/uptime-kuma/node_modules/playwright-core/lib/server/registry/index.js&lt;/code&gt;) and modify the indicated line (in this case, 258). Change the line &lt;code&gt;if (process.platform === 'linux')&lt;/code&gt; to &lt;code&gt;if (process.platform === 'freebsd')&lt;/code&gt; and save it.&lt;/p&gt;
&lt;p&gt;Uptime-Kuma will then be able to import the necessary libraries and function, listening on port 3001.&lt;/p&gt;
&lt;p&gt;At this point, it will be possible to use it normally, as well as use pm2 to start and manage it automatically, but this goes beyond the scope of this article.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Mon, 22 Jul 2024 07:42:00 +0000</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2024/07/22/install-uptime-kuma-freebsd-jail/</guid><category>freebsd</category><category>monitoring</category><category>server</category><category>networking</category><category>hosting</category><category>tutorial</category><category>security</category><category>jail</category><category>container</category><category>ownyourdata</category></item><item><title>FreeBSD Tips and Tricks: Limiting Process Priority in a FreeBSD Jail</title><link>https://it-notes.dragas.net/2024/07/11/limiting-process-priority-in-freebsd-jail/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/lock_jail.webp" alt="FreeBSD Tips and Tricks: Limiting Process Priority in a FreeBSD Jail"&gt;&lt;/p&gt;&lt;p&gt;FreeBSD allows for quite comprehensive resource limitation for one or more jails. Beyond the official documentation, there is a good description &lt;a href="https://mwl.io/nonfiction/os#fmjail"&gt;in MWL's book&lt;/a&gt;. There's also &lt;a href="https://klarasystems.com/articles/controlling-resource-limits-with-rctl-in-freebsd/"&gt;an interesting article from Klara Systems&lt;/a&gt; that describes some functionalities.&lt;/p&gt;
&lt;p&gt;Sometimes, however, we only need the processes running within a specific jail to have a specific priority - higher or lower than others.&lt;/p&gt;
&lt;p&gt;There are many methods to achieve this, but the simplest one, in my opinion, is to leverage the properties of the &lt;code&gt;nice(1)&lt;/code&gt; command. The main property, in fact, is to transmit the set priority to child processes, so all processes launched by the command that received a different level of "niceness" will inherit its priority.&lt;/p&gt;
&lt;p&gt;For example, to give the minimum priority to the services launched within a jail, just modify the &lt;code&gt;.conf&lt;/code&gt; file of the jail (in the case of a standard BastilleBSD installation, the file will be &lt;code&gt;/usr/local/bastille/jails/jailname/jail.conf&lt;/code&gt;) and change the command&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;exec.start = '/bin/sh /etc/rc';
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;to:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;exec.start = '/usr/bin/nice -n 20 /bin/sh /etc/rc';
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In this way, when &lt;code&gt;rc&lt;/code&gt; starts at the jail's boot, it will have a niceness of 20 and will transmit it to all the processes that &lt;code&gt;rc&lt;/code&gt; itself launches (thus all the services of the jail).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This will only apply to child processes of &lt;code&gt;rc&lt;/code&gt;, not to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Commands manually launched from the jail console&lt;/li&gt;
&lt;li&gt;Services launched by running &lt;code&gt;service servicename start&lt;/code&gt; (or restart) from the jail shell. This is because, in this case, the process will not be a child of &lt;code&gt;rc&lt;/code&gt; but will derive directly from the console in use.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using the &lt;code&gt;nice&lt;/code&gt; command to set the priority of processes within a FreeBSD jail is a simple and effective method. However, it is important to be aware of the limitations of this approach and ensure that manually executed commands are managed accordingly.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Thu, 11 Jul 2024 08:41:00 +0200</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2024/07/11/limiting-process-priority-in-freebsd-jail/</guid><category>freebsd</category><category>jail</category><category>server</category><category>hosting</category><category>tutorial</category><category>series</category><category>tipsandtricks</category></item><item><title>Blocking Access from or to Specific Countries Using FreeBSD and pf</title><link>https://it-notes.dragas.net/2024/06/16/freebsd-blocking-country-access/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/lock_iphone.webp" alt="Blocking Access from or to Specific Countries Using FreeBSD and pf"&gt;&lt;/p&gt;&lt;p&gt;In recent times, there has been an exponential increase in malicious (or simply rude) traffic from specific countries. Alternatively, sometimes we simply do not need visitors from other parts of the world accessing our server for various reasons.&lt;/p&gt;
&lt;p&gt;On FreeBSD, this operation is very simple, and I have been using a reliable and secure system to manage it automatically for a long time. Of course, as with all geolocation blocks, there is never certainty about the result, as sometimes certain IP blocks officially belong to one country but are actually used by another, or users can resort to VPNs to bypass these types of blocks. However, it remains a valid method to filter out unwanted traffic, especially when it comes to rogue bots that do not respect the robots.txt file and bombard our machines with repeated requests, generating real DDoS attacks.&lt;/p&gt;
&lt;h2&gt;Installation of ipdbtools&lt;/h2&gt;
&lt;p&gt;First, install the ipdbtools package (&lt;a href="http://www.freshports.org/sysutils/ipdbtools"&gt;http://www.freshports.org/sysutils/ipdbtools&lt;/a&gt;):&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;pkg install ipdbtools
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next, download the updated global lists:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;/usr/local/bin/ipdb-update.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Configuring pf&lt;/h2&gt;
&lt;p&gt;Then, modify the pf configuration. To do this, add the following line at the beginning of the filtering part of your firewall configuration (/etc/pf.conf):&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-pf"&gt;block drop log quick from &amp;lt;blocked_countries&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point, simply type:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;service pf reload
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;to reload the pf configuration and start considering the values in the &lt;code&gt;&amp;lt;blocked_countries&amp;gt;&lt;/code&gt; table.&lt;/p&gt;
&lt;p&gt;If you do not already have a pf.conf because the only requirement was this, simply insert this line in an empty pf.conf and then run:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;service pf enable
service pf start
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Updating the Blocked Countries List&lt;/h2&gt;
&lt;p&gt;Next, create a file (in my case, I called it &lt;code&gt;/usr/local/sbin/update_blocked_countries.sh&lt;/code&gt;) with content similar to the one below. Replace "CC" with the country codes you want to block. For example, to block France, Germany, and Italy, insert "FR:DE:IT".&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UPDATE&lt;/strong&gt;: pf seems to have difficulty processing very large lists, resulting in errors. For this reason, I modified the following script to insert the lists in 'chunks,' in order to prevent the procedure from failing.&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;#!/bin/sh

# Original file containing the IP addresses
original_file=&amp;quot;/var/db/blocked_countries.txt&amp;quot;

# Generate the blocked countries file
/usr/local/bin/ipup -p -t CC &amp;gt; $original_file

# Number of records per batch
batch_size=10000

# Temporary file for the current batch
temp_file=&amp;quot;/tmp/blocked_countries_temp.txt&amp;quot;

# Initialize the line counter
line_count=0

# Function to add a batch of records
add_batch() {
    echo &amp;quot;Adding records from $temp_file to pf table...&amp;quot;
    /sbin/pfctl -t blocked_countries -T add -f &amp;quot;$temp_file&amp;quot;
    if [ $? -ne 0 ]; then
        echo &amp;quot;Error adding records from $temp_file. Exiting.&amp;quot;
        exit 1
    fi
    # Empty the temporary file
    &amp;gt; $temp_file
}

# Replace the table with an empty file to avoid conflicts
echo -n &amp;gt; /tmp/empty_blocked_countries.txt
/sbin/pfctl -t blocked_countries -T replace -f /tmp/empty_blocked_countries.txt

# Read the original file line by line
while IFS= read -r line; do
    # Add the line to the temporary file
    echo &amp;quot;$line&amp;quot; &amp;gt;&amp;gt; &amp;quot;$temp_file&amp;quot;
    line_count=$((line_count + 1))

    # If we've reached the batch size, add the records and reset the counter
    if [ $line_count -ge $batch_size ]; then
        add_batch
        line_count=0
    fi
done &amp;lt; &amp;quot;$original_file&amp;quot;

# Add any remaining records
if [ $line_count -gt 0 ]; then
    add_batch
fi

echo &amp;quot;All records added successfully.&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Make it executable:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;chmod a+rx /usr/local/sbin/update_blocked_countries.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Running the command &lt;code&gt;/usr/local/sbin/update_blocked_countries.sh&lt;/code&gt; will show the status of the operation, for example:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;314159 addresses added.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This means that the list has been generated for the inserted country codes, and pf has updated the &lt;code&gt;&amp;lt;blocked_countries&amp;gt;&lt;/code&gt; table.&lt;/p&gt;
&lt;p&gt;It is advisable to update the lists occasionally since IP ranges "move" from one country to another.&lt;/p&gt;
&lt;h2&gt;Automating with Cron&lt;/h2&gt;
&lt;p&gt;We can use cron to perform this task. Edit the file &lt;code&gt;/etc/crontab&lt;/code&gt; by adding a line like:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-cron"&gt;55 9 * * *    root    /usr/local/bin/ipdb-update.sh &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; /usr/local/sbin/update_blocked_countries.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In this example, at 9:55 every day, the databases will be updated, lists generated, and fed to pf.&lt;/p&gt;
&lt;p&gt;This setup will also work correctly on a read-only FreeBSD system on UFS, &lt;a href="https://it-notes.dragas.net/2024/05/31/freebsd-tips-and-tricks-native-ro-rootfs/"&gt;as described in a previous article&lt;/a&gt;. The only precaution in this case is to ensure the crontab runs at every boot since, on every machine restart, the contents of /var will be erased and recreated from scratch. Therefore, also add a line like:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-cron"&gt;@reboot    root    /usr/local/bin/ipdb-update.sh &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; /usr/local/sbin/update_blocked_countries.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Additionally, in a read-only system, you need to add a mount for tmpfs for the &lt;code&gt;/usr/local/etc/ipdb/IPRanges&lt;/code&gt; directory in the &lt;code&gt;/etc/fstab&lt;/code&gt; file to make it writable:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-fstab"&gt;tmpfs /usr/local/etc/ipdb/IPRanges/     tmpfs rw 0 0
&lt;/code&gt;&lt;/pre&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Sun, 16 Jun 2024 07:40:45 +0000</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2024/06/16/freebsd-blocking-country-access/</guid><category>freebsd</category><category>tutorial</category><category>pf</category><category>hosting</category><category>firewall</category><category>networking</category><category>security</category></item></channel></rss>