Skip to main content

Security Guide

Security considerations and best practices for AgentVisor™ deployments.

Threat Model​

AgentVisor's threat model assumes an agent that cannot be trusted to stay within its instructions. The goal is confinement: no ambient authority, mediated effects, a protected control plane, and a record the agent cannot alter.

Threats Mitigated​

ThreatMitigation
Malicious agent codegVisor syscall isolation
Network bypassNo direct network access outside the hostlink channel (--network=none on gVisor/Docker-Linux; nftables-restricted egress on Docker macOS/Windows)
Policy bypassAll requests through host proxy
Data exfiltrationPolicy controls on endpoints
Privilege escalationgVisor + non-root container
Resource exhaustionPer-process rlimits (process count, open files, file size) plus optional per-run address-space and CPU limits, in both docker and gvisor. Memory, CPU and PID limits for the sandbox as a whole are enforced in docker mode, and in gvisor mode when the host runtime runs as root with a writable cgroup v2 mount; otherwise apply them to the container or pod that runs the host runtime
Sandbox escapegVisor userspace kernel
Proxy identity spoofingUID-based identity via /proc/net/tcp (hardened modes)

Trust Boundaries​

Run Cancellation as a Containment Control​

Cancelling a run (POST to the cancel endpoint) is not only a UX convenience — it is also the mechanism an operator reaches for to contain a runaway agent, a suspected compromise, or a policy violation caught mid-run. Two things a cancelled run's guest process is holding onto matter here: the process itself, and the brokered/substituted proxy credentials (see Credential Brokering) its outbound HTTP requests were using. Both are released together, by the same cleanup path, the moment cancellation actually lands — so the time it takes cancellation to land is also the upper bound on how long a cancelled agent could still make outbound calls with real, substituted credentials.

AgentVisor delivers cancellation two ways (see Run Cancellation Latency for the full picture):

  • Fast path (the common case): a session-scoped fast-path cancellation call reaches the exact worker process running the agent directly, typically resolving in low single-digit seconds.
  • Fallback: if the fast path is unavailable for any reason, cancellation still lands via Temporal's own heartbeat-driven activity cancellation — reliable, but with a real floor of roughly 48 seconds regardless of load.

An operator relying on cancellation as a containment control should plan around the fallback's ~48-second floor as the worst case, not the fast path's typical latency — the fast path is an additive improvement, not a replacement, and a session or worker failure can always fall back to it. agentvisor_run_cancel_fast_path_total and agentvisor_run_cancel_fallback_total, tagged trigger=cancel for an explicit run cancellation (see Monitoring for Scale), show which path actually delivered cancellation in a live deployment.

Sandbox Security​

Sandbox Mode Selection​

Both Docker and gVisor are production-grade sandbox modes. gVisor is recommended for Linux production deployments:

# gVisor (recommended for Linux — strongest isolation)
AGENTVISOR_GUEST_SANDBOX=gvisor

# Docker (production-grade; works on Linux, macOS, Windows)
AGENTVISOR_GUEST_SANDBOX=docker

gVisor advantages over Docker:

  • Syscall-level interception via userspace kernel (Sentry)
  • Kernel vulnerabilities don't expose host

Both modes share: network isolation, read-only rootfs, privilege separation, capability dropping, seccomp filtering, and environment curation. See Sandbox Modes for the full comparison.

gVisor Privilege Modes​

gVisor runs in rootless mode by default, requiring zero host capabilities:

ModeDocker FlagsTrade-offs
Rootless (default)NoneZero-capability, uses 9P filesystem, requires unprivileged userns
Privileged--user root --privilegedFull defense-in-depth, DirectFS (fastest)

Rootless mode (default — no extra flags):

AGENTVISOR_GUEST_SANDBOX=gvisor
# No extra Docker flags needed

Privileged mode (opt-in for maximum performance):

AGENTVISOR_GUEST_SANDBOX=gvisor
AGENTVISOR_GUEST_ROOTLESS=false
# Requires: docker run --user root --privileged
Kernel Prerequisites for Rootless Mode

Rootless mode requires unprivileged user namespaces enabled on the host:

  • Linux 5.15+ supports this by default on most distributions
  • Some distributions require: sysctl -w kernel.unprivileged_userns_clone=1
  • Container images built with agentvisor build include newuidmap/newgidmap (provided by shadow-utils on UBI 9) and subuid/subgid pre-configured
Privilege Model

In rootless mode (default), the process never starts as root — no privilege is acquired or dropped. In privileged mode, the host runtime starts as root only to initialize the gVisor sandbox, then immediately and irreversibly drops privileges to uid/gid 1000 via setuid()/setgid() syscalls. All subsequent operations run as the unprivileged user.

Choosing a mode
  • Use rootless mode (default) — works on any modern kernel with unprivileged user namespaces
  • Use privileged mode when you need maximum DirectFS performance or lack userns support
  • Both modes provide strong syscall-level isolation via gVisor's Sentry

Resource Limits​

Prevent resource exhaustion:

# Memory limit (512MB)
AGENTVISOR_GUEST_MEMORY_LIMIT=536870912

# CPU limit (1 core)
AGENTVISOR_GUEST_CPU_LIMIT=1.0

# Process limit
AGENTVISOR_GUEST_PIDS_LIMIT=1024

Timeouts​

Prevent runaway processes:

AGENTVISOR_GUEST_STARTUP_TIMEOUT=30s
AGENTVISOR_GUEST_SHUTDOWN_TIMEOUT=10s
AGENTVISOR_PROXY_REQUEST_TIMEOUT=30s

Guest Sandbox Hardening​

In addition to gVisor syscall isolation, the guest sandbox applies several defense-in-depth measures:

Read-Only Root Filesystem​

The guest container's root filesystem is mounted read-only. A compromised agent cannot modify system binaries, libraries, or configuration files. Specific directories that require writes are mounted as size-limited tmpfs:

MountSizePurpose
/tmp128 MBTemporary files, CA bundle
/home/guest64 MBGuest-runtime working data
/home/agent64 MBAgent process home directory. Each run gets its own private subdirectory, removed when the run ends
/app/agent/.agentvisor8 MBSchema cache
Python Compatibility

Python handles read-only filesystems gracefully by skipping __pycache__ bytecode caching. No special configuration is needed.

Minimal Linux Capabilities​

The sandbox runs with a reduced set of 4 Linux capabilities — CAP_CHOWN, CAP_SETGID, CAP_SETUID, CAP_KILL — down from Docker's default 14 (in Docker mode, CAP_SETPCAP and CAP_DAC_OVERRIDE are also present for a maximum of 6). These are placed in the effective/permitted/inheritable/ambient sets but never in the bounding set, which is drained to empty: gVisor resets the bounding set to the OCI spec value on exec, so an empty bounding set guarantees agent processes always run with CapBnd=0 regardless of sandbox mode.

Socket Directory Protection​

The Unix socket used for host-guest communication is protected with:

  • noexec, nosuid, nodev mount options — prevents execution of binaries placed in the directory
  • 0770 directory permissions — restricts access to the shared group (GID 1000) used by both host and guest

Privilege Separation
Sandboxed Modes Only
​

When process_isolation is enabled (gVisor and both Docker modes — see the auto-detection matrix below), the guest-runtime uses OpenSSH-style privilege separation. The container starts as UID 0 but immediately forks a minimal spawner subprocess (retains UID 0 for fork/setuid/exec only) and then irreversibly drops to UID 2000. All gRPC handling, proxy management, and CA operations run as the unprivileged user. Communication with the spawner uses a private Unix socketpair with length-prefixed JSON and SCM_RIGHTS for fd passing.

UID Isolation
Sandboxed Modes Only
​

When process_isolation is enabled (gVisor and both Docker modes), each agent process is assigned a unique UID from a recycling pool (2001–65534) with umask 0077. The spawner subprocess (UID 0) handles the fork/setuid/exec. This provides cross-tenant memory isolation (/proc/<pid>/mem access blocked between agents) and prevents cross-agent file reads in shared directories. All users share GID 1000 (agentvisor) for socket directory access.

Resource Limits (rlimits)​

The OCI spec enforces per-process resource limits to prevent resource exhaustion:

LimitSoftHardPurpose
Open files10244096Prevent FD exhaustion
Processes256 (configurable)512 (configurable)Prevent fork bombs
File size100 MB200 MBLimit individual file writes
Core dumps00Disable core dumps (prevent credential leakage)

Two further limits are available per run, off by default: an address-space ceiling and a CPU-time limit. They are applied to each agent run individually, in both docker and gvisor; see Per-Run Limits. The process-count limit above is already per run, because each run has its own UID and the kernel counts processes per UID.

Rlimits vs cgroups

Rlimits constrain individual processes (e.g., per-process FD count). Cgroups constrain total resource consumption (e.g., total memory). AgentVisor applies them in docker mode, and in gvisor mode when the host runtime runs as root with a writable cgroup v2 mount (for example a privileged container). In the default rootless gvisor deployment, memory, CPU and PID limits for the sandbox as a whole come from the container or pod that runs the host runtime (for example Kubernetes resource limits or docker run --memory); set guest.hardening.resource_limits: enabled to fail startup rather than run without them. No per-run memory budget exists in either mode: the address-space limit is a per-process ceiling, not a total.

User Namespace Blocking​

User namespace creation is blocked by the OCI seccomp profile, which blocks the unshare syscall and clone with CLONE_NEWUSER. This prevents agents from gaining capabilities within a user namespace, which would otherwise allow mount operations, device node creation, and chroot escapes.

A newer syscall, clone3, can also create a user namespace, but its arguments are passed as an opaque memory pointer rather than a plain register value, so it cannot be selectively filtered the way clone can — it is blocked outright instead. Docker sandbox mode blocks it with an error that lets normal thread and process creation keep working on modern Linux distributions. The gVisor sandbox blocks the same syscall through an equivalent, independently-enforced mechanism inside the guest, since gVisor's runtime does not support the more selective error signal Docker's runtime does. Both sandbox modes fully block clone3-based user namespace creation; only the enforcement mechanism differs.

Seccomp Syscall Filter​

An OCI seccomp profile blocks high-risk syscalls including namespace manipulation (unshare, setns, clone with CLONE_NEWUSER, clone3), filesystem mounting (mount, pivot_root), process debugging (ptrace), kernel module loading, BPF operations, and device creation (mknod). In gVisor mode, the --oci-seccomp flag is passed to runsc to enable the profile (gVisor ignores OCI seccomp by default); clone3 blocking under gVisor is enforced by a separate, equivalent guest-side mechanism rather than this OCI profile — see User Namespace Blocking above.

Process Visibility Restriction​

The /proc filesystem is mounted with hidepid=2, requesting that each process only see /proc/<pid>/ entries for processes with the same UID.

gVisor residual

gVisor's virtual procfs does not implement hidepid, so in gVisor mode hidepid=2 has no effect. Processes inside the sandbox can list the PIDs of other processes in the same sandbox and read their command lines. Reading their environment (environ), open file descriptors (fd) and memory (mem), attaching with ptrace, and signalling them remain denied across UIDs. Host processes are never visible, because the sandbox has its own procfs. The residual exposure is limited to peer PIDs and command lines within the sandbox, so avoid passing secrets on process command lines.

Per-Run Home and Process Cleanup​

With the default hardening, every agent run in serve mode gets its own private home directory (mode 0700, owned by that run's UID) and HOME points at it. It is removed when the run ends. This stops one run from planting startup files (shell, git, pip and Node configuration, Python startup hooks) that a later run would load. In exec mode, and whenever a mount targets /home/agent (for example --mount-home), the home is a single shared directory instead, because persistence across sessions is the point there.

When a run ends, AgentVisor also kills any process that run left behind (for example a daemonized child) and removes its home before its UID can be reused. Otherwise a leftover process would share a UID, and therefore memory, files and signals, with the next, unrelated run.

Accepted Residuals​

These are deliberate trade-offs rather than gaps awaiting a fix:

  • Executable /tmp: /tmp allows execution because package installers, Node tooling and coding agents need it. Detecting execution from /tmp depends on tracing being enabled.
  • No per-run memory budget: the sandbox's in-guest memory and CPU cgroup controllers record limits without enforcing them. The per-run address-space limit is enforced per process, so it bounds a single process rather than a run's total memory. Whole-sandbox memory is bounded by memory_limit where resource_limits is enforced, and otherwise by the container or pod that runs the host runtime.
  • curl and pip in the Python guest image: the binaries are present, and network egress policy is the control that restricts what they can reach.

Guest Image Hardening​

The guest image uses UBI 9 (Red Hat Universal Base Image) as its base, providing a minimal, enterprise-compatible footprint. Dangerous binaries are removed individually (unshare, nsenter, mount, umount, chroot, newgrp, sg), all setuid/setgid bits are stripped, and the microdnf package manager and RPM database are removed after image build. While agents can still invoke syscalls via Python's ctypes, these measures block the easiest attack paths.

Environment Isolation
Sandboxed Modes Only
​

When env_curation is enabled (gVisor and both Docker modes), environment variables are curated using a two-layer model:

  • Allowlist for system environment (os.Environ()) — only safe variables like PATH, HOME, PYTHONPATH are inherited
  • Blocklist for user-supplied variables — variables from framework.env and .env files pass through, but AGENTVISOR_GUEST_* vars are blocked

This allows users to set arbitrary environment variables for their agents while preventing leakage of internal runtime state. AgentVisor-injected variables (HTTP_PROXY, SSL_CERT_FILE, etc.) are added last and cannot be overridden.

HTTP Proxy Identity Resolution
Sandboxed Modes Only
​

In multi-agent scenarios, agents share the same network namespace. Without protection, a malicious agent could scan localhost ports to discover other agents' proxy endpoints and make requests using their identity.

When process_isolation is enabled (gVisor and both Docker modes), AgentVisor uses UID-based identity resolution to prevent this attack:

  • Single shared proxy port: All agents connect to the same proxy port
  • OS-level identity: On each connection, the proxy resolves the caller's TCP source port to a UID via /proc/net/tcp
  • Non-spoofable: The UID is mapped to the agent's thread-id via a registry populated at spawn time

This eliminates the identity spoofing vector because identity is determined by kernel-level process information, not which port an agent connects to. An agent cannot change its UID or forge another process's TCP source port.

Granular Hardening Configuration​

AgentVisor's security hardening is organized into seven independently-controllable feature groups (process_isolation, env_curation, network_isolation, network_filtering, filesystem_hardening, kernel_hardening, resource_limits), each defaulting to auto — enabled automatically when the sandbox mode supports it. Forcing a feature enabled on a combination that doesn't support it fails startup rather than silently degrading; forcing one disabled logs a warning, since that's opting out of a real protection deliberately.

These seven groups are the configuration surface for the twelve implementation-level hardening mechanisms enumerated above under Guest Sandbox Hardening — several groups (e.g. process_isolation) govern more than one mechanism. Product-level material (e.g. Why AgentVisor) cites a broader 14 independent security layers, which also counts protections outside guest.hardening's scope entirely, such as credential brokering and stdio MCP server sandboxing. The three numbers describe the same architecture at different levels of granularity, not three different feature sets.

See Sandbox Modes: Granular Hardening for the full feature table and the auto-detection matrix showing which sandbox/platform combinations enable which features by default.

Policy Security​

Default Deny​

Always configure default deny:

resources:
# Explicit allows first
- name: allowed-apis
selector: ["mrn:agentvisor:http:api\\.allowed\\.com.*"]
group: "mrn:iam:resource-group:allowed"

# Default deny must be last
- name: default
selector: ["mrn:agentvisor:http:.*"]
group: "mrn:iam:resource-group:denied"

Internal Networks and Cloud Metadata​

Don't write policy selectors to block RFC 1918 ranges, cloud metadata addresses, or localhost — an MRN is built from the request's host string, before any DNS lookup, so a hostname that happens to resolve to an internal address sails past a selector written this way. AgentVisor™'s SSRF guard handles this instead: it's on by default, requires no configuration, and checks the request's resolved address rather than the hostname text. See Egress Controls for the full model, including the narrow proxy.ssrf_allowed_cidrs exemption mechanism for legitimately internal upstreams.

Specific Path Policies​

Limit access to specific API paths:

resources:
# Allow only chat, not fine-tuning
- name: openai-chat
selector: ["mrn:agentvisor:http:api\\.openai\\.com/v1/chat.*"]
group: "mrn:iam:resource-group:allowed"

# Block sensitive endpoints
- name: openai-other
selector: ["mrn:agentvisor:http:api\\.openai\\.com.*"]
group: "mrn:iam:resource-group:denied"

Authentication​

OIDC​

For production, enable OIDC authentication:

AGENTVISOR_API_AUTH_ENABLED=true
AGENTVISOR_API_AUTH_OIDC_ISSUER=https://auth.example.com
AGENTVISOR_API_AUTH_OIDC_AUDIENCE=agentvisor

JWKS URL Override​

When running in Kubernetes, the OIDC issuer's discovery document may return an external HTTPS URL for JWKS that's unreachable from inside the cluster (due to DNS or TLS certificate issues). Use AGENTVISOR_API_AUTH_OIDC_JWKS_URL to override with an internal URL:

# External issuer for JWT validation (must match token's iss claim)
AGENTVISOR_API_AUTH_OIDC_ISSUER=https://keycloak.example.com/realms/myrealm

# Internal K8s URL for fetching JWKS (bypasses external DNS)
AGENTVISOR_API_AUTH_OIDC_JWKS_URL=https://keycloak-service.keycloak.svc.cluster.local:8443/realms/myrealm/protocol/openid-connect/certs

This pattern is common when:

  • External hostnames don't resolve from inside the cluster
  • You want to avoid external network traffic for JWKS fetching

The override must be https. A plaintext http:// JWKS endpoint lets anyone on the network path substitute the signing keys and mint tokens for any principal, so AgentVisor rejects it at startup. If your in-cluster provider genuinely cannot serve TLS, you can opt in with AGENTVISOR_API_AUTH_OIDC_ALLOW_INSECURE=true — a loud warning is logged and this should never be used in production.

TLS​

Enable TLS for the API:

AGENTVISOR_API_TLS_ENABLED=true
AGENTVISOR_API_TLS_CERT_FILE=/etc/ssl/certs/server.crt
AGENTVISOR_API_TLS_KEY_FILE=/etc/ssl/private/server.key

Kubernetes Security​

Pod Security​

AgentVisor uses rootless mode by default. gVisor creates a user namespace via CLONE_NEWUSER, so the pod needs an unconfined seccomp profile to allow that syscall:

Rootless mode (default):

spec:
containers:
- name: agent
securityContext:
seccompProfile:
type: Unconfined # Allows CLONE_NEWUSER needed by gVisor rootless mode
appArmorProfile:
type: Unconfined # Required on Ubuntu 24.04+, GKE, AKS

Privileged mode (opt-in for maximum performance):

spec:
containers:
- name: agent
securityContext:
privileged: true
runAsUser: 0
env:
- name: AGENTVISOR_GUEST_ROOTLESS
value: "false"

In privileged mode, the host runtime immediately and irreversibly drops privileges to uid/gid 1000 after gVisor initialization. In rootless mode, the process never starts as root.

Network Policies​

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: agentvisor-egress
spec:
podSelector:
matchLabels:
app: agentvisor
policyTypes:
- Egress
egress:
# Only allow Temporal and external HTTPS
- to:
- namespaceSelector:
matchLabels:
name: temporal-system
ports:
- port: 7233
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- port: 443

RBAC​

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: agentvisor
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: agentvisor
subjects:
- kind: ServiceAccount
name: agentvisor
roleRef:
kind: Role
name: agentvisor
apiGroup: rbac.authorization.k8s.io

Secrets Management​

Don't Embed Secrets​

Never embed secrets in agent code or images:

# Bad - secret in env
env:
API_KEY: sk-secret123

# Good - secret reference
env:
API_KEY: ${API_KEY} # Injected at runtime

Use Secret Management​

Kubernetes Secrets:

apiVersion: v1
kind: Secret
metadata:
name: agent-secrets
type: Opaque
stringData:
openai-api-key: sk-...

External secret managers:

  • HashiCorp Vault
  • AWS Secrets Manager
  • GCP Secret Manager

Audit Logging​

AgentVisor provides automatic audit logging for authorization decisions through the PolicyEngine. For application-level logging, use structured logging to capture significant actions:

import logging

logger = logging.getLogger(__name__)

# Log significant actions with structured data
logger.info("api_call", extra={"endpoint": url, "method": method})
logger.info("decision", extra={"action": action, "reason": reason})
logger.info("tool_use", extra={"tool": name, "result": "success"})

Structured logs are forwarded from the guest sandbox to the host runtime and can be captured by your logging infrastructure (stdout, file, or external log aggregators).

Security Checklist​

Before production:

  • Sandbox mode configured (gVisor recommended for Linux; Docker is production-grade on any platform)
  • Read-only rootfs with sized tmpfs overlays (automatic in docker and gvisor modes)
  • Default deny policy configured
  • SSRF allowlist exemptions reviewed (proxy.ssrf_allowed_cidrs — see Egress Controls)
  • Authentication enabled
  • TLS configured
  • AGENTVISOR_ENV=production set (hard-blocks test mode, allowall authorization, and an unauthenticated codec server, even when TLS terminates upstream)
  • Resource limits set (memory, CPU, PIDs)
  • Timeouts configured
  • Network policies applied
  • Secrets not in images
  • Audit logging enabled
  • Pod security context configured