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:
- Command Substring Blocking - Hard blocks commands containing dangerous substrings
- Rule-based Allow/Deny - Pattern-based command matching with glob support
- Network Controls - IP/CIDR allowlists and blocklists
- Execution Limits - Timeouts, output size caps, and host key requirements
- 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
- Block Check: If IP is in
block_ipsorblock_cidrs, deny connection - Allow Check: If
allow_ipsorallow_cidrsare configured, IP must be in one of them - 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
- Deny Substrings Check: Commands containing any substring in
deny_substringsare blocked - Rule Matching: Rules are evaluated in order until a match is found
- 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_substringsare evaluated before any rule logic. If a command is blocked there (for example, it containssudo), later allow rules will never run. To permit that operation, remove or override the substring entry for the specific hosts.- Always test with
ssh_planto 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):
- Alias Overrides - Specific host alias settings
- Tag Overrides - Host tag settings (only if not set by alias)
- Global Limits - Settings in the
limitssection - 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:
- Remove
sudofrom the globaldeny_substringslist or override it for these hosts. - Allow only the exact command patterns required (non-interactive apt commands in this case).
- Increase per-host
max_secondssodpkghas enough time, and extendtask_result_ttlso you can retrieve async results later. - Test with
ssh_plan, run viassh_run_async, then monitor withssh_get_task_status/resultand 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
-
Invalid YAML syntax
# Check syntax python -c "import yaml; yaml.safe_load(open('config/policy.yml'))" -
Policy rule conflicts
# Test specific rules ssh_plan --alias "web1" --command "uptime" -
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
-
Rule not matching
# Enable debug mode export MCP_SSH_DEBUG=1 ssh_plan --alias "web1" --command "uptime" -
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
- Usage Cookbook - Practical policy examples
- Security Model - Security architecture details
- Troubleshooting - Common policy issues
- Deployment - Production policy configuration