Root cifrata su ZFS con FreeBSD
Premessa
Nel 2023, gestisco ancora il mio mailserver. Sì, perché mi piace avere il controllo (almeno in parte) della mia vita digitale, e mi piace avere diversi nomi di dominio su cui tenere le mie cose. Tuttavia, stavo pagando 30€/mese ad AWS per avere in cambio 2 core, 2GiB di RAM e 40G di disco, appena sufficienti per far girare IMAP+SMTP+MySQL+Clamd, figurarsi qualsiasi forma di protezione antispam o ricerca full-text nel corpo delle email.
Insomma, stavo pagando un botto di soldi per far girare un servizio merdoso, e avevo persino pensato di chiudere tutto e spostare la posta e i siti web su qualche servizio completamente managed.
Però voglio ancora farlo
Beh, per ospitare quattro domini con solo qualche redirect email più i siti web che gestisco, avrei speso di più di quanto stavo già pagando, incatenandomi per di più a qualche fornitore di servizi e alle sue politiche.
Quindi, volevo usare FreeBSD e ho cominciato a cercare nella pagina degli ISP fino a decidere di valutare Hetzner e netcup, che offrono entrambi prezzi aggressivi e un buon vecchio VPS senza fronzoli.
Scelta del provider
Alla fine, ho scelto un netcup VPS 1000 che mi dà, a 1/3 del prezzo che pagavo ad AWS, 4 volte le risorse: 6 core, 8GiB di RAM, 160GiB di SSD RAID10 e un’installazione FreeBSD completamente libera e senza limitazioni.
Tuttavia, l’immagine base fornita da Netcup ha alcune limitazioni:
- Gira su UFS
- Non ha una partizione di swap
- Non ha cifratura
Fare un piano
Dato che ero già in fase di configurazione e non volevo ricominciare da zero (questo è un server vecchio stile, gestito a mano, zero automazione) ho deciso di:
- Avviare server temporanei su Hetzner per sperimentare
- Cercare l’incantesimo necessario per avere una macchina con cifratura completa del disco che booti correttamente
- Copiare la / dal server netcup a Hetzner e vedere se boota
- Ripetere il processo
- Una volta che l’incantesimo è stabile:
- Avviare un server target su Hetzner per tenere temporaneamente tutti i dati
- Riavviare il server sorgente su netcup da un CD per fare rsync di tutti i dati su Hetzner
- Azzerare il disco del server netcup e ricreare tutte le partizioni e i filesystem come piace a me
- Rsync di tutti i dati da Hetzner a netcup e riavvio
Esecuzione
Indovina un po’, funziona davvero. Ho iniziato usando il CD di installazione di FreeBSD, per poi rendermi conto che non avevo bisogno dell’installer perché avevo già un sistema live da migrare, e ho finito per usare mfsbsd sia per avviare il server target, sia per avviare il server sorgente quando è stato il momento di copiare tutto avanti e indietro.
Partendo da questo thread sul forum di FreeBSD e questa pagina wiki per il boot ZFS ho cucinato il seguente incantesimo:
Riavvio dal ramdisk e copia dei dati sul server temporaneo
Questo configura la rete, aggiorna rsync all’ultima versione, monta il filesystem corrente in /mnt e fa rsync di tutto verso una posizione di storage temporanea
ifconfig vtnet0 inet6 2a03:4000:2:33c::42 prefixlen 64
route -6 add default fe80::1%vtnet0
echo 'nameserver 2a03:4000:0:1::e1e6' > /etc/resolv.conf
pkg install rsync
pkg upgrade libiconv
mount /dev/vtbd0p2 /mnt
cd /mnt
rsync --archive --recursive --times --executability --hard-links \
--links --perms --compress --exclude .sujournal --exclude .swapfile \
--exclude .snap --exclude 'dev/*' --exclude 'srv/www/*/dev/*' \
. root@m17.openssl.it:/mnt
Creazione delle partizioni
Qui creiamo una partizione di boot che contiene l’eseguibile gptboot, la
cui responsabilità è caricare ed eseguire il loader di FreeBSD dalla
partizione /boot in chiaro.
Poi creiamo una partizione di swap e infine una partizione zfs che
conterrà il nostro pool ZFS.
gpart destroy -F vtbd0
gpart create -s GPT vtbd0
gpart add -s 472 -t freebsd-boot vtbd0
gpart bootcode -b /boot/pmbr -p /boot/gptboot -i 1 vtbd0
gpart add -s 1G -t freebsd-ufs -l boot vtbd0
gpart set -a bootme -i 2 vtbd0
gpart add -s 2G -t freebsd-swap -l swap vtbd0
gpart add -t freebsd-zfs -l root vtbd0
Creazione di /boot e del device root cifrato
Qui creiamo un filesystem UFS per la partizione /boot non cifrata
che conterrà il kernel e il loader, e parte della chiave di cifratura usata
per cifrare la root. Quella chiave da sola non è sufficiente per accedere
al filesystem, perché è necessaria anche una passphrase aggiuntiva.
newfs -O 2 -U -m 8 -o space /dev/vtbd0p2
mkdir /tmp/ufsboot
mount /dev/vtbd0p2 /tmp/ufsboot
mkdir -p /tmp/ufsboot/boot/geli
dd if=/dev/random of=/tmp/ufsboot/boot/geli/vtbd0p4.key bs=64 count=1
geli init -e AES-XTS -l 256 -s 4096 -bd -K /tmp/ufsboot/boot/geli/vtbd0p4.key /dev/vtbd0p4
cp /var/backups/vtbd0p4.eli /tmp/ufsboot/boot/geli
geli attach -k /tmp/ufsboot/boot/geli/vtbd0p4.key /dev/vtbd0p4
Creazione del pool ZFS
Questo è il mio layout, che uso principalmente per limitare l’eseguibilità dei percorsi che non dovrebbero essere eseguibili, e anche per facilitare lo snapshot di parti separate del filesystem che necessitano di strategie di retention diverse
zpool create -R /mnt -O canmount=off -O mountpoint=none -O atime=off -O compression=lz4 tank /dev/vtbd0p4.eli
zfs create -o mountpoint=/ tank/ROOT
zfs create -o mountpoint=/tmp -o exec=off -o setuid=off tank/tmp
zfs create -o canmount=off -o mountpoint=/usr tank/usr
zfs create -o setuid=off tank/usr/ports
zfs create -o canmount=off -o mountpoint=/var tank/var
zfs create -o exec=off -o setuid=off tank/var/log
zfs create -o atime=on -o exec=off -o setuid=off tank/var/spool
zfs create -o exec=off -o setuid=off tank/var/tmp
zfs create -o canmount=off -o mountpoint=/srv tank/srv
zfs create -o exec=off -o setuid=off tank/srv/mail
zfs create -o exec=off -o setuid=off tank/srv/www
Infine, montiamo la partizione di boot UFS non cifrata sotto la gerarchia del filesystem ZFS,
umount /dev/vtbd0p2
mkdir /mnt/ufsboot
mount /dev/vtbd0p2 /mnt/ufsboot
Copia di tutto!
Ora è il momento di riprendere i dati dalla posizione temporanea dove erano stati messi, e scriverli sul nuovo fiammante pool ZFS sulla root cifrata con GELI:
rsync --archive --recursive --times --executability --hard-links \
--links --perms --compress root@m17.openssl.it:/mnt/ /mnt
mv /mnt/boot/* /mnt/ufsboot/boot
rm -rf /mnt/boot
ln -s ufsboot/boot /mnt
Usiamo un symlink per puntare /boot a /ufsboot/boot, così il sistema
si comporterà come se /boot fosse una directory normale in /. È
necessario mantenere una sottodirectory /boot nella partizione boot
perché molto codice del loader dipende da percorsi /boot hardcoded.
Cosa resta
/etc/fstab, con swap cifrata ovviamente:
/dev/vtbd0p2 /ufsboot ufs rw 0 1
/dev/vtbd0p3.eli none swap sw,ealgo=AES-XTS,keylen=128,sectorsize=4096 0 0
/boot/loader.conf.d/geli.conf:
geom_eli_load="YES"
geli_vtbd0p4_keyfile0_load="YES"
geli_vtbd0p4_keyfile0_type="vtbd0p4:geli_keyfile0"
geli_vtbd0p4_keyfile0_name="/boot/geli/vtbd0p4.key"
zfs_load="YES"
vfs.root.mountfrom="zfs:tank/ROOT"
/etc/rc.conf:
zfs_enable="YES"
Ha funzionato?
Ma certo che sì! E gira felicemente da allora :-)
03:44:10 root@m42:/srv/www/sindro.me/staging
# uname -a
FreeBSD m42.openssl.it 13.2-RELEASE-p2 FreeBSD 13.2-RELEASE-p2 GENERIC amd64
03:44:13 root@m42:/srv/www/sindro.me/staging
# df -hT
Filesystem Type Size Used Avail Capacity Mounted on
tank/ROOT zfs 140G 6.7G 134G 5% /
devfs devfs 1.0K 1.0K 0B 100% /dev
/dev/vtbd0p2 ufs 992M 189M 723M 21% /ufsboot
tank/var/spool zfs 134G 1.1M 134G 0% /var/spool
tank/tmp zfs 134G 220K 134G 0% /tmp
tank/srv/mail zfs 138G 4.8G 134G 3% /srv/mail
tank/srv/www zfs 136G 2.1G 134G 2% /srv/www
tank/var/log zfs 134G 13M 134G 0% /var/log
tank/var/tmp zfs 134G 224K 134G 0% /var/tmp
tank/usr/ports zfs 136G 2.6G 134G 2% /usr/ports
/dev nullfs 1.0K 1.0K 0B 100% /srv/www/admin.openssl.it/dev
/dev nullfs 1.0K 1.0K 0B 100% /srv/www/mail.openssl.it/dev
/dev nullfs 1.0K 1.0K 0B 100% /srv/www/nhaima.org/dev
/dev nullfs 1.0K 1.0K 0B 100% /srv/www/spadaspa.it/dev
tank/usr/src zfs 134G 773M 134G 1% /usr/src
tank/usr/obj zfs 134G 96K 134G 0% /usr/obj
E lo sblocco della root al boot?
Per i 2 anni e mezzo da quando ho scritto questo articolo sono stato alla mercé della console VNC del VPS per inserire la password durante il boot, il che è quanto meno scomodissimo.
Ma non più! Ho configurato un ram disk iniziale e ora posso sbloccare il VPS da remoto via SSH - senza bisogno del browser :-)