Poznaj AWS Secrets Manager. Bezhasłowy dostęp do bazy danych

Przywykliśmy do trzymania konfiguracji w plikach tekstowych do tego stopnia, że bez zastanowienia wrzucamy tam hasła do bazy danych czy inne dane uwierzytelniające. Co dalej? Commit, push i całość ląduje w repozytorium kodu. Jakie zalety ma takie podejście? Jest proste, szybkie i daje łatwy dostęp do bazy danych wszystkim zainteresowanym. A jakie ma wady? Jest proste, szybkie i daje łatwy dostęp do bazy danych wszystkim zainteresowanym.

Sebastian Feduniak. Co-founder of Pattern Match / DevOps Engineer. Programista Java oraz doświadczony konsultant IT. Swoją wiedzę DevOps zastosował już w wielu różnych dziedzinach m.in. Adtech, E-commerce, Travelling oraz IoT. Ma doświadczenie w wirtualizacji systemów fizycznych oraz konteneryzacji. Lubi dzielić się wiedzą w blogach technicznych. Miłośnik gór.


No dobrze. A teraz na poważnie. Jakie wady ma takie podejście?

  • Najczęściej korzystamy z ogólnodostępnych repozytoriów kodu jak Github czy Gitlab, co stanowi zagrożenie jeśli ktoś niepowołany uzyska dostęp do naszego konta.
  • Powyższe zagrożenie jest tym większe, że tak naprawdę wystarczy złamać choćby jednego użytkownika, który ma dostęp do repozytorium kodu.
  • Może dojść do pomyłki, w wyniku której nasze repozytorium kodu staje się publicznie dostępne (nie teoretyzuję, spotkałem się z tym).
  • Każdy kto ma dostęp do repozytorium kodu ma również dane dostępowe do bazy danych na produkcji co powoduje, że ktoś celowo lub omyłkowo może wprowadzić niepożądane zmiany.
  • Zmiana hasła użytkownikowi bazy danych również nie jest taka oczywista, biorąc pod uwagę, że nasza aplikacja cały czas z niej korzysta. Nie mówiąc już o sytuacji, kiedy nie wiesz jak wiele programów używa tej bazy.
  • Kwestia estetyki. Nie odczuwasz lekkiego wstrętu, kiedy mając już pierwsze programistyczne szlify za sobą wpisujesz hasło do pliku plain textem?!

Okazuje się jednak, że jest co najmniej kilka sposobów rozwiązania tego problemu. Jeden z nich to szyfrowanie haseł trzymanych w repo i odszyfrowanie ich w procesie CI/CD. Możliwe, że używasz do tego Ansible Vault. Inny sposób to pobieranie danych z zewnętrznego źródła np. HashiCorp Vault.

W tym wpisie przedstawię natomiast implementację z wykorzystaniem serwisu AWS Secrets Manager. Możesz je zastosować niezależnie od tego czy używasz infrastruktury AWS czy nie. Natomiast jeśli Twoja aplikacja i baza danych są uruchomione na AWS to opisane rozwiązanie zyskuje jeszcze więcej zalet. Zapraszam do lektury! Wróć… Klawiatury w dłoń i kodzimy!

O AWS Secrets Manager słów kilka

Po przeczytaniu opisu na stronie AWS nie pozostaje wiele do dodania. AWS Secrets Manager to menedżer haseł, który zapewnia następujące funkcje:

  • Bezpieczne przechowywanie haseł z wykorzystaniem szyfrowania. Raczej oczywiste.
  • Audyt operacji wykonywanych na hasłach. Dzięki integracji z innymi serwisami AWS można się dowiedzieć np. kiedy hasło zostało zmienione lub skasowane.
  • Dostęp do haseł z wykorzystaniem API. Żądania są płatne natomiast cena nie odstrasza.
  • Szczegółowe zarządzanie dostępem do haseł, dzięki integracji z AWS IAM. Jeśli coś AWS wyszło dobrze to na pewno IAM. Dzięki niemu zapomnisz o problemach z przydzielaniem odpowiedniego dostępu użytkownikom czy aplikacjom.
  • Automatyczna rotacja haseł. Killer feature. Ponieważ aplikacja pobiera hasło przez API, możesz je zmienić w dowolnym momencie, z jednego miejsca i z natychmiastowym efektem we wszystkich aplikacjach, które używają danego hasła. AWS Secrets Manager może to zrobić za Ciebie zgodnie z zadanym interwałem.

Bez obaw! Nie pisałbym o tym gdyby wdrożenie tego rozwiązania było przesadnie skomplikowane. Sam zobaczysz!

Plan na dziś

Zapewniam, że przechodzimy już do konkretów technicznych. Będziemy pracować na aplikacji, którą zbudowałem na potrzebę innych artykułów. Zgrabny opis projektu znajdziesz w repo tutaj. A jeśli interesuje Cię jak powstawał ten projekt to zapraszam do odwiedzin mojego firmowego bloga.

Plan na ten wpis jest następujący:

  • Utworzymy bazę danych w serwisie AWS RDS i stworzymy użytkowników dedykowanych dla testowanej aplikacji.
  • Hasło dostępowe użytkownika umieścimy w AWS Secrets Manager.
  • Zaczniemy od prostej przeróbki programu, dzięki której uzyskamy dostęp do bazy danych z wykorzystaniem AWS Secrets Manager. Użyjemy do tego AWS SDK.
  • Wprowadzone zmiany przetestujemy i przygotujemy pod kątem local development.
  • Corner case: sprawdzimy czy ta prosta przeróbka wytrzyma próbę zmiany hasła.
  • Uruchomimy i zaimplementujemy rotację haseł w AWS Secrets Manager.
  • Zastąpimy naszą implementację biblioteką dostarczoną przez AWS.
  • Sprawdzimy algorytm zastosowany w tej bibliotece, aby obsłużyć rotację haseł oraz możliwy wpływ na koszt używania serwisu AWS.

Ostatecznie zastanowimy się nad bardzo prawdopodobnym scenariuszem, w którym całą zabawę z AWS musimy rozpocząć od migracji bazy danych do AWS RDS.

Spójrz jeszcze na diagram rozwiązania, które wspólnie przygotujemy i zaczynamy!

Zmiany w kodzie będą oznaczone tagami żebyś łatwiej mógł dotrzeć do interesującej Cię części. Zaczynamy od authentication-token-revocation.

Step #1: Stworzenie bazy w AWS RDS

W pierwszym kroku stworzymy bazę danych, która posłuży nam w testach. Otwórz konsolę AWS, przejdź do serwisu RDS i stwórz bazę PostgreSQL według wskazówek na stronie.

Najmniejsza baza z parametrami jak poniżej w zupełności wystarczy.

  • Use case: Dev/Test
  • Class: db.t2.micro
  • Multi-AZ: No
  • Public accessibility: Yes
  • Backup: 0 days (disabled)
  • Enhanced monitoring: disabled
  • Performance insights: disabled
  • Database name: springboot_blog

Nie zaleca się używania master account w aplikacji, dlatego zaloguj się do bazy i stwórz dedykowanego użytkownika komendą jak poniżej.

Od tego momentu nie będziemy już używać konta master.

Step #2: Zapisanie hasła w AWS Secrets Manager

W tym kroku, hasło dla stworzonego użytkownika bazy danych zapiszemy w serwisie AWS. Oprócz zamieszczonych instrukcji możesz posiłkować się oficjalną dokumentacją.

Otwórz konsolę AWS, przejdź do serwisu Secrets Manager i otwórz kreator dodawania sekretu. Wybierz typ Credentials for RDS database, skopiuj nazwę użytkownika i hasło z komendy SQL, którą wykonałeś na bazie i zaznacz bazę RDS, której używasz do testów. W następnym kroku podaj nazwę, następnie zaznacz Disable automatic rotation i zapisz zmiany. Zauważ, że w podsumowaniu na ostatniej stronie kreatora otrzymujesz gotowy kodzik służący do pobierania hasła w Twojej aplikacji. Sprawdzimy czy działający kodzik.

Na tym etapie mamy wszystko po stronie AWS. Przechodzimy do aplikacji i testowania.

Step #3: Zastosowanie AWS SDK do pobierania hasła w trakcie inicjalizacji bazy danych

Tag w repo: rds-and-aws-secrets-manager-sdk

Do tej pory aplikacja używała bazy danych H2 i przechowywała dane w pamięci. W tym kroku dodamy konfigurację PostgreSQL, ale jednocześnie wprowadzimy dwa profile uruchomieniowe: produkcyjny oraz testowy. W testowym działanie aplikacji się nie zmieni. Podobnie, zadbamy o to żeby nasze zmiany nie naruszyły testów automatycznych. W trybie produkcyjnym, aplikacja będzie łączyła się z AWS RDS przy użyciu danych z AWS Secretes Manager. Do dzieła!

W pliku build.gradle dodaj zależności:

Potrzebujemy sterowników do H2 i PostgreSQL w zależności od trybu uruchomienia aplikacji. AWS SDK posłuży do komunikacji z serwisem Secrets Manager. Dodatkowo potrzebna nam jest dowolna biblioteka do parsowania JSON.

Następnie stwórz nową klasę, która będzie odpowiedzialna za tworzenie połączenia do bazy danych w RDS.

Zwróć uwagę na adnotację @Profile(“prod”). Zapewniamy w ten sposób, że obiekt tej klasy będzie tworzony tylko w trybie produkcyjnym. Tworzenie springowego DataSource jest standardowe natomiast pobieranie hasła to bezmyślnie skopiowany kod ze strony kreatora, którą widziałeś w poprzednim kroku. Dodałem tylko parsowanie JSON do prostego POJO. Tak lubię.

Co dalej? Dodaj plik konfiguracyjny dla Spring Framework. Pamiętaj, aby w pliku znalazł się suffix prod, bo na jego podstawie wybierany jest profil aplikacji.

Warto jedynie zauważyć, że wyłączamy autokonfigurację DataSource, ponieważ w trybie produkcyjnym tworzona jest manualnie.

To wszystko! W repozytorium znajdziesz jeszcze kilka zmian, które wspomagają local development. Natomiast teraz skupmy się na testowaniu nowej implementacji.

Step #4: Testujemy!

Zacznij od dodania klucza do Twojego konta AWS w zmiennych systemowych. Jeśli nie posiadasz klucza to utwórz go według oficjalnej dokumentacji, a następnie dodaj trzy zmienne środowiskowe.

Następnie uruchom aplikację.

Jeśli wszystko poszło dobrze to wykonajmy poniższą komendę dla potwierdzenia, że połączenie z bazą danych działa.

Jeśli w odpowiedzi zwrotnej otrzymujesz HTTP/1.1 200 to znaczy, że wszystko działa.

Zatem zrobione, idziemy do domu… Nie! Przecież mamy sprawdzić rotację haseł.

Step #5: Symulacja zmiany hasła i pułapka

Zanim przejdziemy do właściwej rotacji haseł w serwisie AWS Secrets Manager, zróbmy prosty test. Pozostaw uruchomioną aplikację. Podłącz się do bazy danych i zmień hasło Twojego użytkownika modyfikując komendę poniżej.

Powtórz teraz test z poprzedniego punktu. Działa? Działa!

I jest to pułapka, której możemy nie zauważyć w pierwszym momencie. No to zacznijmy od prostego teoretyzowania. Czy to ma prawo działać? Połączenie do bazy danych tworzone jest podczas startu aplikacji. Obiekt klasy DataSource jest singletonem, więc po uruchomieniu aplikacji nie zmieni się bez manualnej interwencji. Natomiast wiemy, że nie dodaliśmy żadnego kodu do obsługi zmiany hasła. To prowadzi do wniosku, że nasz program nie działa po zmianie hasła, chyba że…

…aplikacja tworzy pulę połączeń! Jak widzisz na zrzucie, jest aktywnych 11 połączeń do bazy, które pozostają aktywne nawet po zmianie hasła. W zależności od konfiguracji puli połączeń, ta liczba może się zmieniać w czasie, więc obecna implementacje będzie działać do czasu kiedy nastąpi potrzeba utworzenia nowego połączenia.

Prędzej czy później znajdziesz w logach następujący wpis:

Skoro zaszliśmy już tak daleko to pójdźmy dalej i poszukajmy w kodzie potwierdzenia powyższej tezy. Wielu z nas używa Spring Framework, ale niewielu zadaje sobie pytania o bebechy tego narzędzia. Wyłączam ten profesorski ton i przechodzimy do debugowania.

Step #6: Cała prawdo o puli połączeń

Zacznij od sprawdzenia, której implementacji javax.sql.DataSource używa aplikacja. Okazuje się, że jest to com.zaxxer.hikari.HikariDataSource, która domyślnie używa puli połączeń z domyślnymi ustawieniami co potwierdza ten fragment kodu.

Jeśli spojrzysz dalej na implementację metody javax.sql.DataSource#getConnection() to zobaczysz, że najpierw jest próba pobrania połączenia z puli. Ponadto, domyślne ustawienia powodują, że nieaktywne połączenia są zamykane po 30 minutach, więc problem całkiem szybko wyszedłby na jaw. Pełna lista parametrów konfiguracyjnych jest zamieszczona na stronie biblioteki.

Mamy to! No to lessons learned i zróbmy to porządnie!

Step #7: Wsparcie dla rotacji haseł w Twojej aplikacji

Tag w repo: rds-password-rotation

Zaczniemy od prostej przeróbki w testowanej aplikacji, która zapewni nam dostęp do RDS niezależnie od tego czy rotacja haseł jest włączona czy nie. Zmiany są dość banalne. Ograniczają się do zastosowania odpowiedniej biblioteki dostarczonej przez AWS.

Zacznij od dodania właściwej zależności:

Następnie wprowadź zmianę w produkcyjnym profilu testowej aplikacji.

Zauważ, że zmianie uległ sterownik – za chwilę prześledzimy w kodzie jakie to ma konsekwencje. Nazwa użytkownika bazy powinna teraz zawierać nazwę hasła podaną w AWS Secrets Manager.

Klasę RdsDataSourceConfig.java możesz usunąć. Skorzystamy z autokonfiguracji w SpringBoot.

To wszystko. Uruchom aplikację jak do tej pory. Poszukajmy jeszcze fragmentu w kodzie biblioteki, który potwierdzi, że aplikacja będzie działała przy włączonej rotacji haseł w AWS Secret Manager.

Najważniejsza zmiana jaka zaszła po dodaniu nowej zależności to inny driver do bazy danych. Więcej informacji uzyskamy czytając kod metody connect() która musi być w nim zaimplementowana.

Interesuje nas ten kawałek kodu:

Z konfiguracji data source pobierany jest identyfikator hasła w AWS Secrets Manager i wołana jest jest metoda connectWithSecret(unwrappedUrl, info, credentialsSecretId).

Spójrz na jej implementację:

Mając podany credentialsSecretId następuje próba pobrania hasła z cache. Jeśli uwierzytelnianie się nie powiedzie, następuje pobranie nowego hasła i ponowna próba połączenia z bazą danych.

Zwróć uwagę na jeszcze jeden szczegół. Biblioteka od AWS jest tylko wrapperem, który obsługuje pobranie hasła. Driver używany do realizacji połączenia jest zwracany przez metodę getWrappedDriver().

Step #8: Włączenie automatycznej rotacji haseł i ponowne testowanie

Wykonaliśmy wszystkie zmiany po stronie aplikacji. Czas włączyć rotację haseł, aby dopełnić nasz diagram o dwa brakujące komponenty.

Zaloguj się do konsoli AWS i przejdź do AWS Secrets Manager. Następnie przejdź do trybu edycji sekretu, którego używasz w aplikacji i włącz rotację hasła.

Zapisz i zaczekaj aż zniknie komunikat o tworzeniu niezbędnych komponentów. A co się dzieje pod spodem? AWS tworzy dla Ciebie stack CloudFormation, który utworzy Lambda Function odpowiedzialną za rotację hasła. Automatycznie zostanie także skonfigurowany CloudWatch, dzięki któremu uzyskasz dostęp do statystyk i logów nowo utworzonej funkcji. Dodatkowo, zostanie odpowiednio skonfigurowany dostęp z Lambda Function do bazy danych RDS.

Oczywiście, możesz samemu zaimplementować funkcję zmiany hasła. Kod używany przez AWS jest publicznie dostępny w ich repozytorium.

Jak widzisz, ten etap nie wymagał dużych nakładów pracy. Twoja aplikacja jest gotowa! Chociaż… miałem okazję spędzić trochę więcej czasu w tym miejscu. I chętnie o tym opowiem.

Step #47 Zrozumieć Lambdę

Kiedy testowałem dla Ciebie program, okazało się, że rotacja mojego hasła kończy się błędem. Dziwne, pomyślałem. Przecież niczego nie robiłem manualnie. Wszystko zrobiła za mnie konsola AWS. Postanowiłem jednak nie odpuszczać i wziąłem się za debugowanie.

Od czego zacząć?

Punktem wyjścia powinny być logi CloudWatch, który jest w tym przypadku domyślnie skonfigurowany. Zaloguj się do konsoli AWS, przejdź do Lambda, wybierz swoją funkcję do rotacji a następnie otwórz zakładkę Monitoring i wybierz View Logs in CloudWatch.

Nie znalazłem tam nic więcej niż informację poniżej.

Potrzebuję dodać więcej logów. To nie jest trudne. Mamy do czynienia z prostą aplikacją napisaną w python. Tutaj opcją jest pobranie kodu funkcji i jego modyfikacja.

Przejdź ponownie do Twojej funkcji, ale pozostań w otwartej zakładce. Wybierz Actions a następnie Export Function i Download deployment package.

Tutaj ogranicza Cię już tylko Twoja wyobraźnia 🙂 W moim przypadku chciałem się dowiedzieć dlaczego funkcja nie może połączyć się z bazą danych. Dlatego wypisałem błąd, który zwraca funkcja pgdb.connect.

Żeby zaktualizować Lambda Function, wystarczy zmodyfikowany kod spakować włącznie z pozostałym plikami i przez konsolę AWS wybrać Upload Function Package.

Dodatkowym utrudnieniem w testowaniu zmian jest to, że kolejna rotacja nie zostanie uruchomiona dopóki poprzednia się nie skończy.

Pozostaje jednak możliwość testowania bezpośrednio Lambda Function tworząc test event. Dla rotacji haseł ma on następującą strukturę.

Dwa pierwsze pola są łatwe do uzyskania. SecretId to po prostu ARN funkcji do rotacji haseł. Drugie pole to żądana akcja. Natomiast ClientRequestToken to tak naprawdę identyfikator wersji hasła, które zostanie użyte do próby połączenia z bazą danych.

Można go uzyskać z metadanych pobieranych w tym fragmencie kodu funkcji.

Po uruchomieniu testu okazało się, że powodem błędu jest timeout przy próbie podłączenia do bazy RDS.

Dalej poszedłem tropem braku łączności między Lambda Function a RDS. Po krótkiej inwestygacji okazało się, że RDS jest w VPC, który nie pozwala na dostęp z funkcji spoza tego VPC.

Ponieważ aplikacja służy nam do celów testowych, a baza RDS jest publicznie dostępna, szybkim rozwiązaniem problemu jest modyfikacja Inbound rule.

Natomiast nie zalecam tej metody w rozwiązaniach produkcyjnych. W tym wypadku lepszy rozwiązaniem będzie umieszczenie Lambdy w VPC i odpowiednie skonfigurowanie dostępu sieciowego do RDS.

Po zastosowaniu jednego z opisanych rozwiązań aplikacja powinna działać poprawnie.

Posłowie

Dziękuję, że dotarłeś do tego miejsca. W tym wpisie przedstawiłem zalety AWS Secrets Manger oraz sposób jego użycia i integracji z aplikacją SpringBoot. Ponadto wyjaśniłem zasadę działania rotacji haseł i przeprowadziłem krótką sesję debugowania Lambda Function. Jeśli artykuł był dla Ciebie ciekawy, zostaw proszę gwiazdkę w repozytorium mojego projektu.

Wspomnę jeszcze o dwóch rzeczach bezpośrednio związanych z tematem. AWS Secrets Manager możesz użyć niezależnie od tego czy Twoja aplikacja jest hostowana w AWS czy nie. Natomiast jeśli tak jest, to zyskujesz więcej możliwości dzięki integracji serwisów AWS. W tym przypadku, dzięki odpowiedniej konfiguracji, Twoja aplikacja nie musi znać AWS credentials do kontaktu z AWS Secrets Manager. Wystarczy, że udostępnisz sekret dla maszyny, na której działa Twój program.

Ponadto, mam świadomość, że w wielu przypadkach, przygoda z AWS czy chociażby z RDS będzie musiała rozpocząć się od migracji aplikacji oraz bazy danych do AWS. Te oraz inne problemy związane z chmurą obliczeniową od wielu lat rozwiązuje nasz zespół Pattern Match. Zapraszam do współpracy.


Zdjęcie główne artykułu pochodzi z unsplash.com.

Patronujemy

 
 
Polecamy
Kiedy skupić się na długu, a kiedy na funkcjonalnościach?