Výchozí zabezpečení
Bývalý kolega mě na jaře pozval na setkání OWASP Czech Republic. Zaujala mě tam především krádež session na Seznam.cz (Marek Tóth). A pak ještě popis výchozího zabezpečení některých systémů, což mi připomnělo moji vlastní zkušenost, o kterou bych se s vámi tentokrát chtěl podělit. Je známo, že bezpečnost a uživatelská/programátorská přívětivost jdou proti sobě. Je otázkou, kde najít vhodnou hranici. Odvážím si tvrdit, že se nám v konkrétním případě nakonec podařilo najít pěkné řešení.
Úvod
Předesílám, že věc se udála před lety, všude je to již dávno opravené a reálně zneužitelné to nebylo. Implementovali jsme OpenID Connect protokol. Pro naše vyprávění je podstatné, že v rámci toho se vydává podepsaný JSON Web Token (JWT). A pro podpis potřebujete privátní klíč. Mám rád přístup, který upřednostňuje konvence před konfigurací. A taky, když vývojáři rozběhají projekt za minuty a ne dny. Zkrátka na classpath byl testovací klíč a v instalační příručce pokyny, že si máte v produkci vygenerovat vlastní. Ovšem žádná ruční brzda, která by vás zastavila. A co se nestalo? Hádáte správně, v produkci použili testovací klíč. (Pro zvídavé čtenáře, na samotný útok zdaleka nestačí, křížově se kontroluje ještě UserInfo). Úkol zněl jasně, pro testování se vyhnout složité konfiguraci a zároveň ochránit produkci.
Řešení
Ukážeme si řešení za použití podmíněné konfigurace ve Spring Boot. Byť je omezené na konkrétní framework, tak principy jsou obecně použitelné. Záměrně se vyhýbám kryptografickému API a používám nějaké tajemství (secret).
Popišme si tři případy použití:
- Bez konkrétního zásahu do konfigurace aplikace nenastartuje. Pro produkci bezpečné, pro vývoj nepohodlné.
- Při zapnutí Spring profilu
dev
se vygeneruje náhodné tajemství. Pro vývoj pohodlné, pro produkci bezpečné. Jednak logujeme WARN ale hlavně tajemství se neukládá a po restartu aplikace se generuje nové (co kdyby někdo zapnuldev
na produkci a WARN ho nezastavil). - Pro prostředí vyžadující stabilitu i po restartu (nemusí jít pouze o produkci) konfigurujeme zdroj tajemství ze souboru (ideálně například z HSM, ale to je mimo rozsah tohoto článku).
@Configuration
public class SecretConfiguration {
@Bean
@ConditionalOnProperty(prefix = "cz.zvestov.secret", name = "type", havingValue = "file")
@ConditionalOnMissingBean
SecretService fileSecretService(@Value("${cz.zvestov.secret.file.path}") Path path) {
return new FileSecretService(path);
}
@Bean
@Profile("dev")
@ConditionalOnMissingBean
SecretService devSecretService() {
return new DevSecretService();
}
}
Anotace @ConditionalOnProperty zajistí konfiguraci jen v případě definované potřebné property.
Anotace @Profile nám podmiňuje konfiguraci pro konkrétní profil.
Anotace @ConditionalOnMissingBean nám zajistí, že konfigurujeme nanejvýš jednu beanu.
Pakliže byste potřebovali řídit pořadí vyhodnocení, můžete sáhnout po anotaci @Order.
Nechám na čtenářích k posouzení případ, kdy máte definovaný profil dev
a zároveň i property, kterou zapínáte načítání ze souboru.
Rozhraní SecretService
má v našem příkladu dvě implementace, DevSecretService
a FileSecretService
, ale to už je takové drobné programátorské cvičení.
Celý ukázkový projekt naleznete na GitHubu.
Mimo jiné si povšimněte, že soubor s tajemstvím zůstává na classpath, ale pouze pro unit testy.
Závěr
Napsat do dokumentace, že si máte v produkci změnit výchozí heslo, nestačí. Zároveň bychom měli myslet na pohodlí programátorů. Ukázali jsme si způsob, jak lze dosáhnout obojího při zachování bezpečnosti.