Security Model¶
Overview¶
ADEPT implements defense-in-depth security across three tiers: TLS termination at the reverse proxy, JWT validation at the API gateway, and fine-grained authorization at the orchestration layer. All inter-service communication uses authenticated credentials, and code execution runs in hardened sandboxes.
Authentication Flow¶
sequenceDiagram
participant Client
participant Nginx as Nginx (TLS)
participant GW as Agent Gateway
participant KC as Keycloak
participant Orch as Orchestration Service
Client->>Nginx: HTTPS Request
Nginx->>GW: Forward (plaintext internal)
GW->>KC: Validate JWT Signature
KC-->>GW: Token Valid + Claims
GW->>Orch: Proxy + User Context Headers
Orch-->>GW: Response
GW-->>Nginx: Response
Nginx-->>Client: HTTPS Response The Agent Gateway acts as a pure authentication proxy -- it validates identity but delegates all business logic to the Orchestration Service.
Authorization (RBAC)¶
Role-Based Access Control is enforced via Keycloak groups embedded in JWT claims:
| Group | Permissions |
|---|---|
admin | Full system access, user management, tool registration |
gateway-service | Service-to-service communication, health checks |
notebook-users | Standard agent interaction, file operations |
Principle of Least Privilege
Users receive only the permissions required for their role. Tool access is further restricted by per-tool ACL rules beyond group membership.
Per-Tool ACL¶
Each registered tool specifies which groups may invoke it:
# Tool access filtered at runtime
authorized_tools = await tool_manager.get_authorized_tools(
user_id=user.id,
groups=user.groups # From JWT claims
)
Users only see and can invoke tools their group membership permits.
Sandbox Security¶
The sandbox_mcp_server executes user-submitted code in isolated containers with multiple security layers:
| Control | Implementation |
|---|---|
| Container isolation | Dedicated container per execution |
| No network access | Network disabled during code execution |
| Resource limits | CPU, memory, and time constraints enforced |
| Non-root execution | Code runs as unprivileged user |
| Import restrictions | Dangerous modules blocked at import time |
| Timeout enforcement | Hard kill after configurable deadline |
Code Execution Guardrails¶
Before execution, submitted code is analyzed for policy violations:
- Blocked imports:
os.system,subprocess,socket, network libraries - Filesystem restrictions: Write access limited to session-scoped temp directory
- Resource caps: Memory ceiling and CPU time limits prevent resource exhaustion
- Output limits: stdout/stderr truncated to prevent memory bombs
Defense in Depth
Even if code bypasses import restrictions, container-level controls (no network, resource limits, non-root) provide secondary containment.
Kernel-Level Syscall Filtering (nsjail)¶
For deployments requiring maximum isolation, ADEPT supports optional kernel-level syscall filtering via nsjail. When enabled, all code execution occurs inside a seccomp-BPF jail that blocks 22 dangerous system calls (container escape, kernel modification, process inspection, filesystem escape).
nsjail is configurable per deployment:
| Deployment Type | nsjail Setting | Rationale |
|---|---|---|
| Exploratory research | false (default) | LLM-generated code needs flexible filesystem and package management |
| Digital twins / lab automation | true | Code targets physical equipment; syscall restrictions prevent unintended I/O |
| Classified environments | true | Defense-in-depth is mandatory regardless of usability tradeoffs |
| Multi-tenant (untrusted users) | true | Container isolation alone is insufficient for adversarial inputs |
Deployment Flexibility
nsjail is not all-or-nothing. Operators choose the security posture appropriate for their trust model. The 6 existing security layers (container isolation, restricted env, network egress control, import validation, capability drop, resource limits) remain active regardless of the nsjail setting.
Security Level Configuration¶
The security_level parameter controls the strictness of pre-execution code analysis. Operators choose the appropriate level based on their trust model:
| Level | Import Policy | Behavior | Use Case |
|---|---|---|---|
strict | Whitelist | Only approved scientific libraries allowed (numpy, pandas, scipy, matplotlib, etc.) | Untrusted user code, public-facing deployments |
medium (default) | Blacklist | Blocks known-dangerous imports (subprocess, socket, os.system, http clients) | General-purpose deployments with authenticated users |
permissive | Same as medium | Identical to medium; name indicates operator intent | HPC environments needing license-server network access |
Configure via the execute_code tool parameter or set the default in your deployment configuration:
# In docker-compose environment:
SANDBOX_DEFAULT_SECURITY_LEVEL: "medium" # strict | medium | permissive
When enable_security_validation is set to false, all pre-execution checks are bypassed entirely. This is not recommended for untrusted code.
Security Level Does Not Replace Container Isolation
The security level controls pre-execution static analysis only. Container-level controls (network isolation, resource limits, non-root execution) apply regardless of the configured security level and cannot be bypassed from within the sandbox.
Resource Limits¶
The sandbox enforces memory and CPU constraints at two levels:
Container-level limits (Docker Compose deploy.resources):
| Resource | Default | Configuration |
|---|---|---|
| Memory limit | 10 GB | deploy.resources.limits.memory in compose |
| Memory reservation | 2 GB | deploy.resources.reservations.memory in compose |
| CPU limit | 4 cores | deploy.resources.limits.cpus in compose |
| tmpfs session space | 1 GB | /tmp/sandbox_sessions (noexec, nosuid) |
Per-execution limits (environment variables in .env):
| Variable | Default | Description |
|---|---|---|
SANDBOX_MEMORY_LIMIT | 20g | Memory available to Docker-in-Docker child containers |
SANDBOX_MEMORY_SWAP_LIMIT | 6g | Swap space for DinD child containers |
SUBPROCESS_MEMORY_LIMIT_GB | 30 | Memory cap for subprocess execution mode |
SANDBOX_DEFAULT_TIMEOUT | 30 | Default execution timeout (seconds) |
SANDBOX_MAX_TIMEOUT | 600 | Maximum allowed timeout (10 minutes) |
SANDBOX_DOWNLOAD_TIMEOUT | 120 | Timeout for download operations |
SANDBOX_MAX_CONCURRENT_EXECUTIONS | 5 | Max parallel subprocess executions per worker |
SANDBOX_MAX_CONCURRENT_DIND | 3 | Max parallel DinD containers per worker |
SANDBOX_PROCESS_POOL_WORKERS | 4 | ProcessPoolExecutor workers for parallelism |
Tuning for Scientific Workloads
For memory-intensive workloads (COBRApy, genome assembly, large dataset processing), increase SUBPROCESS_MEMORY_LIMIT_GB to match or exceed SANDBOX_MEMORY_LIMIT. The container-level memory limit should always be greater than the per-execution limit to accommodate the Python runtime overhead.
Network Egress Control¶
By default, sandbox containers have no external network access. Operators can selectively allowlist specific hosts and ports via a YAML configuration file that is validated at container startup.
| Property | Behavior |
|---|---|
| No config file present | All external egress denied |
| Valid config with rules | Only listed host:port pairs reachable |
| Invalid config (parse error) | All external egress denied (fail-closed) |
| Internal services | Always reachable regardless of config |
The allowlist uses nftables rules applied during container initialization with Pydantic schema validation ensuring only well-formed rules are accepted. Wildcards, CIDR notation, and localhost targets are rejected.
Response Hardening (CSP)¶
All responses from nginx_proxy include a Content-Security-Policy header that prevents browser-side exfiltration of sensitive model output:
img-src 'self' data: blob:;
script-src 'self' 'unsafe-inline' 'unsafe-eval';
connect-src 'self' ws://$host wss://$host;
frame-ancestors 'none';
This blocks attacks where a model is tricked into emitting  -- the browser will refuse to load the external image. Same-origin presigned URLs and base64-encoded plot images remain functional.
Service Communication¶
Inter-service authentication uses OAuth2 Client Credentials flow:
# Service obtains token from Keycloak
POST /realms/{realm}/protocol/openid-connect/token
grant_type=client_credentials
client_id=agent-gateway-client
client_secret=<rotatable-secret>
Service credentials are:
- Generated during stack bootstrap
- Stored in credential directories (not environment variables)
- Retrieved via the
SecretsManagerwith file-based lookup - Rotatable without service restart via credential regeneration
Session Isolation¶
User data is isolated at multiple levels:
| Level | Mechanism |
|---|---|
| File storage | Per-session directories: data/uploaded_files/{session_id}/ |
| Vector stores | Session-scoped ChromaDB collections |
| Code execution | Isolated containers with session-scoped filesystems |
| Agent state | PostgreSQL checkpoints keyed by thread ID |
| Tool context | Multi-tier session IDs propagated through all calls |
No user can access another user's uploaded files, RAG indexes, execution results, or conversation history through any API surface.
Credential Management¶
ADEPT uses file-based secret retrieval as the default pattern:
Lookup order:
1. /run/secrets/{SECRET_NAME} (Docker/K8s secrets mount)
2. /app/credentials/{SECRET_NAME} (Credentials directory)
3. os.getenv("{SECRET_NAME}") (Environment variable fallback)
This approach supports Docker Compose, Kubernetes, and local development without code changes, and credentials persist across container recreations.