Jak używać sekretów w AWS korzystając z AWS-SDK i Middy?


"Jak zarządzać sekretami w AWS?"

Ten artykuł to kontynuacja wpisu Jak zarządzać sekretami w AWS?, więc jeśli jeszcze go nie czytałeś to zacznij właśnie od niego. Tym razem przedstawię Ci praktyczne metody pobierania sekretów do naszych funkcji Lambda z usług SSM Parameter Store oraz Secrets Manager. Skupimy się na użyciu AWS-SDK oraz bardzo popularnej w świecie serverless biblioteki Middy.

Wszystkie przykłady z tego artykuły umieściłem na githubie w projekcie sample-secrets-handling.

Bezpieczeństwo i dostępy

Zanim przejdziemy do praktycznych przykładów bardzo ważne jest, abyś ustawił odpowiednie dostępy dla roli, która będzie użyta przez Twoje funkcje Lambda. Ze względu na to czy będziesz używać SSM Parameter Store czy Secrets Managera to są to oczywiście inne zasoby i akcje.

W artykule (i przykładowej aplikacji) posługuję się dwoma sekretami, odpowiednio po jednym na każdą z usług. Dostępy do niech są zdefiniowane w sekcji provider/iamRoleStatements w pliku serverless.yml.

Jeśli chcesz ruchomić projekt na swoim koncie to musisz ręcznie dodać we Frankfurcie dwa sekrety:

  • /sample/mysecret w SSM Parameter Store
  • sample/my-second-secret w Secrets Manager z kluczem secret.
serverless.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
provider:
...
iamRoleStatements:
- Effect: Allow
Action:
- ssm:GetParameters* # needed by Middy
- ssm:GetParameter # needed by AWS-SDK
Resource:
- arn:aws:ssm:eu-central-1:*:parameter/sample/mysecret
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- arn:aws:secretsmanager:eu-central-1:*:secret:sample/my-second-secret*

Już na samym wstępie, przy konfiguracji roli mamy kilka ciekawostek, które mogą doprowadzić do irytacji.

Po pierwsze Middy - co to jest wyjaśnię później - korzysta wewnętrznie z innych metod API AWS, dlatego potrzebuje innych przywilejów do pobrania parametru z SSM niż równoważna metoda korzystająca wprost z AWS-SDK (linijka 6 vs 7).

Po drugie, sekret zdefiniowany w Secrets Managerze ma ARN składające się z nazwy sekretu oraz randomowego suffixa. Dlatego w ostatniej linijce na samym końcu jest *, po to, aby nie trzeba było znać tego suffixa. Niekoniecznie to jest oczywiste, szczególnie, że nie jest spójne z SSM Parameter Store. Wykorzystanie * może bardzo ułatwić życie, szczególnie wtedy gdy tworzymy sekrety automatycznie przez API. Traktują tę gwiazdkę jak taki life-hack 😁

Jak pobrać sekrety do funkcji Lambda?

Przygotowałem dla Ciebie, aż pięć przykładów, każdy w osobnej funkcji lambda.

Pobranie sekretu z usługi:

  1. SSM Parameter Store przy użyciu AWS-SDK
  2. SSM Parameter Store przy użyciu AWS-SDK i skeszowanie go pomiędzy wywołaniami funkcji
  3. SSM Parameter Store przy użyciu Middy i skeszowanie go pomiędzy wywołaniami funkcji
  4. Secrets Manager przy użyciu AWS-SDK i skeszowanie go pomiędzy wywołaniami funkcji
  5. Secrets Manager przy użyciu Middy i skeszowanie go pomiędzy wywołaniami funkcji

1. Pobranie sekretu z usługi SSM Parameter Store przy użyciu AWS-SDK

Kod przykładu jest bardzo prosty. W handlerze wywołujemy funkcję getSsmSecret z nazwą parametru. Tak, cała ta “ścieżka” to pojedyncza nazwa.

/src/ssm-simple/function.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const SSM = require('aws-sdk/clients/ssm')

const handler = async (event) => {
const secret = await getSsmSecret('/sample/mysecret')
return `My secret is ${secret}`
}

const getSsmSecret = async (Name) => {
const ssm = new SSM()
console.log('Getting secret from SSM')
const response = await ssm.getParameter({ Name, WithDecryption: true }).promise()
return response.Parameter.Value
}

module.exports = {
handler
}

Korzystamy z metody API AWS.SSM.getParameter() do której, co ważne, musimy przekazać obiekt składający z dwóch pól, nazwy Name oraz WithDecryption ustawionego na true, gdyż nasz parametr w SSM jest oczywiście zaszyfrowany. W przeciwnym wypadku, dostalibyśmy zaszyfrowanego stringa. I w sumie to wszystko 🙂

To jest najprostszy przykład, stąd też funkcja Lambda nazywa się ssmSimple.

Wywołując tę funkcje dwukrotnie pod rząd (sls invoke -f ssmSimple -l), otrzymasz tego typu wyniki jak poniżej. Zwróć uwagę, że w logach za każdym razem mamy Getting secret from SSM.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ sls invoke -f ssmSimple -l
"My secret is super tajne haslo"
--------------------------------------------------------------------
START RequestId: 440cc17a-7548-45c0-818d-4ab248529342 Version: $LATEST
2020-06-18 17:48:00.373 (+02:00) 440cc17a-7548-45c0-818d-4ab248529342 INFO Getting secret from SSM
END RequestId: 440cc17a-7548-45c0-818d-4ab248529342
REPORT RequestId: 440cc17a-7548-45c0-818d-4ab248529342 Duration: 893.89 ms Billed Duration: 900 ms Memory Size: 128 MB Max Memory Used: 79 MB Init Duration: 228.44 ms

$ sls invoke -f ssmSimple -l
"My secret is super tajne haslo"
--------------------------------------------------------------------
START RequestId: a89e1077-298f-4692-8c34-6ec749a33e84 Version: $LATEST
2020-06-18 17:48:05.423 (+02:00) a89e1077-298f-4692-8c34-6ec749a33e84 INFO Getting secret from SSM
END RequestId: a89e1077-298f-4692-8c34-6ec749a33e84
REPORT RequestId: a89e1077-298f-4692-8c34-6ec749a33e84 Duration: 71.26 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 79 MB

Cache’owanie

Pomimo iż rozwiązanie działa prawidłowo to jego minusem jest to, że przy każdym wywołaniu funkcji, Lambda strzela do API usługi SSM Parameter Store. Co przy dużym wolumenie może skutkować throttling’iem (błąd 429), czego oczywiście nie chcemy, dlatego warto umieścić pobraną wartość w cache’u tak, aby kolejna inwokacja rozgrzanej funkcji Lambda już tego nie robiła. Dzięki czemu, skrócimy też czas działania kolejnych wywołań.

Skąd wziąć cache? Najprostszy będzie w pamięci funkcji Lambda.

2. Pobranie sekretu z Parameter Store przy użyciu AWS-SDK + cache

Kod drugiego przykładu bazuje na poprzednim. Jedyne co się zmienia to to, że wartość sekretu przypisujemy do zmiennej w przestrzeni globalnej (linijka 3 i 15), czyli poza handler‘em funkcji Lambda.

/src/ssm-simpleCache/function.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const SSM = require('aws-sdk/clients/ssm')

let cachedSecret = null

const handler = async (event) => {
const secret = await getSsmSecret('/sample/mysecret')
return `My secret is ${secret}`
}

const getSsmSecret = async (Name) => {
if (!cachedSecret) {
const ssm = new SSM()
console.log('Getting secret from SSM')
const response = await ssm.getParameter({ Name, WithDecryption: true }).promise()
cachedSecret = response.Parameter.Value
}
return cachedSecret
}

module.exports = {
handler
}

Tym razem jak uruchomimy naszą funkcję dwukrotnie to tylko raz zobaczymy Getting secret from SSM.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ sls invoke -f ssmSimpleCache -l
"My secret is super tajne haslo"
--------------------------------------------------------------------
START RequestId: 4d6d103c-e658-4ca8-9422-8d1ff9a17a68 Version: $LATEST
2020-06-18 18:06:50.763 (+02:00) 4d6d103c-e658-4ca8-9422-8d1ff9a17a68 INFO Getting secret from SSM
END RequestId: 4d6d103c-e658-4ca8-9422-8d1ff9a17a68
REPORT RequestId: 4d6d103c-e658-4ca8-9422-8d1ff9a17a68 Duration: 884.97 ms Billed Duration: 900 ms Memory Size: 128 MB Max Memory Used: 78 MB Init Duration: 217.76 ms


$ sls invoke -f ssmSimpleCache -l
"My secret is super tajne haslo"
--------------------------------------------------------------------
START RequestId: a7f51c93-d784-44fb-9ce5-b4d78373b41a Version: $LATEST
END RequestId: a7f51c93-d784-44fb-9ce5-b4d78373b41a
REPORT RequestId: a7f51c93-d784-44fb-9ce5-b4d78373b41a Duration: 1.57 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 79 MB

Jeśli nie rozumiesz, jak ta wartość się skeszowała to przeczytaj Co to jest cold start Lambdy? Wyjaśnię Ci go w 4 minuty, szczególnie paragraf Cold start a skalowalność.

Zanim przejdziemy dalej to musimy odpowiedzieć na jedno pytanie.

Co to jest Middy?

Middy to bardzo popularna w serverless biblioteka, dostępna pod adresem https://middy.js.org.

Jest to tak zwany middleware, gdyż działa pomiędzy wywołaniem handlera funkcji Lambda, a naszym kodem biznesowym wewnątrz tej funkcji. Czyli jest gdzieś pośrodku, stąd też nazwa 🤔

Troszkę przypomina to aspekty np. w Java, pozwala się wpiąć z kodem przed uruchomieniem innej metody, w tym wypadku handlera.
Middy

Możemy sobie przypiąć kilka takich middleware do naszej funkcji i one się wywołają przed lub po naszym handlerze. W przykładach poniżej korzystam tylko z jednego middleware na raz, wywoływanego przed uruchomieniem handlera. Są to odpowiednio moduły do obsługi ssm oraz secrets-manager.

3. Pobranie sekretu z SSM Parameter Store przy użyciu Middy + cache

Korzystając z Middy, kod naszej funkcji Lambda wygląda zdecydowanie inaczej.

/src/ssm-middy/function.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const middy = require('@middy/core')
const ssm = require('@middy/ssm')

const handler = middy(async (event, context) => {
const secret = context.SECRET
return `My secret is ${secret}`
})

handler.use(ssm({
cache: true,
names: {
SECRET: '/sample/mysecret'
},
setToContext: true
}))

module.exports = {
handler
}

Funkcja handler (linijka 4) to tak na prawdę wynik owrapowania lub dekoracji biblioteką Middy anonimowej funkcji, która przyjmuje dwa parametry event oraz context. Są one potrzebne, gdyż w context został ustawiony nasz sekret.

Skąd on się tam wziął?

Sekret został ustawiony, przy wykorzystaniu modułu ssm (linijka 9). Odpowiada za to właśnie Middy. Zanim zostanie wywołany “nasz kod” (linijki 5 i 6) w funkcji handler Middy łączy się z SSM Parameter Store i pobiera dla nas sekret w momencie startu funkcji Lambda.

Proste? Niekoniecznie.
Wygodne? Pewnie!

4. Pobranie sekretu z Secrets Manager przy użyciu AWS-SDK + cache

Ostatnie dwa przykłady dotyczą pobierania sekretów zdefiniowanych w usłudze Secrets Manager. Koncepcja jest taka sama jak przy SSM Parameter Store, natomiast różni się w szczegółach.

Poniższy kod nawiązuje do drugiego przykładu. Zasada działania i keszowania wartości jest tożsama.

/src/sm-simpleCache/function.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

const SecretsManager = require('aws-sdk/clients/secretsmanager')

let cachedSecret = null

const handler = async (event) => {
const secret = await getSmSecret('sample/my-second-secret')
return `My secret is ${secret}`
}

const getSmSecret = async (Name) => {
if (!cachedSecret) {
const sm = new SecretsManager()
console.log('Getting secret from Secrets Manager')
const response = await sm.getSecretValue({ SecretId: Name }).promise()
cachedSecret = JSON.parse(response.SecretString).secret
}
return cachedSecret
}

module.exports = {
handler
}

Natomiast to gdzie mamy największą różnicę to fakt, że sekret jest zwrócony do nas przez AWS jako JSON i musimy go sparsować (linijka 16).

Również na komentarz zasługuje słowo secret w tej samej linijce. Jest to nazwa pola (klucz) w JSONie, który jest zapisany w moim sekrecie w Secrets Manager. Może to być dowolna nazwa wybrana przez Ciebie. Co więcej, możemy mieć wiele takich par klucz wartości w jednym sekrecie. Np. wszystkie dane potrzeba do połączenia się z bazą danych lub jakimś zewnętrznym API.

5. Pobranie sekretu z Secrets Manager przy użyciu Middy

I przyszła pora na ostatni przykład.

/src/sm-middy/function.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const middy = require('@middy/core')
const sm = require('@middy/secrets-manager')

const handler = middy(async (event, context) => {
const { secret } = context.SECRET // returns parsed JSON as Object with property 'secret'
return `My secret is ${secret}`
})

handler.use(sm({
cache: true,
secrets: {
SECRET: 'sample/my-second-secret'
},
setToContext: true
}))

module.exports = {
handler
}

Jest bardzo podobny do trzeciego.

Lecz tym razem korzystamy z innego modułu rozszerzającego Middy (linijka 2). Reszta jest analogiczna, zwracam tylko uwagę na linijkę 11. Dla SSM Parameter Store mamy names, a przy Secrets Manager używamy secrets do podania listy sekretów, które ma nam Middy pobrać.

Taki szczegół można łatwo przeoczyć przy migracji na inne rozwiązanie do przechowywania sekretów i stracić sporo czasu na debugowanie. Dlatego uczulam na tę drobną różnicę.

Posumowanie

Mam nadzieję, że powyższe przykłady jasno tłumaczą w jaki sposób pobierać sekrety o których rozpisałem się w poprzednim artykule.

Wszystkie użyte przykłady umieściłem na githubie w kompletnym projekcie sample-secrets-handling. Możesz dowolnie kopiować ten kod, tak aby błyskawicznie skorzystać z sekretów w Twoim projekcie.

Powodzenia w tworzeniu bezpiecznych aplikacji 😃