Skip to content

Security: Parcha-ai/ati

Security

docs/SECURITY.md

ATI Security Model

Overview

ATI (Agent Tools Interface) is designed to give AI agents access to external tools without exposing API keys. It supports two execution modes with different security properties.

Execution Modes

Local Mode (default)

Keys are encrypted and provisioned into the sandbox. ATI decrypts them in process memory, makes API calls directly, and the keys never appear in environment variables or readable files.

Trade-off: Keys ARE in the sandbox (encrypted, memory-locked), but no external infrastructure needed.

Proxy Mode (opt-in via ATI_PROXY_URL)

Zero credentials in the sandbox. ATI forwards tool calls to an external proxy server that holds the real API keys. The sandbox never sees any key material.

Trade-off: Stronger isolation, but requires running a proxy server and adds a network hop.

Mode Auto-Detection

ati run checks the ATI_PROXY_URL environment variable:

  • Set → proxy mode (forward to proxy server)
  • Not set → local mode (use keyring)

The agent never needs to know which mode is active.

Local Mode Security

Key Delivery

  1. Orchestrator generates a random 256-bit session key per sandbox
  2. Orchestrator encrypts scoped API keys with session key → keyring.enc
  3. Session key written to /run/ati/.key on tmpfs (mode 0400)
  4. keyring.enc + tool manifests uploaded to sandbox

First ATI Invocation

  1. ATI reads /run/ati/.key, immediately unlink()s the file
  2. ATI decrypts keyring.enc with AES-256-GCM
  3. mlock() on decrypted memory (prevents swap-out)
  4. Session key zeroed from stack
  5. Keys exist only in ATI's locked heap

Encryption Details

  • Algorithm: AES-256-GCM (authenticated encryption with associated data)
  • Session key: 256-bit random, unique per sandbox session
  • Nonce: 96-bit random, prepended to ciphertext
  • Format: [12-byte nonce][ciphertext + 16-byte GCM tag]
  • Rust crate: aes-gcm v0.10

Attack Surface (Local Mode)

Attack Vector Mitigation
env / printenv No secrets in environment variables
cat /run/ati/.key File deleted after first read
strings /usr/local/bin/ati Binary contains only encrypted blobs
cat ~/.ati/keyring.enc Encrypted; session key is gone
/proc/$(pgrep ati)/environ Clean — no secret env vars
/proc/$(pgrep ati)/mem Requires ptrace — blocked by seccomp
strace ati run ... Blocked by sandbox seccomp (no ptrace)
Memory dump / core dump madvise(DONTDUMP) excludes secret pages
Swap file mlock() prevents swap-out
Agent imports ATI internals ATI is compiled Rust binary, not importable

Honest Limitations (Local Mode)

  • If the sandbox has no seccomp profile, ptrace can read ATI's memory
  • If the agent can run arbitrary code with sufficient privileges, it could theoretically extract keys from ATI's process memory during execution
  • The session key exists in /run/ati/.key briefly before ATI reads it — a race condition is theoretically possible
  • mlock() is best-effort on some kernels (may silently fail if RLIMIT_MEMLOCK is low)

Proxy Mode Security

How It Works

  1. Orchestrator starts the proxy server: ati proxy --port 8090 --ati-dir /path/to/ati
  2. Orchestrator sets ATI_PROXY_URL in the sandbox environment
  3. ATI sends POST /call with {tool_name, args} to the proxy
  4. Proxy server validates the request, injects API keys from its keyring, calls the upstream API
  5. Proxy returns the result to ATI in the sandbox

The proxy server is built into the same ati binary — run ati proxy to start it. It loads manifests and keyring from its own ATI directory.

What's in the Sandbox (Proxy Mode)

  • ATI binary
  • Tool manifests (.toml files) — tool definitions only, no secrets
  • Scope config (scopes.json) — which tools are allowed
  • ATI_PROXY_URL — URL of the proxy server

What's NOT in the Sandbox (Proxy Mode)

  • No keyring.enc
  • No session key (.key file)
  • No API keys in any form (encrypted or otherwise)

Attack Surface (Proxy Mode)

Attack Vector Mitigation
Any local key extraction No keys exist in the sandbox
ATI_PROXY_URL leak URL is not secret; proxy validates requests
Man-in-the-middle on proxy Use HTTPS for proxy URL
Proxy server compromise Same risk as any credential store (Vault, etc.)
Agent sends malicious requests Proxy validates JWT scopes on every request
Agent enumerates tools/skills it shouldn't see Discovery endpoints are scope-filtered

Honest Limitations (Proxy Mode)

  • The proxy URL is visible via printenv — not a secret, but reveals infrastructure
  • Proxy server is a single point of failure; if it's down, all tool calls fail
  • Extra network hop adds latency (~10-50ms per call)
  • Proxy server itself must be secured — it holds the real API keys

Scope Enforcement

Scope behavior is intentionally split between production and development:

  • JWT configured: ATI requires a valid ATI_SESSION_TOKEN and enforces scopes strictly.
  • No JWT configured: ATI runs in unrestricted dev mode for local setup and testing.

When JWT validation is configured, scopes are enforced consistently across:

  • ati run in local mode
  • proxy /call
  • proxy /mcp (tools/list and tools/call)
  • proxy metadata and discovery endpoints (/tools, /skills, /help)

Other scope properties:

  • Scopes are generated by the orchestrator and carried in the JWT scope claim
  • Denied tools return clear authorization errors
  • Scope expiry is enforced via JWT exp
  • Wildcards such as tool:github:* are supported
  • Help access requires the help scope when JWT auth is enabled

Scope-Driven Discovery

Tool and skill listing is scope-filtered — callers only see what their JWT allows:

  • GET /tools returns only tools the caller's scopes permit
  • GET /skills returns only skills reachable from the caller's tool/provider/category scopes
  • MCP tools/list returns only the scoped tool subset
  • ati assist / POST /help builds its LLM context from visible tools and skills only

This prevents scope enumeration: an agent with tool:web_search cannot discover the existence of other tools, providers, or skills.

Skill Scope Resolution

Skills are resolved transitively from tool scopes:

  1. skill:X → load skill X directly
  2. tool:Y → skills whose tools binding includes Y
  3. Tool Y's provider → skills whose providers binding includes that provider
  4. Provider's category → skills whose categories binding includes that category
  5. Any loaded skill's depends_on → transitively load those dependencies

Legacy underscore JWT scopes (e.g. tool:github_search_repos) are normalized to their colon-namespaced equivalents (tool:github:search_repos) for scope checking, maintaining backward compatibility without weakening enforcement.

SSRF Protection

ATI's HTTP executor optionally blocks Server-Side Request Forgery attacks via ATI_SSRF_PROTECTION:

Mode Behavior
Not set (default) No SSRF checking
warn Log a warning but allow the request
1 or true Block requests to private/internal addresses

When enabled, ATI resolves the target hostname and rejects requests to:

  • Loopback addresses (127.0.0.0/8, ::1)
  • Private network ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
  • Link-local addresses (169.254.0.0/16, fe80::/10)
  • Other IPv6 local ranges

DNS resolution is performed at check time to cover hosts that resolve to private IPs. This applies to hand-written HTTP tools; MCP and CLI tools are not currently in scope for SSRF checking.

GCS Skill Registry Security

Two registry transports are supported via ATI_SKILL_REGISTRY:

ATI_SKILL_REGISTRY=gcs://bucket — direct GCS access:

  • GCS credentials (gcp_credentials keyring key) are a full GCP service account JSON — treat them with the same care as any API key
  • In local mode, credentials come from the ATI keyring (encrypted at rest)
  • In proxy mode, credentials live only on the proxy server; agent sandboxes never see them
  • Remote skill content is fetched on demand and cached in memory for the session; nothing is written to disk

ATI_SKILL_REGISTRY=proxy — fetch skills through the ATI proxy:

  • Requires ATI_PROXY_URL to be set; ATI_SESSION_TOKEN provides auth
  • The sandbox holds zero GCS credentials — skill content is served by the proxy's /skillati/* endpoints
  • Useful for network-restricted sandboxes that cannot reach GCS directly but can reach the proxy
  • Skill access is still JWT-scope-gated on the proxy side

Both transports: skill content is injected into LLM context via ati assist — operators should audit skills in their registry for prompt injection risks before deploying.

Memory Security (Local Mode)

  • Decrypted keys held in HashMap<String, String> with Zeroize on drop
  • Raw JSON bytes also zeroized on drop
  • mlock() prevents kernel from swapping secret pages
  • madvise(MADV_DONTDUMP) excludes from core dumps
  • All security functions degrade gracefully on non-Linux (warning, not error)

Choosing a Mode

Scenario Recommended Mode
Standard sandbox deployment Local — simpler, no extra infrastructure
High-security / regulated workloads Proxy — zero key exposure
Air-gapped or offline sandboxes Local — no network dependency
Multi-tenant with shared sandboxes Proxy — strongest isolation
Development / testing Local — easiest to set up

Verifying Your ATI Binary

Every release binary is cryptographically signed and auditable. Three levels of verification:

GitHub Attestation (recommended)

Release binaries are attested by GitHub Actions using artifact attestations (SLSA Level 3). This cryptographically proves the binary was built from a specific commit in the official repo by the official CI workflow.

gh attestation verify ./ati --repo Parcha-ai/ati

If this fails, do not use the binary. It was not built by our CI.

SHA256 Checksum

Each release includes .sha256 files alongside the binaries:

sha256sum -c ati-x86_64-unknown-linux-musl.sha256

Inspecting Embedded Dependencies

ATI binaries are built with cargo-auditable, which embeds a compressed dependency manifest inside the binary itself. You can inspect what dependencies were compiled into any ATI binary:

cargo audit bin ./ati

If an attacker rebuilt the binary with injected dependencies, they show up here. This is forensic detection — the audit trail lives inside the binary, not in a separate file that could also be swapped.

CI/CD Security

Dependency Policy

ATI enforces a strict dependency policy via cargo-deny (checked on every push):

  • No unknown registries — all crates must come from crates.io (blocks typosquatting via alt registries)
  • No git source dependencies — prevents "point Cargo.toml at an evil fork" attacks
  • Known vulnerabilities denied — checked against the RustSec advisory database
  • License allowlist — only OSI-approved permissive licenses

Pinned GitHub Actions

All CI/CD workflow actions are pinned to full commit SHAs, not mutable version tags. This prevents the class of attack demonstrated by CVE-2025-30066 (tj-actions/changed-files), where an attacker force-pushed malicious code to a version tag.

There aren't any published security advisories