A [pREST](https://github.com/prest/prest) middleware plugin that puts the [guard-core](https://github.com/Guard-Core/guard-core-go) engine in front of pREST's CRUD routes: per-client rate limits, IP policy (blacklist, whitelist, exemptions), optional payload inspection across 19 attack categories, and opt-in auto-banning.
Website · Playground · Dashboard · Discord
Guard Core is the Python engine. Framework adapters are thin wrappers that translate native request/response types into Guard Core's protocols. The telemetry agents ship security events and metrics to the monitoring backend. Parallel engine implementations exist for Go, PHP, TypeScript (on npm), and Rust (on crates.io) - all ports of the same reference semantics, conformance-tested against the shared adversarial corpus.
| Package | Role | PyPI |
|---|---|---|
| guard-core | Framework-agnostic security engine | |
| guard-agent | Telemetry agent | |
| fastapi-guard | FastAPI / Starlette adapter | |
| flaskapi-guard | Flask adapter | |
| djapi-guard | Django adapter | |
| tornadoapi-guard | Tornado adapter |
Go modules published via GitHub releases. Production-ready.
| Package | Role | Release |
|---|---|---|
| guard-core-go | Go engine | |
| nethttp-guard | net/http adapter | |
| gin-guard | Gin adapter | |
| echo-guard | Echo (v4) adapter | |
| fiber-guard | Fiber (v3) adapter | |
| guard-agent-go | Telemetry agent |
Published on Packagist under the rennf93 vendor. Production-ready.
| Package | Role | Packagist |
|---|---|---|
| guard-core-php | PHP engine | |
| laravel-guard | Laravel adapter | |
| symfony-guard | Symfony adapter | |
| psr15-guard | PSR-15 adapter | |
| slim-guard | Slim 4 adapter | |
| guard-agent-php | Telemetry agent |
Published under the @guardcore npm scope; source in the guard-core-ts monorepo. Production-ready.
| Package | Role | npm |
|---|---|---|
| @guardcore/core | Core engine | |
| @guardcore/express | Express adapter | |
| @guardcore/nestjs | NestJS adapter | |
| @guardcore/fastify | Fastify adapter | |
| @guardcore/hono | Hono (edge) adapter | |
| guardagent | Telemetry agent |
Published on crates.io. Production-ready.
| Package | Role | crates.io |
|---|---|---|
| guard-core-engine | Core engine crate | |
| guard-core-rs | Facade crate (consumer entry point) | |
| actix-guard-rs | Actix Web adapter | |
| axum-guard-rs | Axum adapter | |
| tower-guard-rs | Tower adapter | |
| rocket-guard-rs | Rocket adapter | |
| guard-agent-rs | Telemetry agent |
| Package | Role | PyPI |
|---|---|---|
| guard-core-mcp | MCP server: config validation, docs search, detection sandbox |
- Rate limiting per client IP with Redis-shared state
- IP policy: blacklist, whitelist, and exemptions
- Payload inspection across 19 attack categories, bounded by a measured default body budget
- Opt-in auto-banning with per-category thresholds
- Strict fail-closed config: every raw value validated, invalid config is a loud error
- Passive preview mode: log what would be blocked, reject nothing
📚 Documentation - full technical documentation for this package.
🛡️ Guard Core - the engine's reference documentation.
🤖 Monitoring Agent Integration - monitor your Guard instance with a monitoring agent.
pREST loads Go native plugins (-buildmode=plugin), and the Go runtime
requires a plugin to share its build identity with the host binary: build
prest-guard with the same Go toolchain minor prestd was built with
(1.26.x today) and keep shared dependencies at the same versions (the
plugin's go.mod pins negroni v3.1.1, matching prest main).
# 1. Build the plugin (CGO is required for -buildmode=plugin)
CGO_ENABLED=1 go build -buildmode=plugin -trimpath -ldflags "-s -w" \
-o lib/middlewares/guard.so .
# 2. Register it in your prest config
# [[pluginmiddlewarelist]]
# file = "guard"
# func = "Guard"
# 3. Turn it on
export PREST_GUARD_ENABLED=true
export PREST_GUARD_RATE_LIMIT=100file and func produce the GuardMiddlewareLoad symbol the pREST plugin
loader looks up, from lib/middlewares/guard.so.
All settings come from PREST_GUARD_* environment variables (pREST passes
no configuration to middleware plugins). List values are comma-separated.
Every value is validated as its raw string: a typo like
PREST_GUARD_RATE_LIMIT=100/min is a loud config error, never a silent
disable. With the guard enabled, config errors fail closed (500 with a
generic JSON body); with it disabled they only log a warning.
| Variable | Default | Meaning |
|---|---|---|
PREST_GUARD_ENABLED |
false |
Mount the guard at all |
PREST_GUARD_PASSIVE |
false |
Log what would be blocked, reject nothing (rule preview) |
PREST_GUARD_RATE_LIMIT |
0 |
Requests per window per client IP; 0 disables |
PREST_GUARD_RATE_LIMIT_WINDOW |
60 |
Window seconds |
PREST_GUARD_INSPECT_PAYLOADS |
false |
Payload inspection (query params, URL path, headers, bodies) |
PREST_GUARD_INSPECT_CATEGORIES |
all | Restrict inspection to categories (sqli,xss,cmd_injection, ...) |
PREST_GUARD_AUTO_BAN |
false |
Ban IPs after repeated violations |
PREST_GUARD_AUTO_BAN_THRESHOLD |
10 |
Violations per category before a ban |
PREST_GUARD_AUTO_BAN_DURATION |
3600 |
Ban length in seconds |
PREST_GUARD_BLACKLIST |
empty | Blocked IPs/CIDRs (403) |
PREST_GUARD_WHITELIST |
empty | Allowed IPs/CIDRs; when set, everyone else is denied |
PREST_GUARD_EXEMPT_IPS |
empty | IPs/CIDRs that skip checks (still subject to active bans) |
PREST_GUARD_EXCLUDE_PATHS |
empty | Extra paths whose subtree the engine scopes as excluded (detection and policy checks skip; IP bans, IP policy and rate limits still run, see below) |
PREST_GUARD_TRUSTED_PROXIES |
empty | Proxy IPs/CIDRs whose X-Forwarded-For is trusted |
PREST_GUARD_TRUSTED_PROXY_DEPTH |
engine default (1) |
How many forwarded hops from the right belong to your own proxies |
PREST_GUARD_BLOCK_CLOUD_PROVIDERS |
empty | Block datacenter ranges (AWS,GCP,Azure) |
PREST_GUARD_MAX_BODY_BYTES |
8192 |
Body prefix the inspection layer reads |
PREST_GUARD_REDIS_URL |
empty | Share rate limit and ban state across instances |
PREST_GUARD_REDIS_PREFIX |
prest_guard |
Key namespace in Redis |
PREST_GUARD_REDIS_FAIL_OPEN |
true |
Keep serving from per-instance memory when Redis is down |
PREST_GUARD_ENGINE_CONFIG |
empty | Inline JSON object over the full engine config (see below) |
PREST_GUARD_ENGINE_CONFIG_FILE |
empty | Path to a JSON file with the same content; set only one of the two |
The variables above are a subset of the engine's SecurityConfig. Everything else (excluded detection headers, params and body fields, Detection tuning, FailSecure, endpoint rate limits, EnforceHTTPS, security headers, CORS, custom error responses, global behavior rules, blocked user agents, country lists, dynamic rules, emergency mode, agent settings, and the rest) is reachable through PREST_GUARD_ENGINE_CONFIG or PREST_GUARD_ENGINE_CONFIG_FILE: a JSON object whose keys are guardcore.SecurityConfig field names. It is decoded over the config the PREST_GUARD_* variables produce, so a key named in the JSON wins over the matching variable and the keys it does not name keep their values. Nested structs and maps merge into the engine defaults, slices and scalars replace them, durations are integer nanoseconds, and ExcludePaths can only add to the built-in defaults. Unknown keys, malformed JSON, func and interface fields, and values the engine rejects are loud config errors: an enabled guard answers 500 instead of running with a config you did not ask for.
export PREST_GUARD_ENGINE_CONFIG='{"BlockedUserAgents":["sqlmap"],"CustomErrorResponses":{"403":"blocked"},"ExcludedDetectionParams":{"_where":true}}'Fields JSON cannot carry (CustomResponseModifier, AgentHandler, GeoIPHandler, CustomRequestCheck, OnError) are set from Go: a plugin built from a fork wraps Load with its own GuardConfig.Configure, which receives the fully built engine config last.
func GuardMiddlewareLoad() negroni.Handler {
return Load(func() GuardConfig {
cfg := ParseGuardConfigFromEnv()
cfg.Configure = func(c *guardcore.SecurityConfig) {
c.CustomRequestCheck = myCheck
}
return cfg
})
}CustomResponseModifier takes effect once the pinned nethttp-guard release applies it (the nethttp adapter wires the engine's response modifier from the release after v1.4.0).
Three defaults exist specifically because most pREST deployments sit behind a load balancer:
/_health,/_ready, and the docs/static paths are always excluded, and operator entries inEXCLUDE_PATHSare merged with them, not replacing them. Exclusion is the engine's own scoping, the same as in the other adapters and the reference: detection and the route and request policy checks skip these paths, and their responses still carry the security headers and count in the metrics. IP bans, the IP blacklist and whitelist, and rate limits are still enforced on them. Health probes share the proxy's identity with all user traffic, so list the probe source inEXEMPT_IPS(and setTRUSTED_PROXIES) to keep a rate limit from taking pods out of rotation.- Auto-banning is opt-in. With an empty
TRUSTED_PROXIES, every client shares the load balancer's IP, so a handful of violations would ban all users at once. Starting the guard withoutTRUSTED_PROXIESlogs a loud warning saying exactly that. - Rate limiting works without Redis (per instance) and without payload inspection. The WAF is a separate switch.
The engine resolves the client identity (the reference extract_client_ip), the plugin only hands it the connection's RemoteAddr. When the direct peer is in TRUSTED_PROXIES, the client is the hop TRUSTED_PROXY_DEPTH places from the right of X-Forwarded-For (default 1, the entry your proxy appended; set it to the number of proxies in front of pREST). A depth that over-counts the hops falls back to the right-to-left walk past trusted entries, and a chain shorter than the depth keeps the peer. Forwarded headers from untrusted peers are ignored, and the engine flags them as spoof attempts. Earlier versions walked the chain themselves and took the rightmost untrusted hop; with several trusted proxies in a row, set the depth instead.
redis_fail_open = true(default): if Redis is unreachable, the guard serves from per-instance memory (limits become per-process) and logs a warning. pREST stays up.redis_fail_open = false: requests answer 503 while initialization retries with backoff in the background, and the live guard swaps in when Redis returns. Health probes keep flowing either way. This replaces the older "500 forever after a failed startup" behavior with one that recovers.
The guard reads its own PREST_GUARD_REDIS_URL only. A REDIS_URL set for
pREST's own cache is never picked up by the guard.
pREST mounts plugin middlewares in the CRUD stack, after JWT auth. The
guard therefore covers the CRUD routes; the /auth endpoint, custom query
routes, the MCP endpoint, and the health endpoints are outside it (the
health endpoints by design). Global-stack coverage would be a small
follow-up in prest itself, and is the natural next step if wanted.
Payload inspection is off by default, and body size bounds its cost. The
inspection layer reads at most MAX_BODY_BYTES of the body; content past
the bound reaches your handler unscanned, which is a deliberate trade, not
an oversight. Measured on an Apple M2 Pro (single request, guard enabled,
inspection on, full middleware path):
| Body size | Pass path (prose JSON) | Reject path (threat) |
|---|---|---|
| 8 KiB (default) | ~28 ms | ~20 ms |
| 16 KiB | ~47 ms | ~39 ms |
| 64 KiB | ~0.22 s | ~0.20 s |
| 256 KiB | ~1.2 s | ~1.2 s |
Cost still grows with body size, so keep the default unless you have measured acceptance for the cost at your bound. The engine caps what it scans (its large-value truncation, Detection.MaxBodyInspectBytes and the scan budget), which is why the 1 MiB bound costs about the same as 256 KiB. Treat MAX_BODY_BYTES as a CPU budget, not a feature dial. Rate limits and IP policy alone do not read bodies and cost about 9 microseconds per request.
| Situation | Status |
|---|---|
| Blacklisted IP, banned IP, non-whitelisted peer | 403 |
| Rate limit exceeded | 429 |
| Detected attack payload (inspection on) | 400 |
| Guard misconfigured while enabled | 500, generic JSON body |
Redis down with redis_fail_open = false |
503, recovers automatically |
Blocked (and, in passive mode, would-have-blocked) requests are logged with check name, reason, client IP, method, and path. Request headers, query strings, and body content never reach the logs.
make fmt # gofmt
make vet # go vet
make test # go test -race
make bench # benchmarks (inspection cost table)
make so # build lib/middlewares/guard.soThe conformance story lives in guard-core-go: the engine is CI-tested against the same 448-case adversarial corpus as the Python reference (byte-identical fixtures, verdicts generated by running the reference engine). This repo tests the plugin contract: config parsing, engine mapping, proxy resolution, exclusions, Redis modes, fail-closed/fail-open behavior, and realistic pREST traffic passing with inspection on.
MIT