06.3 policy.yml - samerfarida/mcp-ssh-orchestrator GitHub Wiki

6.3 policy.yml

Purpose: Define security policies, access controls, and execution limits for mcp-ssh-orchestrator using a deny-by-default model.

Security Note: Host key verification is always enforced (CWE-295). Unsafe policies (host_key_auto_add: true, require_known_host: false) are deprecated and ignored. All SSH connections require a known_hosts entry.

Overview

The policy.yml file implements a deny-by-default security model where commands must explicitly match an "allow" rule to execute. It provides multiple layers of security controls:

  1. Command Substring Blocking - Hard blocks commands containing dangerous substrings
  2. Rule-based Allow/Deny - Pattern-based command matching with glob support
  3. Network Controls - IP/CIDR allowlists and blocklists
  4. Execution Limits - Timeouts, output size caps, and host key requirements
  5. Per-host/Tag Overrides - Granular control for specific hosts or host groups

File Structure

# policy.yml
known_hosts_path: "/app/keys/known_hosts"

limits:
  max_seconds: 60
  max_output_bytes: 1048576
  host_key_auto_add: false
  require_known_host: true
  deny_substrings:
    - "rm -rf /"
    - "shutdown*"
    - "reboot*"

network:
  allow_cidrs:
    - "10.0.0.0/8"
    - "192.168.0.0/16"
  block_ips:
    - "0.0.0.0"
    - "255.255.255.255"
  require_known_host: true

rules:
  - action: "allow"
    aliases: ["prod-*"]
    tags: ["production"]
    commands:
      - "uptime*"
      - "df -h*"
      - "systemctl status *"

overrides:
  aliases:
    prod-db-1:
      max_seconds: 20
      max_output_bytes: 131072
  tags:
    production:
      max_seconds: 30
      require_known_host: true

Configuration Sections

Section Purpose Required
known_hosts_path Path to SSH known_hosts file No
limits Global execution limits and security settings No
network Network access controls and IP filtering No
rules Command allow/deny rules with pattern matching No
overrides Per-host and per-tag limit overrides No

Root Level Settings

Field Type Required Default Description Example
known_hosts_path string No None Path to SSH known_hosts file for host key verification "/app/keys/known_hosts"

Limits Section

The limits section defines global execution limits and security settings that apply to all hosts unless overridden.

Field Type Required Default Description Example
max_seconds integer No 60 Maximum command execution time in seconds 30
max_output_bytes integer No 1048576 Maximum combined stdout/stderr output size in bytes 524288
host_key_auto_add boolean No false Deprecated: Ignored for security (CWE-295). Always use require_known_host: true true
require_known_host boolean No true Security: Always enforced. Require host to exist in known_hosts before connection. Prevents MITM attacks false
deny_substrings array No See below List of substrings that will block any command containing them ["rm -rf", "shutdown"]

Command Denial Bypass Prevention

Commands are normalized before checking against deny_substrings to prevent bypass attempts:

  • Quote Removal: Single and double quotes are stripped (e.g., 'rm -rf /'rm -rf /)
  • Escape Handling: Escaped characters are normalized (e.g., rm\ -rf\ /rm -rf /)
  • Whitespace Normalization: Multiple spaces/tabs are collapsed (e.g., rm -rf /rm -rf /)
  • Dual Checking: Both original and normalized commands are checked

Security: Bypass attempts detected via normalization are logged as security events.

Note: Complex obfuscation (encoding, variable substitution) may still bypass. Focus is on common techniques.

Default deny_substrings

The following dangerous command substrings are blocked by default:

deny_substrings:
  # Destructive commands
  - "rm -rf /"
  - ":(){ :|:& };:"  # Fork bomb
  - "mkfs "
  - "dd if=/dev/zero"
  - "shutdown -h"
  - "reboot"
  - "userdel "
  - "passwd "
  # Lateral movement / egress tools
  - "ssh "
  - "scp "
  - "rsync -e ssh"
  - "curl "
  - "wget "
  - "nc "
  - "nmap "
  - "telnet "
  - "kubectl "
  - "aws "
  - "gcloud "
  - "az "

Network Section

The network section controls which IP addresses and networks are allowed for SSH connections.

Field Type Required Default Description Example
allow_ips array No [] List of specific IP addresses to allow ["10.0.0.1", "192.168.1.100"]
allow_cidrs array No [] List of CIDR networks to allow ["10.0.0.0/8", "192.168.0.0/16"]
block_ips array No [] List of specific IP addresses to block ["0.0.0.0", "255.255.255.255"]
block_cidrs array No [] List of CIDR networks to block ["169.254.0.0/16", "224.0.0.0/4"]
require_known_host boolean No true Override for host key verification (overrides limits setting) false

Network Policy Evaluation

  1. Block Check: If IP is in block_ips or block_cidrs, deny connection
  2. Allow Check: If allow_ips or allow_cidrs are configured, IP must be in one of them
  3. Default: If no allow lists are configured, allow all (after block checks)

Rules Section

The rules section defines command allow/deny rules using glob pattern matching.

Field Type Required Default Description Example
action string Yes "deny" Rule action: "allow" or "deny" "allow"
aliases array No [] List of host aliases to match (glob patterns) ["prod-*", "web1"]
tags array No [] List of host tags to match (glob patterns) ["production", "web"]
commands array No [] List of command patterns to match (glob patterns) ["uptime*", "df -h*"]

Rule Matching Logic

A rule matches when ALL specified conditions are met:

  • aliases: If specified, host alias must match at least one pattern
  • tags: If specified, at least one host tag must match at least one pattern
  • commands: If specified, command must match at least one pattern

If any condition is empty ([]), it matches all values.

Overrides Section

The overrides section allows per-host and per-tag customization of limits.

Aliases Subsection

Override limits for specific host aliases.

Field Type Required Default Description Example
{alias_name} object No N/A Host alias name (exact match) "prod-web-1"
max_seconds integer No From limits Override max execution time 30
max_output_bytes integer No From limits Override max output size 524288
host_key_auto_add boolean No From limits Override host key auto-add false
require_known_host boolean No From limits Override host key requirement true
deny_substrings array No From limits Override deny substrings list ["rm -rf", "shutdown"]

Tags Subsection

Override limits for hosts with specific tags.

Field Type Required Default Description Example
{tag_name} object No N/A Tag name (exact match) "production"
max_seconds integer No From limits Override max execution time 30
max_output_bytes integer No From limits Override max output size 524288
host_key_auto_add boolean No From limits Override host key auto-add false
require_known_host boolean No From limits Override host key requirement true
deny_substrings array No From limits Override deny substrings list ["rm -rf", "shutdown"]

Glob Pattern Matching

The policy engine uses Python's fnmatch module for pattern matching, supporting:

Pattern Description Matches Doesn't Match
* Matches any characters uptime, systemctl status None
? Matches single character cat, cut cat, cats
[seq] Matches any char in seq [abc] matches a, b, c d, ab
[!seq] Matches any char not in seq [!abc] matches d, e a, b, c

Common Patterns

Pattern Purpose Example Matches
* Match all commands Any command
uptime* Commands starting with "uptime" uptime, uptime -s
systemctl status * systemctl status with any service systemctl status nginx, systemctl status apache2
prod-* Hosts starting with "prod-" prod-web-1, prod-db-1
*prod* Hosts containing "prod" prod-web-1, staging-prod-1

Rule Evaluation Order

  1. Deny Substrings Check: Commands containing any substring in deny_substrings are blocked
  2. Rule Matching: Rules are evaluated in order until a match is found
  3. Default Deny: If no rule matches, command is denied

Rule Resolution Details

  • Rules are processed top-to-bottom and the last matching rule wins. Each match updates the pending decision, so place broad deny rules after their paired allows if you intend them to override.
  • Because matches overwrite previous decisions, keep conflicting rules close together and comment them for future reviewers.
  • deny_substrings are evaluated before any rule logic. If a command is blocked there (for example, it contains sudo ), later allow rules will never run. To permit that operation, remove or override the substring entry for the specific hosts.
  • Always test with ssh_plan to verify the final decision after ordering changes.
rules:
  - action: "allow"
    aliases: ["prod-*"]
    commands: ["systemctl status *"]

  - action: "deny"
    aliases: ["prod-*"]
    commands: ["systemctl *"]  # Overrides the allow above

In the example, systemctl status nginx is denied because the later rule matches the same alias and command pattern.

Override Hierarchy

When multiple overrides apply, precedence is (highest to lowest):

  1. Alias Overrides - Specific host alias settings
  2. Tag Overrides - Host tag settings (only if not set by alias)
  3. Global Limits - Settings in the limits section
  4. Default Values - Hardcoded defaults in the policy engine

Deny Substring Overrides in Practice

Alias/tag overrides can redefine deny_substrings, which is useful when you want a strict global block list but still need an escape hatch for a small set of hosts:

limits:
  deny_substrings:
    - "sudo "
    - "rm -rf /"
    - "kubectl "

overrides:
  aliases:
    docker-prod-manager1:
      deny_substrings:  # Clone the list without sudo
        - "rm -rf /"
        - "kubectl "

In this pattern, sudo stays blocked for every host except docker-prod-manager1. Pair the override with explicit allow rules and higher max_seconds values so privileged operations are both auditable and predictable.

Default Values

Setting Default Value Description
max_seconds 60 Maximum command execution time
max_output_bytes 1048576 Maximum output size (1 MiB)
host_key_auto_add false Deprecated: Ignored for security (CWE-295)
require_known_host true Security: Always enforced. Prevents MITM attacks
deny_substrings 14+ patterns Dangerous command substrings
allow_ips [] No IP allowlist (allow all)
allow_cidrs [] No CIDR allowlist (allow all)
block_ips [] No IP blocklist
block_cidrs [] No CIDR blocklist
known_hosts_path None Use system default

Policy Examples

Basic Read-Only Policy

# Basic read-only policy
known_hosts_path: "/app/keys/known_hosts"

limits:
  max_seconds: 30
  max_output_bytes: 262144
  host_key_auto_add: false
  require_known_host: true

network:
  allow_cidrs:
    - "10.0.0.0/8"
    - "192.168.0.0/16"
  require_known_host: true

rules:
  # Basic system information
  - action: "allow"
    aliases: ["*"]
    tags: []
    commands:
      - "uname*"
      - "uptime*"
      - "whoami"
      - "hostname*"
      - "date*"
      - "id*"

  # Disk and memory usage
  - action: "allow"
    aliases: ["*"]
    tags: []
    commands:
      - "df -h*"
      - "free -h*"
      - "lsblk*"

  # Process information
  - action: "allow"
    aliases: ["*"]
    tags: []
    commands:
      - "ps aux*"
      - "top -n 1*"

  # Service status (read-only)
  - action: "allow"
    aliases: ["*"]
    tags: []
    commands:
      - "systemctl status *"
      - "systemctl is-active *"
      - "systemctl is-enabled *"

Production Environment Policy

# Production environment policy
known_hosts_path: "/app/keys/known_hosts"

limits:
  max_seconds: 20
  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*"
    - "apt *"
    - "yum *"
    - "docker run*"
    - "kubectl *"

network:
  allow_cidrs:
    - "10.0.0.0/8"
  block_cidrs:
    - "0.0.0.0/0"  # Block all public internet
  require_known_host: true

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

  # Explicit deny for production
  - action: "deny"
    aliases: ["prod-*"]
    tags: ["production"]
    commands:
      - "systemctl restart*"
      - "systemctl stop*"
      - "systemctl start*"
      - "apt *"
      - "yum *"
      - "docker *"
      - "kubectl *"

overrides:
  aliases:
    prod-db-1:
      max_seconds: 10
      max_output_bytes: 65536
    prod-web-1:
      max_seconds: 15
      max_output_bytes: 131072

Development/Staging Policy

# Development/staging policy
known_hosts_path: "/app/keys/known_hosts"

limits:
  max_seconds: 60
  max_output_bytes: 1048576
  require_known_host: true  # Always enforced (CWE-295)

network:
  allow_cidrs:
    - "10.0.0.0/8"
    - "192.168.0.0/16"
    - "172.16.0.0/12"
  require_known_host: true  # Always enforced (CWE-295)

rules:
  # Read-only commands
  - action: "allow"
    aliases: ["*"]
    tags: []
    commands:
      - "uname*"
      - "uptime*"
      - "df -h*"
      - "ps aux*"
      - "systemctl status *"

  # Development-specific commands
  - action: "allow"
    aliases:
      - "dev-*"
      - "stg-*"
    tags:
      - "development"
      - "staging"
    commands:
      - "systemctl restart *"
      - "systemctl stop *"
      - "systemctl start *"
      - "docker ps*"
      - "docker logs *"
      - "kubectl get *"
      - "kubectl describe *"

  # Network diagnostics for dev/staging
  - action: "allow"
    aliases:
      - "dev-*"
      - "stg-*"
    tags:
      - "development"
      - "staging"
    commands:
      - "ping*"
      - "traceroute*"
      - "ss -tulpn*"
      - "netstat*"

overrides:
  tags:
    development:
      max_seconds: 120
      # Note: host_key_auto_add and require_known_host=false are deprecated (CWE-295)
    staging:
      max_seconds: 90
      # Note: host_key_auto_add and require_known_host=false are deprecated (CWE-295)

Privileged Maintenance (Non-Interactive Upgrades)

Long-running maintenance commands (like apt upgrades) often require sudo, additional flags, and longer timeouts. Combine explicit allow rules with per-alias overrides:

rules:
  - action: "allow"
    aliases:
      - "docker-prod-manager1"
      - "docker-prod-manager2"
      - "docker-prod-manager3"
    commands:
      - "sudo apt-get update*"
      - "DEBIAN_FRONTEND=noninteractive sudo apt-get upgrade -y*"

overrides:
  aliases:
    docker-prod-manager1:
      max_seconds: 300
      task_result_ttl: 1800
    docker-prod-manager2:
      max_seconds: 300
      task_result_ttl: 1800
    docker-prod-manager3:
      max_seconds: 300
      task_result_ttl: 1800

Workflow:

  1. Remove sudo from the global deny_substrings list or override it for these hosts.
  2. Allow only the exact command patterns required (non-interactive apt commands in this case).
  3. Increase per-host max_seconds so dpkg has enough time, and extend task_result_ttl so you can retrieve async results later.
  4. Test with ssh_plan, run via ssh_run_async, then monitor with ssh_get_task_status/result and revert overrides if they were temporary.

YAML Style Guide

For consistency and readability, follow these YAML array formatting guidelines:

When to Use Inline Arrays []

  • Empty arrays: allow_ips: []
  • Single-item lists: aliases: ["*"], tags: ["production"]

When to Use Multi-line Dash Syntax

  • Two or more items (always use multi-line for readability):
    deny_substrings:
      - "rm -rf /"
      - "shutdown -h"
      - "reboot"
    
  • Commands in rules (always multi-line for readability):
    commands:
      - "systemctl status *"
      - "docker ps*"
    
  • Network blocks/lists with comments:
    block_ips:
      - "0.0.0.0"           # Block all zeros
      - "255.255.255.255"   # Block broadcast
    

General Principles

  • Empty arrays → Inline: tags: []
  • Single item → Inline: aliases: ["*"]
  • Two or more items → Multi-line for readability
  • Commands → Always multi-line
  • Network blocks/lists → Multi-line (allows comments)
  • Consistency → When in doubt, use multi-line for clarity

Validation and Testing

Policy Validation

# Validate policy.yml syntax
python -c "import yaml; yaml.safe_load(open('config/policy.yml'))"

# Validate policy rules
python -c "
from mcp_ssh.policy import Policy
policy = Policy('config/policy.yml')
print('Policy validation:', policy.validate())
"

Policy Testing

# Test policy rules with dry-run
ssh_plan --alias "web1" --command "uptime"
ssh_plan --alias "prod-web-1" --command "systemctl restart nginx"

# Test network policies
ssh_plan --alias "web1" --command "ping 8.8.8.8"

Policy Debugging

# Enable debug logging
export MCP_SSH_DEBUG=1
ssh_plan --alias "web1" --command "uptime"

# Check policy evaluation
python -c "
from mcp_ssh.policy import Policy
policy = Policy('config/policy.yml')
result = policy.evaluate('web1', 'uptime', ['production'])
print('Policy result:', result)
"

Troubleshooting

Common Issues

  1. Invalid YAML syntax

    # Check syntax
    python -c "import yaml; yaml.safe_load(open('config/policy.yml'))"
    
  2. Policy rule conflicts

    # Test specific rules
    ssh_plan --alias "web1" --command "uptime"
    
  3. Network policy issues

    # Check network configuration
    python -c "
    from mcp_ssh.policy import Policy
    policy = Policy('config/policy.yml')
    print('Network config:', policy.network_config)
    "
    

Policy Debugging

  1. Rule not matching

    # Enable debug mode
    export MCP_SSH_DEBUG=1
    ssh_plan --alias "web1" --command "uptime"
    
  2. Override not applying

    # Check override hierarchy
    python -c "
    from mcp_ssh.policy import Policy
    policy = Policy('config/policy.yml')
    print('Overrides:', policy.overrides)
    "
    

Next Steps