Inline LDAP proxy · Rust

Stop bulk lockouts before
they reach your directory.

SCORPION protect sits between your clients and your real directory server (AD, OpenLDAP, 389 DS). Every ordinary request passes through byte-for-byte, untouched — but a runaway script or an over-eager AI agent trying to lock, delete, or reset thousands of accounts hits a configurable blast-radius policy first, and never reaches the directory at all.

Apache-2.0 · CI on stable/beta/nightly · fuzz-tested decoder
RUST_LOG=info cargo run
$ RUST_LOG=info cargo run
listening on 127.0.0.1:3890 → 127.0.0.1:389
 
ALLOW modify identity=10.1.2.14 blast_radius=1
ALLOW modify identity=svc-hr-sync blast_radius=3
BLOCK modify identity=svc-agent-runner blast_radius=812 reason=max_per_request(10)
→ UnwillingToPerform sent to client, upstream never contacted
 
BLOCK del identity=svc-agent-runner window=41/50 in 60s reason=max_per_window
→ audit log: logs/ldap.log (json)
 
$

The problem

Directory protocols trust the caller completely.

LDAP has no concept of "that's a lot of accounts at once." A misconfigured script, a compromised credential, or an AI agent given just enough access to "clean up stale accounts" can lock, delete, or reset a huge slice of your directory in the time it takes a human to notice — and by the time they do, it's already done.

1 line of code

is the difference between a scoped cleanup script and one that iterates every entry in the directory instead of the ten it meant to.

<1 minute

is enough for an automated caller with valid credentials to lock or delete accounts faster than any human review process can react.

0 code changes

are needed anywhere else — ai-protect drops in as a TCP hop between client and directory, no directory-side config required.

How it works

Decode only what matters. Forward everything else.

ai-protect parses just enough of the wire protocol to recognize account-lock modifications, deletes, creates, renames/moves, and password-reset extended operations. Every other request is relayed byte-for-byte — it never has to understand your whole schema to protect it.

Client request
read_frame
Decode
rasn / BER (RFC 4511)
Actionable?
lock · del · add · modDN · pwd reset
Within policy → forward
relayed to upstream
Over limit → reject
LDAP error, logged
Everything not actionable — searches, binds, ordinary single-entry modifies — skips policy evaluation entirely and is forwarded as opaque bytes, untouched, in both directions.

What's built in

Everything a proxy sitting in front of production auth needs.

Transparent by default

Only frames the system needs to act on are parsed. Everything else forwards byte-for-byte, both directions, concurrently.

Configurable blast-radius policy

Block any single request over a per-request cap, plus a sliding-window cap per identity or globally as a backstop.

Verified identity, not IP guesswork

Keys policy off a simple bind DN only once its BindResponse confirms success — closing identity-churn bypass gaps.

TLS on every hop, independently

LDAPS or plaintext-then-StartTLS on client and upstream sides, each optional and each with its own mutual-TLS support.

Load balancing & failover

Round-robin, random, or least-connections across upstream targets, with automatic failover and passive health tracking.

Structured audit logging

Every allow and block decision, stdout + newline-JSON to disk — the forensic trail, decoupled from policy logic.

Zero-downtime policy reload

SIGHUP hot-reloads policy files; SIGTERM/SIGINT drains in-flight connections before shutdown.

Metrics, health checks, fuzzing

Prometheus metrics, liveness/readiness endpoints, and a cargo-fuzz harness on every byte parsed off the wire.

Policy as config

One TOML file. No redeploy to tune.

Policy lives in its own gitignored file, separate from network config, so it can change on its own cadence — and reload on SIGHUP without dropping a connection.

  • Per-request cap blocks a single oversized operation outright.
  • Sliding-window cap catches an identity accumulating damage over time.
  • PerIdentity or Global scope — stack both as a layered backstop.
  • Optional SQLite or Redis/Valkey backing so history survives a restart or spans instances.
policies/ldap.toml
[[policy]]
type = "threshold"
scope = "per_identity"
max_per_request = 10
max_per_window = 50
window_secs = 60

# shared backstop no single identity can reset
[[policy]]
type = "threshold"
scope = "global"
max_per_request = 25
max_per_window = 200
window_secs = 60
state_db = "policy-state.sqlite"

Get started

Up and running in about a minute.

Point ai-protect at your real directory, copy the example config and policy files, and run it. Every client just needs to talk to a new port instead.

  1. git clone the repo & cargo build
  2. cp config.example.toml config.toml
  3. cp policies/ldap.example.toml policies/ldap.toml
  4. cargo run — listens on 127.0.0.1:3890
  5. Point clients at ai-protect instead of the directory