Security / How we protect your runs

# Defense in depth. On by default.

Every request crosses independent security layers — perimeter, auth, policy, execution, data. A failure in one is caught by the next. Your workflows run in ephemeral sandboxes that are destroyed after each step, and every run leaves a tamper-evident trail.

posture
model zero trust
sandboxes ephemeral
audit SHA-256 chain
residency EU enforceable
Ephemeral sandboxes HMAC-SHA256 keys 30+ credential patterns SSRF prevention SHA-256 hash-chain audit EU data residency Fernet encryption at rest Hard budget caps Global emergency stop On by default Ephemeral sandboxes HMAC-SHA256 keys 30+ credential patterns SSRF prevention SHA-256 hash-chain audit EU data residency Fernet encryption at rest Hard budget caps Global emergency stop On by default
The controls — for the record

## What's built in.

Every measure below ships with Sandcastle out of the box. No plugins, no paid tiers, no configuration required for the defaults.

01

### Sandbox isolation

Sandboxing

AI agent code runs in ephemeral sandboxes that are destroyed after each execution. No shared state, no persistent access to host resources.

- E2B — cloud-hosted microVMs with SOC 2 Type II certification
- Docker — `CapDrop: ALL`, seccomp syscall allowlist, PID limits, CPU quotas, non-root user (UID 1000), bridge networking, auto-remove
- Seccomp profiles — block ptrace, mount, kexec_load, keyctl, reboot, swapon and other dangerous syscalls
- Cloudflare Workers — edge V8 isolates with per-request timeout
- Local — subprocess mode for development only (no isolation)
- Runner file validation blocks path traversal (`..`) and absolute paths before execution

02

### Hardened code steps

In-process sandbox

The in-process `code` step runs user expressions through a restricted evaluator, not Python's `eval`/`exec`. Policy expressions are evaluated with `simpleeval`, and file operations from runner code are validated before they touch disk.

- Safe expression evaluation via `simpleeval` — no raw Python `eval`/`exec`
- Runner file validation blocks `..` traversal and absolute paths
- For untrusted code, the same step can be routed to an isolated sandbox backend (Docker / E2B / Workers)

03

### Authentication

API keys / HMAC

API key authentication with HMAC-SHA256 hashing, key rotation with grace periods, expiry enforcement, and per-key IP allowlisting.

- HMAC-SHA256 with configurable server-side pepper (`API_KEY_PEPPER`)
- Keys accepted via `X-API-Key`, `Authorization: Bearer`, or query param (SSE)
- Rotation — `POST /api/api-keys/{id}/rotate`; old key enters a configurable grace period
- Expiry — `expires_at` enforced in middleware, returns 401 `KEY_EXPIRED`
- IP allowlisting — per-key CIDR allowlist (IPv4 + IPv6); empty list allows all
- `sc_` prefix, 32-byte URL-safe token, first 8 chars stored as `key_prefix`
- `last_used_at` updated on every authenticated request; soft deletion via `is_active` preserves audit trail

04

### Multi-tenant isolation

Tenant scoping

Each API key is scoped to a tenant. Tenant-scoped queries filter all data access to prevent cross-tenant leaks.

- `tenant_id` set from the API key on every request via middleware
- Admin keys (no `tenant_id`) can access all data
- Tenant keys only see their own runs, workflows, and schedules
- Per-key cost limits (`max_cost_per_run_usd`)

05

### Policy engine

Redact / block / approve

Declarative policy rules evaluate step outputs in real time — automatically redact PII, block secrets, and trigger approvals.

- PII patterns — email, phone, SSN, credit card regex detection
- 30+ credential patterns — Slack, GitHub, AWS, Stripe, OpenAI and more
- Actions — redact, block, inject_approval, alert, log
- Severity levels — critical, high, medium, low
- Safe expression evaluation via `simpleeval` (no Python eval/exec)
- Auto-generated credential policies when tools are used
- Redacted output stored separately — originals preserved for LLM context

06

### Credential management

Secrets

Tool credentials are encrypted at rest with Fernet, resolved from environment variables or database-stored named connections. Never logged, never exposed in outputs.

- Encryption at rest — Fernet (AES-128-CBC + HMAC-SHA256) via `CREDENTIAL_ENCRYPTION_KEY`
- Graceful fallback — no key configured means plaintext passthrough (backwards compatible)
- `TOOL_*` prefix convention for environment variables
- Named connections (`postgresql:analytics`) stored in DB with unique constraint
- Credential masking for UI — shows first 4 + last 4 chars only
- Injected into the sandbox env at runtime, not stored in workflow YAML
- Export API (`/workflows/{name}/export`) sanitizes credentials from YAML

07

### SSRF prevention

Outbound guard

All webhook callback URLs are validated against private network ranges before any HTTP request is made.

- Blocks loopback (`127.0.0.0/8`, `::1`)
- Blocks private networks (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`)
- Blocks link-local (`169.254.0.0/16`) and IPv6 ULA (`fc00::/7`)
- DNS resolved before the IP check — prevents DNS rebinding via TOCTOU
- Scheme validation — only `http://` and `https://`

08

### Path traversal protection

Filesystem

All file operations are sandboxed within base directories. Resolved paths are validated to prevent escape.

- `Path.resolve()` + `is_relative_to()` check on all storage operations
- Raises `ValueError` on traversal attempts
- Dashboard SPA fallback rejects paths containing `..`
- Runner file validation blocks `..` and absolute paths

09

### Rate limiting

Abuse control

Pluggable rate limiting with in-memory and distributed Redis backends. A sliding-window counter per tenant or IP prevents abuse of sandbox endpoints.

- Default: 10 requests per 60 seconds on execution endpoints
- Keyed by `tenant:{id}` when authenticated, `ip:{addr}` when anonymous
- In-memory backend — default, zero config, sliding window with stale-entry pruning
- Redis backend — distributed sorted-set pipeline, auto-selected when `REDIS_URL` is set
- HTTP 429 with `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining` headers

10

### Security headers & CSP

Perimeter

Middleware injects hardened HTTP headers on every response. Content Security Policy is applied to dashboard routes, with a report-only mode for gradual rollout.

- `X-Content-Type-Options: nosniff` — prevents MIME sniffing
- `X-Frame-Options: DENY` — blocks clickjacking
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy` — disables camera, microphone, geolocation, payment
- CSP — `default-src 'self'` with scoped overrides for styles, scripts, images, fonts
- CSP applied to dashboard paths only (not `/api`) to avoid breaking JSON responses
- `CSP_REPORT_ONLY=true` uses the report-only header for safe testing

11

### Webhook signing

Outbound integrity

Outgoing webhooks are signed with HMAC-SHA256. Recipients can verify authenticity using the signature header.

- HMAC-SHA256 signature over the JSON payload
- Header: `X-Sandcastle-Signature`
- Timing-safe comparison via `hmac.compare_digest()`
- 10-second timeout on webhook delivery

12

### Circuit breaker & failover

Resilience

A three-state circuit breaker protects against cascading backend failures and recovers automatically when the backend stabilizes. Provider failover keeps runs moving.

- CLOSED — normal operation, requests pass through
- OPEN — after 5 consecutive failures, reject immediately
- HALF_OPEN — after a 30s cooldown, allow one test request; success resets to CLOSED
- Auto-failover triggers on HTTP 429, 5xx, and connection timeout
- Per-provider cooldown (default 60s) prevents retry storms
- Failover respects data-residency constraints; every event is logged

13

### EU data residency

Data residency

Set `DATA_RESIDENCY=eu` and all AI processing routes through EU-based providers (Mistral) or stays local (Ollama). This is hard enforcement at the routing layer — not a policy document.

- The routing layer rejects non-EU providers when EU mode is active
- Failover chain respects residency — only EU/local providers in the chain
- The provider audit trail logs the region for every AI call
- Dashboard shows residency status per provider
- Auto-generated GDPR privacy notice via `GET /compliance/privacy-notice`

14

### Tamper-evident audit trail

Audit trail

A SHA-256 hash-chain audit trail. Every event links to the previous one via a cryptographic hash — tamper-evident and independently verifiable.

- Hash chain — each AuditEvent stores `entry_hash` (SHA-256 of payload) and `prev_hash`
- 7 executor hooks — workflow/step started, completed, failed, and approval_requested
- 9 admin hooks — emergency_stop, emergency_reset, key created/rotated/revoked, credential stored/deleted, config & compliance changes
- `GET /audit/runs/{run_id}` — full event log for a run
- `GET /audit/verify/{run_id}` — verify chain integrity, returns broken links
- `GET /audit/admin` — paginated admin action log
- Records PolicyViolation and ApprovalRequest entries with full context

15

### Provider audit & SLO routing

Model routing

Every AI call logs which provider handled it, which model, which region, and whether it was a failover. SLO-aware routing matches the model to the task without manual config.

- Provider, model, and region logged per advisor call
- Failover events include the original provider and reason
- Cost tracked per provider for billing transparency
- Integrated with the SHA-256 hash-chain audit
- Quality tiers: high (generation), medium (analysis), low (formatting)
- Override with `ADVISOR_QUALITY_MODE=always_best` or `always_cheapest`

16

### Telemetry anonymization

Privacy

Opt-in error reporting via Sentry with aggressive anonymization. Disabled by default. No user data, no secrets, no hostnames.

- Opt-in only: `TELEMETRY_ENABLED=true` + `SENTRY_DSN`
- `send_default_pii=False`, `server_name=None`, zero tracing
- Before-send hook strips API keys, tokens, secrets, file paths, request & user data, stack-frame variables
- 40+ sensitive env var names blocklisted from reporting
- Local fallback: failed sends saved to `data/error_reports/`

17

### EU AI Act toolkit

Audit-ready

Built for the EU AI Act era. Risk classification, transparency reports, technical-documentation stubs, and a global emergency stop — configured in YAML, enforced at runtime.

- Risk classification — minimal / limited / high / unacceptable per Annex III; `risk_level: high`
- Transparency reports — per-run, `GET /runs/{id}/transparency-report`
- Annex IV docs — auto-generated stubs via `GET /workflows/{name}/annex-iv`
- Global emergency stop — `POST /admin/emergency-stop` halts running & queued workflows
- Compliance mode — `COMPLIANCE_MODE=eu_ai_act` blocks high-risk workflows without approval steps

Credential detection

## The automatic secret scrubber.

The policy engine detects and redacts credentials from 30+ services in step outputs. Two-layer defense — PEM key blocks first, then token regex. Idempotent and safe to run twice. Patterns are applied before data reaches storage, webhooks, or logs.

Category | Services / patterns | Examples
--- | --- | ---
Communication | Slack, Discord, Twilio, SendGrid, Resend, WhatsApp, Intercom | xoxb-xoxp-SG.
AI providers | OpenAI, Anthropic, ElevenLabs, Tavily | sk-sk-ant-
Cloud & DevOps | AWS, Vercel, Datadog, PagerDuty, Cloudflare | AKIA Bearer
Version control | GitHub, Jira, Linear | ghp_gho_
Data & storage | Supabase, Pinecone, Airtable, Snowflake, Redis | eyJ (JWT), UUID patterns
Payments & ERP | Stripe, Shopify, Plaid, QuickBooks, DocuSign | sk_live_sk_test_
CRM | HubSpot, Salesforce, Zendesk | pat- Bearer tokens
PII | Email, Phone, SSN, Credit Card | Regex for common PII formats
Connection URLs | PostgreSQL, Redis, MySQL, MongoDB strings | postgres://user:pass@host
PEM private keys | RSA, EC, DSA, ENCRYPTED key blocks | -----BEGIN RSA PRIVATE KEY-----
Cloud credentials | Azure Storage AccountKey, AWS compound keywords | AccountKey=…aws_secret_access_key
JSON secrets | JSON-quoted key/value pairs containing secrets | "password": "…""secret": "…"

Privacy router · 7 PII patterns redacted before data leaves your infrastructure
user@company.com
↓
[EMAIL]
+1 (555) 123-4567
↓
[PHONE]
123-45-6789
↓
[SSN]
4111 1111 1111 1111
↓
[CREDIT_CARD]
192.168.1.1
↓
[IP_ADDRESS]
DE89 3704 0044 0532
↓
[IBAN]
15/03/1990
↓
[DOB]
privacy.mode
↓
redact

Coordinated disclosure

## Report a vulnerability.

Found a security issue? Please report it responsibly. We respond within 24 hours and aim to fix critical issues within 72 hours. Please give us a chance to ship a fix before disclosing publicly.

Email security@sandcastle-ai.eu
Response within 24h
Critical fix aim ≤ 72h
Public issues GitHub ↗
Report a security issue GitHub issues ↗ Back to home