Automatyczne, szyfrowane backupy serwera na dysk podpięty do RPI

Opisuję, jak backupuję różne serwery którymi się opiekuję na dysk podłączony do raspberry pi u mnie w mieszkaniu. Backupy są szyfrowane i przyrostowe – tzn. unikam zapisywania na dysku wielokrotnie takich samych wersji plików.


UPDATE: napisałem ansiblowy playbook do stawiania takiego backupu


Ustanowienie połączenia VPN pomiędzy serwerem a RPI

Restic działa na zasadzie “push” – tzn serwer wysyła backupy, a raspberry musi cały czas na nie nasłuchiwać. Komunikacja pomiędzy rpi a serverem będzie odbywać się przez ssh za pośrednictwem OpenVPN.

Najprościej będzie skorzystać z konfiguracji static-key dla OpenVPN. Na stronie OpenVPN znajduje się poręczny tutorial, który dla mojej przyszłej wygody maksymalnie tutaj streszczę.

Na serwerze:

cd /etc/openvpn/server
openvpn --genkey --secret rpi-to-server.key
cat rpi-to-server.key

Skopiuj zawartość pliku .key do schowka.

Na rpi:

cd /etc/openvpn/client
nano rpi-to-server.key

Wklej klucz skopiowany z serwera.

Utwórz plik tekstowy /etc/openvpn/server/rpi-to-server.conf na serwerze:

dev tun
ifconfig 10.8.5.1 10.8.5.2
secret rpi-to-server.key
keepalive 10 60
ping-timer-rem
persist-tun
persist-key
log-append /var/log/openvpn-rpi.log

Oraz plik tekstowy /etc/openvpn/client/rpi-to-server.conf na rpi (trzeba wstawić tam domenową nazwę serwera):

remote moja.domena.com
dev tun
ifconfig 10.8.5.2 10.8.5.1
secret rpi-to-server.key
keepalive 10 60
ping-timer-rem
persist-tun
persist-key
log-append /var/log/openvpn-yuno.log

Otwórz port 1194 na serwerze:

ufw allow 1194

Następnie na serwerze odpal:

systemctl start openvpn-server@rpi-to-server
systemctl enable openvpn-server@rpi-to-server

Na rpi:

systemctl start openvpn-client@rpi-to-server
systemctl enable openvpn-client@rpi-to-server

Powinno działać pingowanie 10.8.5.1 z rpi i pingowanie 10.8.5.2 z serwera.

Autoryzacja rpi i serwera

Na rpi utworzymy użytkownika, który będzie miał dostęp tylko do katalogu z backupami:

adduser server-backup # utwórz hasło i zapisz je w menedżerze haseł
mkdir /mnt/hdd/Backups/server-backup
cd /mnt/hdd/Backups/server-backup
chown root: .
chmod 755 .
mkdir data
chmod 755 data
chown server-backup: data

Uwaga – każdy z przodków wybranego katalogu (w przykładzie powyżej jest to /mnt/hdd/Backups/server-backup) musi należeć do użytkownika root i mieć prawa dostępu 755

Na serwerze dodaj taką linijkę do /etc/hosts:

10.8.5.2    rpi

Na serwerze wygeneruj klucz ssh bez hasła za pomocą:

$ ssh-keygen -t ed25519 -b 4096 -C "rpi-backup" -f /root/.ssh/rpi-backup -N ""

W pliku ~/.ssh/config na serwerze dodaj:

Host rpi
    User server-backup
    IdentityFile /root/.ssh/rpi-backup

Na serwerze wykonaj:

ssh-copy-id -i /root/.ssh/rpi-backup rpi

I wpisz tam zapisane wcześniej hasło do usera server-backup na rpi.

Na rpi dodaj do pliku /etc/ssh/sshd_config:

Match User server-backup
	ForceCommand internal-sftp
	PasswordAuthentication yes
	ChrootDirectory /mnt/hdd/Backups/server-backup
	PermitTunnel no
	AllowAgentForwarding no
	AllowTcpForwarding no
	X11Forwarding no

I zrestartuj sshd na rpi:

systemctl restart sshd

Aby sprawdzić poprawność autoryzacji, wpisz na serwerze:

ssh rpi

Powinno zwrócić This service allows sftp connections only.

Za to sftp rpi powinno pozwolić Ci listować pliki w zadanym katalogu.

(Jeżeli napotykasz “broken pipe”, zakomentuj linijkę UsePAM yes w konfiguracji sshd na rpi).

Powinieneś zalogować się do sftp-owego shella rpi.

Logika backupów

Utwórz na serwerze plik, który będzie hasłem szyfrującym backup:

dd if=/dev/urandom of=/backup-pwd bs=1k count=1

Wyświetl go w base64 i zachowaj w manadżerze haseł:

base64 < /backup-pwd

Zakładam, że na serwerze jest zainstalowany restic w przyzwoicie świeżej wersji.

Zainicjuj repozytorium na rpi:

restic init --password-file=/backup-pwd -r sftp:server-backup@rpi:data

Następnie przygotujemy skrypt, który będzie przygotowywał pliki do zbackupowania. Idea jest taka, że dumpujemy wszystko co chcemy zbackupować do katalogu /backup albo bierzemy prosto z istniejącego katalogu. Ten skrypt będzie za każdym razem usuwał zawartość katalogu /backup.

W katalogu domowym (lub gdziekolwiek chcesz) na serwerze utwórz plik backup.sh:

#!/bin/bash

REPO=sftp:server-backup@rpi:data
PWD_FILE=/backup-pwd
RESTIC=/usr/bin/restic

echo "Removing current backups..."
rm -fr /backup/*

mkdir -p /backup

####### Prepare data

# tutaj wpychamy dane do zbackupowania do /backup.
# nie musimy ich tu wpychać, jeżeli po prostu są na dysku - wystarczy podać je jako argument do pierwszej komendy w sekcji "Send Backups".
# do /backup najlepiej wgrać artefakty, które nie są aktualnie w backupowalnej postaci na dysku, np. dumpy mysql

# przykłady skryptów backupujących dodałem na końcu, w sekcji "bonusy"

###### Send backups

echo "Sending the backup to the destination..."

# podaj tutaj listę katalogów do zbackupowania. Warto backupować katalog `/etc`.
$RESTIC -r "$REPO"  --password-file=$PWD_FILE backup /backup /etc /var/phabricator-files /etc/nginx/sites  # ... + inne pliki / katalogi

echo "Pruning the backup on the destination..."
# poniższa konfiguracja będzie trzymała backupy z każdego z pięciu ostatnich dni, po jednym backupie dla każdego z ostatnich 10 tygodni, po jednym dla ostatnich 12 miesięcy i po jednym dla ostatnich 100 lat.

$RESTIC -r "$REPO"  --password-file=$PWD_FILE forget --prune  --keep-daily 5 --keep-weekly 10 --keep-monthly 12 --keep-yearly 100

Protip: używaj ścieżek absolutnych do binarek, aby CRON nie miał z nimi problemów

Cron

Pozostaje tylko zautomatyzować wysyłanie backupu:

Na serwerze:

crontab -e

I dodajemy linijkę:

15 3 * * * /root/backup.sh

TODO: opisać, jak otrzymywać powiadomienia email o każdym backupie

Bonusy

Automatyczne czyszczenie starych zcache'owanych mediów na Mastodonie w wersji yunohost:

RAILS_ENV=production /opt/rbenv/shims/ruby /var/www/mastodon/live/bin/tootctl media remove-orphans
RAILS_ENV=production /opt/rbenv/shims/ruby /var/www/mastodon/live/bin/tootctl media remove --days=2
###### Mongo backup example

echo "Dumping mongo..."
mongodump -o /backup/mongo

###### Yunohost backup example

yunohost backup create --no-compress -o /backup


###### Phabricator backup example

echo "Moving files from db to local disk...";
mysql -e "Use  phabricator_file; SELECT id FROM file WHERE storageEngine != 'local-disk' AND byteSize > 2048;" | grep -o -E '[0-9]+' | xargs -P 4 -L 1 -I {} bash -c "/var/www/phabricator/phabricator/bin/files migrate --engine local-disk F{} || exit 0"


echo "Stopping php (and Phabricator...)"
systemctl stop php7.4-fpm

echo "Dumping mysql databases..."
mkdir -p /backup/mysql
mysql -N -e 'show databases' | while read dbname; do mysqldump --complete-insert --routines --triggers --single-transaction "$dbname" > "/backup/mysql/$dbname".sql; done

echo "Starting php (and Phabricator...)"
systemctl start php7.4-fpm

Gdy jest podpięty volumen w katalogu /home i tam najlepiej zgrywać backupy yuno:

TARGET=/home/backup-dir

echo "Removing current backups..."
rm -fr $TARGET*

mkdir -p $TARGET
mkdir -p $TARGET/data
touch $TARGET/.nobackup

####### Yunohost backup
echo "Backing up yunohost..."
BACKUP_CORE_ONLY=1 yunohost backup create --method copy -o $TARGET/data


Backupowanie invidiousa na yunohost jest problematyczne, z uwagi na ten issue. Aby zbackupować wszystkie aplikacje poza invidiousem, można użyć:

APPS=$(yunohost app list | grep -v invidious | grep 'id:' | awk '{print $2}' | tr '\n' ' ' )                                                                                                  
                                                                                                                                                                                              
BACKUP_CORE_ONLY=1 yunohost backup create --method copy -o $TARGET/data --apps $APPS                                                                                                          
                                                                                      

#backup