Skip to content
OpenSmartRoute

Security Model

Threat model, guard middleware, PII handling, provenance and the red-team suite.

docs/SECURITY.md

OpenSmartRoute is an LLM control plane: it decides who answers. That makes its integrity a security property in its own right (Shafran et al., "Rerouting LLM Routers", 2025). This document lists assets, threats, and the controls implemented, mapped to OWASP Top 10 / OWASP LLM Top 10.

Assets#

  1. Routing decisions (integrity, availability)
  2. Request content (confidentiality – may contain PII / secrets)
  3. Learner state (integrity – poisoning changes future decisions)
  4. Configuration & credentials of downstream targets
  5. Audit trail

Threat model & controls#

#ThreatOWASP refControlCode
T1Confounder gadgets – attacker appends token soup to force routing to the expensive model (cost DoS) or to a weaker model (quality attack)LLM01, LLM04Head/tail naturalness analysis; signals computed on cleaned head; optional reject; learned gadget detector (InputGuard(learned=True), trained with osr train --gadget); router-directed sentences ("this is extremely complex", [complexity=1]) are scrubbed before signal extractionsecurity.InputGuard, security.gadget.GadgetDetector, security.injection.strip_steering, GuardMiddleware
T2Prompt injection into LLM judge – request text tells the judge which target to pickLLM01Request wrapped in a fenced block; role markers and code fences neutralised; control/zero-width chars stripped; judge output parsed as strict JSON, item by item: non-object items, unparseable / NaN scores and ids not in the candidate list are dropped, scores clipped to [0,1]; judge only enabled below a confidence threshold; judge failures carry zero confidence so the ensemble ignores themsecurity.sanitize_for_prompt, LLMJudgeStrategy
T3Policy bypass – PII to a cloud model, EU data to US region, tenant reading another tenant's targetsA01 Broken access controlPolicy evaluated before scoring and cannot be outweighed; tenant middleware; allow/deny listspolicy.Policy, TenantMiddleware
T4Data leakage via logs / cache / feedbackA09 Logging failures, LLM06Logs contain hash+length only; cache keys are SHA-256; Redactor masks PII before routing; feedback store records ids, not textLoggingTelemetry, CacheMiddleware, Redactor, FeedbackStore
T5Learner poisoning – fake outcomes to steer routingLLM03 Training-data poisoningOutcomes only accepted via learn() from your code path (auth is your boundary); graded rewards clipped to [0,1]; Bayesian priors + confidence ramps limit the impact of few samples; Page–Hinkley drift alerts on sudden shifts; state files atomic and hash-addressable; corrupt / incompatible state is quarantined on load instead of being trusted or crashing (AutoLearner.quarantined); federated merge() adds only evidence beyond the priorAutoLearner, math.estimators, learning.merge_learners
T6Resource exhaustion – huge prompts, unbounded stateA04 Insecure design / DoSmax_chars/max_tokens guard; every in-memory map is bounded; per-target rate limits and budgets; breaker isolates failing targetsInputGuard, realtime
T7Path traversal in state storeA01Keys hashed to filenames; writes are atomic to a fixed rootFileStateStore
T8Secrets in configA02 Cryptographic failures / A05 MisconfigTargets YAML never contains credentials; load_secret() reads env or *_FILEsecurity.load_secret
T9DeserialisationA08 Software & data integrityOnly JSON / yaml.safe_load; no pickle anywhereTargetRegistry.from_file, learners
T10Audit tamperingA09Hash-chained audit records; the chain resumes from the last record across restarts (no second genesis) and FileAuditSink.verify(path) re-walks it naming the first edited, removed or unparseable lineFileAuditSink
T11Supply chainA06 Vulnerable componentsZero runtime dependencies in core; optional extras pinned by minimum version; CI runs bandit, ruff -S, mypypyproject.toml, CI
T12Jailbreak content reaching a modelLLM01jailbreak_risk ≥ 0.5 → only safety-tagged targets or humans are admissible; run_safety_suite() / osr safety regression-tests that safety routing survives paraphrase, gadgets and steeringPolicy.check, security.safety
T13Poisoned catalogue entries – MCP tool descriptions, A2A agent cards or skill files carry instructions to the router or modelLLM01, A08Instruction-injection lexicon + learned gadget score on every imported description; poisoned tools dropped at import time and reported; signed MCP manifests (HMAC-SHA256 or Ed25519, freshness window) so only vetted catalogues loadsecurity.injection, adapters.mcp.tools_from_mcp, sign_manifest / verify_manifest
T14Parameter provenance – a tool result or retrieved document smuggles a value into a state-changing tool call ("send the refund to this account")LLM01, A01OriginPolicy: sensitive parameters of state-changing tools must originate in the user turn; violations raise OriginViolation before executionsecurity.provenance
T15Resource amplification in agentic plans – runaway loops, recursive tool calls, token / cost blow-upA04, LLM10Per-task caps on steps, tool calls, depth, tokens, cost and wall-clock enforced as middleware; multi-resource knapsack bandit checks hard meters before commitment and audits every pruned armsecurity.limits.ResourceLimiter, ResourceLimitMiddleware, math.bandits.MultiKnapsackBandit
T16Learner state at rest – snapshots reveal usage patterns or are modified on diskA02AES-256-GCM EncryptedStateStore with key rotation (cryptography extra); versioned store refuses to load unknown schema versionsenterprise.stores.EncryptedStateStore, VersionedStateStore

Secure defaults#

  • Core has no network access; it never calls a model unless you wire an LLMJudgeStrategy or a target handler.
  • Logging is content-free, and so is tracing: span and event attributes carry ids, numbers, short labels and a text digest + length (observability.text_digest), never the request text. The hosted platform reads traces per workspace only (GET /api/v1/trace/{request_id} is 404 for another workspace's id).
  • The LLM judge is off by default and, when on, only consulted below a confidence threshold.
  • Redactor is opt-in (GuardMiddleware(redact=True)); when on, the original text is kept only in request.context["_pii_map"] for the executor and is never persisted by the SDK.

What OpenSmartRoute does not do#

  • Authenticate callers. Put it behind your API gateway / service mesh.
  • Encrypt state at rest by default. Use disk/volume encryption or wrap your store in enterprise.stores.EncryptedStateStore.
  • Guarantee the downstream target's safety. Routing to a "safe" target is a policy you write.

Threat-model delta: 0.3 -> 0.4#

What changed for operators upgrading, and which residual risks remain:

Area0.30.4Residual risk
Gadget / steering (T1)heuristic head/tail analysis+ learned detector, + steering-sentence scrub before signal extractiondetector is trained on synthetic gadgets; re-train on your traffic (osr train --gadget)
Judge injection (T2)fenced prompt, strict JSON+ per-item filtering, id allow-list, NaN / type guards, zero-confidence failuresa compromised judge can still bias ranks among legitimate ids; keep escalate_llm_judge_below <= 0.5
Catalogue poisoning (T13)noneinjection lexicon + gadget score on imported descriptions; signed manifestslexicon is English-centric; sign manifests for anything you did not author
Parameter provenance (T14)noneOriginPolicy for state-changing toolspolicy is per tool / parameter; unlisted tools are not checked
Resource amplification (T15)per-target rate limits and budgets+ per-task caps (steps, calls, depth, tokens, cost, wall-clock), knapsack meters before commitmentcaps are enforced in the routing process; a handler that ignores the returned budget can still spend
Learner integrity (T5)atomic writes+ quarantine of corrupt / incompatible state, versioned schema, federated merge adds evidence onlya valid-looking poisoned snapshot is indistinguishable from real learning; restrict write access to the store
Audit (T10)hash chain per process+ chain continuity across restarts, verify()the log file itself must be write-once at the OS level to make verification meaningful
State at rest (T16)plain JSON+ AES-256-GCM store with rotationkey management is yours (load_secret)
Deploymentnonenon-root container, hardened Helm chart (read-only rootfs, drop all capabilities, NetworkPolicy)the chart exposes /route unauthenticated inside the cluster; front it with your gateway

External review of this delta is outstanding; SECURITY_REVIEW.md is the pack a reviewer works from and its review log is where the report is linked (tracked in ROADMAP.md).

Hardening checklist for operators#

  • Run osr serve behind TLS termination and authn/z (OIDC / mTLS).
  • Enable GuardMiddleware(redact=True) for any tenant that may send PII.
  • Set TargetConstraints.pii_allowed=False and data_boundary on every cloud target.
  • Configure HealthRegistry.configure(rate_per_s=…, budget_limit=…) per expensive target.
  • Ship MetricsTelemetry to your monitoring; alert on AutoLearner.drifted and breaker opens.
  • Rotate and restrict access to the state directory; back it up (it is your learned policy).
  • Alert on AutoLearner.quarantined (corrupt state was replaced) and run FileAuditSink.verify() on the audit log from a read-only replica.
  • Sign MCP manifests you import (osr mcp-manifest) and fail closed on verify_manifest errors.
  • Run osr safety --learned-guard in CI so safety routing regressions block a release.
  • Pin the package version; watch the security advisories on this repo.

Reporting#

See SECURITY.md.