Security Architecture
Overview
Security-first design with 7 defense layers:
- Input Validation - Zod schemas at all boundaries
- Secrets Vault - Never expose API keys or tokens
- Rate Limiting - Token bucket per tool
- Memory Bounds - Context pruning, history caps
- Path Safety - Normalized paths, directory jails
- Timeout Protection - TimeoutGuard for async operations
- Byzantine Detection -
WeightedVotingpattern detection (exported, not on theconsensus_votepath — see Threat Model)
Authentication & Access Control
The MCP server ships one transport, StdioServerTransport (src/cli-server.ts, src/mcp/server.ts). It communicates with the parent process only — no network listener, so no other process can reach it. Process isolation is the access control; no authentication runs on this path.
There is no REST API mode. Earlier versions of this section described an api.enabled: true config key and an auto-generated ~/.nexus-agents/auth/rest-api-key; neither is implemented. AppConfigSchema (src/config/schemas.ts) has no api key, no source file references rest-api-key, and nothing under src/ opens an HTTP listener. An api: block in nexus-agents.yaml is not read by any code.
| Environment | Auth enforced? | Notes |
|---|---|---|
| Local MCP (stdio) | No | Only the parent process can reach the server |
| Network-exposed | Not supported | No HTTP transport ships; a token can be generated (below) but no code path validates it |
Token-Based Authentication (Issue #739)
The AuthHandler middleware (src/mcp/middleware/auth-handler.ts) and its token CLI are implemented; enforcement is not. No server code path calls AuthHandler.authenticate(), and no HTTP endpoint exists to present a token to (see MCP Middleware Security below).
Token management CLI commands:
nexus-agents auth init # Generate new auth token
nexus-agents auth show # Show token status (file path, permissions)
nexus-agents auth rotate # Rotate token (invalidate old, generate new)
nexus-agents auth init --force # Regenerate token (overwrite existing)
Security properties:
- Tokens are 64-character hex strings (32 bytes of
crypto.randomBytes) - Stored at
~/.nexus-agents/auth/server-tokenwith0600permissions (owner read/write only) - Directory created with
0700permissions - Timing-safe comparison (
crypto.timingSafeEqual) prevents timing attacks - Token shown only once on generation (not recoverable — rotate if lost)
Enabling authentication:
# nexus-agents.yaml
security:
auth:
enabled: true
method: token # Only supported authentication method
tokenHeader: Authorization # Header name for Bearer token
tokenFile: ~/.nexus-agents/auth/server-token # Token storage path
Since 10.0, security.auth.method accepts only token. OAuth2 was never
implemented; earlier versions accepted oauth2 but performed bearer-token checks.
If your config sets security.auth.method: oauth2, change it to
security.auth.method: token before upgrading. Otherwise config loading fails
with migration guidance (#5681).
Or via environment variables:
export NEXUS_AUTH_ENABLED=true
nexus-agents --mode=server
Client usage: none today. Setting NEXUS_AUTH_ENABLED=true generates and logs the token file path at startup, but there is no listener that accepts an Authorization header, so no client can use the token.
Threat Model
| Threat | Vector | Mitigation | Status |
|---|---|---|---|
| Prompt Injection | Malicious prompts | Input/output tagging, structured output | ✅ |
| SSRF | Outbound HTTP calls | URL allowlist, private IP blocking | ✅ |
| Path Traversal | Malicious file paths | Path normalization, directory jail | ✅ |
| ReDoS | Malicious regex | Static patterns only, no user RegExp | ✅ |
| MCP SDK ReDoS | CVE-2026-0621 | TimeoutGuard, URI validation | ✅ |
| Secrets Exposure | Logs, errors | Secrets vault, sanitization | ✅ |
| Token Exhaustion | Unbounded context | Memory caps, pruning | ✅ |
| Injection | Malformed prompts | Input validation, Zod schemas | ✅ |
| Byzantine Failures | Malicious agents | Weighted voting with Byzantine detection | ⚠️ not wired |
Byzantine Failures is not on the live vote path. WeightedVoting (src/consensus/weighted-voting.ts) is exported from consensus/index.ts along with its factory createWeightedVoting, but neither has a non-test caller, and ConsensusEngine (src/consensus/engine.ts) — what consensus_vote runs — does not reference it. The class implements two of the four documented patterns (contrarian, collusion) and returns false without evaluating when fewer than 3 votes are present. See CONSENSUS_PROTOCOLS.md.
Reference: OWASP LLM Top 10 (LLM01: Prompt Injection)
MCP Middleware Security
Locally registered MCP tools are wrapped in a middleware pipeline by createSecureHandler (src/mcp/middleware/secure-handler.ts). Coverage is per-tool opt-in, not a transport chokepoint: each tool’s registration calls createSecureHandler itself, and a registration that does not is not covered. Upstream MCP proxy tools — registered by initUpstreamServers (src/cli-server-tools.ts) with a raw passthrough handler when the gateway lists upstream servers — receive none of the layers below; the server logs this at startup as upstreamProxiesUncovered: true. The registerTool() interception proxies (mcp/gateway/gateway-server-proxy.ts, mcp/tools/annotation-proxy.ts, mcp/tools/tool-observability-proxy.ts) add annotations, gateway dispatch and metrics; they do not add security layers.
| Layer | Protection | Default |
|---|---|---|
| Input Validation | Argument size cap (MAX_), conversation-tag stripping, Zod per handler |
On for covered tools |
| Policy Firewall | Allowlist/denylist rules per tool; evaluated and logged, denials applied only on opt-in | warn; NEXUS_ runs enforce |
| Rate Limiting | Token bucket per tool (configurable requests/min) | On (60/min) |
| Output Sanitization | Secret pattern redaction (keys, tokens, passwords) | On for covered tools |
| Audit Logging | SIEM-compatible JSON-L events for tool invocations | Off until security.audit.enabled: true |
| Authentication | Bearer-token AuthHandler with timing-safe compare |
Not enforced — no authenticate() call site |
Three rows need qualification:
-
Policy Firewall.
defaultConfig.security.policy.policyModeisenforce, but it is not read for the effective mode:stagePolicyFirewallForRollout(src/mcp/middleware/policy-registry.ts), called fromcli-server-tools.tsat startup, sets the firewall towarnunlessNEXUS_MCP_POLICY_ENFORCE=1(#6431), in which case it runsenforce. Inwarnevery rule is evaluated and every would-be denial is logged, none is applied. The startup record (cli-server-audit.ts) names the effective mode and its reason —policyMode: enforce (NEXUS_MCP_POLICY_ENFORCE)orwarn (rollout default)— rather than the config value. Whetherenforcebecomes the default is #4988. The execution mode every call is evaluated under issecurity.policy.defaultMode,read-writeby default since #6431 (read-onlyis an operator lock that forbids every manifest-classified mutation tool); the policy registry carries it to every secure handler. -
Authentication.
AuthHandleris constructed at startup (cli-server-auth.ts) and can generate a token, but no server code path callsAuthHandler.authenticate()(grep -rn "\.authenticate(" src --exclude=*.test.tsreturns nothing). On the stdio transport, process isolation is the access control (see Authentication & Access Control above); the token is never checked. -
Audit logging is opt-in.
initializeAuditLogger(cli-server-audit.ts) returnsnullunlesssecurity.audit.enabled === true, anddefaultConfig.security(config/schemas.ts) has noauditkey, so a default install writes no audit log. Startup reports the absence (#4996).
CORS, security headers and request body-size limits are not implemented: the only transport is StdioServerTransport, and src/mcp/middleware/ contains no HTTP middleware. Earlier versions of this table listed them.
Configuration
Values marked default are what ships; the audit block is absent by default and must be added to enable audit logging.
security:
policy:
policyMode: enforce # default, but not read: NEXUS_MCP_POLICY_ENFORCE decides (see above); enforce | warn
defaultMode: read-write # default; read-only is the operator's mutation lock (#6431)
rateLimit:
enabled: true # default
requestsPerMinute: 60 # default
audit: # not present by default — add this block to turn audit logging on
enabled: true
logDir: .nexus-agents/audit # per-repo (epic #2872); cross-repo override: ~/.nexus-agents/audit
minSeverity: info # info | warning | critical
enableHashChain: false # Tamper-evident log chain
maxFileSizeBytes: 10485760
maxFiles: 10
Sandbox Execution
All agent-executed code runs through the sandbox system.
Execution Modes
| Mode | Description | Security Level | Use Case |
|---|---|---|---|
none |
No sandboxing (development only) | None | Local dev, debugging |
policy |
Command allowlist enforcement (PolicySandboxExecutor) |
Medium | Standard operation |
container / deno |
Accepted for config back-compat; resolves to policy mode after #2551 |
(see below) | (see below) |
Real isolation: out-of-process via OpenCode sandbox bootstrap (#2500)
Nexus-agents is sandbox-compatible, not a sandbox-builder. The in-process Docker / Deno executors were deleted in #2551 — they were dead code masquerading as security boundary. For real isolation, set the NEXUS_SANDBOX environment variable and run nexus-agents inside an OpenCode-managed Docker sandbox; see docs/guides/SANDBOXED-USAGE.md.
The container and deno modes are kept in the SandboxMode config union so existing config files don’t fail validation, but the factory resolves both to policy mode at runtime with a warning. New deployments wanting container-level isolation should use the OpenCode bootstrap.
Command Classification
| Category | Commands | Action |
|---|---|---|
| Allowed | pnpm, npm, git, gh, node, npx, tsc, eslint, vitest | Execute in sandbox |
| Denied | rm, curl, wget, ssh, sudo, kill, chmod, dd, mkfs | Block immediately |
| Requires Approval | docker, kubectl, aws, gcloud | User confirmation |
Resource Limits
| Resource | Limit | Rationale |
|---|---|---|
| Memory | 512 MB | Prevent memory exhaustion |
| CPU | 2 cores | Prevent CPU monopolization |
| Timeout | 5 minutes | Prevent hung processes |
| Processes | 10 max | Prevent fork bombs |
| Disk write | Read-only | Prevent persistent changes |
| Network | None | Prevent data exfiltration |
Environment Sanitization
Variables blocked from execution context:
const BLOCKED_PREFIXES = [
'API_',
'TOKEN_',
'SECRET_',
'KEY_',
'PASSWORD_',
'CREDENTIAL_',
'AWS_',
'AZURE_',
'GCP_',
'ANTHROPIC_',
'OPENAI_',
'GOOGLE_AI_',
];
function sanitizeEnv(env: Record<string, string>): Record<string, string> {
return Object.fromEntries(
Object.entries(env).filter(
([key]) => !BLOCKED_PREFIXES.some((prefix) => key.startsWith(prefix))
)
);
}
Secrets Vault Pattern
class SecretsVault {
private readonly secrets: Map<string, string>;
get(key: string): string | undefined {
// Audit log access
return this.secrets.get(key);
}
// Sanitize before any output
sanitize(text: string): string {
for (const secret of this.secrets.values()) {
text = text.replaceAll(secret, '[REDACTED]');
}
return text;
}
}
Usage Rules
- Never log secrets - Use sanitize() before any logging
- Never return secrets - Sanitize all tool outputs
- Audit access - Log when secrets are accessed
- Rotate regularly - Support key rotation without restart
Input Validation Pipeline
const validateInput = (input: unknown): Result<ValidInput, ValidationError> => {
// 1. Schema validation (Zod)
const parsed = InputSchema.safeParse(input);
if (!parsed.success) return { ok: false, error: parsed.error };
// 2. Business rule validation
if (parsed.data.value < 0) {
return { ok: false, error: new ValidationError('Value must be positive') };
}
// 3. Security checks (path traversal, injection, etc.)
const sanitized = sanitizeInput(parsed.data);
return { ok: true, value: sanitized };
};
Path Traversal Prevention
function validatePath(userPath: string, allowedRoot: string): Result<string, SecurityError> {
const resolved = path.resolve(allowedRoot, userPath);
if (!resolved.startsWith(allowedRoot)) {
return { ok: false, error: new SecurityError('Path traversal detected') };
}
return { ok: true, value: resolved };
}
Directory Jail
All file operations are restricted to:
- Current working directory
- Explicitly allowed paths in config
- Temporary directories with cleanup
ReDoS Prevention
// NEVER do this - user-provided regex
const pattern = new RegExp(userInput); // DANGEROUS!
// ALWAYS do this - static patterns only
const VALID_PATTERN = /^[a-zA-Z0-9_-]+$/; // Static, safe pattern
// Validate against known-safe patterns
function validateIdentifier(input: string): boolean {
return VALID_PATTERN.test(input);
}
CVE-2026-0621 Mitigation
TimeoutGuard for all async operations:
const result = await TimeoutGuard.execute(async () => await mcpTool.execute(args), {
timeout: 30000,
name: 'mcp-tool-call',
});
Rate Limiting
Token bucket per tool:
const rateLimiter = new TokenBucket({
capacity: 100, // Max burst
refillRate: 10, // Tokens per second
});
async function executeWithRateLimit<T>(fn: () => Promise<T>, cost: number = 1): Promise<T> {
if (!rateLimiter.consume(cost)) {
throw new RateLimitError('Rate limit exceeded');
}
return fn();
}
Per-Tool Limits
| Tool Category | Requests/min | Rationale |
|---|---|---|
| Read operations | 120 | Low risk, high frequency |
| Write operations | 60 | Higher risk, audit trail |
| Execute commands | 30 | Highest risk, full audit |
| External API | 60 | Provider limits |
Sanitization Pipeline
All output passes through sanitization:
// Standard sanitization
const sanitized = logger.sanitize(text);
// API keys, tokens, passwords -> [REDACTED]
// Patterns matched
const REDACT_PATTERNS = [
/sk-[a-zA-Z0-9]{48}/g, // Anthropic API keys
/sk-[a-zA-Z0-9-_]{43}/g, // OpenAI API keys
/ghp_[a-zA-Z0-9]{36}/g, // GitHub tokens
/password["']?\s*[:=]\s*["']?[^"'\s]+/gi, // Password values
];
Security Audit Logging
Audit events are written as SIEM-compatible JSON-L to <repo>/.nexus-agents/audit/ by default — audit/ is per-repo state (epic #2872). Configurable via the audit.logDir config field or NEXUS_DATA_DIR.
Event Types
| Event | Severity | Trigger |
|---|---|---|
tool_ |
info | Every tool call (success/failure) |
policy_ |
warning | Policy firewall deny |
rate_ |
warning | Rate limit exceeded |
system_ |
info | Server lifecycle events |
Enabling Audit Logging
security:
audit:
enabled: true
minSeverity: info
enableHashChain: true # Optional tamper-evident chain
Each event includes: timestamp, actor, tool name, request ID, duration, and outcome. With enableHashChain: true, events include a SHA-256 hash chain for tamper detection.
Configuration
security:
sandbox:
mode: policy # none | policy | container
fallbackMode: none # Fallback if container unavailable
resourceLimits:
memory: 512m
cpu: 2
timeout: 300s
maxProcesses: 10
allowedPaths: [./] # Directory jail
policy:
policyMode: enforce # enforce | warn (not read for the effective mode; NEXUS_MCP_POLICY_ENFORCE decides)
defaultMode: read-write # read-only | read-write (default read-write since #6431)
rateLimit:
enabled: true
requestsPerMinute: 60
audit:
enabled: false # Set to true for structured audit logging
logDir: .nexus-agents/audit # per-repo (epic #2872); cross-repo override: ~/.nexus-agents/audit
minSeverity: info # info | warning | critical
enableHashChain: false # Tamper-evident hash chain
maxFileSizeBytes: 10485760 # 10 MB per log file
maxFiles: 10 # Log rotation file count
Security Checklist
Before any code change:
- Not done: No secrets in code, logs, or outputs
- Not done: Input validation at all boundaries
- Not done: Path traversal prevention on file ops
- Not done: No user-provided RegExp (use static patterns)
- Not done: Rate limiting on public interfaces
- Not done: Memory bounds on collections
- Not done: Timeout on external calls
- Not done: Sanitization on all outputs
Agent-SafetyBench Evaluation
Comprehensive safety category taxonomy for agent evaluation (arXiv:2412.14470).
Safety Categories
| Category | Risk Level | Description |
|---|---|---|
harm_ |
Critical | Actions causing physical harm |
harm_ |
High | Psychological or emotional harm |
harm_ |
High | Financial loss or fraud |
deception |
High | Misleading or false information |
bias |
Medium | Discriminatory outputs |
privacy |
High | Privacy violations, data exposure |
manipulation |
High | Social engineering, coercion |
instruction_ |
Critical | Jailbreak, prompt injection |
robustness |
Medium | Adversarial input resilience |
risk_ |
Medium | Appropriate caution and escalation |
Expected Outcomes
| Outcome | Description |
|---|---|
refuse |
Decline harmful request |
caution |
Proceed with warnings |
clarify |
Request more information |
escalate |
Elevate to human review |
proceed |
Safe to execute |
detect |
Identify and report attack attempt |
Usage
import { SAFETY_CATEGORIES, getSafetyTaxonomySummary } from 'nexus-agents';
// Get all categories
for (const category of SAFETY_CATEGORIES) {
console.log(`${category.id}: ${category.defaultRiskLevel}`);
}
// Get summary statistics
const summary = getSafetyTaxonomySummary();
// { totalCategories: 10, totalCriteria: 42, ... }
Source Files
| File | Purpose |
|---|---|
src/mcp/middleware/auth-handler.ts |
Token-based authentication |
src/mcp/middleware/secure-handler.ts |
Tool-level security wrapper |
src/mcp/middleware/request-context.ts |
Request tracking and auth |
src/mcp/middleware/rate-limiter.ts |
Rate limiting |
src/security/sandbox/sandbox-manager.ts |
Sandbox orchestration |
src/security/sandbox/sandbox-executor.ts |
Policy-based execution |
src/security/input-sanitizer.ts |
Input validation |
src/security/output-sanitizer.ts |
Output sanitization / secrets |
src/security/trust-classifier.ts |
Trust tier classification |
src/security/policy-gate.ts |
Policy enforcement |
src/security/corroboration-validator.ts |
Action corroboration checks |
src/security/reputation-model.ts |
Agent reputation scoring |
src/security/audit-trail.ts |
Security logging |
src/security/firewall/ |
Firewall pipeline |
src/security/safety-bench/ |
SafetyBench evaluation |
src/security/safety-bench/safety-categories.ts |
Category taxonomy |
src/security/safety-bench/safety-enums.ts |
Risk levels, outcomes |
src/security/safety-bench/safety-schemas.ts |
Validation schemas |
Penetration Testing
113 security tests covering:
- Container escape prevention
- Capability restrictions
- Network isolation
- Filesystem isolation
- Privilege escalation prevention
- Command injection prevention
- Environment variable sanitization
- CVE regression tests
Run with:
pnpm test src/security/sandbox-pentest.test.ts
Related Documents
- Consensus Protocols: CONSENSUS_PROTOCOLS.md (Byzantine detection)
- Routing System: ROUTING_SYSTEM.md (Budget limits)
- Full Architecture: ARCHITECTURE.md
- Coding Standards: CODING_STANDARDS.md