Security Guide
Security considerations and best practices for AgentVisor™ deployments.
Threat Model
Threats Mitigated
| Threat | Mitigation |
|---|---|
| Malicious agent code | gVisor syscall isolation |
| Network bypass | No direct network access outside the hostlink channel (--network=none on gVisor/Docker-Linux; nftables-restricted egress on Docker macOS/Windows) |
| Policy bypass | All requests through host proxy |
| Data exfiltration | Policy controls on endpoints |
| Privilege escalation | gVisor + non-root container |
| Resource exhaustion | cgroups resource limits |
| Sandbox escape | gVisor userspace kernel |
| Proxy identity spoofing | UID-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:
| Mode | Docker Flags | Trade-offs |
|---|---|---|
| Rootless (default) | None | Zero-capability, uses 9P filesystem, requires unprivileged userns |
| Privileged | --user root --privileged | Full 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
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 buildincludenewuidmap/newgidmap(provided byshadow-utilson UBI 9) andsubuid/subgidpre-configured
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.
- 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:
| Mount | Size | Purpose |
|---|---|---|
/tmp | 128 MB | Temporary files, CA bundle |
/home/guest | 64 MB | Guest-runtime working data |
/home/agent | 64 MB | Agent process home directory |
/app/agent/.agentvisor | 8 MB | Schema cache |
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,nodevmount options — prevents execution of binaries placed in the directory0770directory 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:
| Limit | Soft | Hard | Purpose |
|---|---|---|---|
| Open files | 1024 | 4096 | Prevent FD exhaustion |
| Processes | 256 (configurable) | 512 (configurable) | Prevent fork bombs |
| File size | 100 MB | 200 MB | Limit individual file writes |
| Core dumps | 0 | 0 | Disable core dumps (prevent credential leakage) |
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 likePATH,HOME,PYTHONPATHare inherited - Blocklist for user-supplied variables — variables from
framework.envand.envfiles pass through, butAGENTVISOR_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 Group | What It Controls |
|---|---|
process_isolation | Privilege separation, UID isolation, capability dropping |
env_curation | Environment variable curation (allowlist for system env, blocklist for user vars), umask 0077 |
network_isolation | Loopback-only networking (--network=none) |
network_filtering | In-guest nftables ruleset restricting outbound traffic to loopback + hostlink only |
filesystem_hardening | Read-only rootfs, seccomp profiles, tmpfs mounts |
kernel_hardening | Capability bounding set, NoNewPrivileges, user namespace blocking |
Auto-Detection Matrix:
| Sandbox Type | Platform | process_isolation | env_curation | network_isolation | network_filtering | filesystem_hardening | kernel_hardening |
|---|---|---|---|---|---|---|---|
gvisor | Linux | ✓ | ✓ | ✓ | — | ✓ | ✓ |
docker | Linux | ✓ | ✓ | ✓ | — | ✓ | ✓ |
docker | macOS/Windows | ✓ | ✓ | — | ✓ | ✓ | ✓ |
none | Any | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
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