06.3-policy.yml.md•16.8 kB
# 6.3 policy.yml
**Purpose:** Define security policies, access controls, and execution limits for mcp-ssh-orchestrator using a deny-by-default model.
## 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
```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:
- 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 | Automatically add unknown host keys to known_hosts | `true` |
| `require_known_host` | boolean | No | true | Require host to exist in known_hosts before connection | `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:
```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 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
## 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
## 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 | Don't auto-add host keys |
| `require_known_host` | true | Require known_hosts entry |
| `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
- 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
```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"]
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
```yaml
# Development/staging policy
known_hosts_path: "/app/keys/known_hosts"
limits:
max_seconds: 60
max_output_bytes: 1048576
host_key_auto_add: true
require_known_host: false
network:
allow_cidrs:
- "10.0.0.0/8"
- "192.168.0.0/16"
- "172.16.0.0/12"
require_known_host: false
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
host_key_auto_add: true
require_known_host: false
staging:
max_seconds: 90
host_key_auto_add: true
require_known_host: false
```
## 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"
```
- **Commands in rules** (always multi-line for readability):
```yaml
commands:
- "systemctl status *"
- "docker ps*"
```
- **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
- **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
```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