cnctd-service-ssh 0.1.8

SSH command execution service - library and MCP server
Documentation
# 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
docker ps | grep ssh-service
```

#### 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.