Isolation and Security
How Appstrate isolates runs, keeps credentials away from agents and restricts outbound traffic.
Every run executes untrusted, model-driven code next to credentials that must never reach it. Appstrate's answer has four parts: an isolated network per run, a per-run sidecar that holds the credentials, URL authorization on every outbound call, and hardened containers. This page covers what an operator controls. The full threat model is in SECURITY.md, and the protocol details are in SIDECAR.md.
How the guarantees depend on the backend:
| Backend | Network isolation | Filesystem isolation | Use it for |
|---|---|---|---|
docker | Per-run internal network | Per-run containers | Production |
firecracker | MicroVM per run, guest firewall | MicroVM per run | Production, stronger boundary. See FIRECRACKER.md for its status and residual risks |
process | None | None | Local development and evaluation only |
Per-run network (Docker)
For each run the platform creates an internal bridge network named appstrate-exec-<runId>. Two containers join it: the agent and a fresh sidecar. The sidecar also joins appstrate-egress, a shared network with internet access. Nothing on the run network can reach the host, other runs, or the internet except through the sidecar. When the run ends, the containers and the network are removed, and leftovers from a crash are reclaimed at the next boot.
There is no sidecar pool: every run starts its own sidecar, in parallel with the agent. appstrate-egress is created once and never deleted, because several API processes can share a Docker daemon.
Integrations of source.kind: "local" run as extra containers on the same run network. Their egress is bounded by the connection's authorized_uris, enforced by the sidecar.
Credentials stay in the sidecar
Credentials are stored encrypted (AES-256-GCM, envelope format v1:<kid>:...) with CONNECTION_ENCRYPTION_KEY. The agent never receives them. Its only cross-boundary channel is the sidecar's MCP endpoint, where each opted-in integration appears as an api_call tool. The sidecar fetches the credential, injects it into the outbound request, checks the target URL, and returns the response.
The agent container is also denied the means to reach the sidecar's other surfaces:
- Run-scoped secrets (the sidecar URL, the sidecar auth token, the sink credentials) are handed to the runtime over stdin, never through the environment, and the processes holding them are non-dumpable.
- Every sidecar route except
/healthrequires a per-run token, compared in constant time. - The agent has no
RUN_TOKENand no route to the platform API.
LLM calls take the same path. The agent's model SDK talks to the sidecar's /llm endpoint, so provider keys (SYSTEM_PROVIDER_KEYS or an organization's own) are never exposed to it. Outbound proxying (PROXY_URL, system proxies, per-organization proxies) applies on top. See Proxies and Sandbox and Sidecar.
Egress rules
Every outbound call from an integration passes the same checks, on the target and on every redirect hop:
authorized_urisis mandatory. A call is allowed only if its URL matches an entry the integration declares. An empty or missing list authorizes nothing, andallow_all_urisis dropped for any call that carries a credential. An entry whose host the caller could choose (https://**) is refused for credentialed calls, and so is a host wildcard that does not sit under a registrable domain written in the entry, judged with the Public Suffix List (https://*.co.uk/**,https://*.vercel.app/**). Through a wildcard that passes, the credential reaches a host only when that host's own registrable domain lies inside the entry's literal part:https://*.amazonaws.com/**carries it tosts.amazonaws.com, not todynamodb.us-east-1.amazonaws.com. These calls are refused ascredential_exfiltration_refused.- Blocked network ranges. Loopback, link-local, private (RFC 1918), IPv6 unique-local and mapped addresses, and the cloud metadata addresses are refused. So are the host names
localhostand every*.localhostname (loopback by RFC 6761), and the container namessidecarandagentandhost.docker.internal. The platform resolves DNS itself, refuses the call if any record lands in a blocked range, and connects to the validated address (resolve and pin), which defeats DNS rebinding. A host that does not resolve returns502, a blocked one403. - Redirects. A redirect to a host outside
authorized_urisis refused. A redirect to another origin strips credentials and the request body. - Internal hosts are opt-in on both sides. To let an integration reach a private or internal API, the integration's
authorized_urismust name that host literally, and the operator must list it inEGRESS_ALLOW_INTERNAL_HOSTS(comma-separated hostnames). Neither alone is enough. A listed host is trusted by every organization of the instance and by every egress site that reads the variable (OAuth token exchange, LLM upstreams, organization proxies, remote MCP servers). Add only hosts that every organization may call. See the full semantics in Environment Variables. - The sidecar needs working DNS. It resolves the target host of an
api_callitself, even when it sends traffic throughPROXY_URL. Without DNS the call fails with502 Target host could not be resolved. The one exception is a host that rule 4 trusts (named literally inauthorized_urisand listed inEGRESS_ALLOW_INTERNAL_HOSTS): it is not looked up.
The same blocklist guards webhook URLs at creation and at delivery. See Webhooks.
A run that is refused logs Target refused (SSRF) with the host in the sidecar log. A model on a private endpoint (Ollama, a LAN vLLM) is called by the platform's LLM proxy, so its host must be in EGRESS_ALLOW_INTERNAL_HOSTS and reachable from the API process.
Live model catalog
The API process reads a signed model catalog from get.appstrate.dev so that a model a vendor ships becomes selectable without a release. It does this in the background when it starts and then every hour (up to two anonymous GET requests, redirects refused, 10 seconds each), and boot never waits on it. Nothing about your instance is sent and nothing is stored: the accepted file lives in memory. It only adds models an organization can bind with its own credentials, and a file is refused unless its signature verifies against a key built into the platform. A read that fails is logged as a warning (model catalog not refreshed) and the instance keeps serving the bundled registry.
To stop the reads, set MODEL_CATALOG_URL=off; the instance then runs on the bundled registry alone. An empty value is not a switch, it means the default. To read the catalog from a mirror, set the variable to that URL (http or https). The variable is described in Environment Variables, and the design in MODEL_CATALOG.md.
Container hardening and resource limits
Run containers drop all Linux capabilities (CapDrop: ALL), set no-new-privileges, and cap the process count (256 for the agent). Memory and CPU come from the run limits and are applied at the Docker API level:
- Without a hint from the agent manifest, a run gets 1536 MiB and 2 vCPU.
PLATFORM_RUN_LIMITSsets operator ceilingsagent_memory_ceiling_mbandagent_cpu_ceiling(both default to the values above). A manifest hint above the ceiling is capped.PLATFORM_RUN_LIMITS.timeout_ceiling_seconds(default 1800) caps the runtime of any run. A manifest timeout above it is clamped, and a run that hits it ends with thetimeoutstatus.
See Rate Limits for the other keys and the resource guide for the manifest hints.
Liveness watchdog
A run must prove it is alive. Until its first event, the platform vouches for it for up to RUN_BOOT_DEADLINE_SECONDS (default 300), which covers image pulls and container boot. After that, the runner sends a heartbeat every RUN_HEARTBEAT_INTERVAL_SECONDS (default 15), and a run silent for more than RUN_STALL_THRESHOLD_SECONDS (default 60) is failed. A sweep runs every RUN_WATCHDOG_INTERVAL_SECONDS (default 15). Keep the stall threshold at three heartbeats or more.
Runtime image version contract
The platform, PI_IMAGE and SIDECAR_IMAGE change in lockstep. Deploy all three at the same version. Boot fails when the two runtime image tags differ, or when all three are release versions that disagree. Never rebuild only one runtime image. See the PI_IMAGE row in Environment Variables.
The Docker socket
The docker backend needs the Docker socket, and access to it is effectively root on the host. Harden around it:
- Keep the socket owned by
root:dockerwith mode0660, and run the platform container with the host's docker group (group_add: ["${DOCKER_GID:-0}"], as the tier templates do). - The root
docker-compose.ymlships a commentedappstrate-docker-proxyservice that filters Docker API verbs. Read its comment before enabling it: it narrows the reachable API surface but is not a security boundary, because container creation is itself a host-root primitive, and the platform needs a build whose Docker client can target a TCPDOCKER_HOST. - For a real boundary, use
RUN_ADAPTER=firecracker, or a rootless or VM-isolated Docker daemon.
Run containers are siblings of the platform container, not children: there is no Docker-in-Docker.
Authentication and secrets
- Auth mode. Run closed on any instance that is not a public SaaS. See Self-Hosting and AUTH_MODES.md.
- Five required secrets, each independent:
BETTER_AUTH_SECRET,CONNECTION_ENCRYPTION_KEY,UPLOAD_SIGNING_SECRET,RUN_TOKEN_SECRETandCONNECT_SESSION_SECRET. - Rotation is supported for the signing secrets and the encryption key.
UPLOAD_SIGNING_SECRET,RUN_TOKEN_SECRETandCONNECT_SESSION_SECRETaccept a comma-separated keyring: the first key signs, all keys verify. The auth secret rotates through Better Auth's own keyring,BETTER_AUTH_SECRETS(<version>:<secret>pairs, current first): prepend a new version and restart, and leaveBETTER_AUTH_SECRETunchanged, because it still decrypts what was written before the keyring. A rotation ends sessions, in-flight social sign-ins and emailed links signed with the old secret.CONNECTION_ENCRYPTION_KEYrotates through a key id,CONNECTION_ENCRYPTION_KEYSand a re-encryption script. The procedure is in Environment Variables. - Bundle signing.
AFPS_SIGNATURE_POLICY(off,warnby default, orrequired) withAFPS_TRUST_ROOTcontrols verification of package signatures before execution.
Agent HTML previews
Agents can publish HTML. It is untrusted, so the dashboard renders it only inside a sandboxed iframe, and a top-level navigation to the preview URL shows source text, never a rendered page. Set USERCONTENT_URL to a separate registrable domain to give previews their own cookie jar and storage partition.
When Docker is not available
With RUN_ADAPTER=process (the code default), the sidecar and the agent are host subprocesses. Credentials still stay in the sidecar process, but you lose network isolation, filesystem isolation and host protection: the agent can reach anything the host can. A source.kind: "local" integration is refused outright unless you also set INTEGRATION_RUNTIME_ADAPTER=docker, because a same-user child process could read the sidecar's environment. Use this mode for solo development or a trusted evaluation only.
Air-gapped deployments
Appstrate has no required external service at boot. For a network without internet access:
- Mirror the images (
PI_IMAGE,SIDECAR_IMAGE, the fiveRUNNER_IMAGE_*and the platform image) to a private registry and set the variables to those references. Keep the version contract. - Use filesystem storage or an internal S3 endpoint.
- Send outbound traffic through your gateway with
PROXY_URLorSYSTEM_PROXIES. - Point
SYSTEM_PROVIDER_KEYSat a model endpoint you can reach, and list its host inEGRESS_ALLOW_INTERNAL_HOSTSif it is on a private address. - Set
MODEL_CATALOG_URL=off, or the API logs a warning every hour for a catalog it cannot reach (see Live model catalog). - Leave OpenTelemetry off. It exports nothing unless
@appstrate/module-observabilityis inMODULESand an endpoint is configured.
What Appstrate does not do
- It does not terminate TLS. Use a reverse proxy.
- It does not run Docker-in-Docker.
- It does not back up your data. See Production Checklist.
Reporting a vulnerability
Use GitHub private vulnerability reporting or write to [email protected]. Do not open a public issue. The policy, response times and supported versions are in SECURITY.md.
Next
- Production Checklist lists what to do before going live.
- Environment Variables is the full reference.