Modelový prípad 08Bezpečný vývoj softvéru

Nasadzovanie cez FTP: od ručného kopírovania k nasadeniu s návratom

Prerušený prenos cez FTP nechal na serveri zmes starého a nového kódu a rezervácie nefungovali. Zostavenie v CI, prepnutie verzie jedným krokom a návrat späť.

Firma
Cestovná kancelária s rezervačným webom, 20 zamestnancov
Tím
Jeden vývojár a jeden webdizajnér
Technológie
Laravel 11, MySQL, Nginx a PHP-FPM na jednom VPS
Obmedzenia
Žiadne testovacie prostredie, zmeny idú priamo do produkcie

01Východiskový stav

Vývojár upravuje kód lokálne a zmenené súbory nahráva cez FTP klienta priamo do adresára, z ktorého web beží. Potom sa cez SSH prihlási na server a podľa potreby ručne spustí composer install a migrácie. Drobné opravy občas robí priamo na serveri.

Prihlasovacie údaje k FTP sú uložené vo FTP klientovi na dvoch počítačoch. FTP ich po sieti posiela nešifrované.

  1. Počítač vývojára
  2. FTP klient (nešifrované)
  3. VPS: adresár, z ktorého beží web
  4. Nginx / PHP-FPM
  5. MySQL

02Problém

Počas sezóny sa pri nahrávaní väčšej úpravy prerušilo spojenie. Na serveri ostala polovica nových súborov a polovica starých. Stránka rezervácie končila fatálnou chybou PHP, kým vývojár nedohral zvyšok. V logu Nginx bolo vidieť, že sa to týkalo každej rezervácie v tom čase.

O pár dní neskôr sa ukázala druhá chyba: jeden súbor pri nahrávaní zabudli. Chyba sa prejavila iba v jednej ceste rezervácie a prišiel na ňu zákazník.

Návrat k predchádzajúcej verzii nebol možný. Nikto presne nevedel, čo na serveri je, lebo časť opráv vznikla priamo tam a v Gite nebola.

03Technická príčina

Nasadzovanie nebolo atomické. Súbory sa menili jeden po druhom, zatiaľ čo web obsluhoval požiadavky, takže počas nahrávania a po jeho prerušení bežala zmes dvoch verzií.

Neexistovala jedna zostavená a otestovaná verzia. Na server išlo to, čo vývojár práve vybral, bez testov a bez kontroly, či je všetko.

Server sa odlišoval od repozitára, takže repozitár nebol spoľahlivým zdrojom pravdy a nebolo k čomu sa vrátiť.

Popri tom bezpečnostný problém: FTP posiela heslo nešifrovane a FTP klient ho má uložené na disku. Kto ho získa, môže meniť kód živého webu.

04Vyšetrenie

Obsah servera sa stiahol do samostatnej vetvy repozitára. git diff ukázal súbory upravené priamo na serveri, ktoré v Gite chýbali. Tie sa prebrali do hlavnej vetvy, aby sa nič nestratilo.

Logy Nginx a PHP-FPM ukázali, ako dlho výpadok trval a ktoré cesty zasiahol.

V konfigurácii FTP servera sa potvrdilo, že šifrované spojenie nevyžaduje.

05Náprava

Všetko je v Gite a server sa mení výhradne nasadením. FTP je vypnuté a heslo zmenené.

GitHub Actions pri každom zlúčení do hlavnej vetvy: composer install --no-dev --optimize-autoloader, zostavenie frontendu, php artisan test a zabalenie hotovej verzie. Ak testy zlyhajú, nasadenie sa nespustí.

Na server ide balík cez SSH kľúčom, ktorý patrí iba CI a osobitnému používateľovi s právami na adresár aplikácie. Každá verzia sa rozbalí do vlastného adresára a web sa na ňu prepne jedným krokom.

deploy/release.sh
#!/usr/bin/env bash
set -euo pipefail

APP=/srv/booking
REL="$APP/releases/$(date -u +%Y%m%d%H%M%S)"

mkdir -p "$REL"
tar -xzf /tmp/release.tar.gz -C "$REL"      # built and tested in CI
ln -s "$APP/shared/.env" "$REL/.env"
rm -rf "$REL/storage" && ln -s "$APP/shared/storage" "$REL/storage"

cd "$REL"
php artisan migrate --force
php artisan config:cache
php artisan route:cache

# atomic switch: rename(2) replaces the link in one step, ln -sfn does not
ln -s "$REL" "$APP/current.tmp"
mv -T "$APP/current.tmp" "$APP/current"

sudo systemctl reload php8.3-fpm            # drops OPcache entries of the old release
curl -fsS --retry 3 https://rezervacie.example.sk/up > /dev/null

# keep the last five releases for rollback
ls -1d "$APP"/releases/* | head -n -5 | xargs -r rm -rf

Nginx musí symbolický odkaz vyhodnocovať pri každej požiadavke. Inak by si PHP pamätalo cesty starej verzie.

nginx: server block
root /srv/booking/current/public;

location ~ \.php$ {
    include fastcgi_params;
    # resolve the symlink per request, so PHP never mixes two releases
    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    fastcgi_param DOCUMENT_ROOT $realpath_root;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

Návrat späť je ten istý posledný krok s predchádzajúcim adresárom a reload PHP-FPM, teda sekundy. Používateľ, pod ktorým CI nasadzuje, smie cez sudo spustiť iba tento jeden reload, nič iné.

Migrácie sa píšu tak, aby s novou schémou fungovala aj predchádzajúca verzia kódu: stĺpce sa najprv pridávajú a staré sa odstraňujú až v niektorom z ďalších nasadení. Inak by návrat kódu nepomohol.

06Prečo oprava funguje

mv -T na tom istom súborovom systéme volá rename, ktoré nahradí odkaz current jedným krokom. Každá požiadavka vidí buď celú starú, alebo celú novú verziu, nikdy zmes. Príkaz ln -sfn by odkaz najprv zmazal a potom vytvoril nový, s krátkym okamihom bez neho.

$realpath_root v Nginx a reload PHP-FPM zabezpečia, že PHP po prepnutí číta súbory novej verzie a jeho OPcache nedrží cesty tej starej.

Verzia sa zostavuje a testuje v CI, takže na server ide vždy celok, ktorý prešiel testami, nie výber súborov.

Staré verzie ostávajú na disku, takže návrat nič nezostavuje ani nesťahuje. Iba prepne odkaz.

SSH kľúč, ktorý patrí iba CI, nahrádza heslo na FTP, ktoré cestovalo sieťou nešifrované.

Čo to nerieši: migráciu databázy prepnutie odkazu nevráti. Preto sa migrácie píšu spätne kompatibilné.

07Overenie

  • Zámerne pokazený test zastavil nasadenie ešte v CI.
  • Počas nasadenia bežal cyklus požiadaviek na stránku rezervácie a žiadna nevrátila chybu.
  • Návrat na predchádzajúcu verziu bol vyskúšaný naostro a zapísaný do postupu.
  • FTP port už na serveri neodpovedá.

08Výsledky

Opravené

  • Nasadenie už nenechá na serveri zmes dvoch verzií.
  • Na server nejde nič, čo neprešlo testami.
  • Heslo na FTP sa po sieti neposiela, FTP je vypnuté.

Znížené riziko

  • Návrat k predchádzajúcej verzii trvá sekundy.
  • Server a repozitár sa zhodujú, takže vždy vieme, čo beží.

Ostáva otvorené

  • Chýba testovacie prostredie, zmeny sa prvýkrát naplno stretnú s dátami až v produkcii.
  • Migrácie sa dajú vrátiť iba ručne.

09Obmedzenia

  • SSH kľúč v CI je silné oprávnenie. Kto ovládne repozitár alebo jeho tajomstvá, môže nasadiť čokoľvek.
  • Testy sú také dobré, ako je ich pokrytie. Časť rezervácie ich stále nemá.
  • Server je jeden. Nasadenie bez výpadku neznamená prevádzku bez výpadku.

10Poučenia

  • Nasadenie má byť jeden atomický krok, nie séria kopírovaní.
  • Jediným zdrojom pravdy je repozitár. Čo sa zmení priamo na serveri, pri ďalšom nasadení zmizne.
  • Návrat späť treba vyskúšať skôr, než ho budete potrebovať.
  • Migrácie píšte tak, aby s nimi fungovala aj predchádzajúca verzia kódu.
  • FTP s heslom patrí do minulosti. SSH s kľúčom iba na jeden účel je bezpečnejšie aj pohodlnejšie.

11Ďalšie kroky pre malú firmu

  1. 1.Testovacie prostredie na tom istom serveri s vlastnou databázou.
  2. 2.Záloha databázy automaticky pred každou migráciou.
  3. 3.V GitHube vyžadovať pull request a úspešné testy pred zlúčením do hlavnej vetvy.
  4. 4.Doplniť testy na celú cestu rezervácie.
Ozvite sa

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.