HashiCorp Vault – centrum zarządzania sekretami

HashiCorp Vault – centrum zarządzania sekretami

Zapraszam na kolejny wpis z serii poświęconej produktom HashiCorp. Tym razem skupimy się na jednym z najpopularniejszych narzędzi, które oferuje kompleksowy system zarządzania sekretami, szyfrowaniem, obsługą certyfikatów i nie tylko – HashiCorp Vault. To potężne narzędzie dostarcza niezawodnych rozwiązań do zabezpieczania, zarządzania i dystrybucji poufnych informacji w środowiskach IT. We wpisie zostaną zawarte podstawowe wiadomości, które są niezbędne do rozpoczęcia pracy z HashiCorp Vault. Przedstawię funkcjonalności, które są najczęściej wykorzystywane. Na koniec automatycznie wdrożymy Vault do wielopoziomowego środowiska w chmurze AWS. Warto zaznaczyć, że możliwości HashiCorp Vault są tak szerokie, że niemożliwe jest wspomnienie o wszystkich funkcjach w jednym wpisie. Postaram się zatem podczas opisywania treści wartych uwagi linkować do wartościowych źródeł.

Inne wpisy z tej serii dotyczące narzędzi HashiCorp:

  1. HashiCorp Packer – automatyzacja budowy obrazów maszyn
  2. HashiCorp Terraform – infrastruktura jako kod
  3. HashiCorp Vault – centrum zarządzania sekretami ( Jesteś tutaj! )
  4. HashiCorp Consul – networking i zarządzanie konfiguracją
  5. HashiCorp Nomad – prosty orkiestrator aplikacji

HashiCorp Vault

HashiCorp Vault to wszechstronne narzędzie, które oferuje kompleksowe rozwiązania zabezpieczające poufne dane w infrastrukturze IT. Jego możliwości sięgają znacznie dalej niż tylko konfiguracja zaawansowanych mechanizmów dostępu. Vault pełni rolę centralnego punktu zarządzania i szyfrowania danych, oferując funkcje takie jak Encryption as a Service (EaaS), dystrybucja sekretów statycznych i dynamicznych czy rotacja certyfikatów.

Jednym z kluczowych elementów Vault jest możliwość ścisłego zarządzania dostępem do danych poprzez polityki. Dzięki temu możemy precyzyjnie określać, kto ma dostęp do konkretnych sekretów i w jakim zakresie. Wraz z mechanizmami audytu Vault umożliwia śledzenie historii użycia sekretów, co jest niezwykle istotne z perspektywy nadzoru i bezpieczeństwa.

Jeśli to wasze pierwsze spotkanie z HashiCorp Vault wówczas gorąco polecam obejrzenie filmu wprowadzającego, który pomoże Wam zrozumieć jakie problemy twórcy chcieli rozwiązać za pomocą tego narzędzia.

Architektura i pierwsze uruchomienie

Większość osób nie potrzebuje szczegółowej wiedzy na temat architektury Vaulta omówię ją zatem ogólnie. Vault opiera się na modularnej architekturze, dzięki różnym pluginom. Niektóre z tych pluginów są wbudowane i nie można ich wyłączyć, ale istnieje również możliwość rozszerzania funkcjonalności Vaulta przez uruchomienie dodatkowych. Możemy też tworzyć własne pluginy.

Vault jako narzędzie odpowiedzialne za zarządzanie sekretami wymaga wysokiego poziomu bezpieczeństwa. Pierwsze uruchomienie ze względu na dodatkowe mechanizmy jest zatem bardziej skomplikowane niż w przypadku zwykłego programu.

Oto podstawowe komponenty, z których składa się Vault.

żródło: https://developer.hashicorp.com/vault/docs/internals/architecture

Prostokąt nazwany Barrier zawiera wszystkie podstawowe elementy serwera Vault. Wszystko poza barierą jest przez Vaulta uważane za podejrzane. Storage Backend czyli miejsce przechowywania danych przez Vault znajduje się poza tą barierą. Dlatego wszystkie dane, na których operuje Vault są zawsze zaszyfrowane. Nawet w przypadku wycieku backendu dane nie zostaną ujawnione.

Klient komunikuje się z Vaultem za pomocą API. Istnieje także interfejs wiersza poleceń (CLI) i interfejs użytkownika (UI), ale czasami interfejs użytkownika może być niewystarczający w przypadku bardziej zaawansowanych interakcji.

Warto również wiedzieć, że wszystkie operacje w Vault opierają się na ścieżkach. Każda operacja, konfiguracja systemu, silnik sekretów, metody autoryzacji, polityki i tokeny mają przypisane swoje określone ścieżki. Ścieżki są miejscami, do których montujemy funkcjonalności oraz umieszczamy sekrety. Niektóre ścieżki takie jak ścieżki systemowe są predefiniowane.

Przed rozpoczęciem pracy z Vaultem musimy go zainstalować. Na stronie Vaulta można znaleźć jednoliniową komendę instalacyjną dla dowolnej platformy. Po zainstalowaniu powinniśmy mieć dostęp do wszystkich poleceń.

Serwer Vaulta można uruchomić w dwóch trybach: trybie deweloperskim za pomocą polecenia vault server -dev oraz w trybie produkcyjnym.

Tryb deweloperski pozwala na szybkie rozpoczęcie pracy z Vaultem wówczas gdy chcemy tylko sprawdzić podstawowe funkcjonalności. W tym trybie serwer jest automatycznie odblokowany, co oznacza, że nie wymaga on żadnego klucza odblokowującego. Ponadto w trybie deweloperskim dane są przechowywane w pamięci RAM, co oznacza, że przepadną w przypadku restartu serwera.

Tryb produkcyjny wymaga dostarczenia pliku konfiguracyjnego, który zawiera m.in. informacje o rodzaju magazynu danych i portach. Przykładowe uruchomienie w trybie produkcyjnym może wyglądać tak: vault server -config=/etc/vault/config.hcl. Poniżej znajduje się plik konfiguracyjny.

listener "tcp" {
  address = "127.0.0.1:8200"
  tls_disable = 1
}

storage "file" {
  path = "~/vault"
}


api_addr = "http://127.0.0.1:8200"
cluster_addr = "https://127.0.0.1:8201"

ui = true

W pliku konfiguracyjnym możemy zdefiniować różne ustawienia, takie jak nasłuchiwanie na określonym porcie, włączenie lub wyłączenie TLS oraz wybór rodzaju magazynu danych tak jak w tym przypadku „file”.

Unsealing

Podczas pierwszego uruchamiania serwer Vault nie jest w stanie odczytać ani zapisać danych do backendu, ponieważ nie wie, jakiego klucza użyć do szyfrowania. W tym momencie serwer znajduje się w trybie zapieczętowanym (seal). Jeśli w pliku konfiguracyjnym nie zostanie podana konfiguracja dotycząca unseal wtedy domyślnym sposobem odpieczętowania jest mechanizm Shamir’s Secret Sharing.


Przy pierwszej inicjalizacji Vaulta otrzymujemy zestaw kluczy, które umożliwiają nam uzyskanie klucza głównego, dzięki któremu możemy odpieczętować Vaulta. Mamy możliwosć wyboru ile części klucza zostanie wygenerowanych oraz jaka będzie minimalna liczba części potrzebna do uzyskania klucza głównego. Wartości poszczególnych kluczy powinny być rozdzielone pomiędzy zaufane osoby lub jednostki.

Poniżej przedstawiam przykładowy plik z kluczami, które otrzymujemy podczas inicjalizacji:

{
  "keys": [
    "1767bfd68c1c9223f80c8f827465e307fa9ad8b23ec37c8a620db1bcded1726a20",
    "e250eb832e1c8532f8fc885b4e86d8485e4fe4a5e110f889ab01c3e96df8d6a035",
    "d070e919ad1c23e9f8006d150c8a0216a0fbdb03d2d9803aa6fb3a73354a281eb8",
    "9da190130c1c10ebf8e92fd8cc87b13e30542a302e47f2af1a999079966eb8376d",
    "705bfa72961c9150f8231e3a49e03fa190c295b1f6eb29b248edbb18b7461878fa"
  ],
  "keys_base64": [
    "F2e/1owckiP4DI+CdGXjB/qa2LI+w3yKYg2xvN7Rcmog",
    "4lDrgy4chTL4/IhbTobYSF5P5KXhEPiJqwHD6W341qA1",
    "0HDpGa0cI+n4AG0VDIoCFqD72wPS2YA6pvs6czVKKB64",
    "naGQEwwcEOv46S/YzIexPjBUKjAuR/KvGpmQeZZuuDdt",
    "cFv6cpYckVD4Ix46SeA/oZDClbH26ymySO27GLdGGHj6"
  ],
  "root_token": "hvs.o3QcK22bKSgSRjB1O1x51CRR"
}

Gdy posiadamy wymaganą minimalną liczbę cząstek klucza możemy przystąpić do odpieczętowania Vaulta. W przypadku podania trzech cząstek klucza Vault zostanie odpowiednio odblokowany i będziemy mieli dostęp do jego funkcjonalności oraz przechowywanych w nim sekretów.

Możemy skonfigurować odpieczętowywanie automatyczne używając innych zaufanych systemów zarządzania kluczami KMS lub HSM (hardware seciurity module). Po odpieczętowaniu Vault wczytuje konfigurację audytu, metod autoryzacji i secret engines. Wszystkie powyższe czynności możemy też wykonać z użyciem CLI.

Metody autentykacji i tokeny

W Vault dostępne są różne metody uwierzytelniania, które pozwalają na autoryzację użytkowników i aplikacji. Każda z tych metod ma na celu wygenerowanie tokena, który jest następnie używany do operacji na Vault. Do każdego tokena przypisujemy polityki odpowiedzialne za określenie uprawnień.

Vault umożliwia wiele metod autentykacji w zależności od tego, które najbardziej odpowiadają naszemu przypadkowi. Wszystkie metody dążą do tego by w rezultacie zwrócić token, który będzie używany do wszystkich następnych operacji z vaultem. Do każdego tokena przypisane są polityki odpowiedzialne za określenie uprawnień.

Poniżej kilka przykładów dostępnych metod uwierzytelniania w Vault.

  • Okta
  • LDAP
  • AppRole
  • Kubernetes
  • Login MFA
  • Token
  • Username and password
  • Clouds (AWS, Azure, GCP)

Całą listę znajdziemy w dokumentacji.

Aby sprawdzić obecnie uruchomione metody uwierzytelniania w Vault za pomocą CLI wykonujemy komendę vault auth list. Przed rozpoczęciem pracy z CLI należy upewnić się, że zmienne środowiskowe VAULT_ADDR i VAULT_TOKEN są ustawione poprawnie. VAULT_ADDR to adres nasłuchu servera Vault.


Możesz również sprawdzić obecnie uruchomione metody uwierzytelniania w Vault za pomocą UI.

Podczas odpieczętowywania Vault otrzymujemy specjalny token zwany root token, który ma pełne uprawnienia do wszystkich operacji w Vault. Posiada on politykę root, która umożliwia nieograniczone uprawnienia do zarządzania Vaultem.

Bardzo ważne jest aby z odpowiedzialnością korzystać z root tokena i zastosować odpowiednie środki bezpieczeństwa. Zaleca się użyć go tylko w celu skonfigurowania podstawowych funkcjonalności Vaulta, takich jak ustawienie metod uwierzytelniania, polityk, silników sekretów itp. Następnie dobrą praktyką jest usunięcie go, aby zapobiec pojedynczej osobie tego by posiadała nieograniczone uprawnienia.

Usunięcie root tokena jak i każdego innego tokena można zrealizować poprzez wywołanie komendy (vault token revoke <token_id>).

Dbanie o odpowiednie zarządzanie uprawnieniami i ograniczanie dostępu jest istotnym elementem bezpieczeństwa w kontekście Vaulta. Należy unikać sytuacji, w których pojedyncza osoba ma pełne uprawnienia do wszystkich operacji.

Uwaga!

Po prawidłowej konfiguracji metod autentykacji powiniśmy usunąć root token. Nie nalezy używać go w codziennych zadanich. Token ten powinien być używany tylko w awaryjnych sytuacjach. Można wygenerować kolejny root token za pomocą polecenia vault operator generate-root -init

Rodzaje Tokenów

Tokeny są istotne w kontekście komunikacji z Vaultem oraz uwierzytelniania w UI. W przypadku Vaulta wyróżniamy dwa główne typy tokenów: service token i batch token.

Batch token są używane w określonych przypadkach, natomiast większość czasu będziemy korzystać z service token, które posiadają szeroki zakres konfiguracji. W zależności od naszych potrzeb możemy stworzyć różne podtypy service token takie jak:

periodic token – nie posiada ostatecznego czasu wygaszenia (brak max TTL time to live)
orphal token – nie posiada „rodziców” wiec nie ma zależności wymuszajacych rotacje
use limit token – możemy użyć określoną liczbę razy
cidr-bound token – token działający na określonych zakresach adresów IP

W celu stworzenia tokena wykonujemy nastepującą komendę vault token create -policy=platform-engineering -period=24h -explicit-max-ttl=700h. W rezultacie zwrócony zostanie token i kilka dodatkowych informacji. By zobaczyć wszystkie dane wykonujemy polecenie vault token lookup <token_id>

Jeśli w czasie 24h nie odnowimy tokenu z użyciem vault token renew nasz token wygaśnie nawet gdy max ttl ciągle będzie ważne. Można zauważyć, że przy pomocy flagi policy przypisaliśmy do tokenu politykę platform-engineering.

Więcej na temat tokenów znajdziecie w dokumentacji.

Polityki

Polityki w Vaultu są odpowiedzialne za nadawanie tokenom odpowiednich uprawnień. Podczas pierwszego uruchomienia Vault automatycznie tworzy dwie polityki: „root” i „default”. Polityka „root” przyznaje wszystkie możliwe uprawnienia na wszystkich ścieżkach w Vaultu. Natomiast polityka „default” umożliwia podstawowe operacje potrzebne do poprawnego korzystania z tokenu, takie jak przeglądanie własnych uprawnień, odnawianie lub wygaszanie tokenu.

Domyślnie polityka „default” jest przypisywana do wszystkich nowo utworzonych tokenów, ale istnieje możliwość wyłączenia tego zachowania. Polityki mogą być zapisywane przy użyciu składni HCL (HashiCorp Configuration Language) lub JSON.

Wcześniej przypisaliśmy tokenowi politykę „platform engineering”, która umożliwia tworzenie, modyfikację, kasowanie i inne operacje na podanych niżej ścieżkach.

path "*" {
  capabilities = ["read", "list"]
}

path "secret/kv/*" {
  capabilities = ["read", "list", "create", "update", "delete"]
}

W przedstawionej polityce „platform-engineering” znajdują się dwie reguły. Pierwsza reguła, path "*", która umożliwia odczyt wszystkich danych z dowolnej ścieżki w Vault. Oznacza to, że posiadacz tego tokenu będzie miał dostęp do wszystkich danych przechowywanych w Vault.

Druga reguła, path "secret/kv/*" umożliwia wykonanie podstawowych operacji na ścieżce "secret/kv/*". Dzięki tej regule posiadacz tokenu będzie mógł odczytać, przeglądać, tworzyć, aktualizować i usuwać sekrety na tej konkretnej ścieżce.

W przypadku, gdy polityki są ze sobą sprzeczne, to znaczy mają reguły, które się wykluczają, obowiązuje zasada, że brak dostępu ma pierwszeństwo nad udzieleniem dostępu. Oznacza to, że jeśli dana ścieżka lub operacja jest wyłączona w jednej polityce, to niezależnie od innych polityk, posiadacz tokenu nie będzie miał dostępu do tej ścieżki lub operacji.

Podstawowe capabilities to: read, create, update, delete, list, sudo, deny. Myślę, że operacje CRUD nie wymagają komentarza, a pozostałe trzy znajdziecie poniżej:
list – uzyskujemy tylko listę elementów danej ścieżki, nie ich wartości
sudo – wymagane by uzyskać dostęp do niektórych ścieżek vaulta zarezerwowanych dla adminów
deny – używamy gdy chcemy zakazać dostępu do ścieżki, przykład poniżej

path "secret/data/mypath/*" {
  capabilities = ["read", "create", "update", "delete"]
}

path "secret/data/mypath/sensitive" {
  capabilities = ["deny"]
}
path "secret/data/mypath/sensitive/*" {
  capabilities = ["deny"]
}

Powyższa polityka pozwoli na odczytanie sekretów ze ścieżki secret/data/mypath/* i wszystkich podścieżek za wyjątkiem ścieżki secret/data/mypath/sensitive i wszystkich poniżej. Warto zauważyć, że musiałem umieścić dwie reguły zabraniające. Gdybym pominął trzecią regułę sprawiło by to, że ścieżka secret/data/mypath/sensitive/test nie pasowałaby do wzorca zabraniającego i dostęp byłby zagwarantowany przez regułę pierwszą.

Poza znakiem "*", który oznacza dowolną ścieżkę dostępny jest jeszcze znak "+", który oznacza pojedyncze zagnieżdżenie np. path "secret/data/mypath/+/bar" oznacza, że pod ,”+” możemy podstawić foo np. secret/data/mypath/foo/bar, ale już nie foo/extra secret/data/mypath/foo/extra/bar.

Polityki możemy jak zwykle tworzyć za pomocą UI lub z linii komend. Np vault policy write admin admin-policy.hcl doda politykę o nazwie admin z pliku admin-policy.hcl do Vault.

Dodatkowo niektóre pluginy posiadają własne unikalne capabilities. Więcej na temat polityk i ścieżek wymagających uprawnień admina znajdziemy w dokumentacji.

Warto pamiętać, że w przypadku problemów z uprawnieniami możemy użyć flagi -output-policy w naszym zapytaniu, aby precyzyjnie określić jaką politykę potrzebujemy. Dzięki temu w odpowiedzi na nasze zapytanie otrzymamy treść reguł polityki, którą token musi posiadać by wykonać interakcję.

Przykładowo, używając polecenia vault kv put -output-policy -mount=secret/dev/db_secret @secret.json, otrzymamy w odpowiedzi informację o polityce, która jest wymagana dla tokenu, którym wykonujemy request.

Secrets Engines

Secret Engines (SE) są kluczowym elementem w HashiCorp Vault. To dzięki nim możemy tworzyć i bezpiecznie przechowywać zarówno statyczne, jak i dynamiczne sekrety. Dzięki Secret Engines możemy również korzystać z pluginów, które umożliwiają różnorodne funkcjonalności.

Za pomocą SE możemy efektywnie zarządzać sekretami w różnych scenariuszach. Możemy przykładowo użyć pluginów dla użytkowników IAM w naszej infrastrukturze chmurowej. Dodatkowo Vault może być skonfigurowany tak, aby działał jako usługa szyfrowania (encryption-as-a-service) udostępniając punkty końcowe API do bezpiecznego szyfrowania i deszyfrowania danych. Obsługa konfiguracji poświadczeń użytkowników dla popularnych baz danych również jest możliwa. Ponadto Secret Engines umożliwiają automatyczne generowanie i podpisywanie certyfikatów.

Pełną listę SE znajdziemy jak zwykle w dokumentacji.

W tym artykule skupię się na dwóch SE: KV w wersji 2 oraz AWS. Secret Engine KV w wersji 2 jest powszechnie używany.

KV V2 Secret Engine

W trybie deweloperskim HashiCorp Vault automatycznie uruchamia Secret Engine KV w wersji 2. Jednak w przypadku serwera produkcyjnego musimy ręcznie włączyć SE KV jeśli chcemy z niego skorzystać. Możemy to zrobić za pomocą UI Vault lub za pomocą CLI vault secrets enable -path=kv --version=2 kv

Dodawanie sekretu jest bardzo proste. Można wyklikać to z UI lub dodać za pomocą CLI, np:
vault kv put -mount=kv app/backend/db username=super-user password=P@ssw0rd!

Zauważcie, że musimy podać miejsce montowania naszego SE ponieważ możemy posiadać wiele SE KV przypisanych do unikalnych ścieżek.

W przypadku pobierania danych za pomocą komendy vault kv get kv/app/backend/db dane są domyślnie zwracane w formacie tabeli, co może być niewygodne jeśli chcemy przetwarzać je w aplikacji. Istnieje możliwość określenia innego formatu, takiego jak JSON, który jest bardziej przyjazny do automatycznego przetwarzania. Aby pobrać dane w formacie JSON możemy dodać flagę -format=json do naszego polecenia. Flaga -field pozwala na zwrócenie samej wartości danego pola.

Tak jak wcześniej wspomniałem należy pamiętać, że nie wszystkie operacje możemy wykonać za pomocą UI Vaulta. Istnieją pewne komendy i czynności, które są dostępne tylko za pomocą CLI. UI nie pozwoli nam na rollback, cofnięcie kasowania sekretu lub podgląd metadanych.

Hasicorp Vault przechowuje dane w postaci stringów dlatego gdy chcemy zapisywać dane binarne, takie jak certyfikaty z kluczami prywatnymi, pliki p12 itp. wówczas istotne jest odpowiednie przygotowanie tych danych przed umieszczeniem ich w Vault. Jednym ze sposobów radzenia sobie z tym problemem jest konwersja danych binarnych na format base64.

Konwersja danych binarnych na base64 pozwala nam uniknąć potencjalnych problemów związanych z interpretacją niektórych znaków lub sekwencji znaków. Base64 reprezentuje dane binarne za pomocą zestawu znaków alfanumerycznych, które są bezpieczne do przechowywania i przesyłania jako stringi.

W ten sposób eliminujemy potencjalne problemy związane z nieprawidłowym interpretowaniem znaków danych binarnych. Po pobraniu danych z Vaulta możemy dokonać odwrotnej operacji, tj. konwersji base64 na binarny aby odzyskać pierwotne dane w ich oryginalnym formacie.

AWS Secret Engine

Drugim ciekawym Secret Engine, który chciałem przedstawić jest Secret Engine AWS. Silnik ten odciąża nas od konieczności ręcznego tworzenia i zarządzania poświadczeniami do pracy z usługami AWS. Oprócz dynamicznego tworzenia odpowiednich użytkowników automatycznie usuwa również przestarzałe dane po upływie określonego czasu co pomaga utrzymać czystość naszego konta.

Secret Engine AWS umożliwia generowanie poświadczeń na trzy sposoby: tworzenie użytkowników IAM, za pomocą roli assume (AssumeRole) oraz poprzez użycie Federation Token. W tym przykładzie skoncentruję się na konfiguracji silnika do tworzenia użytkowników IAM.

Aby włączyć Secret Engine AWS używamy polecenia vault secrets enable aws. Następnie musimy skonfigurować Secret Engine. Vault potrzebuje własnych poświadczeń, którymi będzie zarządzał w usłudze IAM.

vault write aws/config/root \
    access_key=<AWS_ACCESS_KEY> \
    secret_key=<AWS_SECRET_ACCESS_KEY> \
    region=eu-central-1

Następnie w Secret Engine AWS dodajemy role. Są to identyfikatory, które przypisujemy do roli w AWS, a które określają, jakie polityki i uprawnienia ta rola ma mieć.

vault write aws/roles/my-role \
    credential_type=iam_user \
    policy_document=-<<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ec2:*",
      "Resource": "*"
    }
  ]
}
EOF

Dodaliśmy do Secret Engine AWS rolę o nazwie „my-role”, która posiada politykę umożliwiającą wykonanie wszelkich akcji na instancjach EC2. Teraz za każdym razem, gdy poprosimy o klucz dostępu z Vaulta zostanie automatycznie utworzony użytkownik IAM w AWS z przypisaną powyższą polityką. Po upływie czasu określonego przez lease_duration Vault automatycznie usunie tego użytkownika.
Daje to bardzo duże korzyści. Dla przykładu używając terraform możemy tak skonfigurować czas życia tokenu by zaraz po stworzeniu odpowiednich zasobów token przestał być ważny. Nie musimy korzystać z powyższego pluginu AWS ponieważ terraform posiada swój własny dedykowany SE. Posłużyłem się nim tylko jako przykładem.

Tak prezentuje się użytkownik IAM stworzony przez Vaulta przy użyciu Secret Engine w consoli AWS.

Zabrakło

Temat Hasicorp Vault jest rozległy i nie można go objąć w jednym wpisie. Oto kilka ważniejszych elementów, na które warto zwrócić uwagę:

  • Magazyn danych (Storage) – krótko o nim wspomniałem; jest to miejsce gdzie vault przechowuje wszystkie zaszyfrowane dane
  • Audyt (Audit Devices) – są to urządzenia zapisujące historię interakcji (audit logs) z naszym serwerem vaulta, konieczny element przy rozwiązaniach produkcyjnych
  • Vault High Availability Mode – tworzenie klastra serwerów vaulta wymaga więcej pracy gdy mamy do czynienia z tak ważnym elementem jak bezpieczeństwo, warto zapoznać się z mechanizmem głosowania na lidera

Automatyczny deployment HashiCorp Vault

Podobnie jak w poprzednich wpisach druga część artykułu to nic innego jak wykorzystanie zdobytej wiedzy w praktyce. W tej serii stopniowo budujemy naszą infrastrukturę dodając kolejne jej elementy i wykorzystując narzędzia HashiCorp. We wcześniejszym wpisie zbudowaliśmy trzypoziomową infrastrukturę dla naszej apliakcji workoutrecorder. Tym razem dodamy do naszej infry instancję serwera Vault, która zajmie się obsługą sekretów.

Założenia

W celu ograniczenia dostępu do serwera Vaulta umieścimy go w prywatnej podsieci. Aby zapewnić bezpieczne przechowywanie danych Vaulta i uniknąć ich utraty w przypadku awarii instancji skonfigurujemy Vault tak, aby dane były przechowywane w tabeli DynamoDB zamiast w formie plików na tym samym hoście. Dzięki temu będziemy mieć gwarancję, że dane są trwale przechowywane i łatwe do odzyskania. Ze względu na mechanizm zabezpieczający Vault dodamy automatyczne odpieczętowanie za pomocą AWS Key Management System (AWS KMS).

Do przeprowadzenia wdrożenia infrastruktury i instalacji Vaulta wykorzystamy self-hosted runner z GitHub Actions, który będzie działał w prywatnej podsieci. Do budowy infrastruktury skorzystamy HashiCorp Terraform, a do konfiguracji serwera Vaulta użyjemy Ansible playbook.

Poniżej przedstawiam uproszczony schemat naszej infrastruktury:

Zaczynamy

Podczas naszego wdrożenia będziemy korzystać z kodu Terraform, którego użyliśmy w ostatnim z serii wpisów. Dodamy jednak kilka dodatkowych kroków, aby utworzyć instancję EC2 w chmurze AWS, na której zainstalujemy Vault. Po utworzeniu instancji workflow wykona playbook Ansible, który skonfiguruje całe środowisko. Dodatkowe zmiany wprowadzimy również przy tworzeniu bazy danych RDS. Wykorzystamy provider Vaulta, który automatycznie utworzy hasło i użytkownika dla naszej bazy danych. Dane te będą przechowywane w Vault.

Cały kod znajduje się w dwóch repozytoriach:
Infrastruktura: github.com/red-devops/Workout-recorder-vault
Konfiguracja Vault EC2: github.com/red-devops/Vault-server-ansible

UWAGA NA ZMIANY W REPOZYTORIACH !!!

Proszę zwrócić uwagę na daty commitów, które znajdziecie we wszystkich repozytoriach dostępnych na https://github.com/red-devops. Często korzystam z wcześniejszego kodu w kolejnych wpisach, dostosowując go do bieżących zadań. W związku z tym może się zdarzyć, że kod prezentowany w tym wpisie będzie różnił się od tego, który znajdziecie w najnowszym commicie repozytorium. Niemniej jednak wszystkie główne commity zostawiam, dlatego proszę porównywać daty publikacji wpisu z datami commitów.

Struktura katalogów infrastruktury wygląda jak na zdjęciu poniżej. Pierwsze trzy etapy musimy ręcznie wykonać, ale po wykonaniu kodu terraform z folderu 130-cicd-agent nasz github self-hosted agent automatycznie zarejestruje się do serrwisu Githuba i będzie wykonywał nasz workflow według konfiguracji.

Workflow jest tak przygotowany, że agent dla brancha feature sprawdza tylko poprawność kodu terraform. Dla branchy dev i uat utworzy z kolei osobne terraform workspace i wykona dla każdego podkatalogu 100-app-infra terraform apply.

Warto zauważyć, że zarówno dla instancji Vaulta, jak i dla GitHub Actions nie korzystamy z zapisanych w zmiennych środowiskowych poświadczeniach lecz używamy profilu instancji. W katalogu 140-vault-instance znajduje się plik iam.tf, w którym zdefiniowałem polityki, które są przypisane do instance profile instancji EC2 Vaulta. Zastosowałem przy tym zasadę „principle of least privilege”. Dzięki temu ograniczamy uprawnienia dostępu do minimalnego poziomu.

Po zakończeniu procesu tworzenia instancji Vaulta wywoływany jest playbook Ansible, który wykorzystuje plugin aws_ec2 do wyszukania instancji Vaulta. Playbook Ansible jest odpowiedzialny za utworzenie użytkownika i grupy „vault”. Następnie tworzone są odpowiednie katalogi, do których przesyłane są wypełnione szablony plików konfiguracyjnych, polityk i skryptów. Dodatkowo Vault zostaje dodany do systemu i uruchomiony jako usługa systemowa. W ten sposób konfigurujemy i przygotowujemy środowisko Vaulta do działania.

Konfiguracja servera Vaulta dla środowiska dev wygląda następująco:

listener "tcp" {
  address = "{{ private_ip_address }}:8200"
  tls_disable = 1
}

storage "dynamodb" {
  region     = "eu-central-1"
  table      = "vault-dev-storage-table"
}

api_addr = "http://{{ private_ip_address }}:8200"
cluster_addr = "http://{{ private_ip_address }}:8201"

ui = false

seal "awskms" {
region     = "eu-central-1"
kms_key_id = "{{ kms_key_id }}"
}

W konfiguracji Vaulta zmienna private_ip_address jest dynamicznie uzupełniana adresem IP naszego hosta. Parametr storage "dynamodb" wskazuje na miejsce, w którym będą przechowywane zaszyfrowane dane Vaulta. Tablea dynamodb jest tworzona przez kod terraform na tym samym etapie co instancja vault. Natomiast seal "awskms" określa, który klucz Vaulta ma być używany do automatycznego odpieczętowania i szyfrowania danych.

Automatyczna Inicjalizacja

Jak już wcześniej wspomniałem server Vault nie jest prosty do wdrożenia. Aplikacja podczas pierwszej inicjalizacji tworzy recovery key zamiast unseal key ze wzgledu na to, że do odpieczętowania używamy AWS KMS. Tak naprawdę różnica między unseal, a recovery key jest tylko w nazwie. Chociaż terminologia może się różnić istotne jest, że musimy bezpiecznie pobrać i przechowywać zarówno recovery key, jak i root token.

Skrypt, który wykonuje to zadanie to vault_inicialization.sh. Kod w całości poniżej.

#!/bin/bash 
export VAULT_ADDR=http://{{ private_ip_address }}:8200

cleanup() {
  rm -f tmp_file vault_initialization.json
}

trap cleanup EXIT

sleep 5
      
echo -n "{" > vault_initialization.json
vault operator init > tmp_file
let errors+=$?
let x=1
while [ ${x} -le $(grep "^Recovery Key" tmp_file | wc -l | awk '{print $1;}') ]
do
  echo -n \"vault_recovery_key_${x}\":\"$(grep "^Recovery Key ${x}:" tmp_file | awk '{print $4;}')\", >> vault_initialization.json
  let errors+=$?
  let x+=1
done
echo -n \"vault_initial_root_token\":\"$(grep "^Initial Root Token:" tmp_file | awk '{print $4;}')\" >> vault_initialization.json
let errors+=$?
let  x=1
echo -n "}" >> vault_initialization.json

if [ ${errors} -gt 0 ]
then
  exit ${errors}
else
  aws secretsmanager create-secret --name {{ vault_init_secret }} --description "Vault {{ env }} recovery keys" --secret-string file://vault_initialization.json --region {{ region }}
fi
exit 0

Skrypt wykonuje inicjalizację Vaulta za pomocą komendy vault operator init i zapisuje logi do pliku tmp_file. Następnie skrypt przeszukuje plik tmp_file za pomocą polecenia grep aby wyciągnąć wszystkie 5 kluczy Recovery Key oraz Initial Root Token.

W międzyczasie skrypt buduje plik vault_initialization.json, który zawiera pobrane dane z grep. Plik ten jest później wysyłany do AWS Secret Manager.

Nasz sekret w AWS Secrets Manager prezentuje się następująco:

Root token wykorzystamy w celu stworzenia polityki vault-admin-policy, zamontowania SE KV2 oraz utworzenia sekretu dla bazy danych.

Poniżej fragment ansible playbook pokazujący następujące po sobie zadania.

...
  - name: Start and enable Vault
    systemd:
      name: vault
      state: restarted
      daemon_reload: yes
      enabled: yes
    run_once: true

  - name: Initialization Vault server
    command: "{{ vault_bin_home }}/vault_initialization.sh"
    when: vault_setup == 'null'

  - name: Create policies
    command: "{{ vault_bin_home }}/vault_create_policies.sh"
    tags:
      - policies

  - name: Enable KV2 engine
    command: "{{ vault_bin_home }}/vault_setup_kv_engine.sh"
    tags:
      - kv2

  - name: Create Vault token for CICD
    command: "{{ vault_bin_home }}/vault_create_orphan_token.sh"
    tags:
      - cicd-token

Przed wykonaniem inicjalizacji serwera Vaulta uruchamiany jest moduł ansibla Start and enable Vault, który zapewnia, że serwer Vault jest uruchomiony i gotowy do inicjalizacji. W niektórych przypadkach ansible może być na tyle szybki, że inicjalizacja następuje przed pełnym uruchomieniem serwera. Dodałem zatem 5 sekund oczekiwania w skrypcie vault_initialization.sh.

Po zakończeniu inicjalizacji ansible wykonuje trzy skrypty: vault_create_policies.sh, vault_setup_kv_engine.sh oraz vault_create_orphan_token.sh. Poniżej przedstawiam skrypt vault_create_policies.sh aby pokazać, jak wykorzystuje on wcześniej utworzony token root:

#!/bin/bash 
export VAULT_ADDR=http://{{ private_ip_address }}:8200
export VAULT_TOKEN=$(aws secretsmanager get-secret-value --secret-id {{ vault_init_secret }} --region {{ region }} | jq -r .SecretString | jq -r .vault_initial_root_token)

vault policy write vault-admin-policy {{ vault_policy_home }}/vault-admin.hcl
exit 0

Zmienne środowiskowe VAULT_ADDR i VAULT_TOKEN sa wymagane do komunikacji i autoryzacji z Vault. Adres jest łatwo dostępny i uzupełniany przez ansibla. Aby uzyskać token pobieramy sekret z AWS secret menagera, a następnie przepuszczamy go przez pipe i wyciągamy wartość klucza vault_initial_root_token. Następnie skrypt tworzy politykę vault-admin-policy podając ścieżkę do pliku z jej definicją.

Warto zwrócić uwage na skrypt vault_create_orphan_token.sh, stworzy on token dla procesu CI/CD i przypisze mu politykę vault-admin-policy. Ten token będzie wykorzystywany przy tworzeniu sekretu dla bazy danych. Skrypt tworzy sekret i zapisuje go w usłudze AWS Secrets Manager. Jeśli sekret już istnieje wówczas zostanie on nadpisany.

#!/bin/bash 
export VAULT_ADDR=http://{{ private_ip_address }}:8200
export VAULT_TOKEN=$(aws secretsmanager get-secret-value --secret-id {{ vault_init_secret }} --region {{ region }} | jq -r .SecretString | jq -r .vault_initial_root_token)

TOKEN=$(vault token create -policy=vault-admin-policy -orphan -format=json | jq -r '.auth.client_token')

# Try to create the secret
aws secretsmanager create-secret --name cicd-vault-{{ env }}-token --description "Long term CICD token for Vault-{{ env }}" --secret-string '{"cicd-token":"'"$TOKEN"'"}' --region {{ region }} > /dev/null 2>&1
if [ $? -eq 0 ]; then
    echo "Created a new secret."
else
    # Error - secret already exists, try to overwrite it
    aws secretsmanager update-secret --secret-id cicd-vault-{{ env }}-token --description "Long term CICD token for Vault-{{ env }}" --secret-string '{"cicd-token":"'"$TOKEN"'"}' --region {{ region }} > /dev/null 2>&1
    if [ $? -eq 0 ]; then
        echo "Overwrote the existing secret."
    else
        echo "An error occurred while creating or overwriting the secret."
    fi
fi

Terraform – Vault Provider

Po prawidłowym wykonaniu playbooka GitHub self-hosted agent będzie kontynuował wykonywanie infrastruktury. Stworzy Security Group (SG) z folderu 150-security-groups, a następnie przystąpi do inicjalizacji Terraform dla katalogu 160-data-base. Poniżej znajduje się fragment pliku dependencies.tf z tego folderu.

data "aws_secretsmanager_secret_version" "cicd_vault_token" {
  secret_id = "cicd-vault-${terraform.workspace}-token"
}

...

provider "vault" {
  address = "http://${data.terraform_remote_state.vault_instance.outputs.vault_private_ip}:8200"
  token   = jsondecode(data.aws_secretsmanager_secret_version.cicd_vault_token.secret_string)["cicd-token"]
}

W pliku dependencies.tf blok data pobiera sekret na podstawie jego identyfikatora. Dla środowiska dev, nazwa sekretu to cicd-vault-dev-token.

Moduł terraform instancji Vaulta udostępnia w outputs swój prywatny adres IP, który jest pobierany za pomocą bloku data.terraform_remote_state.vault_instance. Następnie z pobranego tokenu, wykorzystując wbudowaną funkcję jsondecode w Terraform odczytywana jest wartość klucza cicd-token.

Pamiętajmy, że Github agent jak i instancja Vault muszą znajdować się w tej samej VPC, a SG i Access control list (ACL) muszą pozwalać na komunikację między nimi.

Dzięki temu providerowi w pliku main.tf możemy stworzyć nowy sekret dla naszej bazy danych.

resource "random_password" "db_password" {
  length  = 16
  special = false
}

resource "vault_generic_secret" "db_secret" {
  path      = "secret/kv/workoutrecorder/${terraform.workspace}/db_secret"
  data_json = <<EOT
    {
      "username": "admin",
      "password": "${random_password.db_password.result}"
    }
  EOT
}

module "db" {
  source = "git::https://github.com/terraform-aws-modules/terraform-aws-rds.git"

  identifier           = lower("${local.name}")
  engine               = "mysql"
  ...
  port                   = 3306
  db_name                = local.db_name
  username               = vault_generic_secret.db_secret.data["username"]
  password               = vault_generic_secret.db_secret.data["password"]
  create_random_password = false
  ...
}

W ostatnim folderze 170-instances nie zaszły żadne zmiany.

Wyzwolenie potoku

Przed rozpoczeciem Github workflow z repozytorium Workout-recorder-vault tworzymy branch dev i feature. Następnie wysyłamy sztuczny commit na branch feature w celu stworzenia Pull Request (PR). Pierwszy workflow rozpocznie się i sprawdzi konfigurację terraform dla naszych folderów. Po poprawnym zakończeniu tego potoku możemy stworzyć PR z brancha feature na dev.
Nie chcę powtarzać wszystkich szczegółów implementacyjnych dotyczących sposobu wykonywania kodu Terraform z użyciem workflow i PR. Jeśli jesteś zainteresowany szczegółami zapraszam do przeczytania mojego wcześniejszego wpisu, w którym dokładnie to opisałem.

Ze względów bezpieczeństwa wszystkie repozytoria z aktywnymi Actions są prywatne, ale przed publikacją przeniosłem je do publicznego wyłączając możliwość wyzwolenia workflow. Dzięki temu logi z wykonanych akcji będą dostępne przez 3 miesiące od chwili wykonania.

Po pomyślnym zakończeniu wszystkich sprawdzeń tworzymy PR, a to wyzwala potok, który wykonuje terraform plan i dodaje je jako komentarz do naszego PR. Po scaleniu zmian do gałęzi dev zostaje uruchomiony ostatni proces przeprowadzający zmiany na infrastrukturze.

Warto zauważyć, że tylko dla etapu 100-app-infra/140-vault-instance zostaną wykonane dodatkowe zadania związane z konfiguracją serwera Vaulta.

Poniżej znajdziecie pełne logi z wykonania playbooka ansibla.

PLAY [Install Vault Server] ****************************************************

TASK [Check if Vault server is already setup] **********************************
ok: [10.0.1.237]

TASK [Update APT] **************************************************************
changed: [10.0.1.237]

TASK [Install prerequisites] ***************************************************
changed: [10.0.1.237] => (item=jq)
changed: [10.0.1.237] => (item=awscli)

TASK [Create group vault] ******************************************************
changed: [10.0.1.237]

TASK [Create user vault] *******************************************************
changed: [10.0.1.237]

TASK [Create directories] ******************************************************
changed: [10.0.1.237] => (item=/home/vault/vault-server)
changed: [10.0.1.237] => (item=/home/vault/vault-server/bin)
changed: [10.0.1.237] => (item=/home/vault/vault-server/policy)
changed: [10.0.1.237] => (item=/home/vault/vault-server/config)
changed: [10.0.1.237] => (item=/home/vault/vault-server/log)

TASK [Render template file] ****************************************************
changed: [10.0.1.237] => (item=vault_install.sh)
changed: [10.0.1.237] => (item=vault_initialization.sh)
changed: [10.0.1.237] => (item=vault_create_policies.sh)
changed: [10.0.1.237] => (item=vault_setup_kv_engine.sh)
changed: [10.0.1.237] => (item=vault_create_orphan_token.sh)

TASK [Copy Vault policies] *****************************************************
changed: [10.0.1.237] => (item=vault-admin.hcl)

TASK [Copy main configuration] *************************************************
changed: [10.0.1.237]

TASK [Install Vault] ***********************************************************
changed: [10.0.1.237]

TASK [Set systemd vault.service] ***********************************************
changed: [10.0.1.237]

TASK [Start and enable Vault] **************************************************
changed: [10.0.1.237]

TASK [Initialization Vault server] *********************************************
changed: [10.0.1.237]

TASK [Create policies] *********************************************************
changed: [10.0.1.237]

TASK [Enable KV2 engine] *******************************************************
changed: [10.0.1.237]

TASK [Create Vault token for CICD] *********************************************
changed: [10.0.1.237]

PLAY RECAP *********************************************************************
10.0.1.237                 : ok=16   changed=15   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

Wszystkie etapy zostały pomyślnie zakończone, co oznacza, że środowisko dev zostało poprawnie utworzone, a instancja Vaulta jest gotowa do wykonywania zadań.

Dla przypomnienia dodam, że cały kod znajduje się w repozytoriach na stronie GitHub tego bloga: https://github.com/orgs/red-devops/repositories

Uwagi na koniec

  1. Środowisko UAT będzie znajdowało się w innej VPC, dlatego Github agent nie jest w stanie bezpośrednio połączyć się z drugą VPC. Aby naprawić ten problem należałoby stworzyć dodatkowego self-hosted runnera z osobnym tokenem, który byłby w drugiej sieci. Ze względu na złożoność projektu zdecydowałem się póki co pozostawić ten problem nierozwiązany. Wydaje mi się, że w darmowej wersji Github nie pozwala na dodawanie labelek do github self-hosted runnera, etykiet np. typu dev, uat, dzięki którym moglibyśmy przypisać dane workflow do konkretnego agenta. Wykonując te same komendy z AMI Github sprawdziłem ręcznie agenta umieszczonego w drugiej VPC i infrastruktura została poprawnie utworzona.
  2. W tym wpisie wykonałem ansible playbook w workflow razem z budową infrastruktury. Uważam, że jest to mało eleganckie podejście, ale ze wzgledu na wpis chciałem by logi były dostępne w jednym miejscu. W konsekwencji stworzyło to w workflow mało czytelne reguły.

    if: matrix.component-path == '100-app-infra/140-vault-instance' && github.ref == 'refs/heads/dev' && github.event_name == 'push'

    W praktyce o wiele lepszym rozwiązaniem byłoby wydzielenie konfiguracji serwera Vault do osobnego potoku lub dodanie do user-data instancji Vault skryptu wykonującego ansible playbook lokalnie.
  3. Przebudowałem infrastrukturę względem tej ze wpisu o Terraform – Guide. Wcześniej agent GitHub Actions odpowiedzialny za budowę znajdował się w osobnej VPC w porównaniu do reszty infrastruktury. Było to dobre rozwiązanie, gdy używaliśmy poleceń Terraform, które i tak komunikowały się z AWS za pośrednictwem interfejsu API. Jednak gdy chcieliśmy połączyć się z instancją Vault w celu wykonania playbooka Ansible lub z dostawcą Vault, było to niemożliwe. Można by to rozwiązać dynamicznym parowaniem VPC, ale zdecydowałem się na uproszczenie rozwiązania zamiast je komplikować. Postanowiłem więc umieścić osobnego agenta budującego na każdym środowisku.
  4. Podczas inicjalizacji serwera Vault zamiast tworzyć pliki i wyciągać z nich dane za pomocą grep po prostu można użyć komendy vault operator init -format=json. Dzięki temu od razu dostaniemy dane w formie json. To jak również kilka innych ułatwień odkryłem pod koniec tworzenia tego wpisu.
  5. Dla celów szkoleniowych wykorzystałem nieco otwarty zapis w polityce przypisanej do profilu instancji Vaulta, a dokładniej to dla aws secret menager pozwoliłem by Resorce posiadał znak „*” co jest dość niebezpieczne. W przypadku produkcyjnego wykorzystania powinno się użyć dokładnego ARN danego sekretu.

Podsumowanie

HashiCorp Vault to potężne narzędzie do zarządzania tajnymi informacjami i bezpiecznego przechowywania danych. W tym artykule zapoznaliśmy się z architekturą Vault, począwszy od procesu odblokowywania, przez metody autentykacji i zarządzanie tokenami, aż po polityki i secrets engines. Zrozumieliśmy, jak Vault może pomóc w zabezpieczaniu naszych aplikacji i infrastruktury.

Nauczyliśmy się, jak korzystać z różnych SE, takich jak KV V2 i AWS Secret Engine, aby przechowywać i zarządzać poufnymi danymi. Zobaczyliśmy również jak automatycznie wdrożyć Vault przy użyciu narzędzi takich jak Terraform oraz Ansible, które umożliwiają łatwe utworzenie i konfigurację instancji Vault.

Podsumowując, HashiCorp Vault jest wszechstronnym narzędziem, które umożliwia bezpieczne przechowywanie i zarządzanie poufnymi danymi. Jego elastyczna architektura, różnorodne metody autentykacji i SE czynią go niezastąpionym narzędziem dla zespołów deweloperskich i operatorów infrastruktury.

Comments are closed.