Kurs NestJS · Moduł 12: Konteneryzacja i CI/CD
Secrets Management - tajne depesze Imperium
W tej lekcji6
Plik .env poznałeś w module wprowadzającym i lokalnie sprawdza się doskonale. Na produkcji jest inaczej - i warto wiedzieć dlaczego, zanim ktoś przekona Cię, że wystarczy dopisać go do .gitignore.
Cztery powody naraz. Ryzyko przypadkowego commita - jedno git add -f albo nowy programista bez skonfigurowanego .gitignore i hasło jest w historii repozytorium na zawsze. Brak rotacji - zmiana hasła do bazy oznacza ręczną edycję pliku na każdym serwerze. Brak audytu - nikt nie wie, kto i kiedy odczytał sekret. I plain text - hasła leżą jawnym tekstem na dysku, dostępne dla każdego procesu z uprawnieniami do odczytu.
Zauważ, czego wśród tych powodów nie ma: pliki .env nie są wolne, nie zajmują pamięci i działają w Node.js bez zarzutu. Problem nie jest techniczny, tylko organizacyjny.
Rzym nie wysyłał tajnych depesz zwykłym posłańcem. Trafiały do kancelarii, która wydawała je za pokwitowaniem i wiedziała, kto co odebrał.
Kolejność plików .env
Zanim przejdziemy do kancelarii, jedna rzecz o samych plikach. ConfigModule przyjmuje ich listę:
1ConfigModule.forRoot({
2 isGlobal: true,
3 envFilePath: ['.env.local', '.env.development', '.env'],
4});Kolejność rejestracji jest stała: ConfigModule, .forRoot({, opcje z isGlobal: true,, na końcu }).
A teraz rzecz, która myli najczęściej: wygrywa plik podany jako pierwszy. Gdy ta sama zmienna występuje w kilku plikach, wartość bierzemy z .env.local - nie z .env, mimo że to on wygląda na „główny".
Logika jest taka: od najbardziej szczegółowego do najbardziej ogólnego. .env.local to Twoje osobiste nadpisania, których nikt inny nie ma; .env.development to ustawienia całego środowiska; .env to wartości domyślne. Bardziej szczegółowy przesłania ogólniejszy.
Kancelaria - HashiCorp Vault
HashiCorp Vault to narzędzie do centralnego zarządzania sekretami z audytem i rotacją. Nie jest frameworkiem frontendowym, systemem testów ani narzędziem do kompresji obrazów - to skarbiec, który wydaje wartości na żądanie i zapisuje każde wydanie.
Rozwiązuje dokładnie te braki, które wypisaliśmy: sekret nie leży w pliku (nie ma czego zacommitować), można go zmienić w jednym miejscu (rotacja), a każdy odczyt zostaje odnotowany (audyt).
Aplikacja pobiera sekrety przy starcie, zanim cokolwiek ich potrzebuje:
1export async function loadSecrets(): Promise<Record<string, string>> {
2 const vault = new VaultClient({
3 endpoint: process.env.VAULT_ADDR,
4 token: process.env.VAULT_TOKEN,
5 });
6
7 const { data } = await vault.read('secret/data/legion-api');
8
9 return {
10 DATABASE: data.data.DATABASE,
11 JWT_SECRET: data.data.JWT_SECRET,
12 REDIS_PASSWORD: data.data.REDIS_PASSWORD,
13 };
14}Zwróć uwagę na paradoks, który tu widać: żeby dostać się do skarbca, też potrzebujesz klucza - adresu i tokenu, które nadal przychodzą ze zmiennych środowiskowych. Vault nie usuwa tego problemu, tylko sprowadza go do jednego sekretu zamiast dwudziestu. Ten jeden bywa dodatkowo wydawany krótkoterminowo przez mechanizm samej infrastruktury.
Priorytet źródeł
W dojrzałej konfiguracji sekrety mogą pochodzić z kilku miejsc naraz. Kolejność od najwyższego priorytetu wygląda tak:
- HashiCorp Vault - produkcja; ma audyt i rotację.
- AWS Secrets Manager - chmura; to samo, ale wbudowane w platformę dostawcy.
- Zmienne środowiskowe - fallback; działają zawsze, także lokalnie i w testach.
Reguła jest prosta: im wyżej, tym więcej kontroli. Fallback na zmienne środowiskowe zostaje na dole nie dlatego, że jest zły, tylko dlatego, że nie daje ani audytu, ani rotacji - ale za to działa bez żadnej infrastruktury, więc lokalne uruchomienie nie wymaga stawiania Vaulta.
Odczyt w aplikacji
Niezależnie od źródła, w kodzie sięgasz po sekret tak samo:
1const dbUrl = this.configService.get<string>('DATABASE');Kolejność członów jest stała: this, .configService, .get<string>, ('DATABASE'). To ta sama metoda, którą poznałeś przy konfiguracji - i o to chodzi. Serwis nie wie, czy wartość przyszła z Vaulta, z chmury, czy ze zmiennej środowiskowej; zmiana źródła nie dotyka ani jednej linii logiki.
Sekrety w Kubernetes
W klastrze pojawia się dodatkowy kłopot: konfigurację opisujemy plikami YAML, które trzymamy w repozytorium - a sekret w takim pliku to znowu hasło w Gicie.
Sealed Secrets umożliwiają bezpieczne przechowywanie zaszyfrowanych sekretów w repozytorium Git. Szyfrujesz wartość kluczem publicznym kontrolera działającego w klastrze; powstały plik możesz spokojnie zacommitować, bo odszyfrować go potrafi wyłącznie ten kontroler, kluczem prywatnym, którego nigdzie nie wysyłasz.
To rozwiązuje sprzeczność między „wszystko w repozytorium" a „sekretów w repozytorium nie trzymamy". Sealed Secrets nie skalują podów, nie kompresują obrazów i nie monitorują zużycia - szyfrują sekrety tak, by mogły podróżować z kodem.
Podsumowanie
Kancelaria wydaje depesze za pokwitowaniem:
.envna produkcji jest niebezpieczny z czterech powodów naraz: ryzyko przypadkowego commita, brak rotacji, brak audytu, plain text - nie z powodu wydajności czy pamięci,- w
envFilePathwygrywa plik podany jako pierwszy:.env.localprzesłania.env.development, a ten.env, - rejestracja w kolejności:
ConfigModule,.forRoot({,isGlobal: true,,}), - HashiCorp Vault to narzędzie do centralnego zarządzania sekretami z audytem i rotacją,
- dostęp do Vaulta też wymaga sekretu - ale jednego zamiast dwudziestu,
- priorytet źródeł od najwyższego: Vault (produkcja) → AWS Secrets Manager (chmura) → zmienne środowiskowe (fallback),
- odczyt zawsze tak samo:
this+.configService+.get<string>+('DATABASE')- logika nie wie, skąd wartość pochodzi, - Sealed Secrets umożliwiają bezpieczne przechowywanie zaszyfrowanych sekretów w repozytorium Git - odszyfruje je tylko kontroler w klastrze.
W następnej lekcji wrócimy do pipeline'u - tym razem z macierzami budowania, artefaktami i cache'em. A na razie zapamiętaj: plik .env mówi, co jest hasłem; kancelaria mówi dodatkowo, kto je odebrał i kiedy je zmieniono.
Kod do tej lekcji: src/secrets-management.ts
1// Secrets Management - Tajne Depesze Imperium
2console.log("=== SECRETS MANAGEMENT ===\n");
3
4interface SecretProvider {
5 name: string;
6 type: string;
7 features: string[];
8 bestFor: string;
9}
10
11const providers: SecretProvider[] = [
12 {
13 name: 'HashiCorp Vault',
14 type: 'Self-hosted / Cloud',
15 features: ['Dynamiczne sekrety', 'Rotacja', 'Audit log', 'Polityki dostepu'],
16 bestFor: 'Duze organizacje, multi-cloud',
17 },
18 {
19 name: 'AWS Secrets Manager',
20 type: 'Cloud (AWS)',
21 features: ['Natywna integracja AWS', 'Auto-rotacja', 'Lambda triggers'],
22 bestFor: 'Aplikacje na AWS',
23 },
24 {
25 name: 'Kubernetes Secrets',
26 type: 'Kubernetes-native',
27 features: ['Base64 encoding', 'RBAC', 'Mounted jako env/files'],
28 bestFor: 'Aplikacje w Kubernetes',
29 },
30 {
31 name: 'Sealed Secrets',
32 type: 'Kubernetes + Git',
33 features: ['Szyfrowanie kluczem klastra', 'Bezpieczne w Git', 'GitOps'],
34 bestFor: 'GitOps workflow',
35 },
36];
37
38providers.forEach((p, i) => {
39 console.log(`${i + 1}. ${p.name} (${p.type})`);
40 console.log(` Najlepszy dla: ${p.bestFor}`);
41 console.log(` Funkcje:`);
42 p.features.forEach(f => console.log(` - ${f}`));
43 console.log();
44});
45
46// Dlaczego NIE .env na produkcji
47console.log("=== DLACZEGO NIE .env NA PRODUKCJI ===\n");
48const risks = [
49 'Przypadkowy commit do repozytorium',
50 'Brak rotacji - hasla nigdy sie nie zmieniaja',
51 'Brak audytu - kto mial dostep?',
52 'Plain text na dysku - brak szyfrowania',
53 'Reczne kopiowanie na kazdy serwer',
54];
55
56risks.forEach((r, i) => console.log(` ${i + 1}. ${r}`));
57console.log("\nUzywaj: Vault, AWS Secrets Manager lub K8s Secrets");
58Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Dlaczego przechowywanie sekretów w plikach .env na produkcji jest niebezpieczne?
2. Co to jest HashiCorp Vault?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Zdefiniuj asynchroniczny config loader: połącz się z Vault, odczytaj sekrety (DATABASE, JWT_SECRET, REDIS_PASSWORD) i zwróć je jako obiekt konfiguracji
- Układanie w poziomie
Ułóż elementy odczytu zmiennej środowiskowej przez ConfigService:
- Klikanie w kolejności
Ułóż elementy rejestracji ConfigModule w prawidłowej kolejności: