@rytass/secret-adapter-vault-nestjs
Version:
Rytass Sceret Vault nestjs adapter
488 lines (376 loc) • 11.5 kB
Markdown
# 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 /secret-adapter-vault-nestjs /secret-adapter-vault
# or
yarn add /secret-adapter-vault-nestjs /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';
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';
export class AppModule {}
```
### Using VaultService
```typescript
import { Injectable } from '@nestjs/common';
import { VaultService } from '@rytass/secret-adapter-vault-nestjs';
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;
}
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
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
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
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';
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';
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
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
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';
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
export class OldService {
constructor(private config: ConfigService) {}
getValue() {
return this.config.get('MY_KEY');
}
}
// After - Using VaultService
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