@rytass/secret-adapter-vault-nestjs
v0.5.2
Published
Rytass Sceret Vault nestjs adapter
Readme
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
npm install @rytass/secret-adapter-vault-nestjs @rytass/secret-adapter-vault
# or
yarn add @rytass/secret-adapter-vault-nestjs @rytass/secret-adapter-vaultEnvironment Configuration
Configure the following environment variables for Vault connection:
# 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 rootBasic Usage
Module Setup
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:
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
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
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
@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:
@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:
// 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
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
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
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
@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
// 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
@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
// 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
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 rootfallbackFile(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 namevalue: Value to storesyncToOnline: Whether to sync immediately to Vault (default: false)
delete(key: string, syncToOnline?: boolean): Promise<void>
Delete a secret.
Parameters:
key: Secret key namesyncToOnline: Whether to sync deletion to Vault (default: false)
Migration from ConfigService
// 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:
- Check
VAULT_HOSTis accessible - Verify
VAULT_ACCOUNTandVAULT_PASSWORDare correct - Ensure
VAULT_PATHexists in Vault - Check network connectivity to Vault server
Type Safety
Always specify type parameters for better TypeScript support:
// 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
