1 Security Architecture
jfabian edited this page 2026-08-13 11:15:47 -03:00

Security Architecture

Framework Alignment: NIST AI RMF · ISO/IEC 42001 · NIST SP 800-53 · OWASP LLM Top 10


Overview

VOX is a multi-agent orchestration framework designed for real-world deployment in environments where AI systems assist in consequential decision-making. Its security architecture is not a post-hoc addition — it is load-bearing. Every design choice described in this document exists because a less secure alternative was explicitly considered and rejected.

This document maps VOX's security controls to established frameworks. For each control, it shows the implementation, contrasts it with a less secure alternative, and identifies the framework reference that motivates it.


1. Identity Governance and Zero-Trust Communication

Framework refs: ISO 42001 §6.1.2 (risk identification), NIST AI RMF — GOVERN 1.2, NIST SP 800-53 IA-3 (device identification and authentication)

What VOX Does

Every agent in the fleet has a UUIDv4 identity declared in agent.yml. Inter-agent communication is validated against this identity at the point of receipt — not assumed to be trustworthy because it arrived from inside the process.

# vox/agents/base.py — resume() only accepts the declared master
def resume(self, master_id: str):
    if master_id != self.master_id:
        self.log_agent_fail(
            f"Resume REJECTED: sender '{master_id}' is not this agent's master."
        )
        return
# vox/capabilities/comm/gateway/server.py — webhook verified before parsing
if not adapter.verify_request(request, body):
    return web.json_response({"error": "forbidden"}, status=403)

Inbound delivery is wrapped so that a SecurityError (guardrail) is trapped and audited rather than propagating. Per-agent identity filtering happens at the role level; the gateway guarantees the payload is authentic, signed, and from the configured channel before any parsing occurs.

What a Less Secure Design Looks Like

# ❌ Junior approach — trust any message that arrives on the channel
async def _process_update(self, raw_data):
    message = raw_data.get("message", {})
    text = message.get("text")
    if text:
        await self.agent.emit("inbound_message", content=text)

Why This Matters

In a multi-agent system where agents can escalate tasks, approve actions, or trigger downstream workflows, an unauthorized sender is not just a nuisance — it is a potential command injection vector. Zero-trust at the communication boundary is the correct architectural response.


2. Least Privilege — Capability-Scoped Access Control

Framework refs: NIST SP 800-53 AC-6 (least privilege), OWASP LLM Top 10 — LLM08 (Excessive Agency), ISO 42001 §8.4 (AI system controls)

What VOX Does

Roles (the behavioral units containing business logic) cannot access the filesystem, spawn processes, or make network calls directly. All external I/O must go through a capability. Capabilities declare exactly which configuration keys they need and are provisioned only the values they requested — nothing more.

# vox/agents/base.py — capability provisioning firewall
PROTECTED = ["AGENT_ID", "MASTER_ID", "TELEGRAM_BOT_TOKEN"]

for cap_id in self.capabilities:
    cap_class = orchestrator.get_capability_instance(cap_id)
    requested = set(cap_class.get_params())
    if requested & PROTECTED:
        self.log_agent_fail(
            f"Capability '{cap_id}' BLOCKED: Unauthorized data access."
        )
        continue
    bound = cap_class.mount(self, self.config)  # extracts only declared params
    self.capabilities[cap_id] = bound

The Telegram capability needs a token. It gets a token. It does not get the agent's master ID, the Ollama URL, or any other secret. The firewall is enforced at provisioning time, not at call time — the capability never even sees the keys it did not request.

What a Less Secure Design Looks Like

# ❌ Junior approach — pass the full config dict to every capability
class TelegramCapability:
    def __init__(self, agent):
        self.config = agent.config
        self.token = self.config["TELEGRAM_TOKEN"]

Why This Matters

OWASP LLM08 (Excessive Agency) specifically calls out AI systems that operate with broader permissions than their task requires. If a role can open files, it can exfiltrate data. If a capability can read the full config, a supply-chain compromise in one dependency leaks all secrets.


3. Static Application Security Testing (SAST) at Role Load Time

Framework refs: NIST AI RMF — MAP 2.3 (AI system trustworthiness), NIST SP 800-53 SA-11 (developer testing), ISO 42001 §8.5 (verification and validation)

What VOX Does

Before any role is imported and instantiated, VOX parses its source code into an Abstract Syntax Tree (AST) and scans for forbidden primitives. A role that attempts direct filesystem access, process spawning, or dynamic code execution is rejected before it runs — not after.

def _perform_security_audit(self, source: str, role_name: str) -> bool:
    try:
        tree = ast.parse(source)
        forbidden = {"open", "write", "os.system", "subprocess", "eval", "exec"}
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name) and node.func.id in forbidden:
                    return False
                if isinstance(node.func, ast.Attribute) and node.func.attr in forbidden:
                    return False
        return True
    except Exception as e:
        self.log_agent_fail(f"Audit engine failure on {role_name}: {e}")
        return False

This runs before spec.loader.exec_module(module) — the module is never executed if the audit fails.

What a Less Secure Design Looks Like

spec.loader.exec_module(module)
role = module.Role(agent)
# __init__ has already executed. Damage is done before any check runs.

Why This Matters

Runtime behavioral checks are necessary but not sufficient. AST analysis at load time is a pre-execution control — it belongs to the category of shift-left security. In a system that dynamically loads code from the filesystem, pre-execution SAST is the correct gate.


4. Input Sanitization with Recursive Depth and Volumetric Control

Framework refs: NIST SP 800-53 SI-10 (information input validation), OWASP LLM Top 10 — LLM01 (Prompt Injection), ISO 42001 §8.4

What VOX Does

All event payloads pass through InputSanitizer.sanitize() before reaching any role handler. The sanitizer is recursive (handles nested dicts and lists), applies pattern-based threat detection, HTML-escapes output, and enforces a hard size limit.

DANGEROUS_PATTERNS = [
    r'<script',
    r'javascript:',
    r'eval\(',
    r'exec\(',
    r'\.\./\.\.\/',
    r'SELECT.*FROM',
    r'DROP\s+TABLE',
    r'rm\s+-rf',
    r'base64\s+--decode',
]

def _sanitize_string(self, text: str) -> str:
    for pattern in self.DANGEROUS_PATTERNS:
        if re.search(pattern, text, re.IGNORECASE):
            raise SecurityError(...)
    text = html.escape(text)
    MAX_STR_LENGTH = 8192
    return text[:MAX_STR_LENGTH] if len(text) > MAX_STR_LENGTH else text

The 8 KB limit approximates the token budget for a single LLM interaction and prevents memory exhaustion via oversized payloads — a denial-of-service vector frequently overlooked in agentic systems.

What a Less Secure Design Looks Like

async def emit(self, event_name: str, **kwargs):
    for role in self.event_router.get(event_name, []):
        await role.handle_event(event_name, **kwargs)

Why This Matters

OWASP LLM01 (Prompt Injection) is the top concern in agentic systems precisely because LLMs accept natural language as their command interface. Sanitization at the event boundary — before the payload reaches any role, including the LLM role — is the architectural defense.


5. Rate Limiting — Sliding Window Circuit Breaker

Framework refs: NIST SP 800-53 SC-5 (denial of service protection), NIST AI RMF — MANAGE 2.4

What VOX Does

Every emit() call passes through a per-agent sliding window rate limiter before any role executes. The limiter uses a deque for O(1) window maintenance.

def allow(self) -> bool:
    now = time.time()
    while self.calls and self.calls[0] < now - self.window:
        self.calls.popleft()
    if len(self.calls) >= self.max_calls:
        return False
    self.calls.append(now)
    return True

Rate limiting before sanitization is intentional: if a caller is flooding the agent, rejecting at the first gate avoids the CPU cost of pattern matching against every payload.

Why This Matters

In an event-driven agentic system, a rate-limit failure is not just a performance issue — it is a reliability and safety issue. An unbound event storm can starve the HITL confirmation path, causing autonomous actions that were supposed to wait for human approval to time out.


6. Human-in-the-Loop (HITL) as a Structural Constraint

Framework refs: NIST AI RMF — GOVERN 6.1 (human oversight), ISO 42001 §6.1.4 (human review), EU AI Act Art. 14 (human oversight for high-risk AI)

What VOX Does

The HITL model in VOX is not a feature that can be bypassed by a misconfigured role — it is enforced by the event contract. Roles do not execute actions; they emit events. Actions are confirmed by a human before execution.

async def report_to_master(self, event_name: str, **kwargs):
    if not self.master_id or not self.orchestrator:
        self.log_agent_warn("Escalation failed: No Master ID or Orchestrator link.")
        return
    master_agent = self.orchestrator.active_agents.get(self.master_id)
    if master_agent:
        await master_agent.emit(event_name, sender_id=self.id, **kwargs)

The messaging contract (see Messaging-Contract) enforces that subordinate agents detect and report — they never notify the user directly, except in documented system_failure emergencies.

What a Less Secure Design Looks Like

async def handle_message(self, content, **kwargs):
    response = await self.agent.capabilities["ai.llm"].generate(system, content)
    if "send email" in response.lower():
        await self.agent.capabilities["comm.email"].send_email(...)

Why This Matters

The EU AI Act, NIST AI RMF, and ISO 42001 all identify human oversight as a non-negotiable control for AI systems operating in consequential contexts. In VOX, HITL is not a checkbox — it is a structural property of the architecture.


7. Audit Logging and Full Traceability

Framework refs: NIST SP 800-53 AU-2 (event logging), ISO 42001 §9.1 (monitoring and measurement), NIST AI RMF — MANAGE 4.1

What VOX Does

Every security-relevant event in the fleet — capability provisioning, role audits, rate limit breaches, unauthorized access attempts, state transitions, inter-agent escalations — is logged with millisecond-precision timestamps and structured sender context.

# vox/observability/models.py — VOXForensicLogger
logger.ok("Capability provisioned", source=VOXLogSource("agent", name, agent_id))
logger.info("Inbound message verified", source=VOXLogSource("agent", name, agent_id))
logger.warning("Rate limit breach", source=VOXLogSource("agent", name, agent_id))

VOXLogSource carries structured sender context (source type, name, short UUID); both the colored console path and the flat forensic file path are rendered from the same logger — the single output path, no ad-hoc print() anywhere in the codebase.

Why This Matters

ISO 42001 §9.1 requires that organizations monitor their AI systems and retain evidence of operation. NIST SP 800-53 AU-2 requires that systems log sufficient detail to support after-the-fact investigation of security incidents.


8. Process Isolation and Privilege Separation (systemd Hardening)

Framework refs: NIST SP 800-53 SC-39 (process isolation), NIST SP 800-53 AC-6 (least privilege), ISO 42001 §8.3 (AI system security)

What VOX Does

The vox.service unit file applies layered systemd security directives that reduce the attack surface to its operational minimum.

User=vox
Group=vox
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/vox /run/vox
ProtectHome=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallFilter=@system-service
MemoryMax=2G
MemorySwapMax=0

The control socket is created with mode 0o600 (owner read/write only).

Why This Matters

Defense in depth requires each layer of the stack to enforce its own access controls. Running as a non-root user with NoNewPrivileges=true and ProtectSystem=strict means that even if an attacker achieves code execution inside the VOX process, their blast radius is bounded by what the vox service user can do on the filesystem.


9. Hot-Reload with Module Cache Invalidation

Framework refs: NIST AI RMF — MANAGE 3.1 (AI system updates), ISO 42001 §10.1 (continual improvement)

What VOX Does

When an agent is restarted, VOX purges the Python module cache for that agent's roles before reloading them from disk. This guarantees that a restart picks up the current code — not a stale cached version.

spec_name = f"vox_runtime.agents.{self.name.lower()}.roles.{role_name}"
if spec_name in sys.modules:
    del sys.modules[spec_name]
spec = importlib.util.spec_from_file_location(spec_name, role_file)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

The FileWatcher uses content hashing (SHA-256), not modification timestamps, to detect changes.

Why This Matters

In a live agentic system, a restart is often triggered because the operator patched a security vulnerability in a role. If the restart loads stale code, the fix never takes effect — and the operator has no indication that it did not.


10. Bounded Event Queue — Denial of Service Resilience

Framework refs: NIST SP 800-53 SC-5 (DoS protection), NIST AI RMF — MANAGE 2.4 (resilience)

What VOX Does

When a subordinate agent is paused, incoming events are held in a bounded queue rather than discarded silently or accumulated without limit.

class EventQueue:
    DEFAULT_CAPACITY = 256

    def enqueue(self, event_name: str, kwargs: Dict[str, Any]) -> bool:
        if len(self._queue) >= self._cap:
            self._dropped += 1
            return False
        self._queue.append(PendingEvent(event_name, kwargs))
        return True

The capacity limit (256 events) is configurable. On resume, the queue is drained in arrival order and re-emitted through the normal emit() path — including sanitization and rate limiting.

Why This Matters

Resource exhaustion is a class of vulnerability that disproportionately affects agentic systems because they are designed to run unattended for long periods. A bounded queue with explicit drop reporting degrades gracefully rather than catastrophically.


Control Plane Security — Unix Domain Socket

Framework refs: NIST SP 800-53 AC-17 (remote access), NIST SP 800-53 AC-3 (access enforcement)

What VOX Does

The runtime control plane (agent start/stop/restart, status queries) is exposed via a Unix Domain Socket with filesystem-level access control, not a network port.

self._commands: Dict[str, tuple] = {
    "status":  (self._handle_status,  0, "status"),
    "list":    (self._handle_list,    0, "list"),
    "restart": (self._handle_restart, 1, "restart <agent-name>"),
    "stop":    (self._handle_stop,    1, "stop <agent-name>"),
    "start":   (self._handle_start,   1, "start <agent-name>"),
    "pause":   (self._handle_pause,   1, "pause <agent-name>"),
    "resume":  (self._handle_resume,  1, "resume <agent-name>"),
}

The socket defaults to /tmp/vox.sock (overridable via VOX_UDS_PATH) and is created with mode 0o600. Only the owning user can reach the socket at the OS level.


Summary — Framework Control Mapping

VOX Control NIST AI RMF ISO 42001 NIST SP 800-53 OWASP LLM
UUID-based identity, whitelist comms GOVERN 1.2 §6.1.2 IA-3 —
Capability-scoped least privilege — §8.4 AC-6 LLM08
Pre-execution SAST (AST) MAP 2.3 §8.5 SA-11 LLM02
Recursive input sanitization — §8.4 SI-10 LLM01
Sliding window rate limiter MANAGE 2.4 — SC-5 —
Structural HITL enforcement GOVERN 6.1 §6.1.4 — LLM06
Structured audit logging MANAGE 4.1 §9.1 AU-2 —
systemd process isolation — §8.3 SC-39, AC-6 —
Module cache invalidation on reload MANAGE 3.1 §10.1 — —
Bounded event queue MANAGE 2.4 — SC-5 —
UDS control plane + command whitelist — §8.3 AC-17, AC-3 —

This document reflects VOX as of the v2 architecture.