06.3 policy.yml - samerfarida/mcp-ssh-orchestrator GitHub Wiki
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.
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
-
Command Substitution Blocking - Hard blocks all command substitution (
$(), backticks) to prevent bypasses -
Argument-Aware Rules - Version 2 schema with
simple_binariesand structured rules for precise control - 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
Security Note: Command substitution ($(...) and backticks) is hard-banned to prevent policy bypasses. Path-based binaries (e.g., /usr/bin/cat, ./script.sh) are blocked by default for security.
# 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:
# Simple binaries: bulk allow for read-only inspection commands
- action: "allow"
aliases: ["prod-*"]
tags: ["production"]
simple_binaries:
- uptime
- whoami
- hostname
- date
simple_max_args: 6
# Structured rule: specific binary with argument requirements
- action: "allow"
aliases: ["prod-*"]
tags: ["production"]
binary: "df"
arg_prefix: ["-h"]
allow_extra_args: false
# Structured rule: binary with path argument restrictions
- action: "allow"
aliases: ["prod-*"]
tags: ["production"]
binary: "systemctl"
arg_prefix: ["status"]
allow_extra_args: true
overrides:
aliases:
prod-db-1:
max_seconds: 20
max_output_bytes: 131072
tags:
production:
max_seconds: 30
require_known_host: true| 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 |
| 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" |
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 |
task_result_ttl |
integer | No | 300 | Async task result retention time in seconds (5 minutes). Results are kept for this duration after completion | 600 |
task_progress_interval |
integer | No | 5 | Interval in seconds between progress notifications for async tasks | 10 |
deny_substrings |
array | No | See below | List of substrings that will block any command containing them | ["rm -rf", "shutdown"] |
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.
The policy engine validates command chains to prevent policy bypass via chaining operators.
How It Works:
- Commands containing chaining operators (
&&,||,;,|) are parsed into individual commands - Each command in the chain is validated separately against policy rules
- All commands in the chain must be allowed for the entire chain to execute
- If any command is denied, the entire chain is blocked
Supported Operators:
-
&&- Logical AND (run next command only if previous succeeds) -
||- Logical OR (run next command only if previous fails) -
;- Sequential execution (run commands in order) -
|- Pipe (pass output of first command to second)
Command Substitution (BLOCKED):
- Hard-banned: All command substitution is blocked for security
- Backtick substitution:
`command`- BLOCKED - Dollar-paren substitution:
$(command)- BLOCKED - Arithmetic expansion:
$((...))- BLOCKED - This prevents policy bypasses where denied commands could be executed via substitution
Examples:
# Policy allows: uptime*, whoami
# Policy denies: apt list --upgradable*
# ✅ ALLOWED: Both commands are allowed
uptime && whoami
# ❌ DENIED: Second command is denied
uptime && apt list --upgradable
# ❌ DENIED: First command is denied
apt list --upgradable && uptime
# ✅ ALLOWED: All three commands are allowed
uptime && whoami && hostname
# ❌ DENIED: Middle command is denied
uptime && apt list --upgradable && whoamiSecurity Guarantees:
- Command chaining cannot bypass policy restrictions
- Each command in a chain is validated individually
- Order-independent:
cmd1 && cmd2andcmd2 && cmd1are both validated the same way - Operators inside quotes are ignored (e.g.,
echo "hello && world"is treated as a single command)
Error Messages: When a chained command is denied, the error message identifies which specific command in the chain caused the denial:
Policy blocked command in chain: 'apt list --upgradable'
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 "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 |
-
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)
The rules section defines command allow/deny rules using version 2 schema with argument-aware matching. Legacy commands fnmatch patterns are no longer supported for security reasons.
Rules support two types of command matching:
- Simple Binaries - Bulk allow for read-only inspection commands (exact binary name match)
- Structured Rules - Precise control for specific binaries with argument and path restrictions
| 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"] |
simple_binaries |
array | No | [] |
List of binary names to allow (exact match) | ["uptime", "whoami"] |
simple_max_args |
integer | No | None | Maximum number of arguments allowed for simple_binaries | 6 |
binary |
string | No | None | Exact binary name for structured rule | "df" |
arg_prefix |
array | No | [] |
Exact argument prefix sequence | ["-h"] |
allow_extra_args |
boolean | No | true |
Allow additional arguments after prefix | false |
path_args |
object | No | {} |
Path argument restrictions | See below |
Simple binaries allow bulk authorization of read-only inspection commands with argument limits.
Example:
rules:
- action: "allow"
aliases: ["*"]
tags: []
simple_binaries:
- uptime
- whoami
- hostname
- date
- ls
- echo
simple_max_args: 6Matching:
- Binary name must match exactly (case-sensitive)
- Number of arguments must not exceed
simple_max_args - No shell meta characters allowed in arguments (
;,&&,||,|,`,$()
Structured rules provide precise control for specific binaries with argument and path restrictions.
Important: Structured rules must have at least one restriction:
-
arg_prefix- Required argument sequence -
path_args- Path restrictions on specific arguments - Or both
A structured rule with only binary and no restrictions will not match any commands. Use simple_binaries if you want to allow a binary with any arguments.
Path Arguments:
The path_args object restricts which paths can be used in specific argument positions:
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
indices |
array | Yes | Argument positions (0-indexed: 0=binary, 1=first arg, 2=second arg, etc.) |
[1], [3]
|
patterns |
array | Yes | fnmatch patterns for allowed paths | ["/etc/os-release", "/var/log/*"] |
Important: Indices are 0-indexed from the full command:
-
0= binary name -
1= first argument -
2= second argument - etc.
path_args:
indices: [1] # First argument position (after binary)
patterns: # fnmatch patterns for allowed paths
- "/etc/os-release"
- "/etc/*release"Example - Allow cat only for specific files:
rules:
- action: "allow"
aliases: ["*"]
tags: []
binary: "cat"
allow_extra_args: false
path_args:
indices: [1] # First argument (after binary)
patterns:
- "/etc/os-release"
- "/etc/*release"Example - Allow tail with specific arguments:
rules:
- action: "allow"
aliases: ["*"]
tags: []
binary: "tail"
arg_prefix: ["-n", "200"]
allow_extra_args: false
path_args:
indices: [3] # Position 3: tail(0) -n(1) 200(2) <path>(3)
patterns:
- "/var/log/*"Example - Multiple path arguments:
For commands with multiple path arguments, specify all positions:
rules:
- action: "allow"
aliases: ["*"]
tags: []
binary: "cp"
allow_extra_args: false
path_args:
indices: [1, 2] # cp(0) <source>(1) <dest>(2)
patterns:
- "/tmp/*" # Source must be in /tmp/
- "/backup/*" # Destination must be in /backup/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
- simple_binaries OR structured rule (binary + arg_prefix + path_args): Command must match the rule pattern
If any condition is empty ([]), it matches all values.
Evaluation Order:
- Check alias/tag matching
- Check
simple_binariesfirst (if present) - Check structured rule (if present)
- First matching rule wins (rules evaluated top-to-bottom)
- Default deny if no rule matches
For security, path-based binaries are always blocked:
-
/usr/bin/cat- Blocked -
/tmp/evil.sh- Blocked -
./script.sh- Blocked
Only binaries in $PATH are allowed (matched by exact name in rules).
The overrides section allows per-host and per-tag customization of limits.
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 (deprecated, ignored) | false |
require_known_host |
boolean | No | From limits | Override host key requirement | true |
task_result_ttl |
integer | No | From limits | Override async result retention time | 600 |
task_progress_interval |
integer | No | From limits | Override progress notification interval | 10 |
deny_substrings |
array | No | From limits | Override deny substrings list | ["rm -rf", "shutdown"] |
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 (deprecated, ignored) | false |
require_known_host |
boolean | No | From limits | Override host key requirement | true |
task_result_ttl |
integer | No | From limits | Override async result retention time | 600 |
task_progress_interval |
integer | No | From limits | Override progress notification interval | 10 |
deny_substrings |
array | No | From limits | Override deny substrings list | ["rm -rf", "shutdown"] |
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
|
| 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
|
-
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
- 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-*"]
binary: "systemctl"
arg_prefix: ["status"]
allow_extra_args: true
- action: "deny"
aliases: ["prod-*"]
simple_binaries: ["systemctl"] # Denies all systemctl commandsIn the example, systemctl status nginx is denied because the later deny rule matches the same alias and binary name.
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
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.
| 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 |
# 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 (simple binaries)
- action: "allow"
aliases: ["*"]
tags: []
simple_binaries:
- uname
- uptime
- whoami
- hostname
- date
- id
simple_max_args: 6
# Disk and memory usage (structured rules)
- action: "allow"
aliases: ["*"]
tags: []
binary: "df"
arg_prefix: ["-h"]
allow_extra_args: false
- action: "allow"
aliases: ["*"]
tags: []
binary: "free"
arg_prefix: ["-h"]
allow_extra_args: false
- action: "allow"
aliases: ["*"]
tags: []
simple_binaries:
- lsblk
simple_max_args: 6
# Process information
- action: "allow"
aliases: ["*"]
tags: []
simple_binaries:
- ps
simple_max_args: 6
# Service status (read-only) - structured rules
- action: "allow"
aliases: ["*"]
tags: []
binary: "systemctl"
arg_prefix: ["status"]
allow_extra_args: true
- action: "allow"
aliases: ["*"]
tags: []
binary: "systemctl"
arg_prefix: ["is-active"]
allow_extra_args: true
- action: "allow"
aliases: ["*"]
tags: []
binary: "systemctl"
arg_prefix: ["is-enabled"]
allow_extra_args: true# 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"]
simple_binaries:
- uptime
simple_max_args: 6
- action: "allow"
aliases: ["prod-*"]
tags: ["production"]
binary: "df"
arg_prefix: ["-h"]
allow_extra_args: false
- action: "allow"
aliases: ["prod-*"]
tags: ["production"]
binary: "systemctl"
arg_prefix: ["status"]
allow_extra_args: true
- action: "allow"
aliases: ["prod-*"]
tags: ["production"]
binary: "journalctl"
arg_prefix: ["--no-pager", "-n", "20"]
allow_extra_args: true
# Explicit deny for production
- action: "deny"
aliases: ["prod-*"]
tags: ["production"]
simple_binaries:
- apt
- yum
- docker
- kubectl
- action: "deny"
aliases: ["prod-*"]
tags: ["production"]
binary: "systemctl"
arg_prefix: ["restart"]
allow_extra_args: true
- action: "deny"
aliases: ["prod-*"]
tags: ["production"]
binary: "systemctl"
arg_prefix: ["stop"]
allow_extra_args: true
- action: "deny"
aliases: ["prod-*"]
tags: ["production"]
binary: "systemctl"
arg_prefix: ["start"]
allow_extra_args: true
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
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: []
simple_binaries:
- uname
- uptime
- ps
simple_max_args: 6
- action: "allow"
aliases: ["*"]
tags: []
binary: "df"
arg_prefix: ["-h"]
allow_extra_args: false
- action: "allow"
aliases: ["*"]
tags: []
binary: "systemctl"
arg_prefix: ["status"]
allow_extra_args: true
# Development-specific commands
- action: "allow"
aliases:
- "dev-*"
- "stg-*"
tags:
- "development"
- "staging"
binary: "systemctl"
arg_prefix: ["restart"]
allow_extra_args: true
- action: "allow"
aliases:
- "dev-*"
- "stg-*"
tags:
- "development"
- "staging"
binary: "systemctl"
arg_prefix: ["stop"]
allow_extra_args: true
- action: "allow"
aliases:
- "dev-*"
- "stg-*"
tags:
- "development"
- "staging"
binary: "systemctl"
arg_prefix: ["start"]
allow_extra_args: true
- action: "allow"
aliases:
- "dev-*"
- "stg-*"
tags:
- "development"
- "staging"
binary: "docker"
arg_prefix: ["ps"]
allow_extra_args: false
- action: "allow"
aliases:
- "dev-*"
- "stg-*"
tags:
- "development"
- "staging"
binary: "docker"
arg_prefix: ["logs"]
allow_extra_args: true
- action: "allow"
aliases:
- "dev-*"
- "stg-*"
tags:
- "development"
- "staging"
binary: "kubectl"
arg_prefix: ["get"]
allow_extra_args: true
- action: "allow"
aliases:
- "dev-*"
- "stg-*"
tags:
- "development"
- "staging"
binary: "kubectl"
arg_prefix: ["describe"]
allow_extra_args: true
# Network diagnostics for dev/staging
- action: "allow"
aliases:
- "dev-*"
- "stg-*"
tags:
- "development"
- "staging"
simple_binaries:
- ping
- traceroute
- netstat
simple_max_args: 8
- action: "allow"
aliases:
- "dev-*"
- "stg-*"
tags:
- "development"
- "staging"
binary: "ss"
arg_prefix: ["-tulpn"]
allow_extra_args: false
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)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"
binary: "sudo"
arg_prefix: ["apt-get", "update"]
allow_extra_args: false
- action: "allow"
aliases:
- "docker-prod-manager1"
- "docker-prod-manager2"
- "docker-prod-manager3"
binary: "sudo"
arg_prefix: ["apt-get", "upgrade", "-y"]
allow_extra_args: false
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: 1800Workflow:
- 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.
For consistency and readability, follow these YAML array formatting guidelines:
-
Empty arrays:
allow_ips: [] -
Single-item lists:
aliases: ["*"],tags: ["production"]
-
Two or more items (always use multi-line for readability):
deny_substrings: - "rm -rf /" - "shutdown -h" - "reboot"
-
Simple binaries in rules (always multi-line for readability):
simple_binaries: - uptime - whoami - hostname
-
Network blocks/lists with comments:
block_ips: - "0.0.0.0" # Block all zeros - "255.255.255.255" # Block broadcast
-
Empty arrays → Inline:
tags: [] -
Single item → Inline:
aliases: ["*"] - Two or more items → Multi-line for readability
- Simple binaries → Always multi-line
- Network blocks/lists → Multi-line (allows comments)
- Consistency → When in doubt, use multi-line for clarity
# 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())
"# 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"# 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)
"-
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) "
-
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) "
- Usage Cookbook - Practical policy examples
- Security Model - Security architecture details
- Troubleshooting - Common policy issues
- Deployment - Production policy configuration