RE:NODE

Databáze14 min čtení

Zabezpečení databáze: kontrolní seznam

Kontroly databáze, na kterých záleží: co je dostupné zvenku, kdo se může přihlásit, co smí dělat, TLS, injection, limity a záloha, kterou jsi opravdu obnovil.

6 čtenářů

Do databází se málokdy někdo vloupá. Většinou do nich prostě vejde. Drtivá většina incidentů se redukuje na čtyři věci: port odpovídal celému internetu, heslo šlo uhodnout nebo bylo použité jinde, jeden všemocný účet dělal v systému všechno a když se něco pokazilo, nebyla k dispozici použitelná záloha. Všechno chytré z bezpečnostní literatury stojí až za těmito čtyřmi body, a když opravíš jen je, jsi dál než většina produkčních systémů.

Tohle je kontrolní seznam, který projdeš za odpoledne, v pořadí, které nejdřív odstraní nejvíc rizika. Příklady používají PostgreSQL a MongoDB, protože tyhle dva databázové systémy lidé nejčastěji provozují sami, ale princip je stejný pro jakoukoli databázi: rozhodni, kdo se k ní dostane, kdo se může přihlásit, co smí každý účet dělat, zašifruj cestu, omez škody, které může způsobit jeden klient, a drž kopii, o které jsi dokázal, že ji umíš obnovit.

Jedno hledisko si drž celou dobu. Bezpečnost není vlastnost, kterou přidáš na konec, je to sada rozhodnutí o dosahu škod. Každé chybě nezabráníš. Každý krok odpovídá na otázku: když se něco pokazí, jak daleko to dojde.

Dostupnost: první a největší výhra#

Každá databáze, která naslouchá na veřejné adrese, bude nalezena. Ne že by mohla - bude. Hromadné skenery nepřetržitě procházejí celý adresní prostor a hledají 5432, 27017, 3306 a 6379 a nový stroj s otevřeným portem typicky zažije první pokus o přihlášení během několika minut po spuštění. Vlna vydírání MongoDB v roce 2017 nebyla žádný důmyslný exploit; byly to tisíce instancí navázaných na 0.0.0.0 s vypnutým ověřováním.

Začni tím, že zjistíš, co vlastně naslouchá:

bash
$ ss -ltnpState  Recv-Q Send-Q Local Address:Port  ProcessLISTEN 0      244    127.0.0.1:5432      users:(("postgres",pid=812,fd=6))LISTEN 0      4096   0.0.0.0:27017       users:(("mongod",pid=944,fd=11))

127.0.0.1:5432 je dostupné jen z téhož stroje. 0.0.0.0:27017 je dostupné odkudkoli, kam to firewall pustí. Pak to zkontroluj zvenku, protože to, co si stroj myslí, a to, co dělá síť, jsou dvě různé otázky:

bash
$ nc -vz db.example.net 5432Connection to db.example.net 5432 port [tcp/postgresql] succeeded!

Nastavení, která to řídí:

DatabázeNastaveníBezpečná výchozí hodnota
PostgreSQLlisten_addresses v postgresql.conflocalhost nebo jedna privátní adresa
PostgreSQLpravidla host v pg_hba.confKonkrétní adresy, nikdy 0.0.0.0/0
MongoDBnet.bindIp v mongod.conf127.0.0.1 plus konkrétní adresy
JakákoliFirewallPříchozí provoz ve výchozím stavu zakázaný, povolené jmenované zdroje

Pokud aplikace a databáze běží na stejném stroji, naváž na localhost a tuhle část přestaň číst. Pokud ne, povol přesně ty adresy, které to potřebují, a nic dalšího. Pravidla firewallu, na kterých záleží ukazuje, jak taková pravidla psát, aniž by ses sám zamkl venku, a vzdálená připojení k PostgreSQL popisuje tři nastavení, která se musí shodovat, než se vůbec může připojit vzdálený klient.

Ověřování, které nejde uhodnout#

Jakmile je port dostupný pro lidi, kteří ho potřebují, další otázka zní, kdo se smí přihlásit.

PostgreSQL to rozhoduje pomocí pg_hba.conf, čte se shora dolů a vyhrává první odpovídající řádek. Na tomhle pořadí se lidé spálí: povolující řádek nad zakazujícím znamená, že ten zakazující se nikdy neuplatní.

pg_hba.conf
# TYPE    DATABASE  USER     ADDRESS           METHODlocal     all       postgres                   peerhostssl   app       app_rw   10.0.0.0/24       scram-sha-256hostssl   app       app_ro   10.0.0.0/24       scram-sha-256host      all       all      0.0.0.0/0         reject

Používej scram-sha-256, ne md5, a nikdy trust, což znamená žádné heslo. Nastav password_encryption = scram-sha-256 dřív, než vytvoříš uživatele, jinak se jejich hesla uloží podle starého nastavení. hostssl místo host odmítne nešifrované spojení, což je silnější než zdvořile požádat klienta.

MongoDB se dodává s vypnutou kontrolou přístupu, a je to nejdůležitější řádek v mongod.conf:

mongod.conf
security:  authorization: enablednet:  bindIp: 127.0.0.1,10.0.0.5  port: 27017

Bez něj je správcem každý, kdo se dostane k portu. S ním MongoDB používá SCRAM-SHA-256 a každá operace vyžaduje uživatele.

K samotným heslům: generovaná, dlouhá, jedinečná pro každou databázi, uložená ve správci hesel a nikdy nepoužitá znovu mezi stagingem a produkcí. Dvacet náhodných znaků z generátoru porazí zapamatovatelnou frázi s náhradami znaků, protože útočníkův slovník už ty náhrady obsahuje. Pokud bylo heslo někdy vložené do chatu, ticketu nebo screenshotu, je spálené - změň ho.

Jeden uživatel na jednu práci#

Tohle je krok, který z průniku udělá incident místo katastrofy, a je to krok, který se nejčastěji přeskakuje, protože jediný superuživatel v connection stringu vždycky funguje.

Aplikace nepotřebuje za běhu vytvářet tabulky. Analytický dashboard nepotřebuje mazat řádky. Migrační nástroj potřebuje práva na schéma devadesát vteřin měsíčně, ne trvale. Čtyři role pokryjí většinu systémů:

RolePrávaPoužívá
Vlastník nebo superuživatelVšechnoNic rutinního. Drží se pro nouzové případy
Migrační uživatelZměny schématu v jedné databáziJen deploy pipeline
Uživatel aplikaceČtení a zápis v tabulkách, žádné DDLBěžící aplikace
Uživatel jen pro čteníPouze SELECTDashboardy, reporty, lidé, kteří si jen něco prohlížejí

V PostgreSQL to vypadá takhle:

sql
CREATE ROLE app_rw LOGIN PASSWORD 'generated-here';CREATE ROLE app_ro LOGIN PASSWORD 'a-different-one';GRANT CONNECT ON DATABASE app TO app_rw, app_ro;GRANT USAGE ON SCHEMA public TO app_rw, app_ro;GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_rw;GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_rw;GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_ro;ALTER DEFAULT PRIVILEGES IN SCHEMA public  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_rw;ALTER DEFAULT PRIVILEGES IN SCHEMA public  GRANT SELECT ON TABLES TO app_ro;

Řádky s ALTER DEFAULT PRIVILEGES jsou ty, na které se zapomíná. Bez nich bude každá tabulka vytvořená další migrací pro uživatele aplikace neviditelná a ve tři ráno dostaneš v produkci chybu oprávnění. Role a oprávnění v PostgreSQL prochází celý model, včetně toho, proč má PUBLIC ve starších verzích víc práv, než čekáš.

V MongoDB stejná myšlenka používá vestavěné role omezené na jednu databázi:

javascript
db.createUser({  user: "app_rw",  pwd: passwordPrompt(),  roles: [ { role: "readWrite", db: "app" } ]})

Nikdy nedávej aplikaci root, dbOwner ani nic, co končí na AnyDatabase. Connection string pak potřebuje authSource ukazující na databázi, kde byl uživatel vytvořen, což je nejčastější důvod, proč je správné heslo odmítnuto - connection stringy MongoDB mají celou anatomii.

Stejná zásada platí pro lidi. Účty v panelu, deploy klíče a dashboardy by měly mít každé co nejužší roli, která ještě umožní práci, a jmenovaného vlastníka. Tuhle stránku věci pokrývá subuživatelé a nejmenší oprávnění.

Šifruj spojení a ověřuj certifikát#

Pokud databáze a aplikace nejsou na stejném stroji, přihlašovací údaje i každý řádek putují po síti. Zašifrovat to je snadné. Zašifrovat to způsobem, který skutečně dokazuje, s kým ses spojil, chce ještě jeden krok a většina connection stringů se zastaví před ním.

sslmode v PostgreSQL má šest hodnot a jen dvě z nich stojí za použití:

sslmodeŠifrujeOvěřuje serverVerdikt
disableNeNeProstý text
allow, preferMožnáNeprefer je výchozí a tiše se vrací k nešifrovanému spojení
requireAnoNeZranitelné vůči stroji uprostřed
verify-caAnoJen řetěz certifikátůPřijatelné
verify-fullAnoŘetěz i název hostitelePoužívej tohle
code
postgresql://app_rw:pw@db.example.net:5432/app?sslmode=verify-full&sslrootcert=/etc/ssl/certs/ca.pem

U MongoDB platí stejný argument s jiným pravopisem: tls=true v URI, tlsCAFile, pokud certifikát nepochází od veřejné autority, a nikdy tlsAllowInvalidCertificates=true, což vypne ověřování, ale nechá ve stringu slovo TLS, aby tě uklidňovalo.

Drž přihlašovací údaje mimo kód#

Heslo k databázi v git repozitáři je heslo na internetu, ať je repozitář veřejný, nebo ne - přežívá v historii, ve forcích, v zálohách notebooků všech, kdo ho kdy naklonovali, a v čemkoli, co indexuje tvůj git hosting. Přihlašovací údaje patří do proměnných prostředí nebo do úložiště tajemství, vkládané za běhu, a .env patří do .gitignore od prvního commitu, ne od dne, kdy si toho všimneš.

Tři praktická pravidla:

  • Nikdy nedávej heslo do příkazové řádky. Skončí v historii shellu a ve výstupu ps pro všechny uživatele na stroji. Použij ~/.pgpass s režimem 0600, PGPASSWORD v prostředí nebo zadání na výzvu.
  • Různé přihlašovací údaje pro staging a produkci, vždy. Stagingový stroj je podle definice stroj s nižší důvěrou a sdílené heslo z něj dělá cestu dovnitř.
  • Když někdo opustí tým, změň je. Odebraný přístup není totéž co zapomenutý přístup.

Proměnné prostředí a tajemství popisuje mechaniku na panelovém hostingu, kde hodnoty drží karta Startup a nejsou v image ani v repozitáři.

Dotazy, do kterých nejde nic vložit#

Injection je i po desetiletích pořád způsob, jakým se většina databází dostane ke čtení někomu, kdo ji číst neměl. Mechanismus je vždy stejný: uživatelský vstup se zřetězí do dotazu, takže vstup může změnit strukturu dotazu, a ne jen jeho hodnoty.

javascript
// Broken. An email of  ' OR 1=1 --  returns every user.const sql = "SELECT * FROM users WHERE email = '" + email + "'";// Correct. The driver sends the query and the value separately.await client.query("SELECT * FROM users WHERE email = $1", [email]);
python
# psycopg, the same idea. The second argument is never string formatting.cur.execute("SELECT * FROM users WHERE email = %s", (email,))

Parametrizované dotazy nejsou trik s escapováním, jsou to funkce protokolu: příkaz se nejdřív zparsuje, potom se navážou hodnoty, takže hodnota se nemůže stát syntaxí. Používej je všude, i u dotazu, o kterém jsi si jistý, že je bezpečný.

Dvě věci, které parametry nedokážou, a právě tudy se injection vrací:

  • Identifikátory. Názvy tabulek a sloupců nemohou být parametry. Pokud sloupec pro řazení pochází z query stringu, porovnej ho se seznamem povolených názvů sloupců a cokoli jiného odmítni. Nikdy ho nevkládej přímo.
  • Celé klauzule. Skládání částí WHERE zřetězením řetězců je ta samá chyba v hezčím svetru, s ORM i bez něj. Většina ORM je ve výchozím stavu bezpečná a přestane být ve chvíli, kdy použiješ jejich únikový východ pro čisté SQL.

MongoDB má vlastní verzi. Pokud se tělo požadavku v JSON předá přímo do dokumentu dotazu, klient může poslat {"password": {"$ne": null}} a shodnout se s každým záznamem. Přetypuj příchozí hodnoty na očekávaný typ dřív, než se dostanou k dotazu, a vypni vyhodnocování JavaScriptu na straně serveru, místo abys spoléhal na to, že se $where používá opatrně.

Důvod, proč tahle část stojí až za nejmenším oprávněním, je ten, že se obě kombinují. Injection proti účtu, který může jen SELECT ze tří tabulek, je špatný den. Stejná injection proti superuživateli může zapisovat soubory a spouštět příkazy.

Limity, aby jeden klient nemohl shodit databázi#

Dostupnost je součást bezpečnosti a nejsnazší způsob, jak vyřadit malou databázi, je otevírat k ní spojení, dokud žádné nezbude.

  • `max_connections` v PostgreSQL je tvrdý strop a každé spojení stojí paměť, ať pracuje, nebo je nečinné. Na instanci s 1 až 2 GB je i 100 optimistické. Dej před databázi pooler a každé aplikaci přidej vlastní CONNECTION LIMIT pomocí ALTER ROLE app_rw CONNECTION LIMIT 20;. Connection pooly a limity vysvětlují aritmetiku.
  • `statement_timeout` zabrání jednomu splašenému dotazu držet zdroje donekonečna. Nastav ho po rolích, ne globálně, aby uživatel pro reporty dostal delší vodítko než webová aplikace: ALTER ROLE app_rw SET statement_timeout = '15s';
  • `idle_in_transaction_session_timeout` ukončí relace, které otevřely transakci a odešly pryč. Ty blokují vacuum a drží zámky a webová aplikace s chybou jich vyrobí po stovkách.
  • Limity četnosti v aplikaci, před vším, co spouští náročný dotaz pro anonymního návštěvníka. Obvyklou obětí jsou koncové body vyhledávání. Vzor popisuje rate limity a zneužití.

Zálohy, které jsi opravdu obnovil#

Záloha, kterou nikdo neobnovil, je hypotéza. Je to poslední řádek tohoto seznamu a ten, který rozhoduje, jestli bude incident nepříjemnost, nebo konec podnikání.

  1. Automatická, podle plánu. Ruční záloha je záloha, kterou přestaneš dělat v rušném týdnu, kdy ji nejvíc potřebuješ.
  2. Mimo stroj, který chrání. Kopie na stejném disku přežije smazanou tabulku a nic jiného.
  3. Víc než jedna generace. Poškození a zlomyslné změny se často objeví po dnech a jediná noční kopie už do té doby přepíše tu dobrou.
  4. Obnovená záměrně, v čase, který si zvolíš. Jednou za měsíc nebo dva obnov zálohu do zkušební databáze, spočítej pár řádků a spusť proti ní aplikaci. To je jediný důkaz, že záloha funguje.
  5. Brána jako citlivá. Dump je celá databáze v jednom stažitelném objektu. Nenechávej ho v adresáři přístupném z webu, což je překvapivě častý způsob, jak weby vyzradí všechno.

Zálohy a obnova databází obsahují mechaniku pro jednotlivé databáze a vyzkoušení obnovy dřív, než ji potřebuješ je ta půlka, kterou lidé přeskakují. Pokud se nejhorší už stalo, co dělat, když je tvůj server napadený popisuje pořadí kroků, a není to pořadí, které by ti napověděl instinkt.

Logy, které ukážou, co se změnilo#

Nemůžeš vyšetřit to, co jsi nezaznamenal, a výchozí logování většiny databází nezaznamenává skoro nic užitečného.

postgresql.conf
log_connections = onlog_disconnections = onlog_statement = 'ddl'log_min_duration_statement = 1000log_line_prefix = '%m [%p] %q%u@%d from %h '

log_statement = 'ddl' zaznamená každou změnu schématu bez šumu z logování každého dotazu. log_min_duration_statement = 1000 zaznamená všechno pomalejší než vteřinu, což zároveň slouží jako data o výkonu. %h v prefixu dá na každý řádek adresu klienta, což je to, co budeš chtít jako první, když něco vypadá špatně.

Kromě databáze sleduj věci, které se mění zřídka: nová role, změněný pg_hba.conf, uživatel, který dostal víc, než měl. To jsou malé události s velkým významem. Logy, které stojí za uchování pokrývají retenci a aktivitní log panelu u každého serveru zaznamenává, kdo co udělal na straně účtu.

Kontrolní seznam#

Projdi ho jednou pro každou databázi, kterou provozuješ. Cokoli, na co neumíš odpovědět, je další věc k opravě.

#KontrolaHotovo, když
1Port není otevřený světuss -ltnp a externí test portu se shodují
2Ověřování je zapnuté a používá SCRAMŽádné trust, žádné md5, žádná MongoDB bez authorization
3Hesla jsou generovaná a jedinečnáNic se nepoužívá znovu mezi prostředími
4Uživatel aplikace nemůže měnit schémaMigrace běží pod jinou rolí
5Existuje role jen pro čtení pro lidi a dashboardyNikdo se nedotazuje jako vlastník
6Spojení jsou šifrovaná a ověřenáverify-full nebo tls=true se souborem CA
7Přihlašovací údaje žijí mimo repozitář.env ignorovaný, hodnoty vkládané za běhu
8Každý dotaz je parametrizovanýŽádné zřetězování řetězců, identifikátory ze seznamu povolených
9Limity spojení a příkazů jsou nastavenéJeden špatný klient nevyčerpá server
10Zálohy jsou automatické, mimo stroj a vyzkoušenéJednu jsi obnovil v tomto čtvrtletí
11Spojení, DDL a pomalé dotazy se logujíDokázal bys rekonstruovat včerejšek
12Verze databáze a ovladače jsou aktuálníBezpečnostní vydání nasazená do několika týdnů

FAQ#

Stačí velmi dlouhé heslo, když je port veřejný?

Zabrání hádání a nic dalšího nezastaví. Veřejný port tě pořád vystavuje chybám na úrovni protokolu v jakékoli verzi, kterou běžíš, únikům přihlašovacích údajů odjinud a odepření služby vyčerpáním spojení. Omez i zdrojové adresy, vždy. Ověřování a dostupnost jsou samostatné kontroly a chceš obě.

Potřebuju TLS, když je databáze v privátní síti?

Použij ho stejně. Privátní sítě se sdílejí s dalšími nájemci častěji, než název napovídá, napadený interní stroj může sledovat provoz a zapnutí TLS stojí jeden řádek v connection stringu. Jediné místo, kde je potřeba být opatrný, je ověřování: require bez verify-full šifruje, ale nedokazuje, kdo odpověděl.

Ochrání mě ORM před SQL injection?

U dotazů, které si sestaví samo, ano. Riziko je v metodě pro čisté dotazy, kterou poskytuje každé ORM, a v místech, kde se identifikátor nebo klauzule skládá jako řetězec, protože ji query builder neuměl vyjádřit. To jsou řádky ke kontrole a bývají to ty, u kterých je komentář s omluvou.

Jak často se mají měnit hesla k databázi?

Podle plánu je rotace většinou divadlo. Při události je nezbytná: někdo odejde, přihlašovací údaj se objeví v logu nebo na screenshotu, napadne se stroj, který ho držel, nebo má incident nástroj třetí strany, kterému jsi dal přístup. Postav rotaci tak, aby trvala pět minut a nepotřebovala deploy, a uděláš ji, když na tom záleží.

Co je na tomhle seznamu nejcennější?

Omezit dostupnost a potom otestovaná záloha. První zabrání většině toho, co se děje; druhá znamená, že zbytek se dá přežít. Všechno ostatní zužuje škody mezi tím.

Databázi spravuje můj hosting. Co z toho je pořád moje?

Dostupnost, záplatování databáze a podkladový stroj jsou věcí hostingu. Koho vytvoříš, jaká práva mu dáš, jestli connection string ověřuje TLS, jestli jsou tvé dotazy parametrizované, kde jsou uložené přihlašovací údaje a jestli jsi někdy obnovil zálohu, je všechno tvoje, a právě odtud incidenty pocházejí.


Komentáře

Zcela anonymně: žádný účet, žádný e-mail, žádné cookie. Ukládáme jen jméno, které napíšeš, text a čas - nic dalšího. Počet odkazů je omezený a značkování se nevykresluje.

0/2000