Skip to main content
Glama
vault.ts15.9 kB
import nodeVault from 'node-vault'; import { config } from '../config/config'; import { logger } from '../utils/logger'; import crypto from 'crypto'; export interface VaultSecret { [key: string]: any; } export interface VaultHealth { initialized: boolean; sealed: boolean; standby: boolean; performanceStandby: boolean; replicationPerformanceMode: string; replicationDrMode: string; serverTimeUtc: number; version: string; } export interface EncryptionResult { ciphertext: string; keyVersion: number; } export interface DecryptionResult { plaintext: string; } class VaultService { private client: any; private initialized: boolean = false; private transitKeyName: string = 'mcp-encryption-key'; /** * Initialize Vault client */ public async initialize(): Promise<void> { try { this.client = nodeVault({ apiVersion: 'v1', endpoint: config.vault.url, token: config.vault.token, namespace: config.vault.namespace, requestOptions: { timeout: config.vault.timeout, }, }); // Test connection await this.health(); // Initialize transit engine if not exists await this.initializeTransitEngine(); // Create encryption key if not exists await this.createEncryptionKey(); this.initialized = true; logger.info('Vault client initialized successfully', { endpoint: config.vault.url, namespace: config.vault.namespace, kvMount: config.vault.kvMount, }); } catch (error) { logger.error('Failed to initialize Vault client', { error }); throw new Error(`Vault initialization failed: ${error.message}`); } } /** * Check Vault health status */ public async health(): Promise<VaultHealth> { try { const response = await this.client.health(); return response; } catch (error) { logger.error('Vault health check failed', { error }); throw new Error(`Vault health check failed: ${error.message}`); } } /** * Read secret from KV store */ public async read(path: string): Promise<VaultSecret | null> { try { this.ensureInitialized(); const fullPath = `${config.vault.kvMount}/data/${path}`; const response = await this.client.read(fullPath); if (!response || !response.data || !response.data.data) { return null; } logger.debug('Secret read from Vault', { path }); return response.data.data; } catch (error) { if (error.response?.status === 404) { logger.debug('Secret not found in Vault', { path }); return null; } logger.error('Failed to read secret from Vault', { error, path }); throw new Error(`Failed to read secret: ${error.message}`); } } /** * Write secret to KV store */ public async write(path: string, secret: VaultSecret): Promise<void> { try { this.ensureInitialized(); const fullPath = `${config.vault.kvMount}/data/${path}`; await this.client.write(fullPath, { data: secret, }); logger.info('Secret written to Vault', { path }); } catch (error) { logger.error('Failed to write secret to Vault', { error, path }); throw new Error(`Failed to write secret: ${error.message}`); } } /** * Delete secret from KV store */ public async delete(path: string): Promise<void> { try { this.ensureInitialized(); const fullPath = `${config.vault.kvMount}/data/${path}`; await this.client.delete(fullPath); logger.info('Secret deleted from Vault', { path }); } catch (error) { logger.error('Failed to delete secret from Vault', { error, path }); throw new Error(`Failed to delete secret: ${error.message}`); } } /** * List secrets at path */ public async list(path: string): Promise<string[]> { try { this.ensureInitialized(); const fullPath = `${config.vault.kvMount}/metadata/${path}`; const response = await this.client.list(fullPath); if (!response || !response.data || !response.data.keys) { return []; } return response.data.keys; } catch (error) { if (error.response?.status === 404) { return []; } logger.error('Failed to list secrets from Vault', { error, path }); throw new Error(`Failed to list secrets: ${error.message}`); } } /** * Encrypt data using Vault's transit engine */ public async encrypt(plaintext: string, context?: string): Promise<EncryptionResult> { try { this.ensureInitialized(); const requestData: any = { plaintext: Buffer.from(plaintext).toString('base64'), }; if (context) { requestData.context = Buffer.from(context).toString('base64'); } const response = await this.client.write( `transit/encrypt/${this.transitKeyName}`, requestData ); return { ciphertext: response.data.ciphertext, keyVersion: response.data.key_version, }; } catch (error) { logger.error('Failed to encrypt data with Vault', { error }); throw new Error(`Encryption failed: ${error.message}`); } } /** * Decrypt data using Vault's transit engine */ public async decrypt(ciphertext: string, context?: string): Promise<DecryptionResult> { try { this.ensureInitialized(); const requestData: any = { ciphertext, }; if (context) { requestData.context = Buffer.from(context).toString('base64'); } const response = await this.client.write( `transit/decrypt/${this.transitKeyName}`, requestData ); return { plaintext: Buffer.from(response.data.plaintext, 'base64').toString(), }; } catch (error) { logger.error('Failed to decrypt data with Vault', { error }); throw new Error(`Decryption failed: ${error.message}`); } } /** * Generate random data using Vault */ public async generateRandomData(bytes: number = 32): Promise<string> { try { this.ensureInitialized(); const response = await this.client.write('sys/tools/random', { bytes, format: 'base64', }); return response.data.random_bytes; } catch (error) { logger.error('Failed to generate random data from Vault', { error }); throw new Error(`Random data generation failed: ${error.message}`); } } /** * Generate cryptographic hash using Vault */ public async hash(input: string, algorithm: string = 'sha2-256'): Promise<string> { try { this.ensureInitialized(); const response = await this.client.write('sys/tools/hash', { input: Buffer.from(input).toString('base64'), algorithm, }); return response.data.sum; } catch (error) { logger.error('Failed to hash data with Vault', { error }); throw new Error(`Hashing failed: ${error.message}`); } } /** * Create database credentials dynamically */ public async createDatabaseCredentials(role: string, ttl: string = '1h'): Promise<{ username: string; password: string; lease_id: string; lease_duration: number; }> { try { this.ensureInitialized(); const response = await this.client.write(`database/creds/${role}`, { ttl, }); return { username: response.data.username, password: response.data.password, lease_id: response.lease_id, lease_duration: response.lease_duration, }; } catch (error) { logger.error('Failed to create database credentials', { error, role }); throw new Error(`Database credential creation failed: ${error.message}`); } } /** * Revoke database credentials */ public async revokeDatabaseCredentials(leaseId: string): Promise<void> { try { this.ensureInitialized(); await this.client.write('sys/leases/revoke', { lease_id: leaseId, }); logger.info('Database credentials revoked', { leaseId }); } catch (error) { logger.error('Failed to revoke database credentials', { error, leaseId }); throw new Error(`Credential revocation failed: ${error.message}`); } } /** * Renew database credentials lease */ public async renewDatabaseCredentials(leaseId: string, increment?: string): Promise<{ lease_id: string; lease_duration: number; renewable: boolean; }> { try { this.ensureInitialized(); const requestData: any = { lease_id: leaseId }; if (increment) { requestData.increment = increment; } const response = await this.client.write('sys/leases/renew', requestData); return { lease_id: response.lease_id, lease_duration: response.lease_duration, renewable: response.renewable, }; } catch (error) { logger.error('Failed to renew database credentials', { error, leaseId }); throw new Error(`Credential renewal failed: ${error.message}`); } } /** * Create PKI certificate */ public async createCertificate(commonName: string, role: string, options?: { altNames?: string[]; ipSans?: string[]; ttl?: string; format?: 'pem' | 'der' | 'pem_bundle'; }): Promise<{ certificate: string; issuing_ca: string; ca_chain: string[]; private_key: string; private_key_type: string; serial_number: string; }> { try { this.ensureInitialized(); const requestData: any = { common_name: commonName, ...options, }; if (options?.altNames) { requestData.alt_names = options.altNames.join(','); } if (options?.ipSans) { requestData.ip_sans = options.ipSans.join(','); } const response = await this.client.write(`pki/issue/${role}`, requestData); logger.info('Certificate created', { commonName, role }); return response.data; } catch (error) { logger.error('Failed to create certificate', { error, commonName, role }); throw new Error(`Certificate creation failed: ${error.message}`); } } /** * Revoke PKI certificate */ public async revokeCertificate(serialNumber: string): Promise<void> { try { this.ensureInitialized(); await this.client.write('pki/revoke', { serial_number: serialNumber, }); logger.info('Certificate revoked', { serialNumber }); } catch (error) { logger.error('Failed to revoke certificate', { error, serialNumber }); throw new Error(`Certificate revocation failed: ${error.message}`); } } /** * Get Vault token information */ public async getTokenInfo(): Promise<any> { try { this.ensureInitialized(); const response = await this.client.read('auth/token/lookup-self'); return response.data; } catch (error) { logger.error('Failed to get token info', { error }); throw new Error(`Token info retrieval failed: ${error.message}`); } } /** * Renew Vault token */ public async renewToken(increment?: string): Promise<void> { try { this.ensureInitialized(); const requestData: any = {}; if (increment) { requestData.increment = increment; } await this.client.write('auth/token/renew-self', requestData); logger.info('Vault token renewed', { increment }); } catch (error) { logger.error('Failed to renew token', { error }); throw new Error(`Token renewal failed: ${error.message}`); } } /** * Initialize transit encryption engine */ private async initializeTransitEngine(): Promise<void> { try { // Check if transit engine is already enabled const mounts = await this.client.read('sys/mounts'); if (!mounts.data['transit/']) { // Enable transit engine await this.client.write('sys/mounts/transit', { type: 'transit', description: 'Transit encryption engine for MCP server', }); logger.info('Transit encryption engine enabled'); } } catch (error) { // If we can't enable transit engine, log but don't fail // It might already be enabled or we might not have permissions logger.warn('Could not initialize transit engine', { error: error.message }); } } /** * Create encryption key for transit engine */ private async createEncryptionKey(): Promise<void> { try { // Try to read the key first try { await this.client.read(`transit/keys/${this.transitKeyName}`); return; // Key already exists } catch (error) { if (error.response?.status !== 404) { throw error; } } // Create the encryption key await this.client.write(`transit/keys/${this.transitKeyName}`, { type: 'aes256-gcm96', deletion_allowed: false, exportable: false, }); logger.info('Transit encryption key created', { keyName: this.transitKeyName }); } catch (error) { logger.warn('Could not create encryption key', { error: error.message }); } } /** * Ensure Vault client is initialized */ private ensureInitialized(): void { if (!this.initialized) { throw new Error('Vault client not initialized'); } } /** * Get Vault status */ public async getStatus(): Promise<{ initialized: boolean; sealed: boolean; clusterName?: string; clusterId?: string; version: string; }> { try { const health = await this.health(); const status = await this.client.read('sys/seal-status'); return { initialized: health.initialized, sealed: health.sealed, clusterName: status.data.cluster_name, clusterId: status.data.cluster_id, version: health.version, }; } catch (error) { logger.error('Failed to get Vault status', { error }); throw error; } } /** * Setup automatic token renewal */ public async setupTokenRenewal(): Promise<void> { try { const tokenInfo = await this.getTokenInfo(); const renewable = tokenInfo.renewable; const ttl = tokenInfo.ttl; if (renewable && ttl > 0) { // Renew token when it's 2/3 expired const renewInterval = (ttl * 2/3) * 1000; setInterval(async () => { try { await this.renewToken(); logger.info('Vault token auto-renewed'); } catch (error) { logger.error('Failed to auto-renew Vault token', { error }); } }, renewInterval); logger.info('Automatic token renewal setup', { renewInterval: renewInterval / 1000 }); } } catch (error) { logger.warn('Could not setup token renewal', { error: error.message }); } } /** * Check if Vault is properly configured */ public isConfigured(): boolean { return !!config.vault.url && !!config.vault.token; } /** * Check if Vault client is initialized */ public isInitialized(): boolean { return this.initialized; } } // Create singleton instance export const vault = new VaultService(); // Initialize Vault connection export const initializeVault = async (): Promise<void> => { if (!vault.isConfigured()) { logger.warn('Vault not configured, skipping initialization'); return; } try { await vault.initialize(); await vault.setupTokenRenewal(); logger.info('Vault initialization completed'); } catch (error) { logger.error('Vault initialization failed', { error }); throw error; } }; // Graceful shutdown process.on('SIGTERM', () => { logger.info('Received SIGTERM, Vault client cleanup complete'); }); process.on('SIGINT', () => { logger.info('Received SIGINT, Vault client cleanup complete'); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/perfecxion-ai/secure-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server