Skip to main content
Glama

MCP SSH Orchestrator

by samerfarida
SECURITY.md25.4 kB
# Security Guide This document provides security guidance for deploying and operating mcp-ssh-orchestrator in production environments. ## Threat Model ### Assets - **SSH Private Keys**: Used for authentication to target hosts - **Credentials**: Passwords, passphrases, and other secrets - **Target Hosts**: SSH servers under management - **Command History**: Audit logs of executed commands - **Configuration**: Policy rules and host inventory ### Threats 1. **Unauthorized Command Execution**: Attacker gains ability to run arbitrary commands 2. **Credential Theft**: SSH keys or passwords are exposed 3. **Lateral Movement**: Attacker pivots from compromised host to others 4. **Data Exfiltration**: Sensitive output is leaked 5. **Denial of Service**: Resource exhaustion or service disruption 6. **Policy Bypass**: Circumventing allow/deny rules ### Mitigations All threats are addressed through defense-in-depth controls documented below. ## Authentication & Credentials ### Path Traversal Protection **Security Enhancement:** All credential and key path resolution includes path traversal protection to prevent unauthorized file access. #### Secret Path Protection Secret names and paths are validated to prevent directory traversal attacks: 1. **Secret Name Validation**: Only alphanumeric characters, dashes, and underscores are allowed ```yaml # ✅ Valid secret names password_secret: "prod_password" password_secret: "key-passphrase-2024" password_secret: "admin_password_1" # ❌ Invalid (will be rejected) password_secret: "../etc/passwd" # Path traversal password_secret: "/absolute/path" # Absolute path password_secret: "secret.name" # Special characters ``` 2. **Path Normalization**: All paths are normalized and validated to stay within `/app/secrets` - Relative paths are resolved relative to secrets directory - Absolute paths are rejected for secrets - Paths containing `../` or `..\\` are blocked 3. **Security Event Logging**: Path traversal attempts are logged for monitoring: ```json { "level": "error", "kind": "security_event", "type": "path_traversal_attempt", "secret_name": "../etc/passwd", "reason": "path_outside_allowed_directory" } ``` #### SSH Key Path Protection SSH key paths include similar protections: 1. **Traversal Pattern Detection**: Paths containing `..` patterns are rejected ```yaml # ✅ Valid key paths key_path: "id_ed25519" key_path: "prod_key" key_path: "/app/keys/id_ed25519" # Absolute within keys_dir # ❌ Invalid (will be rejected) key_path: "../outside_key" # Path traversal key_path: "/etc/passwd" # Outside keys_dir key_path: "key/../../etc/passwd" # Encoded traversal ``` 2. **Absolute Path Validation**: Absolute paths must be within the configured `keys_dir` - Paths outside `/app/keys` (or custom keys_dir) are rejected - Prevents accessing keys from other locations 3. **Security Event Logging**: All path traversal attempts are logged with full context **Effect**: Prevents reading files outside intended directories, blocking common path traversal attack vectors. #### File Type Validation All resolved paths are validated to ensure they are regular files: 1. **Directory Rejection**: Paths pointing to directories are rejected ```yaml # ❌ Invalid (will be rejected) password_secret: "subdirectory" # Points to a directory, not a file ``` 2. **Symlink Rejection**: Symbolic links are rejected for security ```yaml # ❌ Invalid (will be rejected) password_secret: "symlink_secret" # Points to a symlink, not a regular file ``` **Why reject symlinks?** Symlinks can be manipulated to point outside the allowed directory or to sensitive files, creating security risks. 3. **Regular File Requirement**: Only regular files within the allowed directory are accepted - Non-existent files are allowed for SSH keys (validated when used) - Secrets must exist as regular files (immediate read required) - All paths must stay within their designated directories 4. **Security Event Logging**: File validation failures are logged: ```json { "level": "error", "kind": "security_event", "type": "file_validation_failed", "file_path": "/app/secrets/subdirectory", "reason": "path_is_directory" } ``` **Effect**: Prevents accessing directories or symlinks that could lead to security vulnerabilities or unauthorized access. #### YAML File Size Limits All YAML configuration files are validated for size before loading to prevent resource exhaustion attacks: 1. **Size Limit**: Maximum file size of 10MB per YAML file - Applies to: `servers.yml`, `credentials.yml`, `policy.yml` - Prevents resource exhaustion via oversized configuration files - Files exceeding limit are rejected with security event logging 2. **Size Validation**: File size is checked using `os.path.getsize()` before parsing - Prevents loading files into memory if they exceed the limit - Returns empty dictionary on size limit violation - No YAML parsing performed if file is too large 3. **Security Event Logging**: Size limit violations are logged: ```json { "level": "error", "kind": "security_event", "type": "file_size_limit_exceeded", "path": "/app/config/servers.yml", "file_size": 10485761, "max_size": 10485760, "reason": "yaml_file_too_large" } ``` 4. **Normal Operation**: Files at or below the 10MB limit load normally - 10MB is sufficient for typical configuration files - Large configuration files (multiple thousands of hosts) are supported - Prevents abuse while allowing legitimate use cases **Effect**: Prevents resource exhaustion attacks via oversized YAML files that could consume excessive memory or processing time. #### Input Validation for User-Controlled Parameters All user-controlled parameters are validated before processing to prevent injection attacks and resource exhaustion: 1. **Alias Validation**: - Length limit: 100 characters - Allowed characters: alphanumeric, dash (`-`), underscore (`_`), dot (`.`) - Rejects empty values - Applied to: `ssh_describe_host`, `ssh_plan`, `ssh_run`, `ssh_run_async` 2. **Command Validation**: - Length limit: 10,000 characters - Rejects null bytes (`\x00`) - common injection vector - Rejects control characters (except newline `\n`, tab `\t`, carriage return `\r`) - Allows legitimate multi-line commands - Applied to: `ssh_plan`, `ssh_run`, `ssh_run_on_tag`, `ssh_run_async` 3. **Tag Validation**: - Length limit: 50 characters - Allowed characters: alphanumeric, dash (`-`), underscore (`_`), dot (`.`) - Rejects empty values - Applied to: `ssh_run_on_tag` 4. **Task ID Validation**: - Length limit: 200 characters - Allowed characters: alphanumeric, colon (`:`), dash (`-`), underscore (`_`) - Format validation: expected pattern `alias:hash:timestamp` - Applied to: `ssh_cancel`, `ssh_get_task_status`, `ssh_get_task_result`, `ssh_get_task_output`, `ssh_cancel_async_task` 5. **Security Event Logging**: Invalid input attempts are logged: ```json { "level": "error", "msg": "security_event", "type": "null_byte_injection_attempt", "field": "command" } ``` **Effect**: Prevents injection attacks (null bytes, control characters) and resource exhaustion (length limits) via malformed user inputs. #### Input Length Limits for Configuration Parameters String parameters in configuration files have length limits to prevent resource exhaustion: 1. **Secret Names** (`credentials.yml`): - Maximum length: 100 characters - Validated in `_resolve_secret()` function - Rejects names exceeding limit with security event logging 2. **SSH Key Paths** (`credentials.yml`): - Maximum length: 500 characters - Validated in `_resolve_key_path()` function - Rejects paths exceeding limit with security event logging 3. **MCP Tool Parameters** (from PR6): - **Alias**: Maximum 100 characters - **Command**: Maximum 10,000 characters - **Tag**: Maximum 50 characters - **Task ID**: Maximum 200 characters 4. **Length Validation Order**: Length validation occurs before other validations (character validation, path traversal checks) to prevent processing of oversized inputs. 5. **Security Event Logging**: Length limit violations are logged: ```json { "level": "error", "kind": "security_event", "type": "input_length_limit_exceeded", "field": "secret_name", "length": 150, "max_length": 100, "reason": "secret_name_too_long" } ``` **Effect**: Prevents resource exhaustion attacks via oversized string inputs in configuration files and user-controlled parameters. #### DNS Rate Limiting DNS resolution is rate-limited and cached to prevent DNS-based DoS attacks: 1. **Rate Limiting**: - Maximum 10 DNS resolutions per second per hostname - Per-hostname rate limiting (different hostnames have separate limits) - Time-window based (sliding 1-second window) - Exceeding limit returns empty list (no IPs resolved) 2. **Result Caching**: - DNS results cached for 60 seconds (TTL) - Cached results returned immediately without DNS lookup - Reduces load on DNS servers - Caches both successful and failed resolutions (prevents repeated lookups for invalid hostnames) 3. **Timeout Protection**: - DNS resolution timeout: 5 seconds - Prevents hanging on slow or unresponsive DNS servers - Failed resolutions return empty list 4. **Rate Limit Logging**: Rate limit violations are logged: #### Command Denial Bypass Prevention Command denial logic has been enhanced to prevent bypass attempts via obfuscation: 1. **Command Normalization**: - Removes single and double quotes from commands - Removes escaped characters (e.g., `\ ` becomes space) - Normalizes whitespace (collapses multiple spaces/tabs to single space) - Applied before checking against `deny_substrings` list 2. **Dual Checking**: - Checks original command string (maintains existing behavior) - Checks normalized command string (catches obfuscated bypass attempts) - Both checks must pass for command to be allowed 3. **Token-Based Matching**: - Splits normalized command into tokens - Checks for exact token matches against deny patterns - Prevents partial bypasses (e.g., `rm -rf /var` vs `rm -rf /`) 4. **Bypass Attempt Detection**: Commands that would bypass original checking but are caught by normalization are logged: ```json { "level": "error", "msg": "security_event", "type": "dns_rate_limit_exceeded", "hostname": "malicious-host.example.com", "max_per_second": 10 } ``` 5. **Thread Safety**: Rate limiter and cache are thread-safe for concurrent access. **Effect**: Prevents DNS-based DoS attacks by limiting resolution frequency and caching results, reducing load on DNS infrastructure. "type": "command_bypass_attempt", "alias": "web1", "original_command": "'rm -rf /'", "normalized_command": "rm -rf /", "blocked_pattern": "rm -rf /" } ``` 5. **Supported Bypass Techniques Prevented**: - Quote obfuscation: `'rm -rf /'`, `"rm -rf /"` - Escaped characters: `rm\ -rf\ /` - Whitespace variations: `rm -rf /`, `rm\t-rf\t/` - Mixed techniques: `echo "rm\\ -rf\\ /"` 6. **Limitations**: - Perfect prevention would require full command parsing - Complex obfuscation (base64 encoding, variable substitution) may still bypass - Focus is on common bypass techniques, not all possible obfuscation methods **Effect**: Significantly reduces risk of command denial bypasses through common obfuscation techniques (quotes, escaping, whitespace). ### SSH Key Management **Best Practices:** 1. **Use Ed25519 Keys**: Prefer `ed25519` over RSA for modern security ```bash ssh-keygen -t ed25519 -f ~/.ssh/mcp_orchestrator -C "mcp-ssh-orchestrator" ``` 2. **Key Permissions**: Set private keys to read-only for owner ```bash chmod 0400 /path/to/keys/id_ed25519 ``` 3. **Separate Keys**: Use dedicated keys for the orchestrator (not your personal keys) 4. **Key Rotation**: Rotate keys periodically (quarterly recommended) 5. **Passphrase Protection**: Use passphrases for private keys ```yaml # credentials.yml entries: - name: prod_admin username: ubuntu key_path: id_ed25519 key_passphrase_secret: prod_key_passphrase ``` ### Password Authentication **Avoid password authentication when possible.** If required: 1. **Use Secrets**: Never hardcode passwords in YAML ```yaml # credentials.yml entries: - name: legacy_system username: admin password_secret: legacy_password # Resolved from secret ``` 2. **Secret Resolution Order**: - Environment variable: `MCP_SSH_SECRET_<NAME>` (uppercase) - Docker secret file: `/app/secrets/<name>` 3. **Strong Passwords**: Minimum 16 characters, high entropy 4. **Limited Scope**: Use password auth only for hosts that don't support keys ### Secrets Storage **Docker Secrets (Recommended for Production):** ```bash # Create secret echo "my-passphrase" | docker secret create ssh_key_passphrase - # Use in Docker Compose services: mcp-ssh: secrets: - ssh_key_passphrase secrets: ssh_key_passphrase: external: true ``` **Environment Variables (Development Only):** ```bash docker run -i --rm \ -e MCP_SSH_SECRET_ADMIN_PASSWORD="dev-password" \ ghcr.io/samerfarida/mcp-ssh-orchestrator:0.1.0 ``` **File-Based Secrets (Simple Deployments):** ```bash # Create secrets directory mkdir -p ~/mcp-ssh/secrets chmod 0700 ~/mcp-ssh/secrets # Add secret echo "passphrase" > ~/mcp-ssh/secrets/key_passphrase chmod 0400 ~/mcp-ssh/secrets/key_passphrase ``` ## Network Security ### Host Key Verification **Always enable in production:** ```yaml # policy.yml limits: require_known_host: true network: require_known_host: true # Overrides limits setting ``` **Populate known_hosts:** ```bash # Scan host keys ssh-keyscan -H 10.0.0.11 >> ~/mcp-ssh/keys/known_hosts ssh-keyscan -H 10.0.0.21 >> ~/mcp-ssh/keys/known_hosts # Or copy from existing cp ~/.ssh/known_hosts ~/mcp-ssh/keys/ ``` **Effect**: Prevents MITM attacks by verifying host identity before connection. ### IP Allowlisting **Restrict SSH targets to known networks:** ```yaml # policy.yml network: # Allow only RFC1918 private networks allow_cidrs: - "10.0.0.0/8" - "172.16.0.0/12" - "192.168.0.0/16" # Block specific IPs block_ips: - "10.10.10.10" ``` **Two-Stage Verification:** 1. **Pre-Connect**: DNS resolution must match allowlist 2. **Post-Connect**: Actual peer IP must match allowlist **Effect**: Prevents connections to unexpected IPs, mitigates DNS poisoning. ### Egress Controls **Block lateral movement tools by default:** ```yaml # policy.yml limits: deny_substrings: - "ssh " - "scp " - "rsync -e ssh" - "curl " - "wget " - "nc " - "nmap " ``` **Effect**: Prevents compromised hosts from initiating outbound connections. ## Policy Enforcement ### Deny-by-Default **Start with no access, add only what's needed:** ```yaml # policy.yml rules: # Allow safe read-only commands - action: "allow" aliases: ["*"] tags: [] commands: - "uname*" - "uptime*" - "df -h*" # All other commands denied by default ``` ### Glob Pattern Safety **Use specific patterns to avoid overly broad matches:** ```yaml # BAD: Too permissive commands: - "*" # GOOD: Specific commands commands: - "systemctl status nginx" - "systemctl restart nginx" ``` ### Environment Separation **Use tags to separate production from non-production:** ```yaml # Policy for production rules: - action: "allow" aliases: [] tags: ["production"] commands: - "uptime*" - "df -h*" - action: "deny" aliases: [] tags: ["production"] commands: - "systemctl restart*" # Denied on prod # Policy for staging (more permissive) rules: - action: "allow" aliases: [] tags: ["staging"] commands: - "systemctl restart*" # Allowed on staging ``` ### Per-Host Overrides **Apply stricter limits to sensitive hosts:** ```yaml # policy.yml overrides: aliases: prod-db-1: max_seconds: 20 # Shorter timeout max_output_bytes: 262144 # Smaller output cap require_known_host: true # Strict host key check ``` ## Execution Limits ### Timeout Enforcement **Prevent runaway commands:** ```yaml # policy.yml limits: max_seconds: 60 # Global default overrides: tags: production: max_seconds: 30 # Stricter for prod longrun: max_seconds: 300 # Exception for maintenance ``` ### Output Size Caps **Prevent memory exhaustion:** ```yaml # policy.yml limits: max_output_bytes: 1048576 # 1 MiB default overrides: aliases: log-server: max_output_bytes: 10485760 # 10 MiB for log hosts ``` ### Deny Dangerous Substrings **Block destructive commands:** ```yaml # policy.yml limits: deny_substrings: - "rm -rf /" - ":(){ :|:& };:" # Fork bomb - "mkfs " - "dd if=/dev/zero" - "shutdown" - "reboot" - "userdel" - "passwd" ``` ## Container Security ### Non-Root Execution Container runs as UID 10001 (non-root): ```dockerfile RUN useradd -u 10001 -m appuser USER appuser ``` **Effect**: Limits damage if container is compromised. ### Read-Only Mounts **Mount configuration and keys as read-only:** ```bash docker run -i --rm \ -v ~/mcp-ssh/config:/app/config:ro \ -v ~/mcp-ssh/keys:/app/keys:ro \ ghcr.io/samerfarida/mcp-ssh-orchestrator:0.1.0 ``` **Effect**: Prevents accidental or malicious modification of config/keys. ### Minimal Base Image Uses `python:3.13-slim`: - Smaller attack surface - Fewer packages to patch - Reduced image size ### Health Checks ```dockerfile HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD python -c "import mcp_ssh" || exit 1 ``` **Effect**: Early detection of container failures. ## Audit & Monitoring ### Security Audit Logging Security-relevant events are logged to stderr in structured JSON format via `_log_security_event()`: **Security Audit Log Format:** ```json { "level": "error", "kind": "security_audit", "type": "security_event", "event_type": "path_traversal_attempt", "ts": 1762112167.394149, "timestamp": "2025-11-02T14:36:07-0500", "attempted_path": "../etc/passwd", "resolved_path": "/app/secrets/../etc/passwd", "reason": "path_outside_allowed_directory", "base_dir": "/app/secrets" } ``` **Event Types Logged:** - `path_traversal_attempt`: Path traversal detected in secret/key resolution - `file_validation_failed`: Directory/symlink/non-file paths rejected - `file_size_limit_exceeded`: Oversized YAML files rejected - `input_length_limit_exceeded`: Oversized string inputs rejected - `invalid_secret_name`: Invalid characters in secret names - `dns_rate_limit_exceeded`: DNS resolution rate limit violations - `command_bypass_attempt`: Command denial bypass attempts detected **Audit Log Fields:** - `ts`: Unix timestamp (float) - `timestamp`: ISO 8601 formatted timestamp (string) - `attempted_path`: Original input that triggered the event - `resolved_path`: Resolved/absolute path (if applicable) - `reason`: Human-readable reason for the security event - `additional_data`: Event-specific context (sizes, limits, patterns, etc.) **Security Monitoring:** All security audit events are written to stderr for log aggregation and SIEM integration. ### Command Execution Audit Logging All operations logged to stderr as JSON: ```json { "type": "audit", "ts": 1729512345.67, "alias": "prod-web-1", "hash": "a1b2c3d4e5f6", "exit_code": 0, "duration_ms": 123, "bytes_out": 45, "bytes_err": 0, "cancelled": false, "timeout": false, "target_ip": "10.0.0.11" } ``` **Fields:** - `type`: Event type (audit, policy_decision, progress) - `ts`: Unix timestamp - `alias`: Target host - `hash`: Command hash (first 12 chars of SHA256) - `exit_code`: Command exit status - `duration_ms`: Execution time - `bytes_out/bytes_err`: Output size - `cancelled/timeout`: Termination reason - `target_ip`: Actual peer IP connected ### Log Collection **Docker Compose with logging driver:** ```yaml services: mcp-ssh: logging: driver: "json-file" options: max-size: "10m" max-file: "3" ``` **Forward to SIEM:** ```bash docker logs -f mcp-ssh-orchestrator 2>&1 | \ jq -r 'select(.type == "audit") | @json' | \ curl -X POST https://siem.example.com/ingest -d @- ``` ### Monitoring Recommendations **Alert on:** - Policy denials (especially repeated) - Timeouts - Non-zero exit codes on critical hosts - Unexpected target IPs - High-privilege commands (sudo, systemctl) **Metrics to track:** - Commands per hour - Success rate by host - Average duration - Policy violation rate ## Incident Response ### Compromised Key 1. **Immediately revoke** the key on all target hosts: ```bash # Remove from authorized_keys on each host ssh user@host "sed -i '/mcp-orchestrator/d' ~/.ssh/authorized_keys" ``` 2. **Rotate keys**: ```bash # Generate new key ssh-keygen -t ed25519 -f new_key # Deploy to hosts for host in $(cat hosts.txt); do ssh-copy-id -i new_key.pub user@$host done ``` 3. **Update configuration**: ```yaml # credentials.yml entries: - name: prod_admin key_path: new_key # Updated ``` 4. **Review audit logs** for unauthorized usage ### Policy Bypass Detected 1. **Stop the orchestrator** immediately 2. **Review policy rules** for gaps 3. **Check audit logs** for pattern 4. **Patch policy**: ```yaml # Add deny rule for bypass technique limits: deny_substrings: - "<bypass pattern>" ``` 5. **Reload config**: `ssh_reload_config` ### Unauthorized Access 1. **Check who accessed**: ```bash # Audit log analysis jq -r 'select(.alias == "compromised-host") | [.ts, .hash, .target_ip] | @tsv' < audit.log ``` 2. **Identify command hashes** executed 3. **Correlate with target host logs** 4. **Contain affected hosts** 5. **Rotate credentials** ## Security Framework Alignment ### OWASP LLM Top 10 Coverage **LLM07: Insecure Plugin Design** ✅ - Policy-based command validation prevents unauthorized execution - Input sanitization and dangerous command blocking - Access control for AI plugin operations **LLM08: Excessive Agency** ✅ - Role-based restrictions via host tags - Deny-by-default security model - Command pattern matching limits autonomous actions **LLM01: Prompt Injection Mitigation** - SSH command validation prevents injection attacks - Network egress controls block unauthorized connections - DNS verification prevents DNS rebinding attacks ### MITRE ATT&CK Alignment - **T1071**: Application Layer Protocol (SSH monitoring) - **T1071.004**: DNS (DNS resolution verification) - **T1659**: Content Injection (policy-based command filtering) ### Security Features Supporting Compliance This tool provides security controls that can support organizations seeking compliance with frameworks like SOC 2, ISO 27001, PCI-DSS, and HIPAA: **Access Control & Audit** - Policy enforcement provides least-privilege access - JSON logs provide non-repudiation and complete audit trails - All administrative access logged **Encryption & Network Security** - SSH provides transport encryption - Ed25519/RSA keys for strong cryptography - IP allowlists enforce network segmentation **Secrets Management** - Docker secrets or environment variables (not hardcoded) - No persistent credential storage **Note**: Compliance is ultimately the responsibility of the deploying organization. This tool provides security features that can support compliance efforts but is not itself certified to these standards. ## Security Checklist **Before Production Deployment:** - [ ] Use Ed25519 or RSA 4096-bit keys - [ ] Set private key permissions to 0400 - [ ] Enable `require_known_host: true` - [ ] Populate known_hosts file - [ ] Configure IP allowlists (allow_cidrs) - [ ] Enable deny_substrings for dangerous commands - [ ] Use deny-by-default policy model - [ ] Mount config and keys as read-only (:ro) - [ ] Use Docker secrets or env vars (not hardcoded passwords) - [ ] Set up audit log collection - [ ] Configure alerting on policy violations - [ ] Document incident response procedures - [ ] Test policy with `ssh_plan` before `ssh_run` - [ ] Separate production from non-production (tags) - [ ] Set appropriate timeouts and output limits - [ ] Review and minimize container privileges - [ ] Enable Docker health checks - [ ] Use specific command patterns (avoid overly broad globs) - [ ] Rotate keys quarterly - [ ] Keep Docker image updated ## Reporting Security Issues **Do not open public issues for security vulnerabilities.** Email: security@example.com (replace with your contact) Include: - Description of vulnerability - Steps to reproduce - Potential impact - Suggested fix (optional) We aim to respond within 48 hours and patch critical issues within 7 days.

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/samerfarida/mcp-ssh-orchestrator'

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