Jak zarządzać sekretami w AWS?


"Jak zarządzać sekretami w AWS?"

Update 29 czerwca 2020
Druga, praktyczna część tego artykułu znajduje się tutaj: Jak używać sekretów w AWS korzystając z AWS-SDK i Middy?

Jeśli zastanawiasz się jak należy zarządzać sekretami w AWS to dobrze trafiłeś. AWS oferuje bardzo bogaty ekosystem bezpieczeństwa oparty o usługę IAM, który w wielu wypadkach w ogóle eliminuje sensowność posiadania sekretów. Jednak prędzej czy później przytrafią się nam usługi wewnątrz chmury lub od zewnętrznych dostawców, gdzie będziemy musieli przechowywać hasła, klucze dostępowe i inne tajne składniki autentykacji. Sztandarowym przykładem będzie baza danych MySQL, która jest dostępna w usłudze RDS. Pomimo tego, iż to usługa zarządzana to potrzebujemy mieć do niej użytkownika i hasło. W naturalny sposób rodzi to pytanie, gdzie trzymać owe hasło w sposób bezpieczny?

Z tego artykułu dowiesz się

  • o najczęstszych błędach programistów i niebezpiecznych praktykach
  • jakie możliwości oferuje AWS, jeśli chodzi o zarządzanie sekretami
  • jakie usługi programiści mają do dyspozycji
  • jakie są najlepsze praktyki
  • i które warto wybrać ze względu na nasze indywidualne potrzeby?

Niebezpieczne praktyki

Chciałbym móc nie pisać tego paragrafu, ale życie uczy, że niestety ludzie-programiści często mają bezpieczeństwo w nosie. Zacznijmy od praktyki, którą sam widziałem wielokrotnie i za którą należałoby lać programistów drewnianą linijką po łapach (serio, uważam, że ta metoda dydaktyczna powinna wrócić do szkół i zakładów pracy 🤨).

Komitowanie sekretów do repozytorium kodu

To jest tak oczywiste, że nie należy tego robić, że aż mi się tego nie chce pisać ale obowiązek wzywa. Nawet jak to repo jest tylko “Twoje” firmowe, to nigdy nie masz gwarancji, kto je będzie przeglądał i kiedy. Takie sekrety mogą wypłynąć z firmy długo po tym jak opuścisz projekt. I bądźmy szczerzy, skoro jesteś misiem o niezbyt dużym rozumku (w końcu komitujesz sekrety do repo), to raczej ich nie rotujesz cyklicznie, więc będą one działały do końca świata i jeden dzień dłużej.

Sekret sekretowi nierówny i różne mogą być skutki tego, że wpadnie w niepowołane ręce. Jeśli udostępnisz gdzieś swoje klucze dostępowe do konta AWS (access key i secret access key) to możesz liczyć na nieprzyjemne niespodzianki. Od wysokich rachunków za zasoby powołane przez “włamywaczy” (jak komuś dasz klucze to raczej się nie włamał, tylko wszedł 🙂), wyciek wrażliwych danych, aż po totalny upadek firmy. Tak, słyszałem o historii gdzie, niepowołana osoba korzystając z takich kluczy skasowała wszystkie zasoby firmy z konta AWS i technologiczne zaplecze firmy przestało istnieć w ciągu momentu.

Solucja: dyscyplina, polecam linijki 😜

Jawne sekrety w pliku properties

Plik properties, wyjątkowo często stosowany w aplikacjach serwerowych, stanowi źródło konfiguracji dla aplikacji, zwyczajowo jest ładowany do pamięci w czasie uruchamiania serwera aplikacji (np. Tomcat). Leży sobie gdzieś na dysku sewera. Pomijając skąd on się tam wziął (jakiś proces deploymentu) to trzymanie w nim sekretów jest również złą praktyką. W przypadku dziury bezpieczeństwa, osoba niepowołana jest wstanie przejrzeć taki plik i odczytać nasz hasła wprost z pliku.

Podobnie jest z trzymaniem sekretów jako zmiennych systemowych. Też jest to ekstremalnie niebezpieczne.

Solucja: nie trzymać sekretów w pliku properties. Jeśli już musisz to je szyfruj KMSem (ale to ma swoje reperkusje, komplikuje procesy devops, rotowania kluczy itd.) i używaj tylko tymczasowych gratnów do klucza KMS, tak aby po odszyfrowaniu aplikacja już nie mogła ich ponownie odszyfrować, co uniemożliwi włamywaczowi ich odszyfrowanie. Podejście z grantami można stosować przy immutable architecture.

Lambda - Sekrety jako zmienne środowiskowe

Podobnie jak w przypadków serwerów jest to naganna praktyka. Niestety równie często stosowana, gdyż jest ekstremalnie wygodna dla programisty, a na dodatek nosi znamiona poprawności. O co chodzi? Otóż w kodzie aplikacji nie trzymamy sekretów, a odwołujemy się do nich przez zmienną środowiskową na przykład w taki sposób:

lamdba.js
1
2
3
4
module.exports.handler = async (event) => {
const password = process.env.password
// do something with the password
}

a samo hasło jest ustawiane za pomocą Serverless Framework w następujący sposób

serverless.yml
1
2
3
4
myFunction:
handler: src/function.handler
environment:
password: mojeTajneHasło

Ewidentnie jest to słabe rozwiązanie bo hasło jest jawnie trzymane w AWS jako zmienna środowiskowa, którą można łatwo odczytać używając konsoli webowej.

Dalej, trzymanie hasła w pliku serverless.yml w sumie nie wiele rożni się od trzymania go bezpośrednio w kodzie i też naraża nas na skomitowanie go do repo. Co sprytniejsze osoby, eliminują ten problem stosując lokalnie na swoim komputerze pliki dodane do .gitignore przechowujące te sekrety (lub gotowe rozwiązania typu dotenv). Wtedy nasz plik konfiguracyjny by wyglądał następująco

serverless.yml
1
2
3
4
myFunction:
handler: src/function.handler
environment:
password: ${file:(komputer_lokalny/niekomitowalny_plik.yml):password}

Ta metoda nie jest zupełnie zła, ale wciąż nie eliminuje problemu, że sekret jest jawnie zapisany w konfiguracji Lamdby po jej deploymencie. W związku z tym, w większości wypadków jest nieakceptowalna. Szczerze, mogę ją polecić tylko przy indywidualnym developmencie hobbistycznych projektów 😃

Solucja: w dalszej części artykułu.

Poprawne zarządzanie sekretami w AWS

Istnieją trzy usługi z których możemy wybrać tę, która będzie dla nas najlepsza.

1. KMS - Key Management Service

Pierwsza z nich służy do zarządzania kluczami szyfrującymi dane. KMS jest zintegrowany z wielomą innymi usługami AWS i następne usługi o których Ci powiem też z niego korzystają.

KMS jest w tym zestawieniu usługą najbardziej niskopoziomową, to znaczy, że sami będziemy musieli go użyć, aby zaszyfrować nasz sekret, a następnie użyć KMS API w naszym kodzie, aby go odszyfrować. KMS nie oferuje żadnej “przechowalni” dla naszych sekretów, czyli nie dość, że musimy sami szyfrować to jeszcze musimy się zastanowić gdzie trzymać te zaszyfrowane sekrety, jak je dostarczyć do aplikacji itd. Do tego dochodzi jeszcze jeszcze kwestia zarządzania kluczem CMK i przemyślenie polityki rotowania kluczy.

W serverless, historycznie KMS był używany przed pojawieniem się dedykowanych usług (o których pisze poniżej) dlatego prawdopodobnie znajdziesz w internecie przykłady jego użycia. Wygląda to mniej więcej tak

serverless.yml
1
2
3
4
myFunction:
handler: src/function.handler
environment:
encryptedPassword: niohi9jIOASUD09ASEDJ/APEODU093R72DJUDY0FFDC/SD9FSJNWEIFGH...

W pliku trzymamy uprzednio zaszyfrowany sekret (co dla mnie osobiście jest słabą praktyką), a w kodzie go odszyfrowujemy

lamdba.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const aws = require('aws-sdk')
const kms = new aws.KMS()

module.exports.handler = async (event) => {
let params = {
CiphertextBlob: Buffer.from(process.env.encryptedPassword, 'base64')
}

let secret = null
try {
const decrypted = await kms.decrypt(params).promise()
secret = decrypted.Plaintext.toString('utf-8')
}
catch (exception) {
console.error(exception)
}
}

Źródło: https://stackoverflow.com/a/54029941/4105584

Oczywiście taką odszyfrowaną wartość warto sobie skeszować w Lambdzie, tak aby przy gorącym uruchomieniu funkcji Lambda nie wywoływać API KMS ponownie.

Podsumowując, wykorzystanie KMS jest już w pełni bezpiecznym i profesjonalnym rozwiązaniem, to dobra praktyka. Jego główną wadą jest duża ilość pracy wymaganej na przygotowanie rozwiązania, po części wynikająca z braku przechowalni dla naszych sekretów, co powoduje, że sami musimy o to dbać. W rezultacie, takie zaszyfrowane sekrety często lądują w repozytorium kodu, co może samo w sobie nie jest niebezpieczne ale rodzi problemy przy rotowaniu kluczy i ewentualnym wdrożeniu naszego rozwiązania w innym regionie lub na nowym koncie AWS (np. u innego klienta).

Zwróć również uwagę, że w powyższym przykładzie, gdy zmieni się hasło, to musimy je znów zaszyfrować, umieścić zaszyfrowane hasło w pliku serverless.yml i ponownie zdeployować funkcję lambda. Jest to wysoce manualny proces, który może wykonywany raz na rok nie zajmie wiele czasu, ale z drugiej strony rodzi wielką szansę na to, że o nim zapomnimy i nagle nasza funkcja będzie się posługiwała zdezaktualizowany hasłem, co w efekcie spowoduje, że przestanie działać.

2. Systems Manager Parameter Store

Jest duża szansa, że słyszałeś już o tej usłudze. Jak sama nazwa wskazuje jest to przechowalnia parametrów wszelkiej maści. Dostęp do tych parametrów mamy przez API oraz konsolę webową. Możemy zatem wygodnie, manualnie, z poziomu przeglądarki dodać nasze parametry, które potem będą używane przez naszą aplikację.

Parameter Store integruje się z KMS, dzięki czemu możemy zaszyfrować nasze sekrety. Jest to tak proste jak kliknięcie jednego checkboxa 😀 Taki sekret jest dostępny pod unikalną nazwą w regionie. Nazwy mogą tworzyć swoiste “ścieżki” tak, aby logicznie grupować powiązane parametry np. mojaAplikacja/prod/bazyDanych/mysql/password oraz mojaAplikacja/prod/bazyDanych/mysql/username to się bardzo przydaje przy developmencie. Zwróć uwagę, że każdy stage dev, test, prod może mieć w takiej konfiguracji swoje parametry.

Tworząc parametr mamy do wyboru trzy opcje:

  • String - prosta, pojedyncza wartość
  • StringList - lista stringów odseparowanych przecinkami
  • SecureString - dowolny format znaków do 4KB, który jest zaszyfrowany kluczem KMS

Ten ostatni typ nas oczywiście najbardziej interesuje w kontekście przechowywania sekretów. Ponieważ, mamy sporo przestrzeni do dyspozycji to możemy w SecureString trzymać stringa, który będzie więcej niż pojedynczym hasłem. Osobiście polecam trzymać tam w postaci JSONa cały obiekt ze wszystkimi danymi potrzebnymi do połączenia z bazą danych MySQL (url bazy, użytkownik i hasło). Po odszyfrowaniu parsuje JSONa i mam obiekt ze wszystkimi potrzebnymi credentialami (sorry za żargon 🙂)

Dostęp do parametrów SSM ustalamy za pomocą polityk IAM. Jest to standardowe i wygodne podejście gwarantujące, że tylko wybrane przez nas role będą miały dostęp do naszych parametrów. W połączeniu z indywidualnymi rolami dla funkcji lambda otrzymujemy bardzo bezpieczne rozwiązanie oferujące granularne dostępy.

Niekwestionowaną zaletą parametrów SSM jest to, że mogą być używane przez wiele aplikacji lub komponentów (mikroserwisów) wewnątrz tego samego systemu. Jeśli mamy dwa moduły łączące się do tej samej bazy danych to nie musimy duplikować parametrów. Nadajemy tylko kolejny dostęp w polityce IAM.

Przydaje się to również, gdy musimy napisać jakiś ad-hoc’owy skrypt łączący się do bazy i wyciągający jakieś dane. Credentiale mamy na wyciągnięcie ręki (jeden strzał do API) i nigdy się nie walają odszyfrowane na dysku laptopa. Nie dość, że wygodnie to do tego zgodnie z normami bezpieczeństwa ISO / EIC 27001.

Oczywiście możemy w taki sposób przechowywać dowolne sekrety, a nie tylko dane dostępowe do MySQL. Duh! 😀

3. Secrets Manager

Ostatnią, trzecią usługą jest Secrets Manager. Służy on do przechowywania sekretów ale również ich tworzenia.

Już tłumaczę co chodzi?

Zgodnie z praktyką infrastructure as code wszelkie zasoby w chmurze powinniśmy definiować jako kod. Gdy definiujemy w CloudFormation (czy Terraform) bazę danych, dajmy na to znów MySQL to musimy podać użytkownika i hasło dla pierwszego konta w MySQL, które zostanie stworzone. Rodzi to oczywiście problem, ponieważ musimy to hasło jakoś przekazać do CloudFormation jako parametr stacka. Pytanie oczywiście skąd to hasło wziąć, jeśli je wygenerujemy na bieżąco to trzeba je też gdzieś zapisać bo inaczej nie będziemy w stanie się dostać do bazy danych.

Oczywiście możesz hasło wygenerować, zapisać w Systems Manager Parameter Store i następnie użyć przy tworzeniu bazy danych. Niestety w czystym CloudFormation taki scenariusz jest niemożliwy do realizacji i trzeba by się posiłkować CustomResource, które jest dosyć toporną metodą, której wolę unikać.

Na szczęście właśnie ten problem rozwiązuje usługa Secrets Manager. Jest ona w stanie wygenerować hasło, zapisać je w swojej pamięci i następnie przekazać jako parametr wejściowy przy tworzeniu kolejnego zasobu w CloudFormation. Na chwilę obecną te metoda pozwala tworzyć nowe credentiale dla baz danych w RDS, Redshift oraz DocumentDB (zarządzane MongoDB w AWS).

Poza tym Secrets Manager, zupełnie jak Parameter Store, pozwala na stworzenie sekretu (parametru) ręcznie (w konsoli webowej wybieramy “Other type of secrets”). Odwołujemy się do niego przez API (AWS SDK) analogicznie jak w przypadku poprzedniej usługi. Oczywiście wszystkie sekrety “pod spodem” są szyfrowane przy użyciu usługi KMS.

Zawartość parametru definiujemy jako zbiór par klucz = wartość, które zostaną zwrócone przez API jako JSON. Od strony naszej aplikacji, dostęp do sekretu jest bardzo podobny jak w przypadku Parameter Store.

lamdba.js
1
2
3
4
5
6
7
8
const aws = require('aws-sdk')
const sm = new aws.SecretsManager()

module.exports.handler = async (event) => {
const secretName = 'mojSekret'
const secret = await client.getSecretValue({ SecretId: secretName }).promise()
const credentials = JSON.parse(secret.SecretString)
}

Którą usługę wybrać?

Taka mnogość opcji utrudnia podjęcie decyzji. Poniżej umieściłem najbardziej istotne kryteria, które pomogą Ci w wyborze najlepszego podejścia do zarządzania sekretami w Twojej aplikacji.

KMS Parameter Store Secrets Manager
Koszty ▪️ $1 za klucz miesięcznie
▪️ $0.03 za 10 000 żądań
▪️ Darmowy w wersji podstawowej
▪️ $0.05 za parametr na miesiąc (Advanced)
▪️ $0.05 za 10 000 żądań (Advanced oraz Higher Throughput)
▪️ $0.40 za parametr na miesiąc
▪️ $0.05 za 10 000 żądań
Limity 10-30 tyś żądań na sekundę w zależności od regionu 40 żądań na sekundę
za dopłatą może być zwiększony do 1000
2 000 żądań na sekundę
Wady ❌ Brak przechowalni kluczy
❌ Ręczne szyfrowanie
❌ Trzeba samodzielnie zaimplementować rotację uprzednio zaszyfrowanych sekretów
❌ Konieczny redeployment, gdy zmieni się sekret
❌ Niskie limity, opcja Advanced warta rozważenia
❌ Brak automatycznej rotacji
❌ Najdroższa usługa
Zalety ✔️ Większa wszechstronność (ale tylko wtedy gdy jest nam potrzebna) ✔️ Posiada przechowalnie kluczy
✔️Zmiana sekretu w jednym miejscu, wszystkie korzystające aplikacje od razu mogą z niego korzystać
✔️ Posiada przechowalnie kluczy
✔️ Zmiana sekretu w jednym miejscu, wszystkie korzystające aplikacje od razu mogą z niego korzystać
✔️ Tworzenie nowych sekretów (do wspieranych usług, np. RDS)
✔️ Automatycznie rotowanie sekretów (do wspieranych usług, np. RDS)

Podsumowanie

W takiej ilości współczynników trudno się połapać i podjąć właściwą decyzje. Moja rada dla początkujących jest taka:

  • jeśli tworzysz zasób przy pomocy CloudFormation, który wspiera Secretes Manager (np. bazę danych MySQL w RDS) to wybierz Secrets Managera
  • w pozostałych wypadkach wybierz Parameter Store, aby nie mieć kosztów. Keszuj w działającej lambdzie odszyfrowane wartości parametrów, aby nie dojść do limitów.
  • gdy dojdziesz do limitów to znaczy, że masz taki ruch w swoim systemie, że już nie jesteś początkujący, zatem rozważ wszystko od nowa i wybierz najlepsze podejście do Twojego systemu.

PS. Korzystanie z Secrets Managera rozwiązało u nas w projekcie też taki problem, że już się nikt nikogo nie pyta jakie jest hasło do bazy, bo każdy wie gdzie może sobie sprawdzić to hasło. Mała rzecz, a cieszy 👍😃

A teraz zapraszam Cię do drugiego, praktycznego artykułu w tej serii Jak używać sekretów w AWS korzystając z AWS-SDK i Middy?

Źródła: