Skip to main content

Security Guide

Security considerations and best practices for AgentVisor™ deployments.

Threat Model

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 exhaustioncgroups resource limits
Sandbox escapegVisor userspace kernel
Proxy identity spoofingUID-based identity via /proc/net/tcp (hardened modes)

Trust Boundaries

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
/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)
Rlimits vs cgroups

Rlimits constrain individual processes (e.g., per-process FD count), while cgroups constrain total resource consumption (e.g., total memory). Both are applied for defense in depth.

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.

Seccomp Syscall Filter

An OCI seccomp profile blocks high-risk syscalls including namespace manipulation (unshare, setns, clone with CLONE_NEWUSER), 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).

Process Visibility Restriction

The /proc filesystem is mounted with hidepid=2, restricting each process to only see /proc/<pid>/ entries for processes with the same UID. In gVisor mode, hidepid is not supported by gVisor's virtual procfs; however, gVisor provides sandbox-level isolation so host processes are never visible.

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 six feature groups that can be individually controlled. Each group defaults to auto, which enables features when supported by the sandbox type.

Feature GroupWhat It Controls
process_isolationPrivilege separation, UID isolation, capability dropping
env_curationEnvironment variable curation (allowlist for system env, blocklist for user vars), umask 0077
network_isolationLoopback-only networking (--network=none)
network_filteringIn-guest nftables ruleset restricting outbound traffic to loopback + hostlink only
filesystem_hardeningRead-only rootfs, seccomp profiles, tmpfs mounts
kernel_hardeningCapability bounding set, NoNewPrivileges, user namespace blocking

Auto-Detection Matrix:

Sandbox TypePlatformprocess_isolationenv_curationnetwork_isolationnetwork_filteringfilesystem_hardeningkernel_hardening
gvisorLinux
dockerLinux
dockermacOS/Windows
noneAny

network_isolation and network_filtering are mutually exclusive. Docker on macOS/Windows uses in-guest nftables (network_filtering) instead of --network=none because TCP transport to host.docker.internal is required. The agent's effective outbound restriction is equivalent in both cases.

Configuration:

# Accept auto-detection (recommended)
# No configuration needed - features enable automatically

# Force specific features (use with caution)
AGENTVISOR_GUEST_HARDENING_PROCESS_ISOLATION=enabled
AGENTVISOR_GUEST_HARDENING_NETWORK_ISOLATION=disabled # WARNING logged

See Sandbox Modes for detailed feature descriptions.

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"

Block Internal Networks

Prevent SSRF attacks:

resources:
# RFC1918 private networks
- name: private-10
selector: ["mrn:agentvisor:http:10\\..*"]
group: "mrn:iam:resource-group:denied"

- name: private-172
selector: ["mrn:agentvisor:http:172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"]
group: "mrn:iam:resource-group:denied"

- name: private-192
selector: ["mrn:agentvisor:http:192\\.168\\..*"]
group: "mrn:iam:resource-group:denied"

# Cloud metadata
- name: aws-metadata
selector: ["mrn:agentvisor:http:169\\.254\\.169\\.254.*"]
group: "mrn:iam:resource-group:denied"

- name: gcp-metadata
selector: ["mrn:agentvisor:http:metadata\\.google\\.internal.*"]
group: "mrn:iam:resource-group:denied"

# Localhost
- name: localhost
selector:
- "mrn:agentvisor:http:localhost.*"
- "mrn:agentvisor:http:127\\..*"
group: "mrn:iam:resource-group:denied"

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
  • Internal networks blocked
  • Cloud metadata blocked
  • Authentication enabled
  • TLS configured
  • Resource limits set (memory, CPU, PIDs)
  • Timeouts configured
  • Network policies applied
  • Secrets not in images
  • Audit logging enabled
  • Pod security context configured