Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Index

no genAI, human made By using the “Human Made” badge, I affirm that I am not using data-generated images (“genAI”) or data-generated text (“genAI”) in any asset of my work. My workflow consists of only using my skill crafted from experience and practice.

This site serves as a convenient, central location for standalone code snippets, commands, and tutorials. Some of these used to be on my blog, but I figure that they are best placed here so that potential readers don’t have to look through my blog archives, as my blog mostly contains infodumps about tech and other topics.

Feel free to use any of the content here without obligation. All articles are CC0 and no credits or attributions are required.

Some of these are links to my guides on FOSS Wiki.

Get the latest release from GitHub

Mass delete repos from Forgejo, Gitea, or Codeberg

Profile Sync Daemon config for Waterfox

It’s basically the same as Firefox except for the path to the profiles.

Create the file /usr/share/psd/browsers/waterfox and add the following contents.

if [[ -d "$XDG_CONFIG_HOME/waterfox" ]]; then
    index=0
    PSNAME="$browser"
    while read -r profileItem; do
        if [[ $(echo "$profileItem" | cut -c1) = "/" ]]; then
            # path is not relative
            DIRArr[$index]="$profileItem"
        else
            # we need to append the default path to give a
            # fully qualified path
            DIRArr[$index]="$XDG_CONFIG_HOME/waterfox/$profileItem"
        fi
        (( index=index+1 ))
    done < <(grep '^[Pp]ath=' "$XDG_CONFIG_HOME/waterfox/profiles.ini" | sed 's/[Pp]ath=//')
fi

check_suffix=1

if [[ -d "$HOME"/.waterfox ]]; then
    index=0
    PSNAME="$browser"
    while read -r profileItem; do
        if [[ $(echo "$profileItem" | cut -c1) = "/" ]]; then
            # path is not relative
            DIRArr[$index]="$profileItem"
        else
            # we need to append the default path to give a
            # fully qualified path
            DIRArr[$index]="$HOME/.waterfox/$profileItem"
        fi
        (( index=index+1 ))
    done < <(grep '^[Pp]ath=' "$HOME"/.waterfox/profiles.ini | sed 's/[Pp]ath=//')
fi

check_suffix=1

Add Waterfox to ~/.config/psd/psd.conf.

BROWSERS=(waterfox)

Restart the psd.service.

systemctl --user restart psd.service

Waterfox swim real fast now 😀. In theory, at least.

Profile Sync Daemon config for Waterfox Flatpak

Create the file /usr/share/psd/browsers/waterfox-flatpak and add the following contents:

if ! flatpak override --user --show net.waterfox.waterfox | grep -q "/run/user/$UID/psd"; then
    flatpak override --user net.waterfox.waterfox --filesystem=/run/user/$UID/psd
fi

if [[ -d "$HOME"/.var/app/net.waterfox.waterfox/.waterfox ]]; then
    index=0
    PSNAME="$browser"
    while read -r profileItem; do
        if [[ $(echo "$profileItem" | cut -c1) = "/" ]]; then
            # path is not relative
            DIRArr[$index]="$profileItem"
        else
            # we need to append the default path to give a
            # fully qualified path
            DIRArr[$index]="$HOME/.var/app/net.waterfox.waterfox/.waterfox/$profileItem"
        fi
        (( index=index+1 ))
    done < <(grep '[Pp]'ath= "$HOME"/.var/app/net.waterfox.waterfox/.waterfox/profiles.ini | sed 's/[Pp]ath=//')
fi

check_suffix=1

Add Waterfox Flatpak to ~/.config/psd/psd.conf:

BROWSERS=(waterfox-flatpak)

Restart psd.service.

systemctl --user restart psd.service

Profile Sync Daemon config for Vivaldi Flatpak

Create the file /usr/share/psd/browsers/vivaldi-flatpak and add the following contents:

if ! flatpak override --user --show com.vivaldi.Vivaldi | grep -q "/run/user/$UID/psd"; then
    flatpak override --user com.vivaldi.Vivaldi --filesystem=/run/user/$UID/psd
fi

DIRArr[0]="$HOME/.var/app/com.vivaldi.Vivaldi/config/$browser"
PSNAME="$browser"-bin

Add Vivaldi Flatpak to ~/.config/psd/psd.conf:

BROWSERS=(vivaldi-flatpak)

Restart psd.service.

systemctl --user restart psd.service

Profile Sync Daemon for Zen Browser Flatpak

profile-sync-daemon is a li’l daemon for managing browser profiles in a temporary RAM filesystem and periodically syncing them back to the physical disk. This improves the responsiveness of the browser and reduces wear on physical drives.

Zen Browser is not officially supported by the profile-sync-daemon, but there is community support for the non-Flatpak version of Zen Browser. This can be activated by copying /usr/share/psd/contrib/zen to /usr/share/psd/browsers/. However, this won’t work for the Flatpak version of Zen Browser unless you do the following:

  • Modify the /usr/share/psd/contrib/zen file to use the path to the Flatpak’s browser profile.
  • Give the Flatpak permission to access the temporary RAM filesystem.

To give the Zen Browser Flatpak permission to access the temporary RAM filesystem where PSD stores the profiles, run the following command:

flatpak override --user app.zen_browser.zen --filesystem=/run/user/$UID/psd

Now put the following contents into a file at /usr/share/psd/browsers/zen-flatpak:

if [[ -d "$HOME"/.var/app/app.zen_browser.zen/cache/zen ]]; then
 index=0
 PSNAME="$browser"
 while read -r profileItem; do
  if [[ $(echo "$profileItem" | cut -c1) = "/" ]]; then
   # path is not relative
   DIRArr[$index]="$profileItem"
  else
   # we need to append the default path to give a
   # fully qualified path
   DIRArr[$index]="$HOME/.var/app/app.zen_browser.zen/cache/zen/$profileItem"
  fi
  (( index=index+1 ))
 done < <(grep '^[Pp]'ath= "$HOME"/.var/app/app.zen_browser.zen/.zen/profiles.ini | sed 's/^[Pp]ath=//')
fi

check_suffix=1

Edit ~/.config/psd/psd.conf:

BROWSERS=(zen-flatpak)

Restart the psd.service:

systemctl --user restart psd.service

That’s it.

Home networking and preventing DNS leaks

I think I’ve figured out how to prevent my Fedora Linux desktop from leaking DNS to Xfinity. My Linux desktop is part of a Tailscale network that uses Mullvad’s ad-blocking and malware blocking public DNS server, but I still have DNS leaks because my main network interface is using the Xfinity DNS servers from its DHCP connection via the Xfinity modem. Below are the steps I took to prevent DNS leaks to Xfinity.

systemd-networkd

NetworkManager is the default on Fedora 40. I disabled NetworkManager and enabled systemd-networkd with the following configuration:

[Match]  
Name=eno1  
  
[Network]  
DHCP=yes  
DNS=100.100.100.100  
DNSSEC=allow-downgrade  
  
[DHCPv4]  
UseDNS=no  

In the [DHCPv4] section, UseDNS=no ensures that you’re not using the DNS servers provided by the DHCP connection. In my case, my DHCP connection via my Xfinity modem was setting the DNS to the Xfinity DNS servers. So that is no longer the case now. For good measure, I added my tailnet’s DNS as a static DNS server in the [Network] section.

resolvectl status now shows that my primary network interface eno1 uses only the DNS from my Tailscale network, which is 100.100.100.100.

resolvectl status

Disable IPv6

Another possible source of DNS leaks is IPv6. On Fedora 40, the way to disable IPv6 is by adding a kernel argument to the GRUB bootloader configuration. This can be done with the following command:

sudo grubby --args`ipv6.disable`1 --update-kernel=ALL  

Reboot the system for the change to take effect.

Ensure Firefox or LibreWolf are not using DNS over HTTPS

In most cases Firefox/LibreWolf are configured to use the system DNS resolver by default, but if you have it configured to use one of the “protection” settings for DNS over HTTPS, this could be a source of DNS leaks.

Now you can check for DNS leaks with Mullvad’s connection checker.

Network-wide bullshit-blocking setup with Blocky and Tailscale

I will use an Orange Pi 5 Plus, but any device, including single board computers, should work, as long as they can run the latest stable Debian or Armbian release.

Orange Pi 5 Plus

  • Unbound for recursive DNS resolver on 127.0.0.1:5335
  • Blocky for DNS proxy, ad-blocking, and malware-blocking on 0.0.0.0:53. Uses Unbound on 127.0.0.1:5335 as upstream resolver.
  • Tailscale with –accept-dns=false
  • unbound-resolveconf.service should be disabled, and /etc/resolv.conf should not be managed by any other service.

I just put the following contents into /etc/resolv.conf for the Orange Pi 5 Plus’s local DNS resolution:

nameserver 9.9.9.9
nameserver 149.112.112.112

I have Blocky configured to use the strict strategy for the upstreams setting, so after a timeout of the topmost upstream server it will fallback to the next one, which is Quad9.

An idea I have is to setup a cheap VPS on Vultr or something and run a public DNS resolver on it, but Quad9 is fine for now.

I have the Orange Pi 5 Plus’s Tailnet IP address configured to be my Tailnet’s global nameserver. This can be done through the Tailscale admin console under the DNS tab. So every device on my Tailnet that uses MagicDNS will be using Blocky and Unbound.

Blocky configuration

upstreams:  
  strategy: strict  
  groups:  
    default:  
      - 127.0.0.1:5335  
      - 9.9.9.9  
      - 149.112.112.112  
  
blocking:  
  denylists:  
    ads:  
      - <https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts>  
      - <https://adaway.org/hosts.txt>  
      - <https://v.firebog.net/hosts/AdguardDNS.txt>  
    suspicious:  
      - <https://v.firebog.net/hosts/static/w3kbl.txt>  
    tracking:  
      - <https://gitlab.com/quidsup/notrack-blocklists/raw/master/notrack-blocklist.txt>  
      - <https://v.firebog.net/hosts/Easyprivacy.txt>  
      - <https://v.firebog.net/hosts/Prigent-Ads.txt>  
    malicious:  
      - <http://phishing.mailscanner.info/phishing.bad.sites.conf>  
      - <https://v.firebog.net/hosts/Prigent-Crypto.txt>  
      - <https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews/hosts>  
  
  clientGroupsBlock:  
    default:  
      - ads  
      - suspicious  
      - tracking  
      - malicious  
  
ports:  
  dns: 53  
  http: 4000  
  
prometheus:  
  enable: yes  
  
caching:  
  minTime: 60s  
  maxItemsCount: 10000  
  prefetching: yes  
  prefetchMaxItemsCount: 2000  
  
queryLog:  
  type: csv-client  
  target: /home/jas/dns-query-logs  
  logRetentionDays: 5  
clientLookup:  
  upstream: 10.0.0.1  
  singleNameOrder:  
    - 1  

If you’re using a firewall, make sure ingress traffic to UDP port 53 is allowed on the Tailscale interface.

Tailscale DNS

Go to the Tailscale admin web UI. In the DNS tab, under Global nameservers, add the Orange Pi 5+’s tailnet IP address. Remove any other nameservers from this section. Make sure Override DNS servers is switched on. All the devices on your tailnet that have MagicDNS enabled will now be using Blocky and Unbound to block and resolve DNS queries.

Not using Tailscale?

This setup can be achieved without using Tailscale by setting the local nameserver on each of your devices to the Orange Pi 5+’s LAN IP address. The downside to this is that it is only available to devices connected to your LAN.

Setup a thick VNET jail on FreeBSD

Setup the VNET bridge

Create the bridge.

ifconfig bridge create

Attach the bridge to the main network interface. igc0 in this case. For some reason, the resulting bridge device is named bridge0.

ifconfig bridge0 addm igc0

To make this persistent across reboots, add the following to /etc/rc.conf.

defaultrouter="10.0.0.1"
cloned_interfaces="bridge0"
ifconfig_igc0bridge="inet 10.0.0.8/24 addm igc0 up"

Create the classic (thick) jail

Create the ZFS datasets for the jails. We’ll use basejail as a template for subsequent jails.

zfs create -o mountpoint=/jails naspool/jails
zfs create naspool/jails/basejail

Use the bsdinstall utility to bootstrap the base system to the basejail.

export DISTRIBUTIONS="base.txz"
export BSDINSTALL_DISTSITE=https://download.freebsd.org/ftp/releases/amd64/14.2-RELEASE/
bsdinstall jail /jails/basejail

Run freebsd-update to update the base jail.

freebsd-update -b /jails/basejail fetch install
freebsd-update -b /jails/basejail IDS

We now snapshot the basejail and create a clone of this snapshot for the torrenting jail that we will use for Anna’s Archive.

zfs snapshot naspool/jails/basejail@`freebsd-version`
zfs clone naspool/jails/basejail@`freebsd-version` naspool/jails/torrenting

We now use the following configuration for /etc/jail.conf.

torrenting {
    exec.consolelog = "/var/log/jail_console_${name}.log";
    allow.raw_sockets;
    exec.clean;
    mount.devfs;
    devfs_ruleset = 11;
    path = "/jails/${name}";
    host.hostname = "${name}";
    vnet;
    vnet.interface = "${epair}b";
    $id = "127";
    $ip = "10.0.0.${id}/24";
    $gateway = "10.0.0.1";
    $bridge = "bridge0";
    $epair = "epair${id}";
    
    exec.prestart = "/sbin/ifconfig ${epair} create up";
    exec.prestart += "/sbin/ifconfig ${epair}a up descr jail:${name}";
    exec.prestart += "/sbin/ifconfig ${bridge} addm ${epair}a up";
    exec.start += "/sbin/ifconfig ${epair}b ${ip} up";
    exec.start += "/sbin/route add default ${gateway}";
    exec.start += "/bin/sh /etc/rc";
    exec.stop = "/bin/sh /etc/rc.shutdown";
    exec.poststop = "/sbin/ifconfig ${bridge} deletem ${epair}a";
    exec.poststop += "/sbin/ifconfig ${epair}a destroy";
}

Now we create the devfs ruleset to enable access to devices under /dev inside the jail. Add the following to /etc/devfs.rules.

[devfsrules_jail_vnet=11]
add include $devfsrules_hide_all
add include $devfsrules_unhide_basic
add include $devfsrules_unhide_login
add include $devfsrules_jail
add path 'tun*' unhide
add path 'bpf*' unhide

Enable the jail utility in /etc/rc.conf.

sysrc jail_enable="YES"
sysrc jail_parallel_start="YES"

Start the jail service for torrenting jail.

service jail start torrenting

Setting up Wireguard inside the jail

Since we have the /dev/tun* devfs rule, we now need to install Wireguard inside the jail.

jexec -u root torrenting

pkg install wireguard-tools wireguard-go

Download a Wireguard configuration for ProtonVPN, and save it to /usr/local/etc/wireguard/wg0.conf.

Enable Wireguard to run when the jail boots up.

sysrc wireguard_enable="YES"
sysrc wireguard_interfaces="wg0"

Start the Wireguard daemon and make sure you are connected to it properly.

service wireguard start

curl ipinfo.io

The curl command should display the IP address of the Wireguard server defined in /usr/local/etc/wireguard/wg0.conf.

Setting up qBittorrent inside the jail

Install the qbittorrent-nox package.

pkg install -y qbittorrent-nox

Before running the daemon from /usr/local/etc/rc.d/qbittorrent, we must run the qbittorrent command from the shell so that we can see the default password generated for the web UI. For some reason it is not shown in any logs, and the qbittorrent nox manpage wrongly says the password is “adminadmin”.

pkg install -y sudo
sudo -u qbittorrent qbittorrent-nox --profile=/var/db/qbittorrent/conf --save-path=/var/db/qbittorrent/Downloads --confirm-legal-notice 

Copy the password displayed after running the command. Login to the qBittorrent web UI at http://10.0.0.127:8080 with login admin and the password you copied. In the web UI, open the options menu and go over to the Web UI tab. Change the login password to your own. Save the options to close the menu.

Now press CTRL-c to stop the qbittorrent-nox process. Make the following changes to the torrenting jail’s /etc/rc.conf.

sysrc qbittorrent_enable="YES"
sysrc qbittorrent_flags="--confirm-legal-notice"

Enable the qBittorrent daemon.

service qbittorrent start

Go back to the web UI at http://10.0.0.127:8080. Go to the options menu and go over to the Advanced tab, which is the very last tab. Change the network interface to wg0.

Finding the forwarded port that the ProtonVPN server is using

Install the libnatpmp package.

Make sure that port forwarding is allowed on the server you’re connected to, which it should be if you enabled it while creating the Wireguard configuration on the ProtonVPN website. Run the natpmpc command against the ProtonVPN Wireguard gateway.

natpmpc -g 10.2.0.1

If the output looks like the following, you’re good.

initnatpmp() returned 0 (SUCCESS)
using gateway : 10.2.0.1
sendpublicaddressrequest returned 2 (SUCCESS)
readnatpmpresponseorretry returned 0 (OK)
Public IP address : 62.112.9.165
epoch = 58081
closenatpmp() returned 0 (SUCCESS)

Now create the UDP and TCP port mappings, then loop natpmpc so that it doesn’t expire.

while true ; do date ; natpmpc -a 1 0 udp 60 -g 10.2.0.1 && natpmpc -a 1 0 tcp 60 -g 10.2.0.1 || { echo -e "ERROR with natpmpc command \a" ; break ; } ; sleep 45 ; done

The port allocated for this server is shown on the line that says “Mapped public port XXXXX protocol UDP to local port 0 lifetime 60”. Port forwarding is now activated. Copy this port number and, in the qBittorrent web UI options menu, go to the Connections tab and enter it into the “Port used for incoming connections” box. Make sure to uncheck the “Use UPnP/NAT-PMP port forwarding from my router” box.

If the loop terminates, you’ll need to re-run this loop script each time you start a new port forwarding session or the port will only stay open for 60 seconds.

P2P NAT port forwarding script with supervisord

Install supervisord.

sudo pkg install -y py311-supervisor

Enable the supervisord service.

sudo sysrc supervisord_enable="YES"

Edit /usr/local/etc/supervisord.conf, and add the following to the bottom of the file.

[program:natpmpcd]
command=/usr/local/bin/natpmpcd
autostart=true

Add the following contents to a file at /usr/local/bin/natpmpcd.

#!/bin/sh

port=$(/usr/local/bin/natpmpc -a 1 0 udp 60 -g 10.2.0.1 | grep "Mapped public port" | awk '{print $4}')
echo "$port" | tee /usr/local/etc/natvpn_port.txt
curl -v --cookie /tmp/cookie.txt -d "json={\"listen_port\": \"$port\"}" http://localhost:8080/api/v2/app/setPreferences

while true; do
    date
    if ! /usr/local/bin/natpmpc -a 1 0 udp 60 -g 10.2.0.1 && /usr/local/bin/natpmpc -a 1 0 tcp 60 -g 10.2.0.1; then
        echo "error Failure natpmpc $(date)"
        break
    fi
    sleep 45
done

Ensure the script is executable with chmod +x /usr/local/bin/natpmpcd.

supervisord will start the above shell script automatically. Ensure supervisord service is started.

sudo service supervisord start

Every time it runs, the script will print out the forwarded port number at /usr/local/etc/natvpn_port.txt, and update the listen_port preference in qBittorrent to the new forwarded port.

cat /usr/local/etc/natvpn_port.txt
48565

Install Chimera Linux

Requirements

  • UEFI
  • LVM on LUKS with unencrypted /boot

Disk partitioning

Use cfdisk to create the following partition layout.

Partition TypeSize
EFI+600M
boot+900M
LinuxRemaining space

Format the unencrypted partitions:

mkfs.vfat /dev/nvme0n1p1
mkfs.ext4 /dev/nvme0n1p2

Create LUKS on the remaining partition:

cryptsetup luksFormat /dev/nvme0n1p3
cryptsetup luksOpen /dev/nvme0n1p3 crypt

Create a LVM2 volume group for /dev/nvme0n1p3, which is located at /dev/mapper/crypt:

vgcreate chimera /dev/mapper/crypt

Create logical volumes in the volume group:

lvcreate --name swap -L 8G chimera
lvcreate --name root -l 100%FREE chimera

Create the filesystems for the logical volumes:

mkfs.ext4 /dev/chimera/root
mkswap /dev/chimera/swap

Create mount points for the chroot and mount the filesystems:

mkdir /media/root
mount /dev/chimera/root /media/root
mkdir /media/root/boot
mount /dev/nvme0n1p2 /media/root/boot
mkdir /media/root/boot/efi
mount /dev/nvme0n1p1 /media/root/boot/efi

Bootstrap

Chimera-bootstrap and chroot

chimera-bootstrap /media/root
chimera-chroot /media/root

Update the system:

apk update
apk upgrade --available

Install kernel, cryptsetup, and lvm2 packages:

apk add linux-stable cryptsetup-scripts lvm2

Fstab

genfstab / >> /etc/fstab

Crypttab

echo "crypt /dev/disk/by-uuid/$(blkid -s UUID -o value /dev/nvme0n1p3) none luks" > /etc/crypttab

Initramfs refresh

update-initramfs -c -k all

GRUB

apk add grub-x86_64-efi
grub-install --efi-directory=/boot/efi --target=x86_64-efi

Post-installation

passwd root
apk add zsh bash
useradd -c "Jeffrey Serio" -m -s /usr/bin/zsh -U jas
passwd jas

Add the following lines to /etc/doas.conf:

# Give jas access
permit nopass jas

Set hostname, timezone, and hwclock:

echo "falinesti" > /etc/hostname
ln -sf /usr/share/zoneinfo/America/Chicago /etc/localtime
echo localtime > /etc/hwclock

Xorg and Xfce4

apk add xserver-xorg xfce4

Reboot the machine.

Post-reboot

Login as jas. Run startxfce4. Connect to the internet via NetworkManager.

Ensure wireplumber and pipewire-pulse are enabled:

dinitctl enable wireplumber
dinitctl start wireplumber
dinitctl enable pipewire-pulse
dinitctl start pipewire-pulse

Install CPU microcode:

doas apk add ucode-intel
doas update-initramfs -c -k all

Install other packages

doas apk add chrony
doas dinitctl enable chrony
doas apk add ...

Install Debian with LUKS2 on Btrfs with GRUB

Create an RPM repository

Install Cgit with Caddy

Dependencies

  • xcaddy package from releases page.

Install caddy-cgi:

xcaddy build --with github.com/aksdb/caddy-cgi/v2

Install remaining dependencies:

sudo apt install gitolite3 cgit python-is-python3 python3-pygments python3-markdown docutils-common groff

Configuration

Make a git user.

sudo adduser --system --shell /bin/bash --group --disabled-password --home /home/git git

Configure gitolite for the git user in ~/.gitolite.rc.

UMASK => 0027,
GIT_CONFIG_KEYS => 'gitweb.description gitweb.owner gitweb.homepage gitweb.category',

Add caddy user to the git group.

sudo usermod -aG git caddy

Configure cgit in /etc/cgitrc:

#
# cgit config
# see cgitrc(5) for details

css=/cgit/cgit.css
logo=/cgit/cgit.png
favicon=/cgit/favicon.ico

enable-index-links=1
enable-commit-graph=1
enable-log-filecount=1
enable-log-linecount=1
enable-git-config=1

branch-sort=age
repository-sort=name

clone-url=https://git.hyperreal.coffee/$CGIT_REPO_URL git://git.hyperreal.coffee/$CGIT_REPO_URL ssh://git@git.hyperreal.coffee:$CGIT_REPO_URL

root-title=hyperreal.coffee Git repositories
root-desc=Source code and configs for my projects

##
## List of common mimetypes
##
mimetype.gif=image/gif
mimetype.html=text/html
mimetype.jpg=image/jpeg
mimetype.jpeg=image/jpeg
mimetype.pdf=application/pdf
mimetype.png=image/png
mimetype.svg=image/svg+xml

# Enable syntax highlighting
source-filter=/usr/lib/cgit/filters/syntax-highlighting.py

# Format markdown, rst, manpages, text files, html files, and org files.
about-filter=/usr/lib/cgit/filters/about-formatting.sh

##
### Search for these files in the root of the default branch of repositories
### for coming up with the about page:
##
readme=:README.md
readme=:README.org

robots=noindex, nofollow

section=personal-config

repo.url=doom-emacs-config
repo.path=/home/git/repositories/doom-emacs-config.git
repo.desc=My Doom Emacs config

Setting up Restic with rest-server

Context

I recently decided to start using my own home server to store my dotfiles. The main reasons are simplicity, privacy, and security. I previously stored them in a repository on my GitHub account and installed them with Ansible, but I have increasingly found it cumbersome when trying to keep them updated and in sync. On GitHub, the changes (and mistakes!) I make to my dotfiles are publicly viewable; sometimes I’ll make changes several times a day, sometimes scrapping a change entirely when I later realize it was not such a good idea or breaks something in my activity flow. I also would love the convenience of keeping SSH keys and GPG keychains in sync and updated, and storing them on a public server is obviously not an option, nor even in a private repository hosted on GitHub or GitLab.

Cue Restic

My home server is basically just my old 2013 MacBook Pro running Fedora Server edition. It has a 250GB SSD, which is more than enough for what I need. I also have a 1TB external SSD which I will use to emulate redundancy. I installed and configure the rest-server software to act as a backend for my Restic backups.

Setting up the rest server

First build the rest-server binary and move it to a directory in PATH. This step requires Go 1.11 or higher. Optionally, you can download the latest compiled rest-server binary from its releases page.

* GitHub :: restic/rest-server/releases

git clone https://github.com/restic/rest-server  
cd rest-server/  
CGO_ENABLED=0 go build -o rest-server ./cmd/rest-server  
sudo cp -v rest-server /usr/local/bin/  

I also configured the systemd unit file so that rest-server runs on startup with the appropriate flags. I need only configure the options User, Group, ExecStart, and ReadWritePaths in the [Service] section:

cd ~/rest-server/examples/systemd/  
ls .  

rest-server.service:

[Service]  
Type=simple  
User=restic-data  
Group=restic-data  
ExecStart=/usr/local/bin/rest-server --path /opt/restic-backups --no-auth  
Restart=always  
RestartSec=5  
  
# Optional security enhancements  

NoNewPrivileges=yes  
PrivateTmp=yes  
ProtectSystem=strict  
ProtectHome=yes  
ReadWritePaths=/opt/restic-backups  

Since this is a local home server, I pass the --no-auth flag to the rest-server ExecStart command.

I now create the restic-data user and group.

  • Ensure a default home directory is not created under /home by passing the -M flag.
  • Set a custom home directory for the user at /opt/restic-backups with the -d flag.
  • Ensure the shell is assigned to /sbin/nologin.
  • The restic-data user is not meant to be used for logging in, so we pass the --system flag.
  sudo useradd -c "Restic Data" -M -d /opt/restic-backups -s /sbin/nologin --system restic-data  
  • Ensure the backups path exists and has appropriate permissions.
  • Copy the systemd unit file to a location where systemd will look for it.
  • Enable and start the rest-server systemd service.
  sudo mkdir /opt/restic-backups  
  sudo chown -R restic-data:restic-data /opt/restic-backups  
  sudo cp -v rest-server.service /etc/systemd/system/  
  sudo systemctl daemon-reload  
  sudo systemctl enable --now rest-server.service  

Since I’m using a firewall, I ensure the port the rest-server listens on is allowed locally:

  sudo firewall-cmd --zone`FedoraServer --permanent --add-port`8000/tcp  
  sudo firewall-cmd --reload  

Now on the host, which in this case is my laptop, I have the Restic client installed from my distribution’s package repository.

  • Initialize a Restic storage repository on the server from the host, and supply it with a password. This password will be used every time I attempt to access the storage repository.
  • Backup my dotfiles
    restic -r rest:http://local-server:8000/dotfiles init  
    restic -r rest:http://local-server:8000/dotfiles backup ~/dotfiles  

One of the best features of Restic is that it makes restory backups really simple. It also provides snapshot functionality, so I can restore different versions of specific files from other snapshots.

restic -r rest:http://local-server:8000/dotfiles snapshots  
  
enter password for repository:  
repository 9a280eb7 opened successfully, password is correct  
ID        Time                  Host       Tags        Paths  
------------------------------------------------------------------------------  

11738fec  2021-04-12 09:13:17   toolbox                /var/home/jeff/dotfiles  
dfc99aa3  2021-04-12 10:31:39   toolbox                /var/home/jeff/dotfiles  
f951eedf  2021-04-12 11:25:21   toolbox                /var/home/jeff/dotfiles  
62371897  2021-04-12 18:43:53   toolbox                /var/home/jeff/dotfiles  
------------------------------------------------------------------------------  

4 snapshots  

Since Restic saves the backup’s absolute path, restoring it to / will ensure it is restored to its original location on the local filesystem. To restore a snapshot:

restic -r rest:http://local-server:8000/dotfiles restore dfc99aa3 --target /  

To list files in a snapshot:

restic -r rest:http://local-server:8000/dotfiles ls dfc99aa3  

Yay, very nice!

Resources

Setting up a Bluesky PDS with Podman on CentOS Stream 10

Self-hosted container registry with web UI

Source: <https://github.com/Joxit/docker-registry-ui>

Docker/Podman compose

services:
  registry-ui:
    image: joxit/docker-registry-ui:main
    restart: always
    ports:
      - "127.0.0.1:4433:80"
    environment:
      - SINGLE_REGISTRY=true
      - REGISTRY_TITLE=hyperreal's Container Registry
      - DELETE_IMAGES=true
      - SHOW_CONTENT_DIGEST=true
      - NGINX_PROXY_PASS_URL=http://registry-server:5000
      - SHOW_CATALOG_NB_TAGS=true
      - CATALOG_MIN_BRANCHES=1
      - CATALOG_MAX_BRANCHES=1
      - TAGLIST_PAGE_SIZE=100
      - REGISTRY_SECURED=false
      - CATALOG_ELEMENTS_LIMIT=1000
    container_name: registry-ui

  registry-server:
    image: registry:2.8.2
    restart: always
    environment:
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: '[http://aux-remote.carp-wyvern.ts.net]'
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods: '[HEAD,GET,OPTIONS,DELETE]'
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Credentials: '[true]'
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers: '[Accept,Cache-Control]'
      REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers: '[Docker-Content-Digest]'
      REGISTRY_STORAGE_DELETE_ENABLED: 'true'
    volumes:
      - ./registry/data:/var/lib/registry
    container_name: registry-server

Authorization and Authentication

For a public registry with authentication, the following headers are required:

environment:
  REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers: '[Authorization,Accept,Cache-Control]'

For a private registry without authentication, the following headers are required:

environment:
  REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers: '[Accept,Cache-Control]'

Caddy reverse proxy

Public registry

registry.hyperreal.coffee {
    reverse_proxy localhost:4433
}

Private registry via Tailnet

aux-remote.carp-wyvern.ts.net {
    reverse_proxy localhost:4433
}

Ensure the following is added to /etc/default/tailscaled:

TS_PERMIT_CERT_UID=caddy

The above will ensure Caddy receives SSL certs from the Tailscale daemon.

Using Codeberg, Gitea, or Forgejo as OIDC provider for Tailscale