> ## Documentation Index
> Fetch the complete documentation index at: https://docs.feral.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Security Model

> BlindVault, permission tiers, execution sandboxing, and autonomy gating.

> **Status: Stable** — BlindVault, permission tiers, and execution sandboxing are production-ready.

# Security Model

FERAL assumes the LLM is untrusted. Credentials, tool execution, and autonomy are all gated through layered security primitives that prevent prompt injection from escalating into real-world damage.

## BlindVault

The **BlindVault** stores all secrets (API keys, OAuth tokens, database passwords) encrypted at rest in `~/.feral/credentials.enc` (mode `0600`). Encryption is **ChaCha20-Poly1305** (AEAD) with a key derived from your master passphrase via **Argon2id**; the derived key is cached in your OS keychain (macOS Keychain / GNOME Keyring / Windows Credential Manager) so the vault unlocks transparently on brain start. The LLM never sees raw credential values — the vault injects them at the HTTP layer right before a request leaves the process.

```python theme={null}
from feral_core.security import BlindVault

vault = BlindVault()
vault.store("weather_api", "sk-abc123...")

# When a skill fires, the vault injects the key:
headers = vault.inject("weather_api", {"X-API-Key": "$CREDENTIAL"})
# headers == {"X-API-Key": "sk-abc123..."}
```

The LLM sees only a placeholder like `[CREDENTIAL:weather_api]` in tool descriptions. Even if the model tries to exfiltrate it, the raw value is never in its context window.

### Vault Storage

Credentials are stored during setup (`feral setup`) and persisted to `~/.feral/credentials.enc`. The vault injects secrets at the HTTP layer — the LLM never sees raw values.

## Permission Tiers

Every tool is tagged with a **PermissionTier** that determines what approval is needed before execution.

| Tier         | Auto-execute?   | Examples                                       |
| :----------- | :-------------- | :--------------------------------------------- |
| `passive`    | Always          | Read memory, search web, get weather           |
| `active`     | In hybrid/loose | Send a message, create a file                  |
| `privileged` | Only in loose   | Run shell command, install package             |
| `dangerous`  | Never auto      | Delete files, send money, modify system config |

Tiers are declared in tool definitions:

```python theme={null}
from feral_core.security import PermissionTier

@feral_tool(
    description="Delete a file from the filesystem",
    permission=PermissionTier.DANGEROUS,
)
async def delete_file(self, path: str) -> dict:
    ...
```

## ExecutionSandbox

Tools tagged `privileged` or above run inside an **ExecutionSandbox** that constrains what the subprocess can do.

```python theme={null}
from feral_core.security import ExecutionSandbox

sandbox = ExecutionSandbox(
    allow_network=False,
    allow_fs_write=["/tmp/feral-scratch"],
    max_runtime_seconds=30,
    max_memory_mb=256,
)
result = await sandbox.run(["python3", "untrusted_script.py"])
```

The sandbox uses OS-level isolation (`seccomp` on Linux, `sandbox-exec` on macOS) plus a process timeout. WASM skills get Wasmtime's capability-based sandbox automatically.

## Autonomy Levels

FERAL supports three autonomy modes that control how the PermissionTier system gates execution. See the [Autonomy Levels](/guides/autonomy) guide for full details.

| Mode     | Behavior                                                                |
| :------- | :---------------------------------------------------------------------- |
| `strict` | Every tool call requires user approval                                  |
| `hybrid` | `passive` + `active` auto-execute; `privileged` + `dangerous` ask first |
| `loose`  | Everything except `dangerous` auto-executes                             |

Set via environment variable or config:

```bash theme={null}
export FERAL_AUTONOMY=hybrid
```

```json theme={null}
// ~/.feral/settings.json
{ "autonomy": { "mode": "hybrid" } }
```

## SandboxPolicy Files

For fine-grained control, drop a YAML or JSON policy file in `~/.feral/policies/`:

```yaml theme={null}
# ~/.feral/policies/production.yaml
name: production
autonomy: hybrid

sandbox:
  allow_network: true
  allow_fs_write:
    - /tmp/feral-scratch
    - ~/.feral/memory.db
  max_runtime_seconds: 60
  max_memory_mb: 512

tool_overrides:
  shell_exec:
    permission: dangerous
  web_search:
    permission: passive
  send_email:
    permission: privileged
    require_confirmation_body: true
```

Load a named policy at startup:

```bash theme={null}
feral start --policy production
```

Policies are composable — you can layer a base policy with per-session overrides:

```python theme={null}
from feral_core.security import SandboxPolicy

base = SandboxPolicy.load("production")
session_policy = base.overlay({
    "sandbox": {"allow_network": False},
    "tool_overrides": {"shell_exec": {"permission": "privileged"}},
})
```

## Dangerous-Tool Deny Lists

Even in `loose` mode, certain tools are **always** gated. The `dangerous_tools` surface deny list is hard-coded and cannot be overridden by policy files:

```python theme={null}
DANGEROUS_TOOLS_DENY_LIST = [
    "delete_all_memory",
    "wipe_database",
    "send_payment",
    "modify_system_files",
    "disable_security",
]
```

You can extend (but never shrink) this list in config:

```json theme={null}
// ~/.feral/settings.json
{
  "dangerous_tools_extra": [
    "deploy_production",
    "revoke_all_tokens"
  ]
}
```

## enforce\_safety

The `enforce_safety()` function runs before every tool execution. It checks:

1. The tool's PermissionTier against the current autonomy level.
2. Whether the tool is on the deny list.
3. Whether a SandboxPolicy restricts the action.
4. Whether a standing approval exists (see [Autonomy Levels](/guides/autonomy)).

```python theme={null}
from feral_core.security import enforce_safety

allowed, reason = await enforce_safety(
    tool_name="shell_exec",
    args={"command": "rm -rf /tmp/old-cache"},
    session=current_session,
)
if not allowed:
    await request_approval(tool_name, args, reason)
```

If the check fails, the orchestrator pauses execution and surfaces an approval request to the user via the active channel (web UI, CLI, Telegram, etc.).

## Hardening (v1.2.1)

The v1.2.1 release addressed a comprehensive security audit. All fixes are defense-in-depth measures that apply by default — no configuration required.

### Path Traversal Protection

The catch-all route serving the WebUI static files now uses `Path.resolve()` followed by `is_relative_to()` to reject any request that escapes the static directory (e.g. `../../etc/passwd`).

### SQL Injection Whitelist on Sync

The P2P sync engine only accepts operations targeting a hardcoded whitelist of table names: `notes`, `episodes`, `conversations`, `knowledge`, `wiki_pages`. Any other table name is rejected before query construction.

### CORS Restricted to Localhost

The default CORS `allow_origins` was changed from wildcard `*` to `localhost:5173,localhost:9090`. Production deployments should set explicit origins via configuration.

### XSS Sanitization via DOMPurify

The server-driven UI (SDUI) renderer in the React client now passes all HTML content through DOMPurify before rendering. This prevents any LLM-generated or server-pushed markup from executing scripts.

### Docker Sandbox Host Fallback Disabled

When the Docker sandbox cannot connect to the Docker daemon, it now raises an error instead of falling back to direct host execution. This prevents a missing Docker installation from silently bypassing container isolation.

### Docker Sandbox Runtime Hardening

Every container started by `security/docker_sandbox.py` is launched with these defaults — no configuration needed:

* `--cap-drop ALL` — every Linux capability is dropped.
* `--security-opt no-new-privileges` — execve cannot raise privileges (blocks setuid escapes).
* `--pids-limit 128` — caps process fan-out so a fork-bomb cannot exhaust the host PID namespace.
* `--read-only` root filesystem with a tmpfs mount at `/tmp` (`nosuid`, 128 MB).
* `--network none` unless the caller explicitly requests network access.
* Runs as the unprivileged `sandbox` user inside the image.
* Optional seccomp profile via `FERAL_SANDBOX_SECCOMP_PROFILE` (the literal value `unconfined` is rejected — supply a JSON profile path or leave unset).

Tune any of these via the environment variables documented in [Environment Variables → Docker sandbox hardening](/reference/environment#docker-sandbox-hardening). The minimum effective `pids-limit` is `16` regardless of what you set.

### Command Injection Blocked in Direct Execution

The daemon's direct execution path no longer passes raw strings to a shell. Shell metacharacters and injection patterns are rejected by a safety filter before any command is executed.

### Default Bind Address: 127.0.0.1

The `FERAL_HOST` default was changed from `0.0.0.0` (all interfaces) to `127.0.0.1` (loopback only). This prevents accidental exposure on public networks. Set `FERAL_HOST=0.0.0.0` explicitly if you need LAN/remote access.

### NODE\_API\_KEY Requires Explicit Configuration

The WebSocket authentication token (`NODE_API_KEY`) no longer ships with a default value. If unset, daemon-to-brain authentication is effectively disabled — you must configure it explicitly for any multi-node deployment.

### API Keys in Headers, Not URL Parameters

The Gemini API key is now sent via the `x-goog-api-key` request header instead of as a URL query parameter. This prevents key leakage in server logs, browser history, and referrer headers.

### Tool Safety: CONFIRM Before AUTO

The tool safety classifier now checks CONFIRM patterns before AUTO patterns. Previously, a tool matching both lists would be auto-executed; now it requires user confirmation first.

## Limitations and Caveats

### What FERAL Does NOT Protect Against

* **Physical access attacks**: If someone has physical access to the machine running the Brain, they have access to all data.
* **Supply chain attacks**: Third-party LLM providers can see your prompts (use Ollama for full local processing).
* **Side-channel attacks**: The timing and size of WebSocket messages may reveal information about your activity.
* **Compromised LLM**: If the LLM provider is compromised, tool calls may be manipulated.

### Platform Differences

* **macOS**: Full Accessibility permissions required for desktop automation. Gatekeeper may block unsigned daemons.
* **Linux**: Docker required for code interpreter sandboxing. X11/Wayland differences affect screen capture.
* **Windows**: Limited support. No systemd daemon management.

### Qualified Claims

* Voice latency depends on the provider (OpenAI Realtime \~200ms, Gemini Live \~300ms, local Whisper+Piper \~500ms). FERAL adds \~50ms of WebSocket relay overhead.
* "Local-first" means the Brain runs locally, but cloud LLM providers are used by default. For fully local operation, configure Ollama.
