A Traefik middleware plugin for organization-based IP authentication using Redis/Valkey.
This plugin is provided AS IS as a reference implementation. You must adapt and validate it for your environment.
Your Responsibilities:
- Security audits and testing
- Production readiness assessment
- Fail-open policy review
- Infrastructure-specific configuration
Use at your own risk. No warranties or support commitments.
- IP Allowlist Enforcement: Validates client IPs against org-specific allowlists in Redis/Valkey
- CIDR Block Support: Supports both individual IPs and CIDR notation (e.g.,
10.0.0.0/24) - Multi-Organization Support: Allows access if ANY organization permits the client IP
- Redis Cluster Support: Full cluster support with automatic node discovery and routing
- Configurable Fail-Open Policy: Allows requests when Redis is unavailable (configurable via
failOpen) - Connection Pooling: Efficient Redis connection management with context-aware timeouts
- Multi-Level Caching: IP decision cache + org allowlist cache with LRU eviction
- TLS/SSL Support: Configurable TLS modes (
secure,insecure,disabled) for Redis connections - Buffer Pooling: Reuses buffers to reduce memory allocations under load
- Structured JSON Logging: Only logs errors, not successful requests
- Request Timeouts: Configurable per-request timeout with context cancellation
Helm:
helm upgrade traefik traefik/traefik -n traefik --wait \
--reuse-values \
--set experimental.plugins.orgidauthplugin.moduleName=github.com/traefik-workshops/orgidauthplugin \
--set experimental.plugins.orgidauthplugin.version=v0.1.16Static Config (traefik.yml):
experimental:
plugins:
orgidauthplugin:
moduleName: github.com/traefik-workshops/orgidauthplugin
version: v0.1.16apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: orgid-auth
namespace: apps
spec:
plugin:
orgidauthplugin:
redisAddr: "redis-master.default.svc.cluster.local:6379"
redisUsername: "default"
redisPassword: "your-password"
orgHeader: "X-Org"
poolSize: 500
cacheTTL: "10m"
clusterMode: true
tlsMode: "secure"
keyPrefix: "uuid"
failOpen: true
requestTimeout: "5s"apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: protected-api
namespace: apps
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`api.example.com`)
services:
- name: api-service
port: 8080
middlewares:
- name: jwt-authentication # Must run first to set X-Org header
- name: orgid-auth # Then validates IP| Parameter | Type | Default | Description |
|---|---|---|---|
redisAddr |
string | valkey-redis-master.traefik.svc.cluster.local:6379 |
Redis server address |
redisUsername |
string | "" |
Redis username (Redis 6+ ACL) |
redisPassword |
string | traefik |
Redis password |
orgHeader |
string | X-Org |
Header containing org IDs (set by JWT middleware) |
poolSize |
int | 500 |
Max Redis connections |
maxConnIdleTime |
duration | 5m |
Max idle time before closing connection |
poolWaitTimeout |
duration | 500ms |
Max wait for available connection |
cacheTTL |
duration | 10m |
Cache entry TTL |
cacheMaxSize |
int | 100000 |
Max cached IP entries |
clusterMode |
bool | true |
Enable Redis Cluster support |
tlsMode |
string | secure |
TLS mode: disabled, secure, or insecure |
keyPrefix |
string | uuid |
Redis key prefix for allowlist keys |
failOpen |
bool | true |
Allow traffic when Redis is unavailable |
requestTimeout |
duration | 5s |
Max time for request processing |
For Redis Cluster deployments:
spec:
plugin:
orgidauthplugin:
redisAddr: "redis-cluster.default.svc.cluster.local:6379"
redisUsername: "default"
redisPassword: "your-password"
clusterMode: true
poolSize: 500
cacheTTL: "1s" # Lower for faster updatesHow it works:
- Handles
MOVEDredirects forEXISTS,SISMEMBER, andSMEMBERScommands - Automatically routes requests to the correct cluster node
- Uses hash tags in keys (
{keyPrefix}:{orgID}:allowed) for consistent cluster slot routing - Discovers cluster nodes via
CLUSTER NODEScommand
clusterMode: true, MOVED redirects are not handled, which may cause errors when keys are on different nodes.
The plugin supports encrypted TLS connections to Redis/Valkey via the tlsMode parameter:
secure(default) — TLS enabled with full certificate verificationinsecure— TLS enabled but certificate verification is skipped (for testing with self-signed certs)disabled— plain TCP connection, no TLS
spec:
plugin:
orgidauthplugin:
redisAddr: "redis-cluster.traefik.svc.cluster.local:6380"
redisUsername: "admin"
redisPassword: "traefik"
clusterMode: true
tlsMode: "secure"Insecure mode (testing only):
spec:
plugin:
orgidauthplugin:
redisAddr: "redis-cluster.traefik.svc.cluster.local:6380"
redisUsername: "admin"
redisPassword: "traefik"
clusterMode: true
tlsMode: "insecure" # Skips certificate verificationHow it works:
- When
tlsModeissecureorinsecure, the plugin establishes TLS connections using Go'scrypto/tlspackage tlsMode: "insecure"disables certificate verification (useful for testing with self-signed certs)tlsMode: "disabled"uses plain TCP connections (no encryption)- Works with both single-node and cluster Redis deployments
- Standard TLS port for Redis is 6380 (plain text is 6379)
- Always use
tlsMode: "secure"in production - Only use
tlsMode: "insecure"for testing environments - Ensure Redis/Valkey is configured with valid TLS certificates
- Use proper certificate management (cert-manager, Vault, etc.)
Data Structure:
# Pattern: {keyPrefix}:{orgID}:allowed (default keyPrefix is "uuid")
# Type: SET
# Members: Individual IPs or CIDR blocks
# Note: Hash tag braces {} around orgID ensure cluster slot consistency
# Individual IPs
SADD "uuid:{org-123}:allowed" "10.0.1.5"
SADD "uuid:{org-123}:allowed" "192.168.1.100"
# CIDR blocks
SADD "uuid:{org-123}:allowed" "10.42.0.0/16"
SADD "uuid:{org-123}:allowed" "192.168.0.0/24"
# Mixed (both IPs and CIDRs)
SADD "uuid:{org-456}:allowed" "172.16.50.100" "10.0.0.0/8" "192.168.1.0/24"Managing Allowlists:
# Add individual IPs to allowlist
kubectl exec -n traefik redis-0 -- redis-cli --user default -a password \
SADD "uuid:{org-123}:allowed" "10.0.1.5" "192.168.1.100"
# Add CIDR blocks to allowlist
kubectl exec -n traefik redis-0 -- redis-cli --user default -a password \
SADD "uuid:{org-123}:allowed" "10.42.0.0/16" "192.168.0.0/24"
# Add mixed (IPs and CIDRs)
kubectl exec -n traefik redis-0 -- redis-cli --user default -a password \
SADD "uuid:{org-123}:allowed" "172.16.50.100" "10.0.0.0/8"
# View all allowed IPs/CIDRs
kubectl exec -n traefik redis-0 -- redis-cli --user default -a password \
SMEMBERS "uuid:{org-123}:allowed"
# Check if specific IP is allowed (exact match only)
kubectl exec -n traefik redis-0 -- redis-cli --user default -a password \
SISMEMBER "uuid:{org-123}:allowed" "10.0.1.5"
# Remove IP or CIDR from allowlist
kubectl exec -n traefik redis-0 -- redis-cli --user default -a password \
SREM "uuid:{org-123}:allowed" "10.0.1.5"
# Remove entire allowlist
kubectl exec -n traefik redis-0 -- redis-cli --user default -a password \
DEL "uuid:{org-123}:allowed"- JWT middleware extracts org IDs from token and sets
X-Orgheader - Plugin extracts client IP from
X-Forwarded-For,X-Real-IP, orRemoteAddr - No org header? Request passes through (no authentication required)
- For each org ID:
- Check in-memory IP cache for
orgID:clientIP - Check org allowlist cache for parsed IPs/CIDRs
- On cache miss, fetch all members with
SMEMBERSfrom Redis - Check exact IP match, then check CIDR ranges
- Check in-memory IP cache for
- Decision:
- ✅ Allow if ANY org permits the IP (exact match or CIDR range)
- ✅ Allow if org has no allowlist in Redis (empty set)
- ✅ Allow if Redis unavailable (when
failOpen: true) - ❌ Deny if org exists but IP not allowed
Caching:
- IP Cache: Caches allow/deny decisions per
orgID:clientIPpair (configurable TTL and max size) - Org Allowlist Cache: Caches parsed CIDR blocks and exact IPs per organization (up to 100 orgs)
- Both caches use LRU eviction when full
Logging:
- Structured JSON logging to stdout
- No logs for successful requests
- ERROR logs for Redis connection and command failures
- Migrated repository from
github.com/carlosvillanua/orgidauthplugintogithub.com/traefik-workshops/orgidauthplugin
- Major refactor: replaced raw syscall-based connections with
net.Connthroughout - Removed platform-specific files (
select_darwin.go,select_linux.go) - Replaced
tlsEnabled/tlsCABundle/tlsInsecureSkipVerifywith unifiedtlsMode(disabled/secure/insecure) - Added configurable
keyPrefix,failOpen, andrequestTimeoutparameters - Added org allowlist cache with parsed CIDR blocks for faster lookups
- Added buffer pooling (
sync.Pool) to reduce memory allocations - Added context-aware request handling with configurable timeouts
- Changed key format to use hash tags (
{keyPrefix}:{orgID}:allowed) for Redis Cluster slot consistency - Missing org header now passes request through instead of returning 401
- Switched from
log.Printfto structured JSON logging - Improved connection pool with pending count tracking and channel-based signaling
- Updated defaults:
poolSize10→500,cacheTTL30s→10m,cacheMaxSize1000→100000,clusterModedefault nowtrue
- Added TLS/SSL support for Redis/Valkey connections
- Supports both single-node and cluster TLS connections
- Configurable CA bundle for certificate verification
- Optional insecure skip verify for testing environments
- Backwards compatible with non-TLS deployments
- Added Redis username (ACL) support for Redis 6+
- Updated AUTH command to support both username+password and password-only modes
- Backwards compatible with Redis versions < 6
- Added CIDR block support for IP allowlists
- Implemented SMEMBERS command for single node and cluster mode
- Supports mixed allowlists with both individual IPs and CIDR ranges
- Fast path for exact IP matches, fallback to CIDR checking
- Feature parity with Traefik's built-in IPAllowList middleware
- Fixed critical FD ownership bug causing Redis protocol leaks into HTTP responses
- Replaced os.NewFile() with direct syscall.Read() to prevent FD reuse issues
- Resolves "Unsolicited response received on idle HTTP channel" errors under load
- Stable performance at 300+ req/s with zero errors
- Simplified key pattern to
uuid:{orgID}:allowedfor better performance - Replaced KEYS pattern search with direct EXISTS + SISMEMBER lookups
- Added cluster support for EXISTS command with MOVED redirect handling
- Significantly improved Redis operation efficiency
- Refactored cluster connection logic
- Reduced code duplication
- Fixed cluster HGET for keys on different nodes
- Added MOVED redirect handling
- Added Redis Cluster support
- Cluster-aware KEYS and node discovery
- New
clusterModeconfig option
- Fixed duration parsing from YAML
- Initial release
MIT