Skip to content
OpenSmartRoute
Documentation
Security

Security Model

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

Security 8 min read

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
T17Tool calls made by the model – a plan offers tools and the executor runs what the model calls: a prompt-injected model could call an offered tool with attacker-chosen argumentsLLM01, LLM07Only tool targets that passed the policy filter and the quality floor for this request are offered (tool_slots, tool_quality_floor; OSR_ROUTING_TOOL_SLOTS=0 turns the slot off); calls to tools the router did not offer are handed back, never run; arguments are parsed as JSON and passed as context["tool_arguments"] with the model-only context keys stripped; the PII boundary applies to the tool; rounds are capped (tool_rounds); OriginPolicy and ResourceLimiter (T14, T15) apply to the tool's handler as to any otherexecution._offer_tools, execution._tool_request, RoutingSettings
T18Catalogue and knowledge pulled from the network – marketplace listings synced into a running router, article feeds read by the self-improvement loopA08, LLM03Marketplace: https only (or localhost http), manifests validated as Open Capability Manifests, third-party descriptions risk-scored like any import; MarketplaceSync follows only the kinds / query / slugs you configure. Feeds: https only, byte-capped, stdlib XML parser without external-entity expansion, articles can only make a priced catalogue card eligible as a candidate - they never set a quality prior and never bypass the champion / challenger promotion gatesadapters.marketplace, adapters.websearch.parse_feed, learning.self_improve
T19Leakage and injection in the answer – a model prints a credential it saw in a file, echoes personal data that was never in the prompt, or relays a poisoned tool result as an instruction to the next agentLLM02 Insecure output handling, LLM06OutputGuard screens every model / agent answer after the handler returns: credential shapes (SECRET_PATTERNS: OpenAI, Anthropic, AWS, GitHub, Slack keys, private-key blocks, JWTs), the input guard's PII entities and inspect_injection on the answer text. flag records the findings (output_guard.flagged event, context["output_guard"]), redact replaces them with typed placeholders, block raises SecurityError (output_guard.blocked); a streamed answer under redact / block is buffered (default) or screened delta by delta through StreamScreen before anything reaches the consumer, so a secret split across chunks never leaves. POST /v1/moderations exposes the same screens without a provider callsecurity.OutputGuard, security.output.StreamScreen, security.moderate, execution._guard_output, GuardMiddleware(output=), RouterBuilder.with_guard(output=), serve --output-guard

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; External review pack is the pack a reviewer works from and its review log is where the report is linked.

Hardening checklist for operators

  • Run osr serve behind TLS termination and a token (osr serve --require-auth --token ...); give dashboards and CI read-only tokens (<token>:ro - they cannot execute, spend or teach the learners).
  • Turn the controls on for the served router - no Python needed: osr serve --guard --redact-pii --output-guard redact --health --audit /state/audit.jsonl --origin-policy --limits --state-store redis://... (or OSR_SERVER_GUARD=1, OSR_SERVER_REDACT_PII=1, OSR_SERVER_OUTPUT_GUARD=redact, OSR_SERVER_HEALTH=1, OSR_SERVER_AUDIT_PATH, OSR_SERVER_ORIGIN_POLICY=1, OSR_SERVER_RESOURCE_LIMITS=1, OSR_SERVER_STATE_STORE, OSR_SERVER_STATE_KEY_ENV in the Helm chart's env). In code the same is enterprise.harden(router, guard=True, redact_pii=True, ...) or RouterBuilder.with_guard(redact=True) .with_origin_policy().with_limits().
  • Set TargetConstraints.pii_allowed=False and data_boundary on every cloud target.
  • Treat live web context as untrusted input. --web-grounding (WebGroundingMiddleware) puts search hits in front of the model for requests about current facts; every title and snippet is scored with the injection lexicon and dropped at OSR_SLM_PAGE_RISK_THRESHOLD (LiveContext.dropped, web.grounded events carry the count), the query is the request's first sentence only, fetches are https-only and byte-capped, and the hits never reach a tool call - keep --output-guard on so a page that slips through cannot exfiltrate through the answer either.
  • 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 Reporting vulnerabilities.

Something wrong or out of date on this page? Tell us - the page is named for you, so a one-line note is enough.