@rytass/secret-adapter-vault
v0.4.4
Published
Rytass Secret Vault adapter
Readme
Rytass Utils - HashiCorp Vault Secret Adapter
Comprehensive secret management adapter for HashiCorp Vault, providing secure storage and retrieval of sensitive configuration data. Features both online and offline modes with automatic token management, caching mechanisms, and support for Key-Value V2 secret engines.
Features
Authentication
- [x] Username/Password authentication (userpass)
- [x] Token-based authentication
- [x] Automatic token renewal
- [x] Configurable token TTL
Secret Engines
- [x] Key-Value V2 engine support
- [x] Online mode (real-time operations)
- [x] Offline mode (cached operations)
- [x] Version control and conflict resolution
- [x] Batch synchronization
Advanced Features
- [x] Automatic caching for offline operations
- [x] Event-driven architecture
- [x] Error handling and retry mechanisms
- [x] TypeScript type safety
- [x] Connection health monitoring
Installation
npm install @rytass/secret-adapter-vault
# or
yarn add @rytass/secret-adapter-vaultBasic Usage
Online Mode (Real-time Operations)
import { VaultSecret } from '@rytass/secret-adapter-vault';
// Online mode - all operations hit Vault directly
const onlineVault = new VaultSecret('apps/myapp/config', {
host: 'https://vault.company.com',
online: true, // Enable online mode
auth: {
account: 'myapp-service',
password: 'secure-password',
},
});
// Wait for connection to be ready
await new Promise(resolve => {
onlineVault.onReady(() => resolve(undefined));
});
// Get secret (async operation)
const dbPassword = await onlineVault.get<string>('DATABASE_PASSWORD');
console.log('DB Password:', dbPassword);
// Set new secret (async operation)
await onlineVault.set<string>('API_KEY', 'new-api-key-value');
// Delete secret (async operation)
await onlineVault.delete('OLD_CONFIG_KEY');Offline Mode (Cached Operations)
import { VaultSecret } from '@rytass/secret-adapter-vault';
// Offline mode - operations use local cache
const offlineVault = new VaultSecret('apps/myapp/config', {
host: 'https://vault.company.com',
online: false, // Default offline mode
auth: {
account: 'myapp-service',
password: 'secure-password',
},
onReady: async () => {
console.log('Vault cache ready for offline operations');
// All operations are synchronous and use cache
const dbHost = offlineVault.get<string>('DATABASE_HOST');
const dbPort = offlineVault.get<number>('DATABASE_PORT');
// Modify cached values
offlineVault.set<string>('NEW_FEATURE_FLAG', 'enabled');
// Delete from cache
offlineVault.delete('DEPRECATED_CONFIG');
// Sync changes to Vault server
try {
await offlineVault.sync();
console.log('Successfully synced changes to Vault');
} catch (error) {
console.error('Sync failed:', error);
}
},
onError: error => {
console.error('Vault connection error:', error);
},
});Authentication Methods
Username/Password Authentication
const vault = new VaultSecret('secret/myapp', {
host: 'https://vault.example.com',
auth: {
account: 'service-account',
password: 'strong-password',
},
tokenTTL: 3600000, // 1 hour in milliseconds
});Token-based Authentication
// Using existing token
const vault = new VaultSecret('secret/myapp', {
host: 'https://vault.example.com',
auth: {
token: 'hvs.1234567890abcdef...', // Pre-existing token
},
});Configuration Options
VaultSecretOptions
| Property | Type | Required | Default | Description |
| ---------- | ------------------ | -------- | --------- | ------------------------------------------- |
| host | string | Yes | - | Vault server URL |
| auth | VaultAuthMethods | Yes | - | Authentication configuration |
| online | boolean | No | false | Enable online mode for real-time operations |
| tokenTTL | number | No | 2764724 | Token TTL in milliseconds |
| onReady | function | No | - | Callback when vault is ready |
| onError | function | No | - | Error callback |
Authentication Types
// Username/Password auth
interface UsernamePasswordAuth {
account: string;
password: string;
}
// Token auth
interface TokenAuth {
token: string;
}
type VaultAuthMethods = UsernamePasswordAuth | TokenAuth;Advanced Usage
Environment-specific Configuration
// Development environment
const devVault = new VaultSecret('apps/myapp/dev', {
host: 'https://vault-dev.company.com',
auth: {
account: process.env.VAULT_USERNAME!,
password: process.env.VAULT_PASSWORD!,
},
online: true, // Real-time for development
onReady: () => {
console.log('Development vault ready');
},
});
// Production environment
const prodVault = new VaultSecret('apps/myapp/prod', {
host: 'https://vault-prod.company.com',
auth: {
account: process.env.VAULT_PROD_USERNAME!,
password: process.env.VAULT_PROD_PASSWORD!,
},
online: false, // Cached for production performance
tokenTTL: 7200000, // 2 hours
onReady: () => {
console.log('Production vault cache loaded');
},
onError: error => {
console.error('Production vault error:', error);
// Implement alerting/monitoring
},
});Type-safe Secret Management
// Define secret schema
interface AppSecrets {
DATABASE_URL: string;
REDIS_URL: string;
JWT_SECRET: string;
API_TIMEOUT: number;
FEATURE_FLAGS: {
newFeature: boolean;
betaAccess: boolean;
};
EMAIL_CONFIG: {
smtp_host: string;
smtp_port: number;
username: string;
password: string;
};
}
class SecretManager {
constructor(private vault: VaultSecret<any>) {}
async getDatabaseUrl(): Promise<string> {
return await this.vault.get<string>('DATABASE_URL');
}
async getRedisConfig(): Promise<{ url: string; timeout: number }> {
const url = await this.vault.get<string>('REDIS_URL');
const timeout = (await this.vault.get<number>('REDIS_TIMEOUT')) || 5000;
return { url, timeout };
}
async getEmailConfig(): Promise<AppSecrets['EMAIL_CONFIG']> {
return await this.vault.get<AppSecrets['EMAIL_CONFIG']>('EMAIL_CONFIG');
}
async updateFeatureFlag(flag: keyof AppSecrets['FEATURE_FLAGS'], enabled: boolean): Promise<void> {
const currentFlags = (await this.vault.get<AppSecrets['FEATURE_FLAGS']>('FEATURE_FLAGS')) || {};
await this.vault.set<AppSecrets['FEATURE_FLAGS']>('FEATURE_FLAGS', {
...currentFlags,
[flag]: enabled,
});
}
}Offline Mode with Synchronization Strategies
class VaultManager {
private vault: VaultSecret<any>;
private syncInterval?: NodeJS.Timeout;
constructor(secretPath: string, vaultConfig: any) {
this.vault = new VaultSecret(secretPath, {
...vaultConfig,
online: false,
onReady: () => {
this.startPeriodicSync();
},
});
}
// Periodic synchronization
private startPeriodicSync() {
this.syncInterval = setInterval(async () => {
try {
await this.vault.sync();
console.log('Periodic sync completed');
} catch (error) {
console.error('Periodic sync failed:', error);
}
}, 300000); // Sync every 5 minutes
}
// Force sync with conflict resolution
async forceSyncWithBackup(): Promise<void> {
try {
// Create backup of current cache
const currentCache = { ...this.vault.getCacheData() };
// Force sync (ignore version conflicts)
await this.vault.sync(true);
console.log('Force sync completed successfully');
} catch (error) {
console.error('Force sync failed:', error);
// Could implement rollback logic here
throw error;
}
}
// Conditional sync based on last update
async smartSync(maxCacheAge: number = 600000): Promise<boolean> {
const lastSync = this.getLastSyncTime();
const now = Date.now();
if (now - lastSync > maxCacheAge) {
try {
await this.vault.sync();
this.setLastSyncTime(now);
return true;
} catch (error) {
console.error('Smart sync failed:', error);
return false;
}
}
return false; // No sync needed
}
private getLastSyncTime(): number {
// Implementation depends on storage mechanism
return parseInt(localStorage.getItem('vault_last_sync') || '0');
}
private setLastSyncTime(timestamp: number): void {
localStorage.setItem('vault_last_sync', timestamp.toString());
}
destroy(): void {
if (this.syncInterval) {
clearInterval(this.syncInterval);
}
}
}Integration Examples
Express.js Application
import express from 'express';
import { VaultSecret } from '@rytass/secret-adapter-vault';
class AppConfig {
private vault: VaultSecret<any>;
private ready = false;
constructor() {
this.vault = new VaultSecret('apps/express-api/config', {
host: process.env.VAULT_HOST!,
auth: {
account: process.env.VAULT_USERNAME!,
password: process.env.VAULT_PASSWORD!,
},
online: false, // Use cache for better performance
onReady: () => {
this.ready = true;
console.log('Application configuration loaded from Vault');
},
onError: error => {
console.error('Vault configuration error:', error);
process.exit(1);
},
});
}
async waitForReady(): Promise<void> {
return new Promise(resolve => {
if (this.ready) {
resolve();
} else {
this.vault.onReady(() => resolve());
}
});
}
getDatabaseConfig() {
return {
host: this.vault.get<string>('DB_HOST'),
port: this.vault.get<number>('DB_PORT'),
database: this.vault.get<string>('DB_NAME'),
username: this.vault.get<string>('DB_USER'),
password: this.vault.get<string>('DB_PASSWORD'),
};
}
getJwtSecret(): string {
return this.vault.get<string>('JWT_SECRET');
}
getRedisUrl(): string {
return this.vault.get<string>('REDIS_URL');
}
}
// Usage
const config = new AppConfig();
async function startServer() {
// Wait for configuration to load
await config.waitForReady();
const app = express();
// Use configuration
const dbConfig = config.getDatabaseConfig();
const jwtSecret = config.getJwtSecret();
// Setup database connection
// Setup JWT middleware
// ... other setup
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
}
startServer().catch(console.error);NestJS Integration
// Use with @rytass/secret-adapter-vault-nestjs for full NestJS integration
import { Injectable, OnModuleInit } from '@nestjs/common';
import { VaultSecret } from '@rytass/secret-adapter-vault';
@Injectable()
export class ConfigService implements OnModuleInit {
private vault: VaultSecret<any>;
private configLoaded = false;
async onModuleInit() {
this.vault = new VaultSecret('apps/nestjs-api/config', {
host: process.env.VAULT_HOST!,
auth: {
account: process.env.VAULT_USERNAME!,
password: process.env.VAULT_PASSWORD!,
},
online: true, // Real-time for NestJS
onReady: () => {
this.configLoaded = true;
},
});
// Wait for vault to be ready
await new Promise(resolve => {
if (this.configLoaded) {
resolve(undefined);
} else {
this.vault.onReady(() => resolve(undefined));
}
});
}
async getDatabaseUrl(): Promise<string> {
return await this.vault.get<string>('DATABASE_URL');
}
async getSecret(key: string): Promise<any> {
return await this.vault.get(key);
}
async updateSecret(key: string, value: any): Promise<void> {
await this.vault.set(key, value);
}
}Microservices Configuration
class MicroserviceConfig {
private vault: VaultSecret<any>;
private serviceName: string;
constructor(serviceName: string) {
this.serviceName = serviceName;
this.vault = new VaultSecret(`microservices/${serviceName}`, {
host: process.env.VAULT_HOST!,
auth: {
account: `${serviceName}-service`,
password: process.env[`${serviceName.toUpperCase()}_VAULT_PASSWORD`]!,
},
online: false, // Offline for microservice performance
tokenTTL: 1800000, // 30 minutes
onReady: () => {
console.log(`${serviceName} configuration ready`);
this.registerHealthCheck();
},
});
}
private registerHealthCheck() {
// Register with service discovery/health check system
setInterval(() => {
this.healthCheck();
}, 30000);
}
private async healthCheck() {
try {
// Test vault connectivity
await this.vault.get<string>('HEALTH_CHECK');
console.log(`${this.serviceName}: Vault health check passed`);
} catch (error) {
console.error(`${this.serviceName}: Vault health check failed`, error);
// Could trigger alerts or restart procedures
}
}
getServiceConfig() {
return {
port: this.vault.get<number>('PORT') || 3000,
logLevel: this.vault.get<string>('LOG_LEVEL') || 'info',
timeout: this.vault.get<number>('TIMEOUT') || 30000,
retries: this.vault.get<number>('RETRIES') || 3,
};
}
getDatabaseConfig() {
return {
url: this.vault.get<string>('DATABASE_URL'),
maxConnections: this.vault.get<number>('DB_MAX_CONNECTIONS') || 10,
};
}
async rotateSecret(key: string, newValue: any) {
// Store old value for rollback
const oldValue = this.vault.get(key);
try {
this.vault.set(key, newValue);
await this.vault.sync();
console.log(`Secret ${key} rotated successfully`);
} catch (error) {
// Rollback on failure
this.vault.set(key, oldValue);
throw new Error(`Secret rotation failed for ${key}: ${error.message}`);
}
}
}Error Handling
import { VaultSecret } from '@rytass/secret-adapter-vault';
// Comprehensive error handling
const vault = new VaultSecret('apps/myapp/config', {
host: 'https://vault.company.com',
auth: {
account: 'service-account',
password: 'secure-password',
},
online: true,
onReady: () => {
console.log('Vault connection established');
},
onError: error => {
console.error('Vault error occurred:', error);
// Handle different error types
if (error.message.includes('authentication')) {
console.error('Authentication failed - check credentials');
// Could trigger credential rotation
} else if (error.message.includes('network')) {
console.error('Network error - vault unreachable');
// Could trigger fallback to cached values
} else if (error.message.includes('permission')) {
console.error('Permission denied - check vault policies');
}
// Implement alerting/monitoring
sendAlert(`Vault error: ${error.message}`);
},
});
// Graceful error handling in operations
async function safeGetSecret(key: string, defaultValue?: any) {
try {
const value = await vault.get(key);
return value !== undefined ? value : defaultValue;
} catch (error) {
console.error(`Failed to get secret ${key}:`, error);
return defaultValue;
}
}
async function safeSetSecret(key: string, value: any, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await vault.set(key, value);
return true;
} catch (error) {
console.error(`Attempt ${attempt} failed for setting ${key}:`, error);
if (attempt === maxRetries) {
throw new Error(`Failed to set secret ${key} after ${maxRetries} attempts`);
}
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
return false;
}
function sendAlert(message: string) {
// Implement your alerting mechanism
// Could be email, Slack, PagerDuty, etc.
}Security Best Practices
Credential Management
# Environment variables for credentials
export VAULT_HOST="https://vault.company.com"
export VAULT_USERNAME="myapp-service"
export VAULT_PASSWORD="$(cat /run/secrets/vault-password)"
export VAULT_TOKEN_TTL="3600000"Access Policies
# Example Vault policy for application
path "apps/myapp/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "shared/database/*" {
capabilities = ["read"]
}
path "auth/token/renew-self" {
capabilities = ["update"]
}Network Security
// Use HTTPS and validate certificates
const vault = new VaultSecret('apps/myapp/config', {
host: 'https://vault.company.com', // Always use HTTPS
auth: {
account: process.env.VAULT_USERNAME!,
password: process.env.VAULT_PASSWORD!,
},
// Additional security options could be added
});
// Implement network timeouts
const VAULT_TIMEOUT = 30000; // 30 secondsPerformance Optimization
Caching Strategies
class OptimizedVaultManager {
private vault: VaultSecret<any>;
private localCache = new Map<string, { value: any; expiry: number }>();
private cacheTimeout = 300000; // 5 minutes
constructor(secretPath: string, vaultConfig: any) {
this.vault = new VaultSecret(secretPath, {
...vaultConfig,
online: false, // Use offline mode for performance
});
}
async getCached<T>(key: string): Promise<T> {
const cached = this.localCache.get(key);
if (cached && cached.expiry > Date.now()) {
return cached.value;
}
// Cache miss or expired
const value = await this.vault.get<T>(key);
this.localCache.set(key, {
value,
expiry: Date.now() + this.cacheTimeout,
});
return value;
}
clearCache(): void {
this.localCache.clear();
}
// Batch operations for efficiency
async getBatch<T>(keys: string[]): Promise<Record<string, T>> {
const results: Record<string, T> = {};
for (const key of keys) {
results[key] = await this.vault.get<T>(key);
}
return results;
}
}Testing
// Mock Vault for testing
class MockVaultSecret {
private data = new Map<string, any>();
private ready = true;
constructor(
private path: string,
private options: any,
) {
// Populate with test data
this.data.set('TEST_SECRET', 'test-value');
this.data.set('DB_HOST', 'localhost');
this.data.set('DB_PORT', 5432);
}
get<T>(key: string): T {
return this.data.get(key);
}
set<T>(key: string, value: T): void {
this.data.set(key, value);
}
delete(key: string): void {
this.data.delete(key);
}
async sync(): Promise<void> {
// Mock sync operation
}
onReady(callback: () => void): void {
if (this.ready) {
callback();
}
}
}
// Test example
describe('VaultSecret', () => {
let vault: MockVaultSecret;
beforeEach(() => {
vault = new MockVaultSecret('test/secrets', {});
});
it('should retrieve secrets', () => {
const secret = vault.get<string>('TEST_SECRET');
expect(secret).toBe('test-value');
});
it('should store secrets', () => {
vault.set('NEW_SECRET', 'new-value');
expect(vault.get('NEW_SECRET')).toBe('new-value');
});
});License
MIT
