LDAP & Active Directory Guard (ldap)
The LDAP Guard plugin inspects Lightweight Directory Access Protocol version 3 (RFC 4511) connections. It decodes ASN.1 BER message envelopes, tracks BindRequest credentials, detects failed bind operations (resultCode: 49 invalidCredentials), and blocks unauthorized bind DN patterns to stop directory enumeration and credential stuffing against Active Directory and OpenLDAP servers.
Capabilities & Threat Defense
| Threat / Attack Vector | Defense Mechanism | Action Taken |
|---|---|---|
| Credential Stuffing & Password Spraying | Intercepts BindResponse with resultCode: 49; enforces connection close when cap is reached (since v3.4.0) | IP banned after max_auth_failures threshold; connection terminated immediately |
| Directory Enumeration | Enforces rate limits per minute and burst caps | Connection throttled before overloading directory |
| Protected DN Probing | Filters Bind DN against blocked_bind_dn_prefixes | Blocked at the proxy layer |
| DN Suffix Spoofing | Validates BindDN against allowed_bind_dn_suffixes using case-folded literal suffix matching (since v3.4.0) | Prevents Unicode homoglyph and attribute-reordering evasion attacks |
Installation
Install the plugin via CLI or declaratively:
# Install using short nametcp-warden plugins install ldap# Or install via Git repository URLtcp-warden plugins install https://github.com/routewarden/plugins/ldapConfiguration Reference
Add an LDAP guard service to tcp-warden.yaml:
services: ldap_bastion: listen: ":1390" upstream: "127.0.0.1:389" protocol: "ldap" rate_limit: connections_per_minute: 60 burst: 10 max_auth_failures: 3 ban_after_failures: 3 ban_duration: "2h" plugin_config: max_auth_failures: 3 blocked_bind_dn_prefixes: - "cn=Administrator" - "cn=root"Configuration Options
| Field | Type | Default | Description |
|---|---|---|---|
listen | string | ":1390" | Local proxy listen address and port. |
upstream | string | "127.0.0.1:389" | Target LDAP directory server address and port. |
protocol | string | "ldap" | Must be set to "ldap". |
max_auth_failures | int | 3 | Failed bind attempts before banning. Since v3.4.0 the connection is actively closed with an LDAP BindResponse resultCode: 52 (Unavailable) when the cap is reached. |
ban_duration | string | "2h" | Duration of the automated IP ban ("30m", "2h", "24h"). |
plugin_config.blocked_bind_dn_prefixes | []string | [] | List of sensitive DN prefixes forbidden from authenticating via this proxy. |
plugin_config.allowed_bind_dn_suffixes | []string | [] | If non-empty, only BindDN values ending with one of these suffixes are accepted. Matching is case-folded and uses literal suffix comparison to prevent Unicode homoglyph evasion (since v3.4.0). |
Network & Deployment Architecture
Option A: Kernel Firewall Redirection (nftables / iptables)
Redirect external traffic destined for port 389 to RouteWarden port 1390:
# Redirect external traffic destined for port 389 to RouteWarden port 1390sudo nft add rule ip routewarden_nat prerouting iifname "eth0" tcp dport 389 redirect to :1390Option B: LDAPS (Port 636) Protection
For secure LDAPS (LDAP over TLS), configure RouteWarden in front of the LDAPS listener:
# LDAPS (port 636 over TLS)services: ldaps_guard: listen: ":1636" upstream: "127.0.0.1:636" protocol: "generic" rate_limit: connections_per_minute: 60 burst: 10Option C: Docker Compose Network Isolation
Run the LDAP directory server inside an isolated container network, exposing both LDAP (389) and LDAPS (636) services only through RouteWarden:
services: routewarden: image: ghcr.io/routewarden/tcp-warden:latest ports: - "389:1390" # Map host LDAP port 389 to RouteWarden - "636:1636" # Map host LDAPS port 636 to RouteWarden - "9091:9091" # Management API volumes: - ./tcp-warden.yaml:/etc/routewarden/tcp-warden.yaml:ro depends_on: - openldap openldap: image: osixia/openldap:latest environment: - LDAP_ORGANISATION=Example Inc - LDAP_ADMIN_PASSWORD=adminpassword # Do NOT publish host ports (389/636); keep internal to bridge networkTesting & Verification
Verify queries and monitor failed bind responses:
# Verify valid search queryldapsearch -x -H ldap://127.0.0.1:1390 -b "dc=example,dc=org"