Skip to main content
Glama
ssh-key-manager.jsโ€ข10 kB
import { execSync, spawn } from 'child_process'; import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; import { logger } from './logger.js'; // Path to known_hosts file const KNOWN_HOSTS_PATH = path.join(process.env.HOME, '.ssh', 'known_hosts'); const KNOWN_HOSTS_BACKUP = path.join(process.env.HOME, '.ssh', 'known_hosts.mcp-backup'); /** * Parse a known_hosts entry */ function parseKnownHostEntry(line) { if (!line || line.startsWith('#')) return null; const parts = line.split(' '); if (parts.length < 3) return null; return { host: parts[0], keyType: parts[1], key: parts[2], comment: parts.slice(3).join(' ') || '' }; } /** * Get the SSH host key fingerprint for a server */ export async function getHostKeyFingerprint(host, port = 22) { return new Promise((resolve, reject) => { const cmd = spawn('ssh-keyscan', ['-p', port.toString(), '-t', 'ed25519,rsa,ecdsa', host]); let stdout = ''; let stderr = ''; cmd.stdout.on('data', (data) => { stdout += data.toString(); }); cmd.stderr.on('data', (data) => { stderr += data.toString(); }); cmd.on('close', (code) => { if (code !== 0) { reject(new Error(`Failed to get host key: ${stderr}`)); return; } const lines = stdout.trim().split('\n').filter(l => l && !l.startsWith('#')); const fingerprints = []; for (const line of lines) { const entry = parseKnownHostEntry(line); if (entry) { // Calculate SHA256 fingerprint const keyData = Buffer.from(entry.key, 'base64'); const hash = crypto.createHash('sha256').update(keyData).digest('base64'); fingerprints.push({ host: entry.host, type: entry.keyType, fingerprint: `SHA256:${hash}`, fullKey: line }); } } resolve(fingerprints); }); }); } /** * Check if a host key exists in known_hosts */ export function isHostKnown(host, port = 22) { if (!fs.existsSync(KNOWN_HOSTS_PATH)) { return false; } const content = fs.readFileSync(KNOWN_HOSTS_PATH, 'utf8'); const lines = content.split('\n'); // Format host entry as SSH does const hostEntry = port === 22 ? host : `[${host}]:${port}`; for (const line of lines) { if (line.includes(hostEntry)) { return true; } } return false; } /** * Get current host key from known_hosts */ export function getCurrentHostKey(host, port = 22) { if (!fs.existsSync(KNOWN_HOSTS_PATH)) { return null; } const content = fs.readFileSync(KNOWN_HOSTS_PATH, 'utf8'); const lines = content.split('\n'); // Format host entry as SSH does const hostEntry = port === 22 ? host : `[${host}]:${port}`; const keys = []; for (const line of lines) { if (line.includes(hostEntry)) { const entry = parseKnownHostEntry(line); if (entry) { const keyData = Buffer.from(entry.key, 'base64'); const hash = crypto.createHash('sha256').update(keyData).digest('base64'); keys.push({ host: entry.host, type: entry.keyType, fingerprint: `SHA256:${hash}`, fullKey: line }); } } } return keys.length > 0 ? keys : null; } /** * Remove a host from known_hosts */ export function removeHostKey(host, port = 22) { try { const hostEntry = port === 22 ? host : `[${host}]:${port}`; // Use ssh-keygen to remove the host execSync(`ssh-keygen -R "${hostEntry}"`, { stdio: 'ignore' }); logger.info('Host key removed', { host, port }); return true; } catch (error) { logger.error('Failed to remove host key', { host, port, error: error.message }); throw new Error(`Failed to remove host key: ${error.message}`); } } /** * Add a host key to known_hosts */ export async function addHostKey(host, port = 22, keyData = null) { try { // Backup current known_hosts if (fs.existsSync(KNOWN_HOSTS_PATH)) { fs.copyFileSync(KNOWN_HOSTS_PATH, KNOWN_HOSTS_BACKUP); } // If no key data provided, fetch it if (!keyData) { const fingerprints = await getHostKeyFingerprint(host, port); if (fingerprints.length === 0) { throw new Error('No host keys found'); } keyData = fingerprints.map(fp => fp.fullKey).join('\n'); } // Ensure .ssh directory exists const sshDir = path.dirname(KNOWN_HOSTS_PATH); if (!fs.existsSync(sshDir)) { fs.mkdirSync(sshDir, { mode: 0o700, recursive: true }); } // Append to known_hosts fs.appendFileSync(KNOWN_HOSTS_PATH, keyData + '\n'); logger.info('Host key added', { host, port }); return true; } catch (error) { logger.error('Failed to add host key', { host, port, error: error.message }); throw new Error(`Failed to add host key: ${error.message}`); } } /** * Update a host key (remove old, add new) */ export async function updateHostKey(host, port = 22) { try { // Remove old key removeHostKey(host, port); // Add new key await addHostKey(host, port); logger.info('Host key updated', { host, port }); return true; } catch (error) { logger.error('Failed to update host key', { host, port, error: error.message }); throw new Error(`Failed to update host key: ${error.message}`); } } /** * Verify if host key has changed */ export async function hasHostKeyChanged(host, port = 22) { try { const currentKeys = getCurrentHostKey(host, port); if (!currentKeys || currentKeys.length === 0) { // No key in known_hosts return { changed: false, reason: 'not_in_known_hosts' }; } const newKeys = await getHostKeyFingerprint(host, port); if (!newKeys || newKeys.length === 0) { return { changed: false, reason: 'cannot_fetch_key' }; } // Check if any current key matches any new key for (const currentKey of currentKeys) { for (const newKey of newKeys) { if (currentKey.fingerprint === newKey.fingerprint) { return { changed: false, reason: 'key_matches' }; } } } // Keys don't match return { changed: true, reason: 'key_mismatch', currentFingerprints: currentKeys.map(k => k.fingerprint), newFingerprints: newKeys.map(k => k.fingerprint) }; } catch (error) { logger.error('Failed to verify host key', { host, port, error: error.message }); return { changed: false, reason: 'verification_error', error: error.message }; } } /** * List all known hosts */ export function listKnownHosts() { if (!fs.existsSync(KNOWN_HOSTS_PATH)) { return []; } const content = fs.readFileSync(KNOWN_HOSTS_PATH, 'utf8'); const lines = content.split('\n'); const hosts = new Map(); for (const line of lines) { if (line && !line.startsWith('#')) { const entry = parseKnownHostEntry(line); if (entry) { // Extract host and port let host = entry.host; let port = 22; if (host.startsWith('[')) { const match = host.match(/\[([^\]]+)\]:(\d+)/); if (match) { host = match[1]; port = parseInt(match[2]); } } const keyData = Buffer.from(entry.key, 'base64'); const hash = crypto.createHash('sha256').update(keyData).digest('base64'); const hostKey = `${host}:${port}`; if (!hosts.has(hostKey)) { hosts.set(hostKey, { host, port, keys: [] }); } hosts.get(hostKey).keys.push({ type: entry.keyType, fingerprint: `SHA256:${hash}` }); } } } return Array.from(hosts.values()); } /** * Detect SSH key error in command output */ export function detectSSHKeyError(stderr) { const keyErrorPatterns = [ 'WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED', 'Host key verification failed', 'The authenticity of host', 'ECDSA host key for .* has changed', 'RSA host key for .* has changed', 'ED25519 host key for .* has changed', 'Offending key in', 'Add correct host key in' ]; for (const pattern of keyErrorPatterns) { if (stderr.includes(pattern)) { return true; } } return false; } /** * Extract host info from SSH error */ export function extractHostFromSSHError(stderr) { // Try to extract host and port from error message const patterns = [ /Offending (?:RSA|ECDSA|ED25519) key in .+:(\d+)/i, /Host key for \[([^\]]+)\]:(\d+) has changed/i, /Host key for ([^\s]+) has changed/i, /The authenticity of host '\[([^\]]+)\]:(\d+)'/i, /The authenticity of host '([^\s]+) \(/i ]; for (const pattern of patterns) { const match = stderr.match(pattern); if (match) { if (match[2]) { // Host and port return { host: match[1], port: parseInt(match[2]) }; } else { // Just host return { host: match[1], port: 22 }; } } } return null; } /** * Handle SSH key error automatically */ export async function handleSSHKeyError(stderr, options = {}) { const { autoAccept = false, interactive = true } = options; const hostInfo = extractHostFromSSHError(stderr); if (!hostInfo) { throw new Error('Could not extract host information from SSH error'); } logger.warn('SSH host key verification failed', hostInfo); if (autoAccept) { // Automatically update the key await updateHostKey(hostInfo.host, hostInfo.port); return { action: 'updated', ...hostInfo }; } if (!interactive) { throw new Error(`Host key verification failed for ${hostInfo.host}:${hostInfo.port}. Use ssh_key_manage tool to update the key.`); } // In interactive mode, we would prompt the user // For now, just return the error info return { action: 'prompt_required', ...hostInfo, message: `Host key has changed for ${hostInfo.host}:${hostInfo.port}. Use ssh_key_manage tool to verify and update the key.` }; }

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/bvisible/mcp-ssh-manager'

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