Zálohy, ktoré sa nedali obnoviť
Zálohovací skript každú noc skončil bez chyby a ukladal prázdne súbory. Prišiel na to až prvý test obnovy. Čo v skripte chýbalo a ako sa obnova overuje.
- Firma
- Jazyková škola s online rezerváciou kurzov, 14 zamestnancov
- Tím
- Jeden interný vývojár, externý správca podľa potreby
- Prostredie
- Jeden VPS, Docker Compose: aplikácia, PostgreSQL, Nginx
- Zálohy
- Každú noc cez cron: pg_dump na disk servera a kópia do Amazon S3
- Obmedzenia
- Obnova sa nikdy neskúšala, monitoruje sa iba dostupnosť webu
01Východiskový stav
Databáza beží v kontajneri s dátami vo volume Dockeru. Nahraté súbory (zmluvy, faktúry, fotky lektorov) ležia v adresári na serveri pripojenom do kontajnera aplikácie. Skript v crone každú noc spustil pg_dump cez docker exec, výstup skomprimoval gzipom a súbor skopíroval do bucketu v S3. Kľúč na serveri mal k bucketu plné oprávnenia.
- Internet
- Nginx
- Aplikácia (Docker)
- PostgreSQL (volume Dockeru)
- cron: pg_dump | gzip
- Amazon S3: zálohy
02Problém
Súčasťou previerky systému pred rozšírením rezervácií bol test obnovy: stiahnuť poslednú zálohu a obnoviť ju do čistého kontajnera. Posledná záloha mala 20 bajtov. Rovnako všetky za posledných sedem týždňov.
Keby v tom čase zlyhal disk alebo by server niekto zašifroval, škola by prišla o sedem týždňov prihlášok, platieb a zmien v rozvrhu. Nahraté súbory by sa nedali obnoviť vôbec, pretože sa nezálohovali nikdy.
K tomu tretí problém: kľúč na serveri smel v buckete aj mazať. Útočník, ktorý ovládne server, by zmazal databázu aj zálohy naraz.
03Technická príčina
Pred siedmimi týždňami prešla databáza na novú hlavnú verziu PostgreSQL a kontajner pri tom dostal iný názov. docker exec v skripte odvtedy zlyhával, lebo kontajner so starým názvom neexistoval.
Skript to nezistil. Bez set -o pipefail je návratový kód pipeline docker exec ... | gzip > zaloha.gz kódom posledného príkazu, teda gzipu, a ten skončil úspešne: skomprimoval prázdny vstup. Prázdny gzip má presne 20 bajtov.
Chybová hláška išla na chybový výstup úlohy v crone. Cron ho posiela e-mailom, ale na serveri nebola nastavená pošta, takže ho zahodil. Nahraté súbory chýbali od začiatku, skript zálohoval iba databázu.
04Vyšetrenie
Veľkosti súborov v buckete ukázali presný deň, odkedy zálohy nemajú obsah. Zhodoval sa s dňom zmeny verzie databázy v histórii repozitára s konfiguráciou Docker Compose.
Ručné spustenie skriptu vypísalo Error response from daemon: No such container. Systémový log potvrdil, že cron výstup úlohy zahadzoval, pretože na serveri nebol nainštalovaný poštový agent.
Posledná neprázdna záloha sa obnoviť dala a dáta od toho dňa boli stále v bežiacej databáze. O nič sa neprišlo. Išlo o riziko, nie o stratu.
05Náprava
V ten istý deň ručná záloha databázy aj súborov a overenie, že sa dá obnoviť. Až potom opravy.
Skript je prepísaný tak, aby zlyhanie ktoréhokoľvek kroku zastavilo celý beh, oslovuje službu v Docker Compose namiesto názvu kontajnera a úspech hlási sám.
#!/usr/bin/env bash
set -euo pipefail
source /etc/backup.env # HC_UUID and the AWS credentials, mode 600
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
FILE="/var/backups/db/app-$STAMP.dump"
trap 'rm -f "$FILE"' ERR
cd /srv/app
# the service name survives a renamed container; -T: no pseudo-TTY
docker compose exec -T db pg_dump -U app -Fc app > "$FILE"
# an empty or unreadable archive fails here, not on the day of a restore
docker compose exec -T db pg_restore --list < "$FILE" > /dev/null
aws s3 cp "$FILE" "s3://skola-zalohy/db/app-$STAMP.dump"
aws s3 sync /srv/app/uploads "s3://skola-zalohy/uploads/" # no --delete
# only a run that got this far reports in; silence raises the alarm
curl -fsS -m 10 --retry 3 "https://hc-ping.com/$HC_UUID" > /dev/nullpg_dump -Fc vytvára komprimovaný archív vo vlastnom formáte, gzip netreba. pg_restore --list hneď po zálohe overí, že archív je čitateľný. Prepínač -T vypína pseudoterminál, ktorý docker compose exec inak prideľuje, a pri binárnom výstupe presmerovanom do súboru ho treba vypnúť.
Nahraté súbory sa zálohujú príkazom aws s3 sync bez prepínača --delete, takže zmazanie súboru na serveri nezmaže jeho kópiu.
Ochrana pred zmazaním: bucket má zapnuté verziovanie a pravidlo životného cyklu, ktoré staré verzie po 35 dňoch odstráni. Kľúč na serveri smie iba nahrávať (s3:PutObject) a vypisovať obsah pre synchronizáciu (s3:ListBucket). Nemá s3:DeleteObject, s3:DeleteObjectVersion ani právo meniť verziovanie či pravidlá.
Hlásenie úspechu ide do služby Healthchecks.io. Ak v očakávanom čase nepríde, služba pošle e-mail. Rovnako poslúži jej open-source verzia na vlastnom serveri.
Test obnovy raz týždenne: druhý skript stiahne poslednú zálohu, obnoví ju do dočasného kontajnera PostgreSQL a overí, že najnovšia rezervácia nie je staršia ako deň. Výsledok hlási rovnakým spôsobom.
06Prečo oprava funguje
set -e ukončí skript pri prvej chybe, pipefail zabezpečí, že za chybu pipeline sa počíta aj zlyhanie príkazu uprostred, nielen posledného, a set -u zastaví skript pri preklepe v názve premennej. Pasca ERR zmaže rozpracovaný súbor, aby nevyzeral ako záloha.
Názov služby db sa nemení, keď sa zmení názov kontajnera, čo bola pôvodná príčina.
Monitoruje sa úspech, nie chyba. Služba sa ozve, keď hlásenie nepríde, takže pokryje aj prípady, keď skript vôbec nebeží, napríklad po zmazaní úlohy z cronu.
Pri zapnutom verziovaní prepísanie objektu vytvorí novú verziu a pôvodná ostane. Kľúč bez práv na mazanie nevie odstrániť ani objekt, ani jeho staršie verzie. Útočník na serveri teda zálohy nezničí.
Čo to nerieši: záloha nie je vysoká dostupnosť. Keď server spadne, web nepôjde, kým sa neobnoví na novom.
07Overenie
- Testovacia kópia skriptu so zámerne zlým názvom služby skončila chybou, rozpracovaný súbor zmazala a Healthchecks poslal upozornenie.
- Úplná obnova na čistý server podľa zapísaného postupu, databáza aj súbory, s odmeraným časom. Ten je teraz v dokumentácii ako očakávaná doba výpadku pri obnove.
- Pokus zmazať objekt aj jeho verziu kľúčom zo servera skončil
AccessDenied.
08Výsledky
Opravené
- Zálohy databázy majú obsah a zlyhanie sa prejaví do jedného dňa.
- Nahraté súbory sa zálohujú.
Znížené riziko
- Kompromitácia servera už neznamená stratu záloh.
- Obnova je vyskúšaná a zapísaná, nie iba predpokladaná.
Ostáva otvorené
- Pri nočnej zálohe môže škola prísť najviac o jeden deň dát.
- Server je stále jeden. Pri jeho páde web nejde, kým sa neobnoví.
09Obmedzenia
pg_restore --listoverí iba hlavičku a zoznam objektov v archíve, nie všetky dáta. Poškodené dáta odhalí až týždenný test obnovy.- Automatický test overuje databázu. Obnova súborov sa skúša ručne raz za štvrťrok.
- Zálohy obsahujú osobné údaje. Prístup k bucketu a doba uchovávania musia zodpovedať tomu, čo škola uvádza v zásadách ochrany osobných údajov.
- Staré verzie sa mažú po 35 dňoch. Chybu v dátach, ktorú si nikto nevšimne dlhšie, zo záloh nevrátite.
10Poučenia
- Záloha, ktorá nebola obnovená, je iba predpoklad.
- Skript v shelli bez
set -euo pipefailmôže hlásiť úspech aj pri zlyhaní. - Monitorujte úspech, nie iba chyby. Ticho nie je dobrá správa.
- Server nesmie vedieť zmazať vlastné zálohy.
- Zálohujte všetko, čo sa nedá znova vytvoriť, nielen databázu.
11Ďalšie kroky pre malú firmu
- 1.Zapísať postup obnovy tak, aby ho zvládol aj človek, ktorý server nepozná.
- 2.Pridať zálohu databázy aj počas dňa, ak strata jedného dňa nie je prijateľná.
- 3.Zapnúť dvojfaktorové overenie pre konzolu AWS a spravovať zálohy z iného účtu než produkciu.
- 4.Pri raste počtu aplikácií zvážiť spravovanú databázu s automatickými zálohami.
Spoznávate v tom svoju firmu?
Napíšte nám, čoho sa to týka. Ozveme sa do jedného pracovného dňa a povieme, či je to práca pre nás, aj keď odpoveď bude nie.