# cnctd-service-ssh
Secure SSH command execution MCP service for the cnctd ecosystem.
## Overview
This service provides SSH connectivity capabilities through the Model Context Protocol (MCP). It allows AI agents to securely execute commands on remote servers via SSH with **mandatory host key verification**, **whitelisted SSH keys**, and **public key authentication only**.
## Security Features
- ✅ **Mandatory Host Key Verification**: All SSH connections verify host keys - no bypass possible
- ✅ **Whitelisted SSH Keys**: Only pre-approved SSH keys can be used for authentication
- ✅ **Public Key Authentication Only**: Password authentication is completely disabled
- ✅ **No Target Overwrites**: Existing targets cannot be silently replaced - must explicitly unregister first
- ✅ **Comprehensive Logging**: All operations are logged with timestamps and details
- ✅ **Timeout Protection**: Commands have a 120-second timeout to prevent hanging connections
## Features
- **Secure Authentication**: Public key authentication with optional passphrase support
- **Mandatory Host Key Verification**: OpenSSH known_hosts verification always enforced
- **Target Management**: Register multiple SSH targets with unique identifiers
- **Command Execution**: Execute shell commands remotely with timeout support
- **Comprehensive Logging**: Detailed tracing for debugging and monitoring
## Setup Guide
### Complete SSH Keys Setup
Follow these steps to set up SSH access from the service to a target server:
#### 1. Create a dedicated SSH keypair (on the server running the SSH service)
```bash
# SSH into your MCP gateway/service host
ssh user@your-mcp-gateway.com
# Create a dedicated keypair for the SSH service
ssh-keygen -t ed25519 -f ~/.ssh/cnctd_ssh_service -C "cnctd-ssh-service"
# Press Enter for no passphrase (or add one for extra security)
```
#### 2. Copy the public key to your target servers
```bash
# View the public key
cat ~/.ssh/cnctd_ssh_service.pub
# Copy the output, then SSH to your target server
ssh user@target-server.com
# Add the public key to authorized_keys
echo "PASTE_PUBLIC_KEY_HERE" >> ~/.ssh/authorized_keys
# Ensure proper permissions
chmod 600 ~/.ssh/authorized_keys
```
Alternatively, use `ssh-copy-id`:
```bash
ssh-copy-id -i ~/.ssh/cnctd_ssh_service.pub user@target-server.com
```
#### 3. Add target servers to known_hosts
```bash
# Back on your MCP gateway, add the target server's host key
ssh-keyscan -H target-server.com >> ~/.ssh/known_hosts
# Or use the IP address
ssh-keyscan -H 192.168.1.100 >> ~/.ssh/known_hosts
# Verify the host was added
grep target-server.com ~/.ssh/known_hosts
```
#### 4. Mount SSH keys into the Docker container
Update your `docker-compose.yml`:
```yaml
ssh-service:
image: kebtech/ssh-service:latest
restart: unless-stopped
volumes:
- ~/.ssh:/root/.ssh:ro # Mount SSH directory read-only
networks:
- mcp-network
environment:
- RUST_LOG=info
```
#### 5. Restart the service
```bash
docker-compose up -d ssh-service
# Verify it's running
#### 6. Test the connection
Manually test SSH access first:
```bash
ssh -i ~/.ssh/cnctd_ssh_service user@target-server.com hostname
```
If that works, you're ready to use the MCP service!
### Quick Reference: Adding New Servers
For each new server you want to connect to:
```bash
# 1. Add public key to target server
ssh-copy-id -i ~/.ssh/cnctd_ssh_service.pub user@new-server.com
# 2. Add host key to known_hosts
ssh-keyscan -H new-server.com >> ~/.ssh/known_hosts
# 3. Test connection
ssh -i ~/.ssh/cnctd_ssh_service user@new-server.com hostname
# 4. Register via MCP (no restart needed)
# Use ssh_register_target tool with:
# - id: "new-server"
# - host: "new-server.com"
# - user: "user"
# - key_path: "/root/.ssh/cnctd_ssh_service"
```
## MCP Tools
### `ssh_register_target`
Register an SSH target configuration for later use.
**Parameters:**
- `id` (string, required): Unique identifier for this target
- `host` (string, required): Hostname or IP address
- `user` (string, required): SSH username
- `port` (number, optional): SSH port (default: 22)
- `key_path` (string, required): Path to whitelisted private key file
- `key_passphrase` (string, optional): Passphrase for encrypted private keys
- `known_hosts_path` (string, optional): Path to known_hosts file (default: `~/.ssh/known_hosts`)
**Whitelisted Key Paths:**
- `/root/.ssh/cnctd_ssh_service` (recommended dedicated key)
- `/root/.ssh/id_ed25519`
- `/root/.ssh/id_rsa`
**Example:**
```json
{
"id": "my-server",
"host": "example.com",
"user": "ubuntu",
"key_path": "/root/.ssh/cnctd_ssh_service"
}
```
**Security Notes:**
- Host key verification is **always enforced**. The target host must exist in your known_hosts file before registration.
- Target IDs cannot be overwritten - you must unregister a target before re-registering with the same ID.
- Only whitelisted key paths are accepted for additional security.
### `ssh_exec`
Execute a command on a registered target.
**Parameters:**
- `id` (string, required): Target ID previously registered
- `command` (string, required): Shell command to execute
- `timeout_secs` (number, optional): Execution timeout in seconds (default: 120)
**Returns:**
- `exec_id`: Unique execution identifier
- `exit_code`: Command exit code (0 = success)
- `stdout`: Standard output
- `stderr`: Standard error
- `duration_ms`: Execution duration in milliseconds
**Example:**
```json
{
"id": "my-server",
"command": "docker ps --format '{{.Names}}'"
}
```
### `ssh_unregister_target`
Remove a registered SSH target.
**Parameters:**
- `id` (string, required): Target ID to unregister
**Returns:**
- `id`: The target ID that was unregistered
- `existed`: Boolean indicating whether the target existed
## Common Use Cases
### Server Monitoring
```bash
# Check disk space
command: "df -h"
# View recent logs
command: "docker-compose logs --tail=50"
# Check running containers
command: "docker ps"
# System uptime and load
command: "uptime"
```
### Service Management
```bash
# Restart a service
command: "docker-compose restart service-name"
# View service status
command: "systemctl status nginx"
# Check resource usage (snapshot)
command: "docker stats --no-stream"
```
### File Operations
```bash
# Read a config file
command: "cat /etc/nginx/nginx.conf"
# List directory contents
command: "ls -la /var/www"
# Check file permissions
command: "stat /path/to/file"
```
**Note:** Streaming commands (`tail -f`, `docker logs -f`) won't work due to the request-response nature of MCP. Use snapshot commands instead.
## Troubleshooting
### "key_path not in whitelist"
- Only use one of the whitelisted key paths:
- `/root/.ssh/cnctd_ssh_service`
- `/root/.ssh/id_ed25519`
- `/root/.ssh/id_rsa`
### "target already exists"
- A target with that ID is already registered
- Unregister it first with `ssh_unregister_target` before re-registering
### "Host key verification failed"
- Ensure you've added the host to known_hosts: `ssh-keyscan -H hostname >> ~/.ssh/known_hosts`
- The ~/.ssh directory must be mounted into the container
- Restart the container after updating known_hosts
### "Permission denied (publickey)"
- Verify the public key is in the target's `~/.ssh/authorized_keys`
- Check key permissions: `chmod 600 ~/.ssh/cnctd_ssh_service`
- Test manually: `ssh -i ~/.ssh/cnctd_ssh_service user@host`
### "Host not found in known_hosts file"
- Add the host: `ssh-keyscan -H hostname >> ~/.ssh/known_hosts`
- Restart the container to pick up the updated known_hosts file
### "Cannot read key_path"
- Ensure the SSH directory is mounted: `-v ~/.ssh:/root/.ssh:ro`
- Check the key exists: `docker exec container-name ls -la /root/.ssh`
## Security Architecture
### Defense in Depth
1. **Whitelist-Only Keys**: Only pre-approved SSH keys from `/root/.ssh/` can be used. Attackers cannot use arbitrary keys even if they gain MCP access.
2. **Mandatory Host Verification**: Every connection verifies the target server's host key against known_hosts. MITM attacks are prevented.
3. **No Password Auth**: Password authentication is completely removed from the codebase. Credential stuffing and brute force attacks on passwords are impossible.
4. **No Target Hijacking**: Existing target configurations cannot be silently replaced. An attacker must explicitly unregister before re-registering with different settings.
5. **Read-Only Mounts**: SSH keys are mounted read-only in the container, preventing modification even if the container is compromised.
### Attack Surface Analysis
**What attackers CANNOT do** (even with MCP access):
- ❌ Use password authentication
- ❌ Use arbitrary SSH keys outside the whitelist
- ❌ Bypass host key verification
- ❌ Silently replace existing target configurations
- ❌ Connect to hosts not in known_hosts
**What attackers CAN do** (with MCP access):
- ⚠️ Register new targets to servers in known_hosts using whitelisted keys
- ⚠️ Execute commands on registered targets
- ⚠️ Unregister targets
**Mitigation**: Treat MCP access as privileged. Only allow trusted AI agents or users to access the SSH MCP service.
## Security Considerations
1. **Host Key Verification**: Host key verification is **always enforced** and cannot be disabled. This protects against man-in-the-middle attacks.
2. **Dedicated Keys**: Use a separate keypair (`cnctd_ssh_service`) for the SSH service rather than personal keys. This makes key rotation and access control easier.
3. **Key File Permissions**: SSH private keys should have restrictive permissions (600). The Docker container should mount them read-only.
4. **No Password Authentication**: Password auth has been completely removed from the code for security.
5. **Timeout Protection**: Commands have a default 120-second timeout to prevent hanging connections.
6. **Least Privilege**: Only add the service's public key to servers it needs to access. Use different keys for different environments if needed.
7. **Whitelist Maintenance**: Review and update the key whitelist in `src/operations.rs` if you need to add or remove allowed keys.
## Development
### Building
```bash
cargo build --release
```
### Running Locally
```bash
RUST_LOG=info cargo run
```
### Docker
```bash
docker build -t cnctd-service-ssh .
docker run -i -v ~/.ssh:/root/.ssh:ro cnctd-service-ssh
```
## Environment Variables
- `RUST_LOG`: Log level (default: `info`, options: `trace`, `debug`, `info`, `warn`, `error`)
## License
See repository root for license information.