Przejdź do treści
Bezpieczeństwo··16 min czytania

Backup pull z rrsync: ograniczony klucz SSH i test odtworzenia

Serwer kopii inicjuje połączenie, a ograniczony klucz może tylko odczytać wskazany katalog. Prawdziwy test backupu następuje dopiero podczas odtworzenia.

Autor: Zespół LinuxLab

Poniższa procedura tworzy kopię w modelu pull. Host backup-01.example.test łączy się przez SSH z app-01.example.test i odczytuje tylko przygotowany katalog. Chroniony host nie ma klucza ani prawa zapisu do archiwum. Wszystkie nazwy, adresy i ścieżki są przykładowe.

Czy backup pull z rrsync pasuje do tego hosta?

Ten wariant ma sens, gdy chcemy zabrać z hosta aplikacyjnego pliki lub wcześniej przygotowane eksporty, a przejęcie tego hosta nie ma dawać prawa zapisu do archiwum. Jest dobrym punktem wyjścia dla katalogu z plikami aplikacji, konfiguracją albo zrzutem bazy wykonanym przez osobne zadanie.

Nie jest to uniwersalny sposób na pełny backup systemu. rrsync nie podnosi uprawnień: konto backup-pull odczyta tylko pliki, do których ma dostęp. Nie ustawiamy więc ograniczonego katalogu na / ani nie dodajemy szerokiego sudo tylko po to, aby obejść uprawnienia. Dla plików roota przygotowujemy osobny, rootowy eksport albo migawkę, a zakres takiego rozwiązania przeglądamy osobno.

Przed zastosowaniem procedury wybieramy jeden katalog eksportu, na przykład /srv/backup-export, i najpierw uruchamiamy ją na hoście testowym. Nie kopiujemy w ten sposób aktywnych plików bazy danych. Dla bazy przygotowujemy spójny dump albo migawkę zgodną z używanym silnikiem.

Kto łączy się z kim i co ogranicza rrsync?

backup-01.example.test                         app-01.example.test
root: klucz prywatny                           konto: backup-pull
/srv/backup/hosts/app-01/current  -- SSH -->  rrsync -ro /srv/backup-export
           zapis lokalny                         wyłącznie odczyt źródła

W modelu push host aplikacyjny inicjuje połączenie do archiwum. Musi zatem przechowywać poświadczenie i mieć prawo zapisu po stronie kopii. W modelu pull jest odwrotnie: backup-01.example.test inicjuje SSH do app-01.example.test, a klucz prywatny pozostaje tylko na serwerze kopii. Chroniony host nie zna poświadczenia do archiwum.

rrsync (restricted rsync) jest skryptem dostarczanym z rsync. Wpis command= w authorized_keys uruchamia go zamiast polecenia przesłanego przez klienta SSH. Skrypt odczytuje SSH_ORIGINAL_COMMAND, akceptuje tylko serwerową część protokołu rsync, sprawdza opcje i ścieżki, a następnie uruchamia rsync w wybranym katalogu. Szczegóły opcji opisuje oficjalna dokumentacja rrsync.

Przełącznik -ro oznacza, że zdalny rsync może być tylko nadawcą danych. Serwer kopii może więc pobrać katalog, ale nie może użyć tego klucza do wysłania do niego pliku. Dodatkowe opcje SSH blokują terminal i przekierowania portów. Ograniczenie nie chroni jednak przed odczytem danych przez napastnika, który przejmie sam serwer kopii razem z kluczem.

Co przygotować przed utworzeniem klucza?

Element przykładuRola
backup-01.example.test
192.0.2.20
Serwer kopii. To on inicjuje SSH i przechowuje klucz prywatny.
app-01.example.testChroniony host. Udostępnia wyłącznie katalog eksportu przez konto backup-pull.
/srv/backup-exportKatalog źródłowy. Konto kopii ma do niego odczyt i przejście przez katalogi, ale nie zapis.
/srv/backup/hosts/app-01.example.test/currentDestination na serwerze kopii. Trzymamy go na storage przeznaczonym na backup.

Adres 192.0.2.20 należy do puli dokumentacyjnej. Zastępujemy go adresem, który sshd rzeczywiście widzi jako źródło połączenia. Przy NAT będzie to zwykle adres wyjściowy bramy, a nie prywatny adres interfejsu serwera kopii.

  1. Na obu hostach potrzebujemy roota oraz działającego SSH i rsync.
  2. Firewall hosta aplikacyjnego ma przyjmować TCP/22 tylko z adresu serwera kopii.
  3. Na serwerze kopii musi być wolne miejsce na pierwszy pełny transfer oraz na zaplanowane wersje lub snapshoty.
  4. Przed pierwszym połączeniem potwierdzamy odciski klucza hosta SSH przez konsolę albo inny zaufany kanał. Samo ssh-keyscan nie ustanawia zaufania.
  5. Wybieramy sposób tworzenia spójnych danych aplikacji. Dla bazy będzie to zwykle dump albo migawka, a nie kopiowanie otwartego katalogu danych.

Jak utworzyć klucz na serwerze kopii?

Poniższe polecenia wykonujemy jako root na backup-01.example.test. Klucz prywatny powstaje tylko tutaj; na app-01.example.test trafi wyłącznie jego publiczna część. Sprawdzenie przed ssh-keygen celowo przerywa pracę, jeśli plik już istnieje, aby nie nadpisać działającego klucza.

install -d -m 0700 /root/.ssh

if test -e /root/.ssh/id_ed25519_app_01_pull; then
    printf '%s\n' 'Klucz już istnieje; nie nadpisujemy go.'
    exit 1
fi

ssh-keygen -q -t ed25519 -N '' \
    -f /root/.ssh/id_ed25519_app_01_pull \
    -C 'backup-01-to-app-01-pull'

ssh-keygen -lf /root/.ssh/id_ed25519_app_01_pull.pub

Pusty passphrase umożliwia zadanie bez udziału człowieka, dlatego granicą ochrony staje się sam serwer kopii i jego katalog roota. Jeżeli polityka wymaga hasła do klucza, używamy obsługiwanego przez środowisko agenta lub HSM; nie obchodzimy tego przez pozostawienie klucza na hoście aplikacyjnym.

Przed pierwszym SSH potwierdzamy klucz hosta. Pierwsze polecenie wykonujemy przez konsolę lub inny zaufany dostęp do app-01.example.test, drugie jako root na serwerze kopii. Odciski muszą być identyczne.

# app-01.example.test, root, zaufana konsola
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

# backup-01.example.test, root
ssh-keyscan -t ed25519 app-01.example.test | ssh-keygen -lf -

Dopiero po porównaniu odcisków dodajemy klucz do known_hosts. ssh-keyscan sam nie uwierzytelnia hosta; w tym kroku zapisujemy klucz, który został już zweryfikowany inną drogą.

ssh-keyscan -H -t ed25519 app-01.example.test \
    >> /root/.ssh/known_hosts
chmod 0600 /root/.ssh/known_hosts
cat /root/.ssh/id_ed25519_app_01_pull.pub

Ostatnia linia wypisuje klucz publiczny do wklejenia na chronionym hoście. Publiczna część nie jest sekretem, ale nie zastępujemy jej ręcznie skrótem ani przykładowym ciągiem z tego artykułu.

Jak przygotować konto i katalog eksportu?

Następne polecenia wykonujemy jako root na app-01.example.test. Przykład dotyczy Debiana lub Ubuntu, gdzie dostępny jest /bin/dash. Używamy go zamiast Bash, ponieważ dokumentacja rrsync ostrzega przed uruchamianiem plików startowych Bash przed wymuszonym poleceniem. Konto nie dostaje zapisu do swojego katalogu domowego ani do katalogu eksportu.

getent group backup-pull >/dev/null || groupadd --system backup-pull

id backup-pull >/dev/null 2>&1 || useradd \
    --system --gid backup-pull \
    --home-dir /var/lib/backup-pull --create-home \
    --shell /bin/dash --comment 'Restricted pull backup' backup-pull

install -d -o root -g root -m 0755 /var/lib/backup-pull
install -d -o root -g root -m 0700 /var/lib/backup-pull/.ssh
install -o root -g root -m 0600 /dev/null \
    /var/lib/backup-pull/.ssh/authorized_keys

install -d -o root -g backup-pull -m 0750 /srv/backup-export

Ścieżka /srv/backup-export jest granicą danych udostępnionych temu kluczowi. Usługa lub zadanie roota publikuje tam tylko gotowe artefakty, na przykład zakończony dump bazy i pliki aplikacji. Nie daje się kontu backup-pull prawa zapisu. Gdy export zawiera nowy dump, najpierw zapisujemy go pod nazwą tymczasową, sprawdzamy wynik aplikacyjnego zadania, a dopiero potem atomowo przenosimy do katalogu eksportu.

Konto musi mieć prawo przejścia przez katalogi oraz odczytu plików w eksporcie. Przed połączeniem sprawdzamy to na hoście aplikacyjnym:

namei -l /srv/backup-export
runuser -u backup-pull -- test ! -w /srv/backup-export
runuser -u backup-pull -- find /srv/backup-export \
    -xdev -type f ! -readable -print
runuser -u backup-pull -- find /srv/backup-export \
    -xdev -type f -readable -print | sed -n '1,20p'

Drugi i trzeci test mają zakończyć się kodem 0; trzeci nie powinien wypisać żadnego pliku, a czwarty ma wypisać przykładowe pliki eksportu. Błędy Permission denied, niezerowy kod zakończenia lub nazwy w trzecim wyniku oznaczają, że trzeba poprawić właścicielstwo albo prawa dostępu przed uruchomieniem rsync.

Nie zamieniamy tego przykładu w dostęp do całego systemu przez rrsync -ro / ani przez regułę sudo ze znakiem wieloznacznym dla rsync. Jeśli backup wymaga plików dostępnych wyłącznie dla roota, przygotowujemy rootowy eksport lub snapshot o precyzyjnie przejrzanym zakresie. Ograniczony klucz SSH nie jest właściwym miejscem na ogólne podnoszenie uprawnień.

Jak ograniczyć klucz w authorized_keys?

Najpierw na app-01.example.test sprawdzamy ścieżkę dostarczoną przez pakiet rsync. Nie zakładamy jej z góry, ponieważ zależy od dystrybucji.

command -v rrsync
rrsync -h | sed -n '1,6p'

Zakładamy, że pierwsze polecenie zwróciło /usr/bin/rrsync. Jako root otwieramy /var/lib/backup-pull/.ssh/authorized_keys i wklejamy jedną fizyczną linię. Zastępujemy adres, ścieżkę rrsync i cały ciąg klucza publicznego własnymi wartościami.

from="192.0.2.20",command="/usr/bin/rrsync -ro /srv/backup-export",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... backup-01-to-app-01-pull
Część wpisuZnaczenie
from="192.0.2.20"Odrzuca klucz z innego adresu. Używamy go przy stabilnym adresie źródłowym widocznym dla SSH; przy NAT wpisujemy adres NAT.
command="...rrsync -ro ..."Zamiast powłoki uruchamia rrsync, ogranicza katalog i dopuszcza wyłącznie odczyt.
no-ptyNie pozwala przydzielić terminala interaktywnego.
no-port-forwardingNie pozwala użyć sesji jako tunelu do sieci hosta.
no-agent-forwarding, no-X11-forwarding, no-user-rcWyłącza kolejne kanały sesji, które nie są potrzebne do rsync.

rrsync obsługuje jeden ograniczony katalog na jeden klucz. Dla niezależnych zakresów używamy osobnych kluczy albo wybieramy wspólny katalog nadrzędny dopiero po sprawdzeniu, że nie udostępni dodatkowych danych. W automatyzacji zarządzamy całym plikiem authorized_keys jako deklarowanym zestawem, aby wycofany klucz nie pozostał w nim przez przypadek.

chown root:root /var/lib/backup-pull/.ssh/authorized_keys
chmod 0600 /var/lib/backup-pull/.ssh/authorized_keys
sshd -t

Ostatnie polecenie tylko sprawdza składnię konfiguracji SSH. Kod zakończenia 0 oznacza, że nie wykryto błędu składni; nie restartujemy usługi SSH w ramach tej konfiguracji.

Jak sprawdzić połączenie i transfer bez zapisu?

Poniższe polecenia wykonujemy jako root na backup-01.example.test. Najpierw tworzymy pusty destination na storage przeznaczonym na kopie. Ten katalog nie może być ścieżką produkcyjnej aplikacji ani współdzielonym katalogiem roboczym.

install -d -o root -g root -m 0700 \
    /srv/backup/hosts/app-01.example.test/current

Następnie uruchamiamy dry-run. Kropka po dwukropku w źródle jest istotna: dla rrsync oznacza katalog bieżący po przejściu do /srv/backup-export, a nie katalog domowy użytkownika. Nie używamy --protect-args, ponieważ rrsync celowo go odrzuca, gdy nie może sprawdzić przekazanych opcji.

rsync -aH --numeric-ids --itemize-changes --dry-run \
    -e 'ssh -i /root/.ssh/id_ed25519_app_01_pull -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes' \
    backup-pull@app-01.example.test:. \
    /srv/backup/hosts/app-01.example.test/current/

-aH zachowuje atrybuty trybu archiwalnego i hardlinki, a --numeric-ids zachowuje numery UID i GID bez mapowania po nazwach. W tej podstawowej konfiguracji nie dodajemy -A ani -X, ponieważ konto ograniczone do eksportu może nie mieć prawa odczytu wszystkich ACL i extended attributes. Jeżeli są one wymagane, projektujemy osobny, testowany eksport z odpowiednimi uprawnieniami.

Poprawny dry-run wypisuje planowane zmiany i kończy się kodem 0, nie tworząc plików w destination. Błąd host key, Permission denied albo błąd rrsync zatrzymuje procedurę przed transferem danych.

W tej samej chwili testujemy, czy klucz nie daje czegoś więcej niż kopia. Oba polecenia powinny zakończyć się błędem. Pierwsze nie może uruchomić powłoki, drugie nie może wysłać pliku na chroniony host.

ssh -i /root/.ssh/id_ed25519_app_01_pull \
    -o IdentitiesOnly=yes backup-pull@app-01.example.test id

printf '%s\n' test > /tmp/rrsync-write-test
rsync -e 'ssh -i /root/.ssh/id_ed25519_app_01_pull -o IdentitiesOnly=yes' \
    /tmp/rrsync-write-test \
    backup-pull@app-01.example.test:rrsync-write-test

Oczekujemy komunikatu rrsync o niedozwolonym poleceniu lub wysyłaniu do serwera tylko do odczytu. Plik testowy pozostaje wyłącznie w /tmp na serwerze kopii; nie usuwamy ani nie tworzymy danych po stronie hosta aplikacyjnego.

Jak wykonać pierwszą kopię i skontrolować wynik?

Po udanym dry-run uruchamiamy ten sam transfer bez --dry-run, nadal jako root na backup-01.example.test. Polecenie tworzy albo aktualizuje pliki wyłącznie w lokalnym destination.

rsync -aH --numeric-ids --itemize-changes --info=NAME,STATS2 \
    -e 'ssh -i /root/.ssh/id_ed25519_app_01_pull -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes' \
    backup-pull@app-01.example.test:. \
    /srv/backup/hosts/app-01.example.test/current/

Po zakończeniu oczekujemy kodu 0 oraz statystyk przesłanych plików. Celowo nie ma tu --delete. Przy rrsync -ro usuwanie jest blokowane, a pierwsza kopia nie powinna usuwać danych z destination. Usunięty plik źródłowy może więc pozostać w current; nie traktujemy tego katalogu jako jedynej wersji do odzyskania.

Drugi dry-run jest prostą kontrolą stanu. Jeśli po ustabilizowaniu źródła nie wypisze zmian i zakończy się kodem 0, rsync uznaje pliki za zgodne na podstawie używanego porównania czasu i rozmiaru. Nie jest to jeszcze kryptograficzny dowód ani test odtworzenia.

rsync -aH --numeric-ids --itemize-changes --dry-run \
    -e 'ssh -i /root/.ssh/id_ed25519_app_01_pull -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes' \
    backup-pull@app-01.example.test:. \
    /srv/backup/hosts/app-01.example.test/current/

du -sh /srv/backup/hosts/app-01.example.test/current
stat -c '%y %n' /srv/backup/hosts/app-01.example.test/current

Do monitoringu przekazujemy co najmniej kod zakończenia transferu, czas ostatniego udanego przebiegu, liczbę przesłanych danych i wolne miejsce na storage. Dopiero po ręcznym odtworzeniu warto przenieść dokładnie sprawdzoną komendę do systemd timera lub używanego systemu automatyzacji.

Jak rozdzielić bieżącą kopię od retencji?

Katalog current jest bieżącym widokiem eksportu. Zmienione pliki mogą zostać w nim nadpisane podczas następnego udanego przebiegu, nawet gdy sam chroniony host nie ma prawa zapisu do archiwum. Retencja musi zatem istnieć poza tym jednym widokiem: jako snapshoty storage, wersjonowanie w backup software albo osobne niezmienne wersje.

Na przykład, jeżeli current jest punktem montowania datasetu ZFS tank/backup/app-01-current, przed kolejnym transferem można utworzyć snapshot. Polecenie wykonujemy jako root na serwerze kopii tylko po potwierdzeniu nazwy datasetu przez zfs list.

zfs list tank/backup/app-01-current
zfs snapshot tank/backup/app-01-current@before-2026-10-03

Snapshot nie jest automatyczną polityką retencji. Przed zaplanowaniem usuwania wersji ustalamy minimalny okres wykrycia incydentu, zapas pojemności, kopię poza lokalizacją oraz właściciela decyzji o usunięciu. Nie dodajemy zadania kasującego snapshoty tylko dlatego, że kopia już działa.

Model pull zmniejsza ryzyko, że przejęty host aplikacyjny zniszczy archiwum przez swoje poświadczenie. Nie chroni jednak przed pożarem, awarią storage ani przejęciem serwera kopii. Wymagane są także kopia w innej lokalizacji, ochrona danych w spoczynku stosowna do ich klasyfikacji oraz ograniczenie dostępu administratorów do storage.

Jak potwierdzić backup testem odtworzenia?

Test wykonujemy jako root na osobnym hoście testowym albo w izolowanym katalogu na serwerze kopii. Nigdy nie kierujemy odtworzenia na aktywną ścieżkę aplikacji. Najpierw sprawdzamy plan, a potem kopiujemy dane do nowej lokalizacji.

install -d -o root -g root -m 0700 \
    /srv/restore-tests/app-01-2026-10-03

rsync -aH --numeric-ids --dry-run \
    /srv/backup/hosts/app-01.example.test/current/ \
    /srv/restore-tests/app-01-2026-10-03/

rsync -aH --numeric-ids \
    /srv/backup/hosts/app-01.example.test/current/ \
    /srv/restore-tests/app-01-2026-10-03/

Poprawne zakończenie rsync potwierdza, że pliki można zapisać w lokalizacji testowej. Pełny test sprawdza jednak ich użyteczność: importujemy dump do odizolowanej instancji bazy, uruchamiamy aplikację bez dostępu do produkcji i wykonujemy kontrolne zapytanie lub żądanie HTTP. Weryfikujemy też wymagane pliki konfiguracyjne oraz uprawnienia. Dokładne polecenia importu zależą od silnika bazy i aplikacji, więc nie zastępujemy ich przypadkowym ogólnym poleceniem.

Wynik testu zapisujemy z datą, użytym snapshotem lub wersją kopii, czasem odtworzenia i zakresem sprawdzenia. Backup bez udanego testu odtworzenia nie jest potwierdzeniem możliwości odzyskania usługi.

Czego ten model nie gwarantuje?

  • Nie ukrywa danych przed przejętym serwerem kopii. Wykradziony klucz i dostęp do dozwolonego adresu dają odczyt katalogu eksportu. Ograniczenia rrsync nie zastępują ochrony backup hosta.
  • Nie zapewnia spójności aplikacyjnej. Rsync kopiuje to, co otrzyma. Baza danych i aplikacja wymagają własnego, sprawdzonego sposobu przygotowania eksportu.
  • Nie zastępuje retencji ani kopii poza lokalizacją. Kierunek połączenia nie cofnie nadpisania bieżącej wersji ani nie ochroni przed awarią jednego storage.
  • Nie zastępuje kontroli po aktualizacji. Po aktualizacji rsync lub OpenSSH wykonujemy dry-run oraz test zakazanego polecenia, ponieważ zachowanie ograniczeń jest częścią bezpieczeństwa rozwiązania.
  • Nie udowadnia odtwarzalności samym kodem zakończenia. Dopiero udane, izolowane odtworzenie usługi zamyka test backupu.

Co musi być prawdą przed automatyzacją?

  1. Klucz prywatny znajduje się tylko na serwerze kopii, a host aplikacyjny nie ma poświadczeń do archiwum.
  2. Wpis klucza ma wymuszone rrsync -ro, ograniczony katalog i wyłączone kanały SSH niepotrzebne rsync.
  3. Konto backup-pull może czytać eksport, ale nie może go zmieniać ani uzyskać powłoki.
  4. Dry-run działa, a próby uruchomienia id i wysłania pliku na chroniony host są odrzucane.
  5. Istnieje jawna ochrona starszych wersji oraz monitorowanie wieku ostatniej udanej kopii i wolnego miejsca.
  6. Wykonano i zapisano udany test odtworzenia poza produkcją.

Kopia, z której nigdy nie odtwarzano, jest hipotezą

Projektujemy kopie zapasowe tak, żeby przejęcie serwera produkcyjnego nie dawało dostępu do archiwum, i regularnie sprawdzamy, czy odtworzenie faktycznie działa.