Self-hosted request filtering, bot management, and WAF middleware for Django — rate limiting, UA anomaly scoring, JS challenges, nginx blocklist generation, and collective threat feed.
Project description
django-waf
Self-hosted request filtering, bot management, and WAF middleware for Django.
Provides two-layer defence (nginx + Django middleware) with rate limiting, user-agent anomaly scoring, JS proof-of-work challenges, nginx blocklist generation, and collective threat feed integration — all configurable without a reverse-proxy vendor.
Features
- Rate limiting — sliding-window per-IP limits (burst, per-minute, per-5-min)
- UA anomaly scoring — heuristic detection of impossible OS/browser combos, ancient versions, scraper libraries
- JS proof-of-work challenges — hashcash-style SHA-256 challenges for suspicious clients (no CAPTCHAs, no third-party dependencies)
- nginx blocklist generation — exports
map/geoblocks for C-level filtering at < 0.01 ms latency - Anomaly detection — auto-creates expiring rules for UA rotation, subnet bursts, and challenge farms
- Collective threat feed — opt-in sync of anonymised threat intelligence across deployments
- Staff dashboard — HTMX-powered real-time analytics with anomaly management
- Fail-open design — Redis outage never breaks the site
Requirements
- Python >= 3.11
- Django >= 4.2
- Redis (via
django-redis >= 5.4) httpx >= 0.27(for threat feed sync)- Optional:
celery >= 5.3(for scheduled tasks)
Installation
pip install django-waf
Add to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"icv_waf",
]
Add the middleware — place it after SecurityMiddleware and before
other middleware so it can block requests early:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"icv_waf.middleware.WafMiddleware", # <-- here
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
# ...
]
Include the URL routes for the challenge flow and staff dashboard:
# urls.py
from django.urls import include, path
urlpatterns = [
path("waf/", include("icv_waf.urls")),
# ...
]
Configure a Redis cache backend (required for rate limiting):
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/0",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
Run migrations:
python manage.py migrate icv_waf
Settings Reference
All settings are namespaced under ICV_WAF_* and have sensible defaults.
| Setting | Default | Description |
|---|---|---|
ICV_WAF_ENABLED |
True |
Master switch — disable to pass all requests through |
ICV_WAF_EXEMPT_PATHS |
["/static/", "/media/", "/health/", "/favicon.ico"] |
URL prefixes that bypass WAF evaluation |
ICV_WAF_TRUST_X_FORWARDED_FOR |
False |
Trust X-Forwarded-For header for client IP extraction |
ICV_WAF_REDIS_ALIAS |
"default" |
Django cache alias for Redis connections |
ICV_WAF_RATE_LIMIT_BURST |
10 |
Max requests per IP per second |
ICV_WAF_RATE_LIMIT_PER_MINUTE |
120 |
Max requests per IP per minute |
ICV_WAF_RATE_LIMIT_PER_5MIN |
600 |
Max requests per IP per 5 minutes |
ICV_WAF_CHALLENGE_DIFFICULTY |
4 |
Proof-of-work leading zero bits |
ICV_WAF_CHALLENGE_COOKIE_TTL |
86400 |
Seconds a solved-challenge cookie remains valid |
ICV_WAF_LOG_SAMPLE_RATE |
0.01 |
Fraction of allowed requests to log (0.0–1.0) |
ICV_WAF_LOG_RETENTION_DAYS |
30 |
Days to retain RequestLog entries |
ICV_WAF_ANOMALY_THRESHOLD_DISTINCT_UAS |
20 |
Distinct UAs per IP before triggering anomaly |
ICV_WAF_AUTO_RULE_EXPIRY_HOURS |
24 |
Hours before auto-generated rules expire |
ICV_WAF_NGINX_BLOCKLIST_PATH |
"/etc/nginx/conf.d/icv-waf-blocklist.conf" |
Output path for nginx blocklist |
ICV_WAF_ACCESS_LOG_PATH |
"/var/log/nginx/access.log" |
nginx access log path for parsing |
ICV_WAF_FEED_ENABLED |
True |
Enable collective threat feed sync |
ICV_WAF_FEED_URL |
"https://threats.icv.dev/v1/feed.json" |
Threat feed JSON endpoint |
ICV_WAF_FEED_MIN_CONFIDENCE |
0.8 |
Minimum confidence to import feed rules |
ICV_WAF_FEED_REPORT |
False |
Report local detections back to feed (opt-in) |
ICV_WAF_FEED_REPORT_URL |
"https://threats.icv.dev/v1/report" |
Telemetry reporting endpoint |
ICV_WAF_FEED_API_KEY |
"" |
API key for feed authentication |
Celery Beat Schedule
If using Celery, configure the beat schedule for automated tasks:
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
"icv-waf-generate-blocklist": {
"task": "icv_waf.tasks.generate_blocklist",
"schedule": crontab(minute="*/5"),
},
"icv-waf-detect-anomalies": {
"task": "icv_waf.tasks.detect_anomalies",
"schedule": crontab(minute="*/15"),
},
"icv-waf-parse-access-log": {
"task": "icv_waf.tasks.parse_access_log",
"schedule": crontab(minute="*/10"),
},
"icv-waf-expire-rules": {
"task": "icv_waf.tasks.expire_rules",
"schedule": crontab(minute="*/30"),
},
"icv-waf-update-ip-reputation": {
"task": "icv_waf.tasks.update_ip_reputation",
"schedule": crontab(hour="*/6", minute=0),
},
"icv-waf-prune-request-logs": {
"task": "icv_waf.tasks.prune_request_logs",
"schedule": crontab(hour=4, minute=0),
},
"icv-waf-sync-threat-feed": {
"task": "icv_waf.tasks.sync_threat_feed",
"schedule": crontab(hour=4, minute=30),
},
"icv-waf-report-threat-telemetry": {
"task": "icv_waf.tasks.report_threat_telemetry",
"schedule": crontab(hour=5, minute=0),
},
}
Management Commands
| Command | Description |
|---|---|
icv_waf_generate_blocklist |
Generate the nginx blocklist file (--dry-run to preview) |
icv_waf_detect_anomalies |
Run anomaly detectors and auto-create block rules (--dry-run) |
icv_waf_prune_logs |
Delete RequestLog entries older than retention period (--dry-run) |
icv_waf_sync_feed |
Fetch and import rules from the collective threat feed (--dry-run) |
Dashboard
The staff dashboard is available at /waf/dashboard/ for authenticated staff
users. It provides:
- Real-time traffic counters (allowed, blocked, challenged, throttled)
- Top 10 blocked IPs
- Auto-detected anomalies awaiting review
Superusers can confirm auto-generated rules (promoting them to permanent) or reject them (deactivating) directly from the anomalies panel.
Architecture
Client → nginx (C-level blocklist, < 0.01 ms)
→ Django WafMiddleware (dynamic analysis, < 0.5 ms)
→ Application views
The middleware evaluates requests in this order:
- Exempt paths bypass
- Master switch check
- Staff/superuser bypass
- Valid challenge cookie check
- Allow rules → Block rules → Rate limits → UA scoring
- Verdict dispatch (allow / block / challenge / throttle)
- Sampled logging + signal emission
Development
# Run tests
pytest
# Run tests with coverage
pytest --cov=src --cov-report=term-missing
# Lint
ruff check src/ tests/
ruff format src/ tests/
# Type check
mypy src/
Licence
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_waf-0.4.1.tar.gz.
File metadata
- Download URL: django_waf-0.4.1.tar.gz
- Upload date:
- Size: 87.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f88c6cecdeadbdc141c820960b10afd4055bc47c5ab357ae1a3018a16d55365d
|
|
| MD5 |
6c0b2197657e4a0d7f856315a4279275
|
|
| BLAKE2b-256 |
f38c4a2b1e4639ba09a7e9996b67ae35b8990367c6b856245a163126714e8106
|
Provenance
The following attestation bundles were made for django_waf-0.4.1.tar.gz:
Publisher:
publish.yml on nigelcopley/django-waf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_waf-0.4.1.tar.gz -
Subject digest:
f88c6cecdeadbdc141c820960b10afd4055bc47c5ab357ae1a3018a16d55365d - Sigstore transparency entry: 1252378490
- Sigstore integration time:
-
Permalink:
nigelcopley/django-waf@247a9fe788228b186588d5ec8767e059d55deedd -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/nigelcopley
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@247a9fe788228b186588d5ec8767e059d55deedd -
Trigger Event:
release
-
Statement type:
File details
Details for the file django_waf-0.4.1-py3-none-any.whl.
File metadata
- Download URL: django_waf-0.4.1-py3-none-any.whl
- Upload date:
- Size: 67.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9f193d1ebd0ad592e05bff5699c3bac863355dbf0bd63282d441572811bc05d9
|
|
| MD5 |
ee0504c5df806124696bae0a1341a92b
|
|
| BLAKE2b-256 |
87ce75d399f70af717007fc4ee8b39f25e81a308d7f79ff027a556a38da42521
|
Provenance
The following attestation bundles were made for django_waf-0.4.1-py3-none-any.whl:
Publisher:
publish.yml on nigelcopley/django-waf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_waf-0.4.1-py3-none-any.whl -
Subject digest:
9f193d1ebd0ad592e05bff5699c3bac863355dbf0bd63282d441572811bc05d9 - Sigstore transparency entry: 1252378510
- Sigstore integration time:
-
Permalink:
nigelcopley/django-waf@247a9fe788228b186588d5ec8767e059d55deedd -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/nigelcopley
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@247a9fe788228b186588d5ec8767e059d55deedd -
Trigger Event:
release
-
Statement type: