Security Model
How earl isolates AI agents from secrets, enforces network policies, sandboxes code execution, and blocks SSRF attacks
Earl is designed so that AI agents can call APIs without ever seeing credentials or making unconstrained network requests. The security model has five independent layers that compose for defense in depth.
Template-Driven Access Control
Agents never make raw HTTP requests. Every API interaction must match a pre-authored HCL template that declares exactly:
- The URL pattern and HTTP method
- Required and optional parameters with types
- Which secrets the template can access (by key reference)
- Whether the operation is read-only or write (
annotations.mode)
# This works -- matches the github.search_issues template
earl call github.search_issues --query "is:issue" --per_page 5
# There is no "raw request" command -- arbitrary URLs are impossibleWrite-mode commands require explicit confirmation. At the CLI, users must pass --yes or type YES at the interactive prompt. The MCP server has a matching --yes flag for automated environments.
Keychain-Backed Secrets
Secret values are stored in the OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager). They are never written to disk in plaintext.
- Templates reference secrets by key name during request rendering (e.g.,
secrets.github.token). - The
earl secrets getcommand returns metadata only (key name, created/updated timestamps) -- never the plaintext value. - Output templates (
result.output) have no access to thesecrets.*namespace.
An AI agent with full shell access to the earl binary still cannot read your secret values. See Secrets for the complete safety model.
Network Egress Rules
By default, earl allows outbound HTTP/HTTPS requests to any public host. You can restrict egress to specific destinations using [[network.allow]] rules in config.toml:
[[network.allow]]
scheme = "https"
host = "api.github.com"
port = 443
path_prefix = "/"
[[network.allow]]
scheme = "https"
host = "api.stripe.com"
port = 443
path_prefix = "/v1"When any [[network.allow]] entry is present, every outbound request must match at least one rule. A rule matches when all four fields match: scheme (case-insensitive), host (case-insensitive), port (including default ports like 443 for HTTPS), and path prefix. Requests to unlisted destinations are blocked.
SSRF Protection
Earl blocks requests to private, reserved, and metadata IP addresses to prevent Server-Side Request Forgery attacks. Validation happens after DNS resolution, so a public hostname that resolves to a private IP is still blocked.
Blocked IPv4 Ranges
| Range | Description |
|---|---|
10.0.0.0/8 | RFC 1918 private |
172.16.0.0/12 | RFC 1918 private |
192.168.0.0/16 | RFC 1918 private |
127.0.0.0/8 | Loopback |
169.254.0.0/16 | Link-local |
224.0.0.0/4 | Multicast |
255.255.255.255 | Broadcast |
0.0.0.0 | Unspecified |
240.0.0.0/4 | Reserved (former Class E) |
198.51.100.0/24, 203.0.113.0/24, 192.0.2.0/24 | Documentation (RFC 5737) |
100.64.0.0/10 | Shared address space (RFC 6598) |
198.18.0.0/15 | Benchmarking (RFC 2544) |
192.0.0.0/24 | IETF protocol assignments |
169.254.169.254 | Cloud instance metadata endpoint |
100.100.100.200 | Cloud metadata (Alibaba) |
Blocked IPv6 Addresses
| Range | Description |
|---|---|
::1 | Loopback |
:: | Unspecified |
ff00::/8 | Multicast |
fe80::/10 | Link-local |
fc00::/7 | Unique local |
fec0::/10 | Site-local (deprecated) |
2001:db8::/32 | Documentation |
fd00:ec2::254 | AWS EC2 IPv6 metadata |
::ffff:169.254.169.254 | IPv4-mapped metadata |
IPv6-mapped IPv4 addresses (e.g., ::ffff:127.0.0.1) are recursively checked against the IPv4 blocklist.
OAuth Endpoint Validation
OAuth endpoint URLs (token_url, authorization_url, device_authorization_url, and OIDC issuer URLs) are validated with the same SSRF checks. Additionally, only https URLs are permitted -- except http to loopback addresses (127.0.0.1, ::1, localhost) for local development.
Execution Sandboxing
Bash and SQL protocols run in sandboxed environments to limit what agent-invoked operations can do.
Bash Sandbox
Every Bash script runs inside an OS-level sandbox. Earl requires a sandbox tool to be installed:
On Linux, earl uses bubblewrap (bwrap) with:
- Targeted read-only mounts of
/usr,/bin,/sbin,/lib,/lib64,/etc(not the entire root filesystem) /tmpas tmpfs -- an isolated temporary directory/devand/procmounted for basic process operation- PID namespace isolation (
--unshare-pid) - IPC namespace isolation (
--unshare-ipc) - UTS namespace isolation (
--unshare-uts) - Network isolation (
--unshare-net) by default -- templates must explicitly setsandbox.network = trueto allow network access --die-with-parentensures child processes are killed if earl exits- Working directory is mounted read-only; only paths listed in
sandbox.writable_pathsget read-write mounts
On macOS, earl uses sandbox-exec with a dynamically generated Seatbelt profile:
- deny-default policy -- everything is denied unless explicitly allowed
- Network denied by default -- templates must set
sandbox.network = trueto allownetwork*operations - Process execution allowed (
process-exec,process-fork) - File reads allowed system-wide (required for system libraries and dyld shared cache)
- File writes denied except for
/dev/null,/dev/tty, and paths listed insandbox.writable_paths - Mach IPC scoped to a curated list of system services (
com.apple.system.logger,com.apple.SecurityServer, etc.) sysctl-readallowed for basic system queries
Templates can configure sandbox behavior per-command:
operation {
bash {
script = "curl -s https://api.example.com/data"
sandbox {
network = true # Allow network access (default: false)
writable_paths = ["output"] # Paths relative to cwd that can be written
max_time_ms = 30000 # Kill script after 30 seconds
max_output_bytes = 1048576 # Limit stdout/stderr to 1 MB
}
}
}If neither sandbox.max_time_ms nor sandbox.max_output_bytes is set, the transport-level timeout_ms and max_response_bytes apply as fallbacks.
The sandbox kills the entire process group on timeout using SIGKILL via killpg(), preventing child process leaks.
SQL Sandbox
SQL queries execute through parameterized queries with configurable safety controls:
- Read-only mode (
sandbox.read_only = true) enforces read-only at the database level:- PostgreSQL: Wraps the query in
BEGIN READ ONLY/ROLLBACK - MySQL: Uses
START TRANSACTION READ ONLY/ROLLBACK - SQLite: Appends
mode=roto the connection URL
- PostgreSQL: Wraps the query in
- Row limits (
sandbox.max_rows) cap the number of returned rows - Timeouts (
sandbox.max_time_msor transport-leveltimeout_ms) abort long-running queries - Connection credentials are stored in the OS keychain and referenced by secret key (
connection_secret) -- never embedded in templates
operation {
sql {
connection_secret = "mydb.connection_url"
query = "SELECT id, name FROM users WHERE active = ?"
params = [true]
sandbox {
read_only = true
max_rows = 100
}
}
}SQL error messages are passed through the redactor before being shown to the user, preventing accidental credential leakage in error output.
Secret Redaction
If a secret value appears anywhere in command output -- for example, an API that echoes a token in its response -- earl's redactor replaces every occurrence with [REDACTED] before displaying the result.
The redactor detects secrets in multiple encodings:
- Raw -- the literal secret string
- Base64 (standard and URL-safe)
- Hex (lowercase and uppercase)
- URL-encoded (
%-encoded form)
For each secret with 6 or more characters, all encoding variants are generated and searched (six forms total: raw plus five encodings). Longer matches are replaced first to avoid partial replacements.
Redaction applies to both human-readable output and --json structured output (the redactor walks the entire JSON value tree and redacts every string field).
File Permissions
The security model assumes that AI agents cannot modify earl's configuration or template files. If an agent can edit config.toml, it can add permissive network rules. If it can edit templates, it can exfiltrate secrets.
Lock down file permissions after setup:
# Protect config directory
chmod 700 ~/.config/earl
chmod 600 ~/.config/earl/config.toml
chmod -R go-w ~/.config/earl/templates
# Protect workspace templates
chmod -R 700 ./templates/The secrets metadata index at ~/.local/state/earl/secrets-index.json is written with 0600 permissions on Unix systems (owner read-write only). It stores only key names and timestamps, never secret values.
For stronger isolation, run the AI agent as a separate, less-privileged user that can execute earl but cannot modify its configuration files.