UNPKG

@rytass/secret-adapter-vault-nestjs

Version:

Rytass Sceret Vault nestjs adapter

488 lines (376 loc) 11.5 kB
# Rytass Utils - Secret Adapter Vault NestJS NestJS module for HashiCorp Vault integration, providing secure secret management with automatic fallback to environment variables. Seamlessly integrates with NestJS dependency injection and configuration system. ## Features - [x] HashiCorp Vault integration for NestJS - [x] Automatic fallback to environment variables - [x] Global module support - [x] TypeScript support with generics - [x] Asynchronous secret retrieval - [x] Secret writing and deletion - [x] Online/offline sync support - [x] Connection state management - [x] Error handling with graceful degradation ## Installation ```bash npm install @rytass/secret-adapter-vault-nestjs @rytass/secret-adapter-vault # or yarn add @rytass/secret-adapter-vault-nestjs @rytass/secret-adapter-vault ``` ## Environment Configuration Configure the following environment variables for Vault connection: ```bash # Required for Vault connection VAULT_HOST=https://vault.example.com:8200 # Vault service base URL VAULT_ACCOUNT=your-username # Vault username VAULT_PASSWORD=your-password # Vault password # Optional - defaults to root path if not specified VAULT_PATH=/secret/data/myapp # Vault secret path from root ``` ## Basic Usage ### Module Setup ```typescript import { Module } from '@nestjs/common'; import { VaultModule } from '@rytass/secret-adapter-vault-nestjs'; @Module({ imports: [ VaultModule.forRoot({ path: '/secret/data/myapp', // Vault path for secrets fallbackFile: '.env', // Optional: fallback env file }), ], }) export class AppModule {} ``` ### Global Module Configuration Make the VaultService available throughout your application: ```typescript import { Module } from '@nestjs/common'; import { VaultModule } from '@rytass/secret-adapter-vault-nestjs'; @Module({ imports: [ VaultModule.forRoot({ path: '/secret/data/myapp', fallbackFile: '.env', }), ], }) export class AppModule {} ``` ### Using VaultService ```typescript import { Injectable } from '@nestjs/common'; import { VaultService } from '@rytass/secret-adapter-vault-nestjs'; @Injectable() export class ConfigurationService { constructor(private readonly vault: VaultService) {} async getDatabaseConfig() { const host = await this.vault.get<string>('DB_HOST'); const port = await this.vault.get<number>('DB_PORT'); const username = await this.vault.get<string>('DB_USERNAME'); const password = await this.vault.get<string>('DB_PASSWORD'); return { host, port, username, password, }; } async getApiKey(): Promise<string> { return this.vault.get<string>('API_KEY'); } async updateApiKey(newKey: string): Promise<void> { // Update locally and sync to Vault await this.vault.set('API_KEY', newKey, true); } } ``` ## Advanced Usage ### Type-Safe Secret Retrieval ```typescript interface DatabaseConfig { host: string; port: number; username: string; password: string; ssl: boolean; } @Injectable() export class DatabaseService { constructor(private readonly vault: VaultService) {} async getConfig(): Promise<DatabaseConfig> { // Retrieve complex objects return this.vault.get<DatabaseConfig>('database'); } async getConnectionString(): Promise<string> { const config = await this.getConfig(); return `postgresql://${config.username}:${config.password}@${config.host}:${config.port}/mydb`; } } ``` ### Managing Secrets ```typescript @Injectable() export class SecretManagementService { constructor(private readonly vault: VaultService) {} // Create or update a secret async createSecret(key: string, value: any): Promise<void> { // Save locally only await this.vault.set(key, value, false); } // Create and sync to Vault immediately async createAndSyncSecret(key: string, value: any): Promise<void> { await this.vault.set(key, value, true); } // Delete a secret async removeSecret(key: string): Promise<void> { // Delete locally only await this.vault.delete(key, false); } // Delete and sync removal to Vault async removeAndSyncSecret(key: string): Promise<void> { await this.vault.delete(key, true); } } ``` ### Fallback Mechanism When Vault is unavailable, the service automatically falls back to environment variables: ```typescript @Injectable() export class ResilientConfigService { constructor(private readonly vault: VaultService) {} async getConfig() { // If Vault is unavailable, this will read from process.env const apiUrl = await this.vault.get<string>('API_URL'); const apiKey = await this.vault.get<string>('API_KEY'); return { apiUrl, apiKey, }; } } ``` ### Environment File Configuration Specify a fallback environment file for when Vault is unavailable: ```typescript // app.module.ts @Module({ imports: [ VaultModule.forRoot({ path: '/secret/data/production', fallbackFile: '.env.production', // Fallback to .env.production file }), ], }) export class AppModule {} ``` ## Integration Examples ### With TypeORM ```typescript import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { VaultModule, VaultService } from '@rytass/secret-adapter-vault-nestjs'; @Module({ imports: [ VaultModule.forRoot({ path: '/secret/data/database', }), TypeOrmModule.forRootAsync({ imports: [VaultModule], inject: [VaultService], useFactory: async (vault: VaultService) => ({ type: 'postgres', host: await vault.get<string>('DB_HOST'), port: await vault.get<number>('DB_PORT'), username: await vault.get<string>('DB_USERNAME'), password: await vault.get<string>('DB_PASSWORD'), database: await vault.get<string>('DB_NAME'), synchronize: false, logging: true, }), }), ], }) export class DatabaseModule {} ``` ### With JWT Module ```typescript import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; import { VaultModule, VaultService } from '@rytass/secret-adapter-vault-nestjs'; @Module({ imports: [ VaultModule.forRoot({ path: '/secret/data/auth', }), JwtModule.registerAsync({ imports: [VaultModule], inject: [VaultService], useFactory: async (vault: VaultService) => ({ secret: await vault.get<string>('JWT_SECRET'), signOptions: { expiresIn: (await vault.get<string>('JWT_EXPIRY')) || '1h', }, }), }), ], }) export class AuthModule {} ``` ### With Microservices ```typescript import { NestFactory } from '@nestjs/core'; import { Transport } from '@nestjs/microservices'; import { VaultService } from '@rytass/secret-adapter-vault-nestjs'; async function bootstrap() { const app = await NestFactory.create(AppModule); const vault = app.get(VaultService); const microservice = await NestFactory.createMicroservice({ transport: Transport.REDIS, options: { host: await vault.get<string>('REDIS_HOST'), port: await vault.get<number>('REDIS_PORT'), password: await vault.get<string>('REDIS_PASSWORD'), }, }); await app.startAllMicroservices(); await app.listen(3000); } ``` ## Error Handling ```typescript @Injectable() export class SafeConfigService { constructor(private readonly vault: VaultService) {} async getSensitiveConfig() { try { const secret = await this.vault.get<string>('SENSITIVE_KEY'); if (!secret) { throw new Error('Sensitive key not found'); } return secret; } catch (error) { // When Vault is down, it falls back to env vars automatically console.error('Failed to retrieve secret:', error); // You can implement additional fallback logic return process.env.FALLBACK_SENSITIVE_KEY || 'default-value'; } } async trySaveSecret(key: string, value: string): Promise<boolean> { try { await this.vault.set(key, value, true); return true; } catch (error) { // Cannot save when in fallback mode console.error('Failed to save secret:', error.message); return false; } } } ``` ## Best Practices ### 1. Always Use Type Parameters ```typescript // Good - Type-safe const port = await vault.get<number>('PORT'); const config = await vault.get<DatabaseConfig>('db_config'); // Avoid - Returns any const value = await vault.get('SOME_KEY'); ``` ### 2. Handle Fallback Scenarios ```typescript @Injectable() export class ConfigService { constructor(private readonly vault: VaultService) {} async initialize() { try { // Try to save a test value to check if Vault is writable await this.vault.set('health_check', 'ok', true); console.log('Vault is connected and writable'); } catch (error) { console.warn('Running in read-only mode with environment variables'); } } } ``` ### 3. Organize Secrets by Path ```typescript // auth.module.ts VaultModule.forRoot({ path: '/secret/data/auth' }); // database.module.ts VaultModule.forRoot({ path: '/secret/data/database' }); // api.module.ts VaultModule.forRoot({ path: '/secret/data/external-apis' }); ``` ### 4. Use Environment-Specific Paths ```typescript const environment = process.env.NODE_ENV || 'development'; @Module({ imports: [ VaultModule.forRoot({ path: `/secret/data/${environment}`, fallbackFile: `.env.${environment}`, }), ], }) export class AppModule {} ``` ## API Reference ### VaultModule #### `forRoot(options: VaultModuleOptions): DynamicModule` Configure the Vault module. **Options:** - `path` (string, required): Vault secret path from root - `fallbackFile` (string, optional): Path to fallback environment file ### VaultService #### `get<T>(key: string): Promise<T>` Retrieve a secret value. **Parameters:** - `key`: Secret key name **Returns:** Promise resolving to the secret value #### `set<T>(key: string, value: T, syncToOnline?: boolean): Promise<void>` Store a secret value. **Parameters:** - `key`: Secret key name - `value`: Value to store - `syncToOnline`: Whether to sync immediately to Vault (default: false) #### `delete(key: string, syncToOnline?: boolean): Promise<void>` Delete a secret. **Parameters:** - `key`: Secret key name - `syncToOnline`: Whether to sync deletion to Vault (default: false) ## Migration from ConfigService ```typescript // Before - Using ConfigService @Injectable() export class OldService { constructor(private config: ConfigService) {} getValue() { return this.config.get('MY_KEY'); } } // After - Using VaultService @Injectable() export class NewService { constructor(private vault: VaultService) {} async getValue() { return this.vault.get<string>('MY_KEY'); } } ``` ## Troubleshooting ### Vault Connection Issues If you see fallback warnings: 1. Check `VAULT_HOST` is accessible 2. Verify `VAULT_ACCOUNT` and `VAULT_PASSWORD` are correct 3. Ensure `VAULT_PATH` exists in Vault 4. Check network connectivity to Vault server ### Type Safety Always specify type parameters for better TypeScript support: ```typescript // Explicit types prevent runtime errors const port = await vault.get<number>('PORT'); const features = await vault.get<string[]>('FEATURE_FLAGS'); const config = await vault.get<AppConfig>('APP_CONFIG'); ``` ## License MIT