# 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. **Command Substitution Blocking** - Hard blocks all command substitution (`$()`, backticks) to prevent bypasses
3. **Argument-Aware Rules** - Version 2 schema with `simple_binaries` and structured rules for precise control
4. **Network Controls** - IP/CIDR allowlists and blocklists
5. **Execution Limits** - Timeouts, output size caps, and host key requirements
6. **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.
## File Structure
```yaml
# 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
```
## 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` |
| `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"]` |
### 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.
### Command Chaining Behavior
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:**
```yaml
# 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 && whoami
```
**Security Guarantees:**
- Command chaining cannot bypass policy restrictions
- Each command in a chain is validated individually
- Order-independent: `cmd1 && cmd2` and `cmd2 && cmd1` are 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'`
### Default deny_substrings
The following dangerous command substrings are blocked by default:
```yaml
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 **version 2 schema** with argument-aware matching. Legacy `commands` fnmatch patterns are **no longer supported** for security reasons.
### Rule Types
Rules support two types of command matching:
1. **Simple Binaries** - Bulk allow for read-only inspection commands (exact binary name match)
2. **Structured Rules** - Precise control for specific binaries with argument and path restrictions
### Rule Fields
| 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
Simple binaries allow bulk authorization of read-only inspection commands with argument limits.
**Example:**
```yaml
rules:
- action: "allow"
aliases: ["*"]
tags: []
simple_binaries:
- uptime
- whoami
- hostname
- date
- ls
- echo
simple_max_args: 6
```
**Matching:**
- 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
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.
```yaml
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:**
```yaml
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:**
```yaml
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:
```yaml
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/
```
### 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
- **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:**
1. Check alias/tag matching
2. Check `simple_binaries` first (if present)
3. Check structured rule (if present)
4. First matching rule wins (rules evaluated top-to-bottom)
5. Default deny if no rule matches
### Path-Based Binaries Blocked
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).
## 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 (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"]` |
### 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 (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"]` |
## 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.
```yaml
rules:
- action: "allow"
aliases: ["prod-*"]
binary: "systemctl"
arg_prefix: ["status"]
allow_extra_args: true
- action: "deny"
aliases: ["prod-*"]
simple_binaries: ["systemctl"] # Denies all systemctl commands
```
In the example, `systemctl status nginx` is denied because the later deny rule matches the same alias and binary name.
## 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:
```yaml
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
```yaml
# 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
```yaml
# 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
```yaml
# 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)
```
### 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:
```yaml
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: 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):
```yaml
deny_substrings:
- "rm -rf /"
- "shutdown -h"
- "reboot"
```
- **Simple binaries in rules** (always multi-line for readability):
```yaml
simple_binaries:
- uptime
- whoami
- hostname
```
- **Network blocks/lists with comments**:
```yaml
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
- **Simple binaries** → Always multi-line
- **Network blocks/lists** → Multi-line (allows comments)
- **Consistency** → When in doubt, use multi-line for clarity
## Validation and Testing
### Policy Validation
```bash
# 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
```bash
# 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
```bash
# 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**
```bash
# Check syntax
python -c "import yaml; yaml.safe_load(open('config/policy.yml'))"
```
2. **Policy rule conflicts**
```bash
# Test specific rules
ssh_plan --alias "web1" --command "uptime"
```
3. **Network policy issues**
```bash
# 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**
```bash
# Enable debug mode
export MCP_SSH_DEBUG=1
ssh_plan --alias "web1" --command "uptime"
```
2. **Override not applying**
```bash
# Check override hierarchy
python -c "
from mcp_ssh.policy import Policy
policy = Policy('config/policy.yml')
print('Overrides:', policy.overrides)
"
```
## Next Steps
- **[Usage Cookbook](08-Usage-Cookbook)** - Practical policy examples
- **[Security Model](05-Security-Model)** - Security architecture details
- **[Troubleshooting](12-Troubleshooting)** - Common policy issues
- **[Deployment](09-Deployment)** - Production policy configuration