CodeGym /Kursy /Spring Security /Secure-by-default: zamknięte domyślnie

Secure-by-default: zamknięte domyślnie

Spring Security
Poziom 2, Lekcja 1
Dostępny

1. Secure-by-default: zamknięte endpointy

Po podłączeniu security początkujący reaguje zazwyczaj tak samo: „Dodałem zależność i wszystko przestało się otwierać. Znaczy, że coś zepsułem". To naturalne: mózg przywykł, że gdy nic nie jest skonfigurowane, system powinien zachowywać się neutralnie. Ale w bezpieczeństwie neutralność rzadko jest przydatna. Bardzo często oznacza „przypadkowo niebezpieczne".

Widzieliśmy już sam symptom: po podłączeniu startera znane endpointy przestały otwierać się bez autentykacji. Teraz ważne jest zrozumienie, dlaczego to nie jest usterka kontrolerów, lecz bezpieczny baseline platformy.

Powiedzmy wprost: typowa edukacyjna aplikacja Spring Boot przed security żyje w trybie „ktokolwiek może wszystko". Jako start dla REST, kontrolerów i DTO to normalne, ale przenoszenie tego nawyku do prawdziwego developmentu jest już niebezpieczne. W realnym zespole endpointy pojawiają się ciągle. I jeśli platforma domyślnie wszystko otwiera, każdy nowy endpoint staje się loterią: „czy pamiętaliśmy go zamknąć?".

Dlatego zamknięcie „prawie wszystkiego" po podłączeniu startera to nie „Spring się obraził", lecz demonstracja zasady: lepiej zamknięte domyślnie, niż przypadkowo otwarte. W terminach bezpieczeństwa to bardzo dojrzała postawa. W terminach życia programisty – „lepiej dostać nieprzyjemny 401 lokalnie dziś, niż wyciek danych na produkcji jutro". Wycieki, nawiasem mówiąc, często właśnie tak się zdarzają: nikt niczego nie „hakował", ktoś po prostu zapomniał zamknąć jeden niewinny endpoint.

Przydatne jest też to, że ta niespodzianka zdarza się właśnie na początku kursu: zmusza do rozdzielenia dwóch rzeczy. Pierwsza – sens biznesowy endpointów („to jest publiczne", „to jest adminowskie"). Druga – polityka dostępu platformy („co jest dozwolone bez naszej konfiguracji"). Dopóki tego nie rozdzielisz, cały dalszy kurs będzie odczuwany jak magia z adnotacjami.

2. Secure-by-default i default-deny: bezpieczny wybór

Secure-by-default to prosta idea, ale z bardzo mocnymi konsekwencjami. Mówi ona: jeśli system nie dostał od programisty jawnych reguł, wybiera wariant minimalizujący ryzyko. Dla aplikacji webowej najbezpieczniejszy wybór bez znajomości kontekstu to default-deny, czyli „domyślnie odmawiamy dostępu i wymagamy autentykacji".

Dlaczego to jest rozsądne? Bo framework nie wie, co budujesz. Robisz bloga? Serwis bankowy? Wewnętrzny panel admina? API dla aplikacji mobilnej? Endpoint /status w jednym projekcie to niewinny healthcheck, w innym – ujawnienie wersji, środowiska i konfiguracji. Nawet słowo admin w URL może być „żartem" autora albo prawdziwą strefą administracyjną. Platforma nie czyta w twoich myślach i nie powinna.

Można to wyobrazić sobie jako bardzo nudny, ale uczciwy algorytm:

Sytuacja Co jest bezpieczniejsze? Co wybiera secure-by-default
Nie wiemy, czy można pokazywać dane wszystkim Ukryć Ukryć
Nie wiemy, czy można zmieniać stan Zabronić Zabronić
Nie wiemy, czy można wykonywać operację „adminowską" Wymagać przynajmniej logowania Wymagać przynajmniej logowania

Tak, przez pierwsze pięć minut jest to niewygodne. Za to na dłuższą metę jest bardzo wygodne: zespół przyzwyczaja się do jawnego opisywania modelu dostępu. To dokładnie to, co robiliśmy na poziomie 1, gdy rysowaliśmy access matrix projektu. Secure-by-default jakby mówi: „Dobra, zamknąłem wszystko. Teraz pokaż, że naprawdę rozumiesz, co powinno być otwarte. Nie słowami – regułami".

Jest jeszcze jedna myśl inżynierska, którą często się pomija: bezpieczeństwo lubi jednoznaczność. Gdy wszystko jest domyślnie otwarte, masz tysiące sposobów, żeby przypadkowo się pomylić: zapomnieć sprawdzenia, zbyt szeroko otworzyć endpoint, nie uwzględnić nowej ścieżki, pomylić handler. Gdy wszystko jest domyślnie zamknięte, główny rodzaj błędu jest jeden – nie otworzyć tego, co potrzeba. Błąd nieprzyjemny, ale prawie zawsze wykrywany szybko i bezpiecznie: użytkownicy skarżą się „nie otwiera się", a nie „wyciekły nam dane".

3. URL i nazwy kontrolerów – to nie są reguły dostępu

Bardzo częsty błąd w myśleniu początkującego brzmi tak: „Jeśli nazwałem ścieżkę /api/public/..., to jest ona publiczna. Jeśli nazwałem klasę AdminController, to jest ona adminowska". W zwykłej mowie ludzkiej brzmi to logicznie. Ale w security to jest po prostu komentarz, a nie reguła. Komentarze się nie wykonują.

Zróbmy celowo przykład, który prowokuje ten błąd, żeby potem do niego nie wracać.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class PublicInfoController {

    // Nazwa ścieżki służy tylko czytelności i routingowi, a nie jest regułą dostępu
    @GetMapping("/api/public/info")
    String info() {
        // Krótka odpowiedź, żeby było widać: przy baseline do metody może nie dotrzeć przez security
        return "public info";
    }
}

Jeśli podłączyłeś spring-boot-starter-security i nie napisałeś własnej konfiguracji dostępu, ten endpoint nie musi być publiczny. Słowo public to tylko część stringa URL. Spring Security nie wyciąga z tego żadnych wniosków.

To samo często zdarza się z „technicznymi" endpointami. Logika początkującego jest zrozumiała: „No to przecież tylko healthcheck, komu on potrzebny? Niech będzie otwarty". Platforma znowu odpowiada: „Nie wiem, na ile jest niewinny. Znaczy, zamykam".

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class HealthcheckController {

    // Typowy "techniczny" endpoint: z sensu wydaje się bezpieczny,
    // ale platforma nie próbuje zgadnąć, czy można go otwierać
    @GetMapping("/healthcheck")
    String healthcheck() {
        // Dla nas to tylko tekst, ale ważniejsze jest to, że dostępem zarządza oddzielna warstwa security
        return "healthy";
    }
}

I to, powtórzę, nie dlatego, że Spring Security jest „paranoikiem". Po prostu bezpieczeństwo nie lubi zgadywania. W dobrym modelu jest jasny podział: URL i nazwy klas służą czytelności, a reguły dostępu – realnemu sterowaniu.

Jest tu subtelny psychologiczny moment: jeśli przyzwyczaisz się do „oznaczania dostępu" nazwami ścieżek (/public, /admin) i będziesz uważać, że to już ochrona, nabędziesz bardzo niebezpieczny model mentalny. W prawdziwym świecie atakujący też potrafi czytać URL. A jeśli ochrona opiera się na tym, że „no tu przecież jest napisane public", to nie jest to ochrona, lecz samozadowolenie.

Właściwy wniosek na dziś brzmi tak: nazwy mogą sugerować sens, ale nie zapewniają dostępu. Dziś nie piszemy reguł, tylko stwierdzamy, że starter nie próbuje ich za nas zgadywać.

4. Dwie mapy: access matrix i baseline policy

Żeby dalej się nie mylić, przydatne jest trzymanie w głowie dwóch „map", które istnieją równolegle. Pierwsza mapa – to, czego chcemy jako produkt: access matrix naszego projektu. Druga – to, co robi platforma, jeśli nic nie skonfigurowaliśmy: baseline policy.

Wyobraź sobie, że mamy takie strefy w Secure Content Platform API:

  • strefa publiczna (odczyt opublikowanych artykułów);
  • strefa osobista (/api/me);
  • strefa szkiców (w głowie rozumiemy, że będzie tam owner-only);
  • strefy editor/admin.

Na mapie biznesowej chcemy mniej więcej tego. A oto platforma przy pierwszym uruchomieniu robi grubą, ale bezpieczną rzecz: „wszędzie potrzebny jest uwierzytelniony użytkownik". Wychodzi interesująca tabela.

Endpoint (przykład) Czego chcemy biznesowo Co widzimy zaraz po starter-security
GET /api/public/articles public (dostępne dla wszystkich) zamknięte bez logowania
GET /api/me authenticated zamknięte bez logowania (i to zgadza się z oczekiwaniem)
GET /api/drafts owner-only z sensu zamknięte bez logowania (ale ownership jeszcze nie istnieje jako reguła)
GET /api/admin/users admin-only zamknięte bez logowania (ale „admin-only" też jeszcze nie istnieje jako reguła)

Czyli baseline rozwiązuje tylko pierwszy poziom: „wpuszczać człowieka w ogóle". Nie rozróżnia ról, ownership ani szczegółów. I to jest normalne, bo celem baseline jest dać startowe bezpieczeństwo, a nie gotowy model produktu.

Wygodna analogia – kołowrót w budynku. Baseline to kołowrót „wejście tylko z przepustką". Nie wie, czy możesz iść do księgowości, serwerowni czy do gabinetu dyrektora. To już drugi poziom reguł. Ale jeśli kołowrotu w ogóle nie ma, do budynku wchodzi ktokolwiek, a dalsza rozmowa o „gabinecie dyrektora" brzmi już trochę optymistycznie.

Można nawet schematycznie narysować, jak to jest ułożone w głowie:

flowchart TD
    A["Reguły biznesowe projektu (access matrix)"] --> B["Jakie strefy mają być public / authenticated / admin / owner"]
    C["Platforma domyślnie (starter-security)"] --> D["Gruboziarnista polityka startowa: prawie wszystko wymaga logowania"]
    D --> E["Bezpieczny baseline: trudno przypadkowo otworzyć za dużo"]
    B --> F["Później dodamy jawne reguły, żeby otworzyć to, co potrzeba, i ograniczyć resztę zgodnie z domeną"]

Sens tego schematu jest prosty: nie kłóć się z baseline. Nie musi on pokrywać się z twoim projektem biznesowym. Musi być bezpieczny. Dopasowanie zrobimy później, gdy zaczniemy jawnie zarządzać dostępem.

5. Co daje starter-security na starcie

Teraz bardzo chce się zapytać: „A gdzie to się w ogóle dzieje? Przecież nie pisałem żadnego sprawdzenia w kontrolerze". Na dziś odpowiedź jest taka: dzieje się to w infrastrukturze, którą Spring Boot podnosi automatycznie, gdy widzi zależność spring-boot-starter-security. Wewnętrzną trasę żądania omówimy później; dziś potrzebujemy praktycznego obrazu, bez wycieczki po wnętrznościach.

Najważniejsze: zachowanie zmienia się nawet w maksymalnie „pustym" projekcie. Na przykład taka klasa aplikacji w ogóle nic nie mówi o dostępie, ale po starcie z zależnością security już zobaczysz, że endpointy zostały zamknięte:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecureContentApplication {

    public static void main(String[] args) {
        // Aplikacja może nie zawierać ani jednego "sprawdzenia" w kontrolerach,
        // ale warstwa security i tak będzie włączona przy wejściu żądań
        SpringApplication.run(SecureContentApplication.class, args);
    }
}

Skąd bierze się zamknięcie? Bardzo ogólnie i bez szczegółów można to ująć tak: starter włącza „warstwę bezpieczeństwa", która stoi przy wejściu przetwarzania żądania HTTP i decyduje, czy wpuścić żądanie dalej do twojej aplikacji. Jeśli żądanie przychodzi bez potwierdzonego użytkownika, ta warstwa odpowiada odmową i nie pozwala żądaniu dotrzeć do metody kontrolera.

Jeszcze jeden ważny niuans: Spring Boot robi to tak, żebyś mógł sprawdzić, że warstwa rzeczywiście działa. Czyli baseline nie tylko „zamknął drzwi i uciekł", ale też podnosi startowy mechanizm, który pozwala wejść. Ważna jest tu sama idea: secure-by-default nie zamienia aplikacji w cegłę, lecz sprawia, że jest ona domyślnie zamknięta, ale w sposób dający się zweryfikować.

I jeszcze jeden niuans dla spokoju ducha: secure-by-default to nie „zamknąć wszystko na zawsze". To „zamknąć, dopóki programista nie powie, co otwierać". Właśnie dlatego nasze dalsze poruszanie się po kursie jest logiczne: najpierw obserwujemy baseline, potem rozumiemy zasady, i dopiero po tym zaczynamy pisać własne reguły, żeby doprowadzić zachowanie do access matrix projektu.

6. Secure-by-default i przypadkowe dziury

Teraz będzie życiowy scenariusz, który w prawdziwych firmach zdarza się częściej, niż wszyscy by chcieli. Późny wieczór. Masz release. Ktoś dodaje „mały endpoint do debugowania", żeby sprawdzić jedną rzecz. Na przykład: „pokaż aktualne ustawienia", „pokaż listę użytkowników", „daj mi przycisk «wyczyść cache»". Kontroler udało się napisać. A ochronę do niego – nie zdążono albo po prostu zapomniano.

Jeśli platforma jest domyślnie otwarta, taki endpoint może przypadkowo stać się publiczny. Przy czym najczęściej nie odkryjesz tego ty. Odkryje to albo zewnętrzny skaner, albo „życzliwy" użytkownik, albo – w najbardziej ponurym wariancie – ktoś już po incydencie.

Secure-by-default niszczy właśnie ten typ problemów: „zapomniano zamknąć nowy endpoint". Bo przy baseline „zamknięte" nowy endpoint domyślnie nie jest dostępny dla wszystkich, dopóki jawnie nie zdecydujesz, że jest publiczny. To właśnie to, co w bezpieczeństwie nazywa się „fail closed": lepiej przestać działać, niż przypadkowo dać dostęp.

I jest to przydatne nie tylko przeciwko „hakerom w kapturach" (oni, szczerze mówiąc, w kapturach bywają zwykle tylko na konferencjach). Jest to przydatne przeciwko zwykłej ludzkiej naturze: jesteśmy zmęczeni, spieszymy się, rozpraszamy, mylimy gałęzie, zapominamy. Bezpieczna platforma nie udaje, że jesteś idealny. Po prostu zakłada normalny, dorosły default.

Osobną przyjemnością jest to, że secure-by-default dyscyplinuje architekturę. Przestajesz myśleć tak: „no mam kontroler, więc endpoint gotowy". Zamiast tego pojawia się dojrzalsza myśl: „endpoint jest gotowy, gdy ma miejsce w access matrix i jest poprawnie wpisany w model dostępu". To bardzo właściwe myślenie backendowe.

Tak, jest też odwrotna strona: na starcie widzisz wiele odmów i pojawia się odczucie „nic nie działa". Ale to chwilowy ból. W istocie to po prostu sprawdzian rzeczywistości: bezpieczeństwo nie pojawia się samo z siebie z pięknych DTO.

7. Baseline w Secure Content Platform API

Wróćmy do naszego projektu Secure Content Platform API. W nim z góry zaprojektowano strefy: publiczne artykuły, strefa osobista /api/me, szkice, operacje editor/admin. I tu secure-by-default robi bardzo przydatną rzecz: pokazuje, że projekt nie ma jeszcze własnego modelu dostępu, dlatego platforma używa własnej bazowej pozycji obronnej.

Na przykład mamy publiczną strefę artykułów. Biznesowo jest ona publiczna, ale po podłączeniu startera baseline i tak ją zamyka, bo platforma nie umie zgadywać sensu biznesowego.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class ArticlesController {

    // Biznesowo chcemy "public", ale baseline po podłączeniu security i tak wymaga autentykacji
    @GetMapping("/api/public/articles")
    String list() {
        // Demonstracyjna odpowiedź: ważniejsze nie jest zawartość, lecz sam fakt, że dostęp "domyślnie" zamknięty
        return "articles";
    }
}

Strefa osobista /api/me, odwrotnie, biznesowo powinna być dostępna dopiero po zalogowaniu, i tu baseline przypadkowo trafia w dobry kierunek: też wymaga logowania.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class MeController {

    // Przykład strefy, gdzie baseline pokrywa się z oczekiwaniem biznesowym: tutaj naprawdę nie można bez logowania
    @GetMapping("/api/me")
    String me() {
        // Zazwyczaj tu zwraca się bieżącego użytkownika, ale teraz to tylko znacznik "dotarliśmy do metody"
        return "current user";
    }
}

A strefa adminowska to interesujący przypadek. Biznesowo tu potrzeba nie tylko „zalogować się", ale „zalogować się jako admin". Baseline znowu rozwiązuje tylko pierwszy poziom: „zalogować się". To jeszcze nie znaczy, że adminówka jest gotowa. Znaczy tylko, że nie oddajesz już adminowskich odpowiedzi anonimowemu użytkownikowi.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class AdminUsersController {

    // Baseline chroni przed anonimowym dostępem, ale nie sprawdza roli "admin" – to będzie osobna reguła
    @GetMapping("/api/admin/users")
    String users() {
        // Demonstracyjna odpowiedź: istnienie metody nie oznacza, że model dostępu jest już poprawnie skonfigurowany
        return "users";
    }
}

I właśnie tę ważną myśl warto sobie dosłownie zapisać w głowie: aktualny baseline „prawie wszystko zamknięte" to nie błąd projektu i nie „zepsute API". To tymczasowy stan po pojawieniu się warstwy security, który trzeba umieć czytać. Specjalnie go obserwujemy, żeby dalej nie zamieniać konfiguracji bezpieczeństwa w szamanizm.

W terminach projektu wygląda to tak: biznesową mapę dostępu już mamy – wymyśliliśmy ją – a jawnych reguł jeszcze nie ma. I dopóki reguł nie ma, platforma wybiera safe default. Stąd i zasada robocza: nie „walczyć ze Springiem", lecz świadomie otwierać tylko to, co potrzeba, i ograniczać resztę tak, jak wymaga tego domena.

8. Typowe błędy przy secure-by-default

W tym punkcie studenci najczęściej popełniają błędy nie w kodzie – którego jeszcze prawie nie ma – lecz w interpretacji tego, co się dzieje. A jeśli poprawić właśnie interpretację teraz, dalej będzie znacznie łatwiej: przestaniesz walczyć z platformą i zaczniesz z nią współpracować.

Błąd nr 1: odbierać secure-by-default jako „Spring wszystko zepsuł".
Prawie zawsze to emocja z serii „wcześniej działało, teraz nie". Ale w bezpieczeństwie „działało" często oznacza „było otwarte". Właściwa reakcja – nie cofać zależności, lecz zadać sobie pytanie: „jaki dostęp powinien być w naszej access matrix i dlaczego?" Teraz „zamknięte" to nie bug, lecz startowa ochrona.

Błąd nr 2: myśleć, że słowo public w URL robi endpoint publicznym.
To najczęstsze sidło. URL to string do routingu i czytelności. Może być wskazówką dla człowieka, ale nie jest regułą dostępu. Jeśli to zrozumiesz teraz, dalsze tematy kursu, gdzie reguły są opisywane jawnie, będą znacznie łatwiej przyswajalne.

Błąd nr 3: próbować „naprawić" sytuację po prostu otwierając wszystko z powrotem.
Nawet jeśli bardzo chcesz odzyskać odczucie „wszystko odpowiada", to zły nawyk: utrwalasz u siebie model „security przeszkadza – więc wyłączamy". Znacznie przydatniej jest najpierw zrozumieć baseline, potem zobaczyć, że platforma daje ci sposób na sprawdzenie logowania, i dopiero potem stopniowo doprowadzać reguły do access matrix projektu.

Błąd nr 4: uważać, że skoro teraz „potrzebne jest logowanie", adminówka jest już chroniona.
Wymaganie logowania to tylko pierwsza warstwa. Chroni przed anonimowym dostępem, ale nic nie mówi o rolach, prawach, granicach editor/admin i ownership. Inaczej bardzo łatwo dostać fałszywe poczucie bezpieczeństwa: „no przecież zainstalowaliśmy Security – znaczy, jest ok". Nie, to znaczy tylko tyle, że pojawił się wejściowy punkty kontrolny.

Błąd nr 5: mieszać biznesową mapę dostępu i startowe zachowanie platformy w jedną magmę.
Jeśli nie trzymać tych dwóch map osobno, zaczynają się dziwne wnioski. Na przykład: „przecież mamy publiczne artykuły, a tu wymaga logowania – znaczy, że źle zaprojektowaliśmy public". Nie. Public zaprojektowałeś prawidłowo, tylko jeszcze nie opisałeś reguł dostępu. Baseline nie musi odpowiadać twojemu biznesowi – musi być bezpieczny.

1
Zadanie
Spring Security, poziom 2, lekcja 1
Niedostępne
Segment /public nie udostepnia endpointu
Segment /public nie udostepnia endpointu
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION