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