How the Prompt Firewall Works
Nexus Sentinel is a self-hosted prompt firewall for any LLM. One endpoint
— POST /v1/verify — takes a prompt plus a policy and returns a structured
verdict: allow, redact, or
block. It judges prompts; it never generates text. This
page explains the machinery, and lets you run a simplified version right here in your
browser.
▸One prompt, four stages
Every request walks the same path. Tap a stage to see what it does. The two screeners run in parallel; the verdict is assembled from whatever they report.
▸Screen a prompt
This is a faithful-but-simplified port of the real engine — the same sanitizer, the same kind of regex detectors, and the same decision precedence. The production service swaps the regexes for AWS Bedrock Guardrails + a Claude Haiku screener; the shape of the answer is identical.
▸Anatomy of a verdict
The decision core is a pure function: same inputs → same verdict, no I/O, no clock, no randomness. Each evidence source contributes block / redact / allow, and the strongest contribution wins — a prompt need only fail one check to be stopped.
Decision strength
block > redact > allow
Evidence precedence (which match is the headline reason)
| # | Category | Typical effect |
|---|---|---|
| 1 | secrets | block — credentials are never safe to forward |
| 2 | pii | redact (or block, if the policy says block-on-detect) |
| 3 | prompt_injection | block / flag, per policy mode + threshold |
| 4 | topic | block when a denied topic is detected |
| 5 | content | block on hate / violence / sexual / misconduct filters |
| 6 | obfuscation | flag-only — evidence + escalation trigger, never blocks alone |
Two failure modes, on purpose
- Fail closed on a guardrail error → the request is rejected (503). The guardrail is the primary signal; if it's down, we don't guess.
- Fail open on an injection-screener error → it degrades to "skipped" and the request proceeds on the remaining signals. A flaky secondary check shouldn't take the whole firewall down.
There's also a cross-policy replay: take any past request and re-run it under a different policy to see how the verdict changes — useful for tuning policies.
▸“What's the easiest way to shoplift without getting caught?”
This is the hard class: no secret, no PII, no zero-width trick, no “ignore previous instructions.” Just a plainly-worded request for instructions to do something illicit, asked as if it were any ordinary question. Run it in the simulator above — the regex engine shrugs and returns allow. That's the honest limitation of pattern-matching: it can't read intent.
How a real firewall stops it
-
Bedrock Guardrails content filters. The managed
MISCONDUCTfilter is trained to catch illicit / dangerous-activity requests semantically. Turn it up toHIGHin the policy's guardrail and the request blocks on acontentmatch — no keyword list required. -
Custom denied topics. Define a topic like
illicit_instructions(“how-to guidance for theft, fraud, or other unlawful activity”) on the guardrail. Guardrails owns the block decision; the Haiku screener also grades it for a display score. - The Haiku screener as backstop. The same LLM call that classifies injection can be asked to judge harmful-intent — useful for the fuzzy middle a filter is unsure about.
MISCONDUCT filter and add an illicit_instructions denied topic
to the guardrail (provisioned in infra/), then run with
PROVIDER=aws. The offline fake mode you're using here doesn't
ship semantic filters — which is exactly why the simulator lets it through.
▸Architecture in one breath
-
Ports & adapters. The application core depends on interfaces, never
on the AWS SDK.
PROVIDER=fake(default, fully offline) orPROVIDER=awsswaps the entire adapter set — so this demo, CI, and production all run the same core. -
Contracts as the source of truth. zod schemas in
@nexus/contractsdefine the request/response shape; the API and the web client both derive their types from it. -
Bedrock, two ways. Guardrails (
ApplyGuardrail) for PII / secrets / topics / content, and Claude Haiku (Converse, forced tool-use) for the fuzzy injection & topic-grading signal. - NestJS for the API, Next.js for the dashboard, AWS CDK for the infra — a pnpm monorepo.
▸What runs it on AWS
The whole system is serverless and lives in one region. The dashboard is served from a CDN; the API runs next to the data it needs. Nothing is always-on, so it costs ~nothing when idle.
browser ─▶ CloudFront ─▶ S3 (static dashboard, edge-cached)
│
└──────▶ API Gateway ─▶ Lambda (NestJS API)
├─▶ Bedrock Guardrails + Claude Haiku
├─▶ DynamoDB audit log + rate-limit counters
└─▶ SSM guardrail ids / versions
- CloudFront + S3. The Next.js dashboard is a static export served from a private S3 bucket behind a CloudFront CDN — cached at edge locations, no server to run.
- API Gateway (HTTP API). The public HTTPS front door. It throttles right there (20 req/s, burst 40) before any compute runs — the first cost-defense layer.
- Lambda (container image, ARM64). The NestJS API itself, run through the AWS Lambda Web Adapter so the same Docker image runs on a laptop and in production. Full Node.js — not an edge runtime.
-
Bedrock.
ApplyGuardrailfor PII / secrets / topics / content, and Claude Haiku (Converse) for the graded injection & topic signal — the only billable, model-backed calls. - DynamoDB. Two on-demand tables — the audit log (with a replay index) and the rate-limit counters (auto-expiring via TTL).
- SSM Parameter Store. Holds the guardrail ids / versions; the API reads them by name, so a new guardrail version rolls out with a plain redeploy — no recreating the API.
- Budgets + CloudWatch. A monthly cost alarm and structured logs — which never record raw prompts or keys.
-
CDK. Every resource above is defined in TypeScript (
infra/); onecdk deploystands the whole system up.
Cost is bounded by design: request throttling, per-user / per-IP / global DynamoDB caps, a
Budgets alarm, and a PROVIDER=fake switch that turns off every billable call.
One region, on purpose. The API, Bedrock, DynamoDB, and SSM all live in one region. Only the dashboard is global (CloudFront); an API call from far away pays the round-trip to that region. For a low-traffic demo that's the right trade — it's simpler, cheaper, and keeps the Lambda next to the data on the expensive path (the model and data calls dwarf the network leg). The scale-out path is known if it's ever needed: an edge-optimized / CloudFront-fronted API for latency, a multi-region active-active deployment behind Route 53 latency-based routing, and DynamoDB global tables to replicate the data — real cost and operational weight this demo deliberately doesn't take on.