Semantic Verification¶
Pattern matching handles 95%+ of decisions instantly. For the ambiguous rest, Rampart supports LLM-based intent classification via the optional rampart-verify sidecar.
The Problem¶
Pattern matching is fast and reliable for known-dangerous commands. But some commands are ambiguous:
python3 -c "import os; os.system('...')"— dangerous or benign?curl https://internal-api.company.com/admin— legitimate or exfiltration?find / -name "*.pem" -exec cat {} \;— auditing or credential theft?
Static rules can't distinguish intent. An LLM can.
Two-Layer Defense¶
| Layer | Speed | Cost | Handles |
|---|---|---|---|
| Pattern matching | ~5μs | Free | Known patterns — destructive commands, credential paths, exfil domains |
| Semantic verification | ~500ms | ~$0.0001/call | Ambiguous commands — obfuscated payloads, encoded scripts, context-dependent intent |
Pattern matching fires first. If a command matches a webhook rule, it's forwarded to the sidecar. The LLM classifies intent and returns allow or deny. Everything else never touches the LLM.
rampart-verify¶
rampart-verify is a standalone Python sidecar (FastAPI) that classifies commands using LLMs. It integrates with Rampart via the action: webhook policy action.
Features¶
- Secret redaction — API keys, tokens, passwords, and credentials are stripped before commands reach the LLM (13 pattern categories including AWS, Stripe, GitHub, OpenAI, bearer tokens, basic auth URLs)
- Rate limiting — Token bucket limiter, configurable via
VERIFY_RATE_LIMIT(default: 60 req/min) - Decision logging — Every classification logged to
~/.rampart/verify/decisions.jsonl(append-only) - Health check —
GET /healthpings the LLM provider and reports latency - Metrics —
GET /metricsreturns request counts, allow/deny totals, average latency, uptime - Provider fallback — If no API key is set for the requested model, falls back to Ollama (local, free)
- Configurable prompt — Override via
VERIFY_SYSTEM_PROMPTor extend withVERIFY_EXTRA_RULES - Fail-open — If the LLM is down or times out, commands are allowed (configurable)
Supported Models¶
| Tier | Model | Latency | Cost |
|---|---|---|---|
| Free | qwen2.5-coder:1.5b (Ollama) | ~100-600ms | $0 |
| Budget | gpt-4o-mini (OpenAI) | ~400ms | ~$0.0001/call |
| Balanced | Claude Haiku (Anthropic) | ~500ms | ~$0.0003/call |
Any OpenAI-compatible API works (Together, Groq, local vLLM) via OPENAI_BASE_URL.
Setup¶
Install¶
git clone https://github.com/peg/rampart-verify.git
cd rampart-verify
pip install -r requirements.txt
Configure Provider¶
OpenAI (recommended):
Fully offline with Ollama:
export VERIFY_PROVIDER=ollama
export VERIFY_MODEL=qwen2.5-coder:1.5b
# Requires Ollama running locally: ollama serve
Anthropic:
export VERIFY_PROVIDER=anthropic
export VERIFY_MODEL=claude-3-haiku-20240307
export ANTHROPIC_API_KEY=sk-ant-...
Start the Sidecar¶
Or with Docker Compose:
Configure Rampart¶
Add a webhook rule to route ambiguous commands to the sidecar:
policies:
- name: semantic-verify
match:
tool: ["exec"]
rules:
- action: webhook
when:
command_matches:
- "python3 -c *"
- "python3 -m *"
- "node -e *"
- "eval *"
- "base64 *"
webhook:
url: "http://localhost:8090/verify"
timeout: 5s
fail_open: true
How It Works¶
graph LR
A[Agent] -->|command| R[Rampart]
R -->|known pattern| D1[Allow/Deny]
R -->|ambiguous| V[rampart-verify]
V -->|redact secrets| V
V -->|classify intent| LLM[LLM]
LLM -->|ALLOW / DENY| V
V -->|decision| R
R -->|audit log| L[Audit Trail] - Agent executes a command → Rampart evaluates policies
- If a
webhookrule matches → command is forwarded to rampart-verify - rampart-verify redacts secrets from the command
- The redacted command is sent to the configured LLM
- LLM classifies intent → returns ALLOW or DENY with reason
- rampart-verify returns the decision to Rampart
- The full (unredacted) command and decision are logged to Rampart's audit trail
Secret Redaction¶
Commands are sanitized before reaching the LLM. The sidecar strips:
- AWS access keys and secrets
- Stripe live/test keys
- OpenAI, Anthropic, and generic API keys
- GitHub personal access tokens
- Bearer and Authorization headers
- Basic auth credentials in URLs
- Hex tokens (40+ characters)
- Base64 blobs in header values
Example:
Input: curl -H "Authorization: Bearer sk-proj-abc123..." https://api.example.com
Sent: curl -H "Authorization: Bearer [REDACTED]" https://api.example.com
Endpoints¶
| Endpoint | Method | Description |
|---|---|---|
/verify | POST | Main classification endpoint (called by Rampart webhook) |
/health | GET | Health check — pings LLM provider, reports latency |
/metrics | GET | Request counts, allow/deny totals, avg latency, uptime |
/ | GET | Service info and configured model |
Environment Variables¶
| Variable | Default | Description |
|---|---|---|
VERIFY_MODEL | gpt-4o-mini | LLM model to use |
VERIFY_PORT | 8090 | Server port |
VERIFY_HOST | 127.0.0.1 | Bind address |
VERIFY_RATE_LIMIT | 60 | Max requests per minute |
VERIFY_LOG_DIR | ~/.rampart/verify | Log and decision file directory |
VERIFY_SYSTEM_PROMPT | (built-in) | Override the entire system prompt |
VERIFY_EXTRA_RULES | (none) | Append additional rules to the default prompt |
OPENAI_API_KEY | — | OpenAI API key |
OPENAI_BASE_URL | https://api.openai.com/v1 | Custom OpenAI-compatible endpoint |
ANTHROPIC_API_KEY | — | Anthropic API key |
OLLAMA_URL | http://localhost:11434 | Ollama server URL |
Running as a Service¶
systemd (Linux)¶
cat > ~/.config/systemd/user/rampart-verify.service << 'EOF'
[Unit]
Description=Rampart Verify Sidecar
After=network.target
[Service]
WorkingDirectory=/path/to/rampart-verify
ExecStart=/usr/bin/python3 server.py
Restart=on-failure
EnvironmentFile=%h/.rampart-verify.env
[Install]
WantedBy=default.target
EOF
systemctl --user enable --now rampart-verify
Store your API key in ~/.rampart-verify.env:
Permissions
Set chmod 600 ~/.rampart-verify.env — this file contains your API key.
Security Notes¶
- The sidecar binds to
127.0.0.1by default — not accessible from the network - There is no authentication on sidecar endpoints. On shared machines, use a firewall or bind to a Unix socket
- Secret redaction is best-effort — custom secret formats may not be caught. Add patterns via
VERIFY_EXTRA_RULESor contribute toredact.py - The LLM never sees actual secret values, only the command structure