09 Deployment - samerfarida/mcp-ssh-orchestrator GitHub Wiki

9. Deployment

Purpose: Complete guide for deploying mcp-ssh-orchestrator in production environments with security best practices.

Overview

mcp-ssh-orchestrator is designed for containerized deployment with security-first principles. This guide covers production deployment, scaling, and maintenance.

Deployment Methods

Docker (Recommended)

Single Container Deployment:

# Pull the image
docker pull ghcr.io/samerfarida/mcp-ssh-orchestrator:latest

# Run with configuration
docker run -i --rm \
  -v ~/mcp-ssh/config:/app/config:ro \
  -v ~/mcp-ssh/keys:/app/keys:ro \
  -v ~/mcp-ssh/secrets:/app/secrets:ro \
  ghcr.io/samerfarida/mcp-ssh-orchestrator:latest

Production Configuration:

# Create secure directories
mkdir -p ~/mcp-ssh/{config,keys,secrets}
chmod 0700 ~/mcp-ssh/secrets
chmod 0400 ~/mcp-ssh/keys/*

# Run with resource limits
docker run -i --rm \
  --memory=512m \
  --cpus=1 \
  --user=10001:10001 \
  -v ~/mcp-ssh/config:/app/config:ro \
  -v ~/mcp-ssh/keys:/app/keys:ro \
  -v ~/mcp-ssh/secrets:/app/secrets:ro \
  ghcr.io/samerfarida/mcp-ssh-orchestrator:latest

Image & Provenance Verification

Before promoting a new build, validate the Sigstore signature attached to the GHCR image:

COSIGN_EXPERIMENTAL=1 cosign verify \
  --certificate-identity-regexp "https://github.com/samerfarida/mcp-ssh-orchestrator/.github/workflows/release.yml@.*" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/samerfarida/mcp-ssh-orchestrator:latest
  • Record the digest printed by cosign verify (or from the package feed) and pin it in your manifests.
  • For air-gapped promotion, download the signature bundle produced by cosign and store it alongside the image tarball.

Docker Compose

Production Compose:

version: '3.8'

services:
  mcp-ssh:
    image: ghcr.io/samerfarida/mcp-ssh-orchestrator:latest
    user: "10001:10001"
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1'
    volumes:
      - ./config:/app/config:ro
      - ./keys:/app/keys:ro
      - ./secrets:/app/secrets:ro
    healthcheck:
      test: ["CMD", "python", "-c", "import mcp_ssh"]
      interval: 30s
      timeout: 5s
      retries: 3
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Development Compose:

version: '3.8'

services:
  mcp-ssh:
    build: .
    volumes:
      - ./config:/app/config:ro
      - ./keys:/app/keys:ro
      - ./secrets:/app/secrets:ro
    environment:
      - MCP_SSH_DEBUG=1
    healthcheck:
      test: ["CMD", "python", "-c", "import mcp_ssh"]
      interval: 30s
      timeout: 5s
      retries: 3

Production Setup

1. Configuration Setup

Create configuration directory:

mkdir -p /opt/mcp-ssh/{config,keys,secrets}
chown -R 10001:10001 /opt/mcp-ssh
chmod 0700 /opt/mcp-ssh/secrets
chmod 0400 /opt/mcp-ssh/keys/*

Copy example configurations:

cp examples/example-servers.yml /opt/mcp-ssh/config/servers.yml
cp examples/example-credentials.yml /opt/mcp-ssh/config/credentials.yml
cp examples/example-policy.yml /opt/mcp-ssh/config/policy.yml

2. SSH Key Management

Generate production keys:

# Generate Ed25519 key pair
ssh-keygen -t ed25519 -f /opt/mcp-ssh/keys/mcp_prod -C "mcp-ssh-orchestrator-prod"

# Set permissions
chmod 0400 /opt/mcp-ssh/keys/mcp_prod
chmod 0444 /opt/mcp-ssh/keys/mcp_prod.pub

# Deploy public key to target hosts
ssh-copy-id -i /opt/mcp-ssh/keys/mcp_prod.pub [email protected]

Update credentials.yml:

entries:
  - name: "prod_admin"
    username: "ubuntu"
    key_path: "mcp_prod"
    key_passphrase_secret: "prod_key_passphrase"
    password_secret: ""

3. Policy Configuration

Production policy.yml:

known_hosts_path: "/app/keys/known_hosts"

limits:
  max_seconds: 30
  max_output_bytes: 131072
  host_key_auto_add: false
  require_known_host: true
  deny_substrings:
    - "rm -rf /"
    - "shutdown*"
    - "reboot*"
    - "systemctl restart*"
    - "systemctl stop*"
    - "systemctl start*"

network:
  allow_cidrs:
    - "10.0.0.0/8"
  block_cidrs:
    - "0.0.0.0/0"
  require_known_host: true

rules:
  - action: "allow"
    aliases: ["prod-*"]
    tags: ["production"]
    commands:
      - "uptime*"
      - "df -h*"
      - "systemctl status *"
      - "journalctl --no-pager -n 20 *"

overrides:
  aliases:
    prod-db-1:
      max_seconds: 15
      max_output_bytes: 65536

4. Secrets Management

Docker Secrets (Recommended):

# Create secrets
echo "production-passphrase" | docker secret create mcp_prod_passphrase -
echo "admin-password" | docker secret create mcp_admin_password -

# Use in Docker Compose
services:
  mcp-ssh:
    image: ghcr.io/samerfarida/mcp-ssh-orchestrator:latest
    secrets:
      - mcp_prod_passphrase
      - mcp_admin_password
    volumes:
      - ./config:/app/config:ro
      - ./keys:/app/keys:ro

secrets:
  mcp_prod_passphrase:
    external: true
  mcp_admin_password:
    external: true

Environment Variables:

# Set environment variables
export MCP_SSH_SECRET_PROD_KEY_PASSPHRASE="production-passphrase"
export MCP_SSH_SECRET_ADMIN_PASSWORD="admin-password"

# Use in Docker run
docker run -i --rm \
  -e MCP_SSH_SECRET_PROD_KEY_PASSPHRASE \
  -e MCP_SSH_SECRET_ADMIN_PASSWORD \
  -v ~/mcp-ssh/config:/app/config:ro \
  -v ~/mcp-ssh/keys:/app/keys:ro \
  ghcr.io/samerfarida/mcp-ssh-orchestrator:latest

.env File (Consolidated Secrets):

The .env file provides a convenient way to manage all secrets in a single file. This is especially useful for development and smaller deployments.

# Create .env file
cat > ~/mcp-ssh/secrets/.env << EOF
# SSH Key Passphrases
prod_key_passphrase=production-passphrase
staging_key_passphrase=staging-passphrase

# SSH Passwords
admin_password=admin-password
lab_password=lab-password
EOF

# Set secure permissions (critical!)
chmod 600 ~/mcp-ssh/secrets/.env

# Verify permissions
ls -l ~/mcp-ssh/secrets/.env
# Should show: -rw------- (600)

# The .env file is automatically read from /app/secrets/.env in the container
# No additional configuration needed - just mount the secrets directory
docker run -i --rm \
  -v ~/mcp-ssh/config:/app/config:ro \
  -v ~/mcp-ssh/keys:/app/keys:ro \
  -v ~/mcp-ssh/secrets:/app/secrets:ro \
  ghcr.io/samerfarida/mcp-ssh-orchestrator:latest

Security Notes for .env File:

  • File Permissions: Always set to 600 (owner read/write only)

    chmod 600 secrets/.env
    
  • Directory Permissions: Ensure secrets directory is 700 or more restrictive

    chmod 700 secrets/
    
  • Never Commit: The .env file is automatically ignored by Git (via .gitignore)

  • Backup Security: If backing up .env files, encrypt them and store securely

  • Container Mount: Mount as read-only (:ro) to prevent accidental modification

Mixed Approach:

You can use a combination of methods:

  • Use .env file for most secrets (easier management)
  • Use individual files for secrets that need to be managed separately
  • Use environment variables for secrets injected by orchestration systems

The resolution order ensures environment variables take precedence, followed by .env file, then individual files.

Resource Management

Container Optimization

MCP SSH Orchestrator is designed to run efficiently as a single container instance per MCP client connection.

Resource Limits:

# docker-compose.yml
services:
  mcp-ssh:
    image: ghcr.io/samerfarida/mcp-ssh-orchestrator:latest
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1'

Current Architecture:

  • One container instance per MCP client (Claude Desktop, Cursor, etc.)
  • Stateless design allows horizontal scaling at the client level
  • Each MCP client connects to its own orchestrator instance
  • No load balancer required for single-client scenarios

Scaling Considerations:

  • Horizontal fan-out requires external orchestration (load balancers, shared audit storage) and is not bundled with this project.
  • Recommended pattern: keep one orchestrator container per MCP client session for deterministic policy enforcement.

Health Monitoring

Health Check Script:

#!/bin/bash
# health-check.sh
set -e

CONTAINER=${1:-mcp-ssh}

echo "== Container health =="
docker ps --filter "name=${CONTAINER}" --filter "health=healthy"

echo "== Recent logs =="
docker logs --tail=20 "${CONTAINER}"

echo "== Config summary =="
docker exec -i "${CONTAINER}" python - <<'PY'
from mcp_ssh.config import Config
cfg = Config()
print("Hosts configured:", len(cfg.list_hosts()))
policy = cfg.get_policy() or {}
print("Policy rules:", len(policy.get("rules", [])))
print("Deny substrings:", len((policy.get("limits") or {}).get("deny_substrings", [])))
PY

Structured Logging:

  • All policy/audit events are emitted as JSON on stderr. Collect them with docker logs or ship them to your logging stack (Loki, Elastic, CloudWatch, etc.).
  • For scraping-friendly output, pipe stderr into jq or a log forwarder:
docker logs -f mcp-ssh | jq '.'

Security Hardening

Container Security

Non-root execution:

# Container runs as UID 10001
RUN useradd -u 10001 -m appuser
USER appuser

Resource limits:

deploy:
  resources:
    limits:
      memory: 512M
      cpus: '1'
    reservations:
      memory: 256M
      cpus: '0.5'

Read-only filesystem:

docker run -i --rm \
  --read-only \
  --tmpfs /tmp \
  -v ~/mcp-ssh/config:/app/config:ro \
  -v ~/mcp-ssh/keys:/app/keys:ro \
  ghcr.io/samerfarida/mcp-ssh-orchestrator:latest

Network Security

Network isolation:

services:
  mcp-ssh:
    image: ghcr.io/samerfarida/mcp-ssh-orchestrator:latest
    networks:
      - mcp-network
    volumes:
      - ./config:/app/config:ro
      - ./keys:/app/keys:ro

networks:
  mcp-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16

Firewall rules:

# Allow only necessary ports
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw deny 2376/tcp  # Docker daemon
ufw deny 2377/tcp  # Docker swarm

Maintenance and Updates

Configuration Updates

Hot reload:

# Update configuration files
vim /opt/mcp-ssh/config/policy.yml

# Reload without restart
docker exec mcp-ssh-1 python -c "
from mcp_ssh.mcp_server import reload_config
reload_config()
"

Rolling updates:

# Update to new version
docker pull ghcr.io/samerfarida/mcp-ssh-orchestrator:latest

# Rolling update
docker service update --image ghcr.io/samerfarida/mcp-ssh-orchestrator:latest mcp-ssh

Backup and Recovery

Configuration backup:

#!/bin/bash
# backup-config.sh

BACKUP_DIR="/opt/backups/mcp-ssh"
DATE=$(date +%Y%m%d_%H%M%S)

# Create backup
tar -czf "$BACKUP_DIR/config_$DATE.tar.gz" \
  -C /opt/mcp-ssh config/ keys/

# Keep only last 7 days
find "$BACKUP_DIR" -name "config_*.tar.gz" -mtime +7 -delete

Disaster recovery:

#!/bin/bash
# restore-config.sh

BACKUP_FILE="$1"
RESTORE_DIR="/opt/mcp-ssh"

# Stop services
docker-compose down

# Restore configuration
tar -xzf "$BACKUP_FILE" -C "$RESTORE_DIR"

# Restart services
docker-compose up -d

Troubleshooting

Common Issues

Container won't start:

# Check logs
docker logs mcp-ssh-1

# Check configuration
docker exec mcp-ssh-1 python -c "
from mcp_ssh.config import Config
config = Config('/app/config')
print('Config valid:', config.validate())
"

SSH connection failures:

# Test SSH connectivity
ssh -i /opt/mcp-ssh/keys/mcp_prod [email protected]

# Check host key verification
docker exec mcp-ssh-1 ssh-keyscan 10.0.0.11

Policy issues:

# Test policy rules
docker exec mcp-ssh-1 python -c "
from mcp_ssh.policy import Policy
policy = Policy('/app/config/policy.yml')
result = policy.evaluate('prod-web-1', 'uptime', ['production'])
print('Policy result:', result)
"

Next Steps