@marian-craciunescu/ssh-mcp-server-secured
v1.0.15
Published
Secured SSH MCP server with command whitelist/blacklist filtering for safe remote server management
Downloads
324
Maintainers
Readme
SSH MCP Server (Secured)
A secured fork of zibdie/SSH-MCP-Server with command whitelist/blacklist filtering, network device support, and bulk connection management for safe remote server management via MCP (Model Context Protocol).
Key Features
- Command Whitelist/Blacklist: Control which commands can be executed
- Dangerous Pattern Detection: Blocks fork bombs, command injection, and destructive patterns
- Network Device Support: Cisco, Juniper, MikroTik with persistent shell sessions and enable mode
- Jump Shell Support: SSH into a host then enter a nested CLI (telnet to a host, FreeSWITCH fs_cli, etc.) — commands execute inside the nested shell
- Bulk Connection Management: Load dozens of connections from CSV/JSON files
- Environment Variable Credentials: Passwords auto-resolved from env vars by connectionId — no secrets in chat
- Multi-Connection Execution: Run commands across all or selected connections simultaneously
- Connection Health Monitoring: Keepalive tracking, dead connection detection, auto-cleanup
- Configurable Security Policies: Via config file or environment variables
- Audit Logging: Log all blocked command attempts
Installation
Quick Setup (Recommended)
# Add to Claude CLI
claude mcp add ssh-mcp-secured npx '@marian-craciunescu/ssh-mcp-server-secured@latest'Manual Installation
npm install -g @marian-craciunescu/ssh-mcp-server-secured{
"mcpServers": {
"ssh-mcp-secured": {
"command": "ssh-mcp-server-secured"
}
}
}Usage
1. Single Connection
Connect to a host using ssh_connect. You only need to provide host, username, and connectionId — the password is automatically resolved from environment variables:
Connect to host 172.168.0.2 with user admin connectionId=router1The LLM calls ssh_connect with:
{
"host": "172.168.0.2",
"username": "admin",
"deviceType": "cisco",
"connectionId": "router1"
}No password in the tool call. The server automatically looks up ROUTER1_PASSWORD from environment variables.
Credential Resolution Convention
The connectionId is converted to an env var prefix: uppercased, non-alphanumeric characters replaced with _.
| connectionId | Env var for password | Env var for enable password |
|---|---|---|
| router1 | ROUTER1_PASSWORD | ROUTER1_ENABLE_PASSWORD |
| my-connection | MY_CONNECTION_PASSWORD | MY_CONNECTION_ENABLE_PASSWORD |
| dc1.switch.3 | DC1_SWITCH_3_PASSWORD | DC1_SWITCH_3_ENABLE_PASSWORD |
Optionally, <PREFIX>_USERNAME is also resolved if username is not provided.
Set credentials in your MCP configuration:
{
"mcpServers": {
"ssh-mcp-secured": {
"command": "ssh-mcp-server-secured",
"env": {
"SSH_FILTER_MODE": "blacklist",
"ROUTER1_PASSWORD": "admin123",
"ROUTER1_ENABLE_PASSWORD": "enable123",
"SERVER1_PASSWORD": "rootpass",
"SERVER1_USERNAME": "root"
}
}
}
}Credentials live in the MCP config (or are injected via CI/CD, vault, etc.) and never appear in chat or tool calls. If a password is explicitly provided in the tool call, it takes precedence over the env var.
SSH Options for Legacy Devices
When connecting to older devices that require non-default algorithms (the equivalent of ssh -o), use the sshOptions parameter:
In natural language:
Connect to 10.0.0.1 port 2222 as user, connectionId old-switch, with KexAlgorithms +diffie-hellman-group-exchange-sha1 and HostKeyAlgorithms +ssh-rsa
{
"host": "10.0.0.1",
"port": 2222,
"username": "admin",
"connectionId": "old-switch",
"sshOptions": {
"KexAlgorithms": "+diffie-hellman-group-exchange-sha1",
"HostKeyAlgorithms": "+ssh-rsa"
}
}This is equivalent to:
ssh -p 2222 [email protected] -o KexAlgorithms=+diffie-hellman-group-exchange-sha1 -o HostKeyAlgorithms=+ssh-rsaPrefix a value with + to append to ssh2 defaults. Without +, the value replaces the defaults entirely.
| Option | SSH2 equivalent | Use case |
|--------|-----------------|----------|
| KexAlgorithms | algorithms.kex | Legacy key exchange (e.g. diffie-hellman-group1-sha1) |
| HostKeyAlgorithms | algorithms.serverHostKey | Legacy host keys (e.g. ssh-rsa, ssh-dss) |
| Ciphers | algorithms.cipher | Legacy ciphers (e.g. aes128-cbc) |
| MACs | algorithms.hmac | Legacy MACs (e.g. hmac-sha1) |
sshOptions is supported on ssh_connect, ssh_connect_with_jump_command, and JSON files loaded via ssh_load_connections.
Keyboard-interactive auth is enabled automatically (tryKeyboard: true). Legacy devices that reject standard password auth and require keyboard-interactive will work without any extra configuration.
2. Bulk Connections from File
Load multiple connections from a CSV or JSON file using ssh_load_connections. Passwords are resolved from env vars using the same connectionId convention:
CSV format (connections.csv):
host,username,port,deviceType,connectionId
172.168.0.2,admin,22,cisco,router1
10.1.2.15,noc,22,cisco,router2
192.168.1.1,root,22,linux,server1No passwords in the file. The server resolves ROUTER1_PASSWORD, ROUTER2_PASSWORD, SERVER1_PASSWORD from env vars.
NOTE: CSV can't carry objects so SSH options for legacy devices must be set via individual env vars or in JSON file.
JSON format (connections.json):
[
{
"host": "172.168.0.2",
"username": "admin",
"deviceType": "cisco",
"connectionId": "router1"
},
{
"host": "10.1.2.15",
"username": "noc",
"deviceType": "cisco",
"connectionId": "router2",
"sshOptions": {
"KexAlgorithms": "+diffie-hellman-group-exchange-sha1",
"HostKeyAlgorithms": "+ssh-rsa"
}
}
]Profiles: Define reusable connection profiles for connecting to the same type of device with similar settings (e.g. all Cisco switches). Profiles can include default SSH options for legacy devices, so you don't have to repeat them in every connection.
Resolution priority: explicit args > profile env vars > connectionId env vars
export PROFILE_CISCO_USER=admin
export PROFILE_CISCO_PASSWORD=secret123
export PROFILE_CISCO_DEVICE_TYPE=cisco
export PROFILE_CISCO_PORT=2222
export PROFILE_CISCO_SSH_OPTIONS='{"KexAlgorithms":"+diffie-hellman-group-exchange-sha1","HostKeyAlgorithms":"+ssh-rsa"}'BELOW is an example of how profile env vars are resolved when loading connections from CSV/JSON. The PROFILE_CISCO_SSH_OPTIONS value is parsed as JSON and applied to all connections with deviceType of cisco.
| Env Var Example | Field |Value | | ----------------------------- | --------------------------- |-----------------------| | PROFILE_CISCO_USER | username | admin | | PROFILE_CISCO_PASSWORD | password | secret123| | PROFILE_CISCO_DEVICE_TYPE | deviceType | cisco| | PROFILE_CISCO_SSH_OPTIONS | sshOptions (parsed as JSON) | {"KexAlgorithms":"+diffie-hellman-group-exchange-sha1","HostKeyAlgorithms":"+ssh-rsa"}| | PROFILE_CISCO_JUMP_COMMAND | jumpCommand | telnet lh | | PROFILE_CISCO_PRESET | preset | topex | | PROFILE_CISCO_PORT | port | 2222 |
ssh_connect host=10.0.0.1 profile=CISCO connectionId=SWITCH1"
Usage:
Load connections from /path/to/connections.csv and connect to allNote: You can still provide passwords directly in CSV/JSON if preferred — env var resolution only kicks in when the password field is missing or empty.
3. Network Device Types
The server supports different device types with appropriate connection handling:
| Device Type | Behavior | Use Case |
|-------------|----------|----------|
| linux | Standard SSH exec mode (default) | Linux/Unix servers |
| cisco | Persistent shell, enable mode support | Cisco IOS/IOS-XE routers and switches |
| juniper | Persistent shell | Juniper JunOS devices |
| mikrotik | Persistent shell | MikroTik RouterOS |
| network | Generic persistent shell | Other network devices |
| jump_shell | Persistent shell + nested CLI | Used internally by ssh_connect_with_jump_command |
Network devices use PTY-allocated persistent shell sessions instead of standard exec() because many network operating systems close the SSH channel after each exec command.
4. Cisco Enable Mode
Enter privileged EXEC mode on Cisco devices using ssh_cisco_enable:
{
"connectionId": "router1"
}The tool handles the interactive enable password prompt automatically — it sends enable, waits for Password:, sends the stored enablePassword, and verifies the prompt changed to #.
5. Execute on Multiple Connections
Run a command on specific connections using ssh_execute_on_multiple:
{
"command": "show version",
"connectionIds": ["router1", "router2", "switch1"]
}Or run on ALL connections:
{
"command": "show ip interface brief",
"connectionIds": ["*"]
}6. Jump Shell (Nested CLI via SSH)
Use ssh_connect_with_jump_command when you need to SSH into a host and then enter a nested interactive shell before executing commands. This covers scenarios like:
- Telnet to a Topex VoIP gateway from an SSH jump host
- FreeSWITCH
fs_clion a remote server - Any CLI that requires an interactive session after SSH
How it works:
SSH → open shell → send jump command (e.g. "telnet lh") → wait for nested prompt (e.g. "topexsw>") → readyAll subsequent ssh_execute commands on that connectionId run inside the nested shell.
Topex gateway example (with preset):
{
"host": "10.0.0.1",
"username": "admin",
"connectionId": "topex1",
"preset": "topex",
"jumpCommand": "telnet lh"
}The topex preset auto-fills jumpPromptPattern: "topexsw>\\s*$" and jumpExitCommand: "quit". You only need to supply jumpCommand.
Then execute commands inside the Topex CLI:
{
"command": "view portsoncard *",
"connectionId": "topex1"
}FreeSWITCH example (preset fills everything):
{
"host": "10.0.0.5",
"username": "root",
"connectionId": "fs1",
"preset": "freeswitch"
}The freeswitch preset auto-fills jumpCommand: "fs_cli", jumpPromptPattern: "freeswitch@...>", and jumpExitCommand: "/exit". Then:
{
"command": "sofia status",
"connectionId": "fs1"
}Fully custom (no preset):
{
"host": "10.0.0.1",
"username": "admin",
"connectionId": "custom1",
"jumpCommand": "telnet 192.168.1.100",
"jumpPromptPattern": ">\\s*$",
"jumpExitCommand": "quit",
"jumpReadyTimeout": 8000
}Built-in presets:
| Preset | jumpCommand | Prompt pattern | Exit command |
|--------|-------------|----------------|--------------|
| freeswitch | fs_cli | freeswitch@...> | /exit |
| topex | (user provides) | topexsw> | quit |
Presets can be overridden — any explicitly provided parameter takes precedence.
Shell recovery: If the shell drops, ssh_execute automatically reopens the shell and re-enters the jump shell.
Disconnect: ssh_disconnect gracefully sends the exit command to the nested CLI before closing the SSH connection.
7. Logging
Set log level via environment variable:
| Variable | Values | Default |
|----------|--------|---------|
| SSH_LOG_LEVEL | DEBUG, INFO, WARN, ERROR | INFO |
| SSH_LOG_FILE | Path to log file | (none) |
Log format:
[2026-01-22T20:26:02.044Z] [INFO ] ✓ SSH connection established to 172.168.0.2:22
[2026-01-22T20:26:02.046Z] [DEBUG] ♥ Keepalive #1 sent to 172.168.0.2 | {"uptime":"10s"}
[2026-01-22T20:26:12.047Z] [WARN ] ⚠ CONNECTION CLOSED BY REMOTE HOST: router1Configuration
Environment Variables
| Variable | Values | Default | Description |
|----------|--------|---------|-------------|
| SSH_FILTER_MODE | whitelist, blacklist, disabled | blacklist | Command filtering mode |
| SSH_ALLOW_SUDO | true, false | true | Allow sudo commands |
| SSH_LOG_BLOCKED | true, false | true | Log blocked commands to stderr |
| SSH_MCP_CONFIG | file path | - | Path to config JSON file |
| SSH_WHITELIST | comma-separated or JSON | - | Override whitelist commands |
| SSH_BLACKLIST | comma-separated or JSON | - | Override blacklist commands |
| SSH_DANGEROUS_PATTERNS | JSON array | - | Override dangerous regex patterns |
| SSH_LOG_LEVEL | DEBUG, INFO, WARN, ERROR | INFO | Log verbosity |
| SSH_LOG_FILE | path | - | Log to file |
| SSH_HOST_FILTER_MODE| whitelist, blacklist, disabled | disabled | Host filtering mode |
| SSH_HOST_WHITELIST | comma-separated IPs | - | Whitelist of allowed host IPs |
| SSH_HOST_BLACKLIST | comma-separated IPs | - | Blacklist of allowed host IPs |
| SSH_IDLE_TIMEOUT | seconds | 120 | Idle connection timeout |
|SSH_FAILED_CONNECTIONS_LOG|file path | ./ssh-failed-connections.json |/var/log/ssh-failed.jsonl |
Any additional environment variables following the <CONNECTIONID>_PASSWORD convention are automatically used for credential resolution (see Credential Resolution Convention).
MCP Configuration Examples
Host whitelist/blacklist:
Blacklist mode with custom blocked commands:
{
"ssh_mcp": {
"command": "ssh-mcp-server-secured",
"args": [],
"env": {
"SSH_FILTER_MODE": "blacklist",
"SSH_ALLOW_SUDO": "true",
"SSH_LOG_BLOCKED": "true",
"SSH_BLACKLIST": "rm,rmdir,mkfs,fdisk,shutdown,reboot,halt,poweroff,passwd,useradd,userdel,iptables,crontab,conf t,configure terminal"
}
}
}Whitelist mode (strict — only allow specific commands):
{
"ssh_mcp": {
"command": "ssh-mcp-server-secured",
"args": [],
"env": {
"SSH_FILTER_MODE": "whitelist",
"SSH_ALLOW_SUDO": "false",
"SSH_LOG_BLOCKED": "true",
"SSH_WHITELIST": "ls,cat,grep,tail,head,df,du,free,uptime,ps,systemctl,journalctl,docker,kubectl,ping,curl,dig,ss,netstat,show,display"
}
}
}Network operations with credential env vars:
{
"ssh_mcp": {
"command": "ssh-mcp-server-secured",
"args": [],
"env": {
"SSH_FILTER_MODE": "blacklist",
"SSH_ALLOW_SUDO": "true",
"SSH_LOG_LEVEL": "DEBUG",
"SSH_BLACKLIST": "conf t,configure terminal,rm,shutdown,reboot",
"ROUTER1_PASSWORD": "admin123",
"ROUTER1_ENABLE_PASSWORD": "enable123",
"ROUTER2_PASSWORD": "pass123",
"SERVER1_PASSWORD": "pass1234"
}
}
}Now in chat you simply say connect to 172.168.0.2 as admin connectionId=router1 — no passwords exposed.
Via npx (no global install):
{
"ssh_mcp": {
"command": "npx",
"args": ["@marian-craciunescu/ssh-mcp-server-secured"],
"env": {
"SSH_FILTER_MODE": "blacklist",
"SSH_ALLOW_SUDO": "true"
}
}
}Config File
Create config.json or ssh-mcp-config.json:
{
"commandFilter": {
"mode": "whitelist",
"allowSudo": false,
"logBlocked": true,
"whitelist": [
"ls", "cat", "grep", "df", "ps", "systemctl", "docker", "show", "ping"
],
"blacklist": [
"rm", "shutdown", "reboot", "passwd", "conf t", "configure terminal"
],
"dangerousPatterns": [
";\\s*rm\\s+-rf",
"curl.*\\|\\s*bash"
]
}
}Filter Modes
Blacklist Mode (Default)
Commands in the blacklist are blocked. Everything else is allowed. Supports multi-word entries like configure terminal and conf t.
✓ ls -la
✓ docker ps
✓ show ip interface brief
✗ rm -rf /tmp/files → Blocked: 'rm' is in blacklist
✗ configure terminal → Blocked: 'configure terminal' is in blacklist
✗ shutdown now → Blocked: 'shutdown' is in blacklistWhitelist Mode
Only commands in the whitelist are allowed. Everything else is blocked.
✓ ls -la → Allowed: 'ls' is whitelisted
✓ show version → Allowed: 'show' is whitelisted
✗ vim /etc/hosts → Blocked: 'vim' not in whitelist
✗ make install → Blocked: 'make' not in whitelistDisabled Mode
No command filtering (use with caution).
Command Validation Order
- Check if filtering disabled
- Check sudo permission
- Check dangerous patterns (regex)
- Check full command against blacklist (multi-word support)
- Extract base commands from pipes/chains
- Check each base command against blacklist/whitelist
Dangerous Patterns
These patterns are always blocked regardless of filter mode:
| Pattern | Example | Risk |
|---------|---------|------|
| Fork bomb | :(){ :\|:& };: | System crash |
| Piped rm | find . \| rm | Data loss |
| Chained rm | ls && rm -rf / | Data loss |
| Device redirect | > /dev/sda | Disk corruption |
| System config overwrite | > /etc/passwd | System compromise |
| Remote code execution | curl \| bash | Arbitrary code execution |
| Recursive chmod 777 | chmod -R 777 / | Security compromise |
Available Tools
| Tool | Description |
|------|-------------|
| ssh_connect | Connect to a single host (password auto-resolved from <CONNECTIONID>_PASSWORD env var) , supports sshOptions for legacy algorithm negotiation) |
| ssh_connect_with_jump_command | SSH into a host, then enter a nested CLI (telnet, fs_cli, etc.) via a jump command. Supports presets. |
| ssh_load_connections | Load connections from CSV/JSON file (credentials resolved from env vars per connectionId) |
| ssh_execute | Execute a command on one connection |
| ssh_cisco_enable | Enter Cisco privileged EXEC mode (interactive enable password handling) |
| ssh_execute_on_multiple | Execute a command on selected connections (["*"] = all) |
| ssh_disconnect | Disconnect one connection |
| ssh_disconnect_all | Disconnect all connections |
| ssh_list_connections | List active connections with status |
| ssh_check_connections | Health check all connections (dead socket detection, shell status) |
| ssh_upload_file | Upload file via SFTP |
| ssh_download_file | Download file via SFTP |
| ssh_list_files | List remote directory via SFTP |
Example Workflow
1. Load connections from CSV (passwords auto-resolved from env vars)
→ ssh_load_connections { filePath: "devices.csv", connectAll: true }
(ROUTER1_PASSWORD, ROUTER2_PASSWORD resolved automatically)
2. Enter enable mode on Cisco routers
→ ssh_cisco_enable { connectionId: "router1" }
→ ssh_cisco_enable { connectionId: "router2" }
3. Execute show commands on all devices
→ ssh_execute_on_multiple {
command: "show ip interface brief",
connectionIds: ["*"]
}
4. Execute privileged command on specific router
→ ssh_execute {
command: "show running-config | include hostname",
connectionId: "router1"
}
5. Check connection health
→ ssh_check_connections {}
6. Connect to a Topex gateway via jump shell
→ ssh_connect_with_jump_command {
host: "10.0.0.1",
username: "admin",
connectionId: "topex1",
preset: "topex",
jumpCommand: "telnet lh"
}
7. Execute command inside the Topex CLI
→ ssh_execute {
command: "view portsoncard *",
connectionId: "topex1"
}
8. Disconnect all
→ ssh_disconnect_all {}Architecture Notes
Shell Buffer Management
Buffer is cleared before each command. Stability detection uses buffer unchanged for 3 × 500ms = command complete. Password prompts are detected in the last 200 chars of the buffer.
Keepalive System
SSH2 sends keepalives every 10 seconds (keepaliveInterval: 10000). After 3 failed keepalives, the connection auto-closes (keepaliveCountMax: 3). A custom interval logs keepalive count for debugging.
Connection Health Monitoring
The server detects dead connections (socket destroyed), tracks shell status for network devices, auto-cleans dead connections, and attempts shell reopen on network devices if the shell has closed.
Jump Shell
When ssh_connect_with_jump_command is called, the server: (1) opens an SSH connection, (2) opens a PTY shell, (3) sends the jump command (e.g. telnet lh), (4) polls the shell buffer every 300ms for the expected prompt regex, (5) marks the connection as jump_shell with jumpShellActive: true. On disconnect, the nested CLI exit command is sent before closing the SSH session. On shell recovery, the jump command is automatically re-sent.
Environment Variable Credential Resolution
When a connection is created (via ssh_connect or ssh_load_connections), if the password is not provided, the server automatically looks up <PREFIX>_PASSWORD from environment variables, where <PREFIX> is the connectionId uppercased with non-alphanumeric characters replaced by _. The same convention applies to _ENABLE_PASSWORD and _USERNAME. Explicitly provided values always take precedence.
Comparison with Original
| Feature | zibdie/SSH-MCP-Server | This Fork |
|---------|----------------------|-----------|
| Basic SSH/SFTP | ✓ | ✓ |
| Command whitelist | ✗ | ✓ |
| Command blacklist | ✗ | ✓ |
| Multi-word blacklist entries | ✗ | ✓ |
| Dangerous pattern detection | ✗ | ✓ |
| Audit logging | ✗ | ✓ |
| Command validation tool | ✗ | ✓ |
| Config file support | ✗ | ✓ |
| Network device types (Cisco, Juniper, MikroTik) | ✗ | ✓ |
| Cisco enable mode | ✗ | ✓ |
| Jump shell (nested CLI via SSH) | ✗ | ✓ |
| Bulk connections from CSV/JSON | ✗ | ✓ |
| Multi-connection execution | ✗ | ✓ |
| Environment variable credentials | ✗ | ✓ |
| Connection health monitoring | ✗ | ✓ |
| Keepalive tracking | ✗ | ✓ |
| host/hostname compatibility | ✗ | ✓ |
Development
# Clone
git clone https://github.com/marian-craciunescu/ssh-mcp-server-secured.git
cd ssh-mcp-server-secured
# Install dependencies
npm install
# Run in development mode
npm run dev
# Test with MCP Inspector
npx @modelcontextprotocol/inspector node index.jsSecurity Considerations
- Default is blacklist mode — provides protection while remaining flexible
- Dangerous patterns are always checked — even in disabled mode
- Audit logging enabled by default — track blocked attempts
- Sudo can be restricted — set
SSH_ALLOW_SUDO=falsefor high-security environments - Credential isolation — passwords are resolved from env vars by connectionId, never typed in chat or visible in tool calls
License
MIT — see LICENSE file
Credits
- Original: zibdie/SSH-MCP-Server by Nour Zibdie
- Security fork: marian-craciunescu
