# Deployment Hardening

Security controls and configuration recommendations for production deployments.

## Security Headers

Applied to every response by `SecurityHeadersMiddleware`:

| Header | Value | Purpose |
|---|---|---|
| `Content-Security-Policy` | `script-src 'self'; frame-ancestors 'none'` | Prevents XSS and clickjacking |
| `X-Frame-Options` | `DENY` | Prevents clickjacking |
| `X-Content-Type-Options` | `nosniff` | Prevents MIME sniffing |
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Controls referrer information |
| `Permissions-Policy` | Restrictive | Limits browser feature access |
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | Enforces HTTPS (when TLS enabled) |
| `Cache-Control` | `no-store` | Prevents caching of API responses |

Server identification headers (`Server`, `X-Powered-By`) are not exposed.

## TLS

Anteroom generates self-signed ECDSA P-256 certificates for localhost HTTPS:

```yaml
app:
  tls: true
```

When TLS is enabled:

- Self-signed cert auto-generated on first run
- HSTS header enforced on all responses
- `Secure` flag set on all cookies
- SSL verification enabled by default for AI backend connections (`ai.verify_ssl: true`)

For production, use a reverse proxy (nginx, Caddy) with proper certificates instead of the built-in self-signed certs.

## Subresource Integrity (SRI)

All vendor scripts (marked.js, highlight.js, KaTeX, DOMPurify) include SHA-384 hashes. If a CDN or file is tampered with, the browser refuses to execute it.

## Rate Limiting

- **120 requests per minute** per IP address
- Uses LRU eviction for the IP tracking map
- Applied to all endpoints

## Request Size Limits

| Limit | Value |
|---|---|
| Max request body | 50 MB |
| Max file attachment | 50 MB |
| Max files per message | 10 |

## CORS

- Locked to the configured origin (no wildcards)
- Explicit method allowlist (`GET`, `POST`, `PATCH`, `PUT`, `DELETE`)
- Explicit header allowlist
- Authenticated endpoints never use `Access-Control-Allow-Origin: *`

## API Surface Reduction

- OpenAPI/Swagger documentation disabled in production
- Server version headers not exposed
- Error responses return generic messages (no stack traces, no SQL errors)

## File Upload Security

- MIME type allowlist with magic-byte verification (using `filetype` library)
- Filenames sanitized: path components stripped, special characters replaced
- Non-image files force-download (never rendered in-browser)
- Attachments stored outside webroot with path traversal prevention

## Database Security

- Column-allowlisted SQL builder prevents injection
- All queries use parameterized statements (`?` placeholders)
- Database files created with `0600` permissions (owner-only)
- Data directory created with `0700` permissions
- UUID validation on all ID parameters

## Read-Only Mode

Restrict the AI to read-only operations in untrusted or shared environments:

```yaml
safety:
  read_only: true
```

When enabled:

- Only READ-tier tools are available (`read_file`, `glob_files`, `grep`, `introspect`, `ask_user`, canvas tools, `invoke_skill`)
- All WRITE, EXECUTE, and DESTRUCTIVE tools are blocked
- AI cannot modify files, run bash commands, or spawn sub-agents
- Toggle at runtime: `aroom chat --read-only` or `AI_CHAT_READ_ONLY=true`

## Token Budget Enforcement

Denial-of-wallet prevention via configurable token consumption limits:

```yaml
cli:
  usage:
    budgets:
      enabled: true
      max_tokens_per_request: 50000
      max_tokens_per_conversation: 500000
      max_tokens_per_day: 2000000
      warn_threshold_percent: 80
      action_on_exceed: block        # "block" or "warn"
```

### Budget Config Reference

| Field | Type | Default | Env Var | Description |
|---|---|---|---|---|
| `enabled` | bool | `false` | `AI_CHAT_BUDGET_ENABLED` | Enable budget enforcement |
| `max_tokens_per_request` | int | `0` | `AI_CHAT_BUDGET_MAX_TOKENS_PER_REQUEST` | Per-request limit (0 = unlimited) |
| `max_tokens_per_conversation` | int | `0` | `AI_CHAT_BUDGET_MAX_TOKENS_PER_CONVERSATION` | Per-conversation limit (0 = unlimited) |
| `max_tokens_per_day` | int | `0` | `AI_CHAT_BUDGET_MAX_TOKENS_PER_DAY` | Daily limit (0 = unlimited) |
| `warn_threshold_percent` | int | `80` | `AI_CHAT_BUDGET_WARN_THRESHOLD_PERCENT` | Warning at this % of limit |
| `action_on_exceed` | string | `"block"` | `AI_CHAT_BUDGET_ACTION_ON_EXCEED` | `"block"` or `"warn"` |

### Workflow Execution Budgets

Workflow runs have their own budget layer, separate from chat-level token budgets. These caps prevent long-running agentic workflows from consuming unbounded compute, running indefinitely, or triggering denial-of-wallet through automated step loops.

```yaml
workflow:
  budget:
    max_duration_seconds: 3600   # wall-clock cap per run
    max_steps: 100               # total step executions
    max_tokens: 200000           # cumulative token consumption
```

When any limit is hit, the run fails closed with a terminal status of `budget_exceeded:<dimension>` (e.g., `budget_exceeded:max_steps`). Budgets can be set at the definition level (in the workflow YAML) or at the config level; when both are present, the stricter value wins.

### Workflow Credential Binding (#970)

Workflows can securely bind secrets from environment variables or shell commands. Secrets are resolved at step runtime and injected only into the subprocess environment — never logged, stored, or visible in any audit trail.

```yaml
workflow:
  credentials:
    - name: github_token
      env_var: GITHUB_TOKEN
    - name: deploy_key
      command: "op read op://vault/keys/deploy"
```

Security properties:

- Resolved values never appear in logs, error messages, or stored output
- Credentials are validated at definition load time (missing ones fail early)
- Resolution errors are redacted (attacker sees "Credential X failed" not the reason)
- Values exist only in memory during subprocess execution
- Optional `allowed_runners` field restricts a credential to specific runner types (e.g., only shell)

Steps reference credentials via their name:

```yaml
credentials:
  - name: GITHUB_TOKEN
    from: github_token
```

## Sub-Agent Safety

The `run_agent` tool spawns isolated child AI sessions. Multiple layers prevent abuse:

```yaml
safety:
  subagent:
    max_concurrent: 5          # simultaneous sub-agents
    max_total: 10              # total per root request
    max_depth: 3               # nesting levels
    max_iterations: 15         # tool calls per sub-agent
    timeout: 120               # wall-clock seconds (10–600)
    max_output_chars: 4000     # output truncation
    max_prompt_chars: 32000    # prompt size cap
```

### Sub-Agent Config Reference

| Field | Type | Default | Range | Description |
|---|---|---|---|---|
| `max_concurrent` | int | `5` | 1–20 | Simultaneous sub-agents |
| `max_total` | int | `10` | 1–50 | Total per root request |
| `max_depth` | int | `3` | 1–10 | Nesting depth |
| `max_iterations` | int | `15` | 1–100 | Tool calls per sub-agent |
| `timeout` | int | `120` | 10–600 | Wall-clock timeout (seconds) |
| `max_output_chars` | int | `4000` | 100–100,000 | Output truncation |
| `max_prompt_chars` | int | `32000` | 100–100,000 | Prompt size cap |

### Sub-Agent Isolation

- Each sub-agent gets a deep-copied `AIService` config (no shared mutable state)
- Sub-agents have their own message history (cannot see parent conversation)
- Defensive system prompt constrains sub-agent behavior
- At max depth, `run_agent` is removed from the child's available tools
- Parent's approval callback propagates to children (same safety gates)
- Model ID is regex-validated (`^[a-zA-Z0-9._:/-]{1,128}$`)

## MCP Tool Safety

MCP tools are gated by the same safety system as built-in tools, with additional protections:

- **Default tier**: All MCP tools default to `EXECUTE` tier (requires approval in `ask_for_writes` mode)
- **SSRF protection**: DNS resolution validates that target URLs don't point to private IP addresses
- **Shell metacharacter rejection**: Tool arguments sanitized to prevent command injection
- **Tool filtering**: Per-server `tools_include` / `tools_exclude` with fnmatch patterns
- **Trust levels**: Per-server `trust_level` controls [prompt injection defense](prompt-injection-defense.md) envelope wrapping

```yaml
mcp_servers:
  - name: internal-tools
    command: npx
    args: ["-y", "@my-org/tools"]
    trust_level: trusted
    tools_include:
      - "search_*"
      - "read_*"

  - name: external-api
    command: npx
    args: ["-y", "@third-party/api"]
    trust_level: untrusted       # default
    tools_exclude:
      - "admin_*"
```

## Team Config Enforcement

Team administrators can lock security settings across all team members:

```yaml
# team-config.yaml
enforce:
  - safety.approval_mode
  - safety.bash.allow_network
  - safety.bash.allow_package_install
  - safety.denied_tools
  - audit.enabled

safety:
  approval_mode: ask
  bash:
    allow_network: false
    allow_package_install: false
  denied_tools:
    - dangerous_tool

audit:
  enabled: true
```

Enforced fields cannot be overridden by personal config, project config, or environment variables. Project configs require SHA-256 trust verification before being applied.

## Hardening Checklist

!!! tip "Recommended Steps for Production Deployments"

    - [ ] Enable TLS (`app.tls: true`) or use a reverse proxy with proper certificates
    - [ ] Configure `trusted_proxy` if behind a reverse proxy (see below)
    - [ ] Set `session.store: sqlite` for durable sessions
    - [ ] Configure `session.allowed_ips` to restrict access
    - [ ] Set `session.max_concurrent_sessions` to a reasonable limit
    - [ ] Set `safety.approval_mode: ask` or `ask_for_dangerous`
    - [ ] Disable network access in bash: `safety.bash.allow_network: false`
    - [ ] Disable package installs: `safety.bash.allow_package_install: false`
    - [ ] Enable audit logging: `audit.enabled: true`
    - [ ] Enable command audit: `safety.bash.log_all_commands: true`
    - [ ] Configure token budgets to prevent runaway costs
    - [ ] Set up IP allowlisting for known networks
    - [ ] Use team config enforcement for shared security policies
    - [ ] Review MCP server trust levels (default `untrusted` is correct for external servers)
    - [ ] Run `pip-audit` regularly to check for dependency vulnerabilities

## Trusted Proxy Configuration

When Anteroom runs behind a reverse proxy (nginx, Caddy, HAProxy, cloud load balancer), all requests appear to come from the proxy's IP. Without trusted-proxy configuration, rate limiting, IP allowlisting, and session binding operate on the proxy IP rather than the real client IP.

### Example: nginx

```nginx
location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;
}
```

```yaml
trusted_proxy:
  enabled: true
  trusted_cidrs:
    - "127.0.0.0/8"
  header: "X-Forwarded-For"
```

### Example: Caddy

Caddy automatically sets `X-Forwarded-For`. Configure the trusted CIDR to match the Caddy bind address:

```yaml
trusted_proxy:
  enabled: true
  trusted_cidrs:
    - "127.0.0.0/8"
```

### Example: Cloudflare

Cloudflare uses the `CF-Connecting-IP` header for a single client IP (no chain):

```yaml
trusted_proxy:
  enabled: true
  trusted_cidrs:
    - "173.245.48.0/20"
    - "103.21.244.0/22"
    - "103.22.200.0/22"
    - "103.31.4.0/22"
    - "141.101.64.0/18"
    - "108.162.192.0/18"
    - "190.93.240.0/20"
    - "188.114.96.0/20"
    - "197.234.240.0/22"
    - "198.41.128.0/17"
  header: "CF-Connecting-IP"
```

### Security Model

- **Disabled by default**: Direct deployments are unaffected. No forwarded headers are processed unless explicitly configured.
- **Fail closed**: If the socket peer is not in `trusted_cidrs`, forwarded headers are ignored entirely. A direct client cannot spoof its IP via `X-Forwarded-For`.
- **CIDR validation at startup**: Invalid CIDR entries cause a config validation error, preventing silent misconfiguration.
- **Right-to-left walk**: The resolver walks the forwarded chain from right to left, returning the first IP not in the trusted set. This is the same algorithm used by Rails, Express, and Go's standard library.
