Triaxx Web Log

Posts

Let's Encrypt

Obtain or renew a certificate, but do not install it:

email=triaxx@triaxx.io
domains="triaxx_io triaxx_net"
triaxx_io_subdomains="dev mx ns0 ns1 www"
triaxx_net_subdomains="dns mail ns www"

for _domain in ${domains} ; do 
  eval subdomains=\$${_domain}_subdomains
  domain=`echo ${_domain} | sed -e 's,_,\.,g'`
  domain_args="--domain ${domain}"
  for _subdomain in ${subdomains} ; do
    domain_args="${domain_args} --domain ${_subdomain}.${domain}"
  done
  certbot certonly    \
    --non-interactive \
    --agree-tos       \
    --email ${email}  \
    --nginx           \
    ${domain_args}    \
    --expand
done

Plan the automatic (monthly) update:

crontab -e
0  0  1  *  *  /usr/bin/certbot renew

Restart NGINX when new certificate is deployed:

Loop device

Setup

Mount a GPT disk stored on LVM:

losetup --partscan loop0 /dev/vg0/lv-vm0
mount /dev/loop0p3 /mnt

Gentoo

Installation

Fetch an installation image and extract it in a logical volume for use with Xen)

Partitioning

fdisk /dev/xvda 

Welcome to fdisk (util-linux 2.41.3).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Device does not contain a recognized partition table.
Created a new DOS (MBR) disklabel with disk identifier 0xeada774e.

Command (m for help): o
Created a new DOS (MBR) disklabel with disk identifier 0x10aabe84.

Command (m for help): n
Partition type
   p   primary (0 primary, 0 extended, 4 free)
   e   extended (container for logical partitions)
Select (default p): 

Using default response p.
Partition number (1-4, default 1): 
First sector (2048-67108863, default 2048): 
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-67108863, default 67108863): +1G

Created a new partition 1 of type 'Linux' and of size 1 GiB.

Command (m for help): n
Partition type
   p   primary (1 primary, 0 extended, 3 free)
   e   extended (container for logical partitions)
Select (default p): 

Using default response p.
Partition number (2-4, default 2): 
First sector (2099200-67108863, default 2099200): 
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2099200-67108863, default 67108863): +4G

Created a new partition 2 of type 'Linux' and of size 4 GiB.

Command (m for help): t
Partition number (1,2, default 2): 
Hex code or alias (type L to list all): 82

Changed type of partition 'Linux' to 'Linux swap / Solaris'.

Command (m for help): n
Partition type
   p   primary (2 primary, 0 extended, 2 free)
   e   extended (container for logical partitions)
Select (default p): 

Using default response p.
Partition number (3,4, default 3): 
First sector (10487808-67108863, default 10487808): 
Last sector, +/-sectors or +/-size{K,M,G,T,P} (10487808-67108863, default 67108863): 

Created a new partition 3 of type 'Linux' and of size 27 GiB.

Command (m for help): p
Disk /dev/xvda: 32 GiB, 34359738368 bytes, 67108864 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x10aabe84

Device     Boot    Start      End  Sectors Size Id Type
/dev/xvda1          2048  2099199  2097152   1G 83 Linux
/dev/xvda2       2099200 10487807  8388608   4G 82 Linux swap / Solaris
/dev/xvda3      10487808 67108863 56621056  27G 83 Linux

Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.

Filesystem creation

mkfs.xfs -c options=/usr/share/xfsprogs/mkfs/lts_6.18.conf /dev/xvda3
mkfs.ext4 /dev/xvda1
mkswap /dev/xvda2
mkdir --parents /mnt/gentoo
mount /dev/xvda3 /mnt/gentoo
swapon /dev/xvda2
mkdir /mnt/gentoo/boot
mount /dev/xvda1 /mnt/gentoo/boot

Stage installation

wget https://distfiles.gentoo.org/releases/amd64/autobuilds/20260503T164604Z/stage3-amd64-hardened-openrc-20260503T164604Z.tar.xz

Verify files:

DNS

Configure a DNS server on OpenBSD.

nsd

# rcctl enable nsd

Create configuration file:

# cat > /var/nsd/etc/nsd.conf << EOF
remote-control:
        control-enable: yes
        control-interface: /var/run/nsd.sock

server:
        hide-version: yes

zone:
        name: "triaxx.xyz"
        zonefile: "master/triaxx.io.zone"
EOF

Check that configuration is correct:

# nsd-checkconf /var/nsd/etc/nsd.conf
# echo $?
0

Create zone file:

# cat > /var/nsd/zones/master/triaxx.io.zone << EOF
$TTL 86400
$ORIGIN triaxx.io.
@              IN      SOA     ns0.triaxx.io. hostmaster.triaxx.io. (
                        2026043000     ; serial
                              1800     ; refresh (30 mins)
                               900     ; retry   (15 mins)
                            604800     ; expire   (7 days)
                              1200 )   ; minimum (20 mins)
@              IN      NS      ns0
@              IN      NS      ns1
@              IN      MX  10  mx
@              IN      A       175.52.128.39
mx             IN      A       175.52.128.39
ns0            IN      A       175.52.128.39
ns1            IN      A       105.7.42.123
EOF

Check that zone is correct:

OpenBSD

Installation

Fetch the installation image (that can be use with Xen):

# wget https://cdn.openbsd.org/pub/OpenBSD/7.8/amd64/install78.img

The installer can be used to easily install a functional system.

Packages

Install pkg_locate to find files in OpenBSD packages:

# pkg_add pkglocatedb  
quirks-7.147 signed on 2025-10-10T23:15:26Z
pkglocatedb-1.5: ok

Services

Enable and start nsd:

# rcctl enable nsd
# rcctl start nsd

nftables

Netfilter configuration

Enable IP forwarding:

# sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1

Make it persitent:

echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf

Connection forwarding

#!/usr/sbin/nft -f

flush ruleset

define inet_if = "enp1s0"
define inet_ip = "194.153.151.88"
define vm1_ip  = "192.168.0.2"

table inet filter {
  chain input {
    type filter hook input priority filter;
  }

  chain forward {
    type filter hook forward priority filter;
    ct state established,related accept
  }

  chain output {
    type filter hook output priority filter;
  }
}

table ip nat {
  chain prerouting {
    type nat hook prerouting priority dstnat;
    iifname $inet_if tcp dport 80 dnat to $vm1_ip
  }

  chain postrouting {
    type nat hook postrouting priority srcnat;
    ip saddr $vm1_ip oifname $inet_if snat to $inet_ip
  }
}

ACL

Access Control List

Set read + execute permissions to /etc/fooctl for jdoe user:

setfactl --modify user:jdoe:rX /etc/fooctl
ls -adl /etc/fooctl
drwxr-x---+ 2 root root 4096 Apr  8 11:59 /etc/fooctl

Set read permission to /etc/fooctl/fooctl.conf for jdoe user:

setfactl --modify user:jdoe:r /etc/fooctl/fooctl.conf
ls -adl /etc/fooctl/fooctl.conf
-rw-rw-r--+ 1 root root 1943 Apr  8 11:59 /etc/fooctl/fooctl.conf
getfacl /etc/fooctl/fooctl.conf
getfacl: Removing leading '/' from absolute path names
# file: etc/fooctl/fooctl.conf
# owner: root
# group: root
user::rw-
user:jdoe:r--
group::rw-
mask::rw-
other::r--

Remove read permission from /etc/fooctl/fooctl.conf for jdoe user:

Netstat

Show listening TCP connections:

netstat --listening --tcp

Show programs associated to listening TCP and UDP connections (root privilege needed):

netstat --listening --tcp --udp --program

Xen

Dom0 is here Debian GNU/Linux Trixie. Install hypervisor:

apt install xen-hypervisor
systemctl reboot

OpenBSD as DomU

Add file /etc/xen/auto/obsd.cfg

type       = "hvm"
name       = "obsd"
memory     = 4096
vcpus      = 2
disk       = [ '/opt/xen/install78.iso,,xvdc,cdrom', '/dev/vg0/lv-obsd,,xvda' ]
vif        = [ 'mac=00:16:3e:a1:ff:01,bridge=xenbr0' ]
serial     = "pty"
vnc        = 1
vnclisten  = 127.0.0.1
vncdisplay = 42

Create bridge interface:

cat > /etc/systemd/network/xenbr0.netdev << EOF
Name=xenbr0
Kind=bridge
EOF
cat > /etc/systemd/network/xenbr0.network << EOF
[Match]
Name=xenbr0

[Network]
Address=192.168.0.1/24
EOF
systemctl enable --now systemd-networkd

Redirect output to serial console:

xl create -c /etc/xen/auto/obsd.cfg

From the VNC viewer:

boot> set tty com0

LVM

Initialize partition sda5 for use by LVM:

pvcreate /dev/sda5

Create Volume Group vg0 on partition sda5:

vgcreate vg0 /dev/sda5

Create Logical Volume lv-data in Volum Group vg0:

lvcreate --size 64g vg0 --name lv-data

Reduce size of Logical Volume lv-data in Volum Group vg0 when lv-data does not contain filesystem:

lvreduce --size 16g vg0/lv-data

Disk encryption

LUKS

Erase table and filesystem signatures and overwrite device with random data:

wipefs --all /dev/sde
dd if=/dev/urandom of=/dev/sde bs=4M status=progress

Initialize a LUKS partition and open it:

cryptsetup luksFormat /dev/sde

WARNING!
========
This will overwrite data on /dev/sde irrevocably.

Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for /dev/sde: 
Verify passphrase:
cryptsetup open /dev/sde foo
Enter passphrase for /dev/sde:

Make filesystem and mount it:

mkfs.ext4 /dev/mapper/foo
mke2fs 1.47.4 (6-Mar-2025)
Creating filesystem with 111616 1k blocks and 27888 inodes
Filesystem UUID: e4805c08-4d97-48c8-921f-91b203279131
Superblock backups stored on blocks: 
        8193, 24577, 40961, 57345, 73729

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (4096 blocks): done
Writing superblocks and filesystem accounting information: done 

mount /dev/mapper/foo /mnt

GPG

Configuration

See short key id when listing keys:

echo "keyid-format short" >> ~/.gnupg/gpg.conf

Generation

Generate a main key to sign and to certificate and a subkey to encrypt:

gpg --full-generate-key
gpg (GnuPG) 2.5.18; Copyright (C) 2025 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
   (9) ECC (sign and encrypt) *default*
  (10) ECC (sign only)
  (14) Existing key from card
  (16) ECC and Kyber
Your selection?
Please select which elliptic curve you want:
   (1) Curve 25519 *default*
   (4) NIST P-384
   (6) Brainpool P-256
Your selection?
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0)
Key does not expire at all
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name: John Doe
Email address: john@doe.net
Comment: 
You selected this USER-ID:
    "John Doe <john@doe.net>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: revocation certificate stored as '/home/jdoe/.gnupg/openpgp-revocs.d/4960E99609DA1753CC6798800DB4D7504AFFB5E4.rev'
public and secret key created and signed.

pub   ed25519/4AFFB5E4 2026-03-25 [SC]
      4960E99609DA1753CC6798800DB4D7504AFFB5E4
uid                    John Doe <john@doe.net>
sub   cv25519/D76499B8 2026-03-25 [E]
      58C2231CAEAF923DB403051E3CFD617FD76499B8

Edit key to add a signing subkey:

Restic

Use restic to backup a set of files/directories on several repositories.

Repo initialization

mkdir -p ~/.config/restic

echo "sftp:bak.triaxx.io:/srv/restic-repo" > .config/restic/repo-bak

restic --repository-file ~/.config/restic/repo-bak init

Backup

find /etc /home -print > ~/.config/restic/files

restic --repository-file ~/.config/restic/repo-bak \
       backup \
       --files-from-verbatim ~/.config/restic/files \
       --skip-if-unchanged

Linux Automation

Create a systemd template service to execute restic backup for each repository:

mkdir -p ~/.config/systemd/user

cat > ~/.config/systemd/user/restic-backup@.service << EOF
[Unit]
Description=Restic backup service for %i repository
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/bin/restic backup --files-from-verbatim %E/restic/files --skip-if-unchanged
ExecStartPost=/usr/bin/restic forget --keep-last 5 --prune
Environment="RESTIC_PASSWORD_COMMAND=%E/restic/passcmd"
Environment="RESTIC_REPOSITORY_FILE=%E/restic/repo-%i"

[Install]
WantedBy=default.target
EOF

Create a target to backup on several repositories:

EFI

Efibootmgr

List UEFI entries:

efibootmgr

Delete entry 0013:

efibootmgr --delete-bootnum --bootnum 0013

Create new entry:

efibootmgr --create --label "HD001" --loader "\EFI\BOOT\BOOTX64.EFI"

Disk check

Bad blocks

Install on NetBSD:

# pkgin install e2fsprogs

or on macOS:

# brew install e2fsprogs

Unmount device first on macOS:

# diskutil unmountDisk /dev/<device>

Start test:

# badblocks -n -s -v /dev/<device>

where <device> can be identified by executing:

# dmesg | tail

right after plugging in the USB flash storage on GNU/Linux or NetBSD and

# diskutil list

on macOS.

Smartmontools

Print health status of device /dev/sdb:

# smartctl --health /dev/sdb 
smartctl 7.5 2025-04-30 r5714 [x86_64-linux-7.0.3-arch1-1] (local build)
Copyright (C) 2002-25, Bruce Allen, Christian Franke, www.smartmontools.org

=== START OF READ SMART DATA SECTION ===
SMART overall-health self-assessment test result: PASSED

Start long off-line test on device /dev/sdb:

Arch Linux

Installation

Boot image

curl --output archlinux-x86_64.iso https://mirror.srv.fail/archlinux/iso/2026.03.01/archlinux-x86_64.iso
# on macOS
sudo dd if=archlinux-x86_64.iso of=/dev/disk4 bs=1m status=progress
# on NetBSD
dd if=/home/triaxx/archlinux-x86_64.iso of=/dev/sd2d bs=1m progress=100

System configuration

After booting from USB disk:

localectl list-keymaps | grep fi
loadkeys fi
timedatectl set-timezone Europe/Helsinki

Partitionning

Erase table or filesystem signatures:

wipefs --all /dev/sda

Create empty partition table:

echo 'label: gpt' | sfdisk /dev/sda

List available types for GPT:

sfdisk --label gpt --list-types

Create partitions:

echo "size=512MiB, name=efi, type=efi-system" | sfdisk /dev/sda
echo "size=512MiB, name=boot, type=linux-extended-boot" | sfdisk /dev/sda --append
echo "size=36GiB, name=linux-swap, type=linux-swap" | sfdisk /dev/sda --append
echo "size=128GiB, name=linux-root, type=linux-root-x86-64" | sfdisk /dev/sda --append
echo "size=36GiB, name=freebsd-swap, type=freebsd-swap" | sfdisk /dev/sda --append
echo "size=128GiB, name=freebsd-root, type=freebsd-ufs" | sfdisk /dev/sda --append
echo "size=+, name=data, type=freebsd-zfs" | sfdisk /dev/sda --append

Make filesystems:

Figlet

File header

figlet Makefile | sed -e "s|^|# |" > Makefile

Mercurial

Initialization

hg init
cat >> .hg/hgrc << EOF
[ui]
username = triaxx <triaxx@triaxx.io>
EOF

Usage

hg status
hg add <file>
hg forget <file>
hg commit
hg log --graph

Change the message of the last commit:

hg commit --amend

Revert to the last commit version:

hg revert
hg revert --no-backup

Extensions

Enable histedit:

cat >> .hg/hgrc << EOF
[extensions]
histedit =

Hugo

Abstract

Hugo is an open-source static website generator. It is written in Go and uses Markdown file format as input to produce static HTML website.

Initialization

Site

hugo new project <path>
cd <path>
cat > hugo.toml << EOF
baseURL = 'https://triaxx.io/'
languageCode = 'fr-FR'
title = 'Triaxx Web Log'
EOF

Theme

A theme is mandatory to build sites.

hugo new theme <name>
echo "theme = '<name>' >> hugo.toml

A example menu can be copied from themes/<name>/hugo.toml: