NAI Security
Django security package for IP blocking, country blocking, email blocking, rate limiting, and login tracking.
Docs: https://nematiai.github.io/nai-security/
Wiki: https://github.com/nematiai/nai-security/wiki
Wiki source in repo: wiki/
NEMATI AI: https://nemati.ai
Features
- IP Blocking - Block specific IPs manually or automatically
- Country Blocking - Block/allow countries using GeoIP
- Email Blocking - Helpers + admin lists for signup/login in your app (not request middleware)
- Domain Blocking - Helpers + admin lists for disposable/spam domains
- User Agent Blocking - Block bots, scrapers, attack tools
- Path Blocking - Reject scanner probes for dotfiles (
/.git,/.env,/.ssh) and leak paths (/server-status,/phpinfo), keeping/.well-known/reachable - Rate Limiting - Logs django-ratelimit hits;
RateLimitRuleis storage only (not an engine) - Login History - Track user logins with anomaly detection
- Auto-Blocking - Automatically block IPs/countries based on attack patterns
- Security Logs - Comprehensive logging of all security events
- Axes Integration - Dynamic login attempt limits, cooloff time, and per-attempt expiry via admin panel (requires django-axes >= 8.3)
- Whitelisted Users - Exempt specific users from security checks
Installation
pip install nai-security
With Django integration extras (axes, ratelimit, import-export, unfold):
pip install nai-security[all]
Background tasks extra (not included in [all]):
pip install nai-security[celery]
Or install from GitHub:
pip install git+https://github.com/nematiai/nai-security.git
Quick Start
1. Add to INSTALLED_APPS
INSTALLED_APPS = [
...
"nai_security",
]
2. Add Middleware
Important:
nai_security.middleware.SecurityMiddlewaremust be placed afterdjango.contrib.auth.middleware.AuthenticationMiddleware. The package validates this at startup and raisesImproperlyConfiguredif misordered.
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", # must be before
"django.contrib.messages.middleware.MessageMiddleware",
"nai_security.middleware.SecurityMiddleware", # after auth
"nai_security.middleware.RateLimitLoggingMiddleware", # optional
]
3. Configure Settings
GEOIP_PATH = "/path/to/GeoLite2-Country.mmdb"
# Optional: override default exempt paths (default: /health/, /ready/, /favicon.ico)
NAI_SECURITY_EXEMPT_PATHS = ["/health/", "/health", "/ready/", "/ready", "/favicon.ico"]
# Required if you sit behind a reverse proxy. Default False: ignore X-Forwarded-For / X-Real-IP
# so clients cannot spoof their IP.
NAI_SECURITY_TRUST_PROXY_HEADERS = True
4. Run Migrations
python manage.py migrate
5. Download GeoIP Database
python manage.py download_geoip
Dependencies
Required:
- Django >= 5.2
- geoip2 >= 5.0, < 6
- redis >= 5.0, < 9
- requests >= 2.32.4
Optional:
django-axes >= 8.3.1, < 9— login attempt tracking and lockout; without it axes features are silently disableddjango-ratelimit >= 4.1— rate limiting per endpointdjango-import-export >= 4.0, < 5— admin import/export for blocked emails/domains; without it those buttons are hiddendjango-unfold >= 0.90— admin UI theme; without it falls back to standard Django admin (0.100+ needs Python >= 3.12)celery >= 5.3, < 6— background tasks (auto-block processing, sync, reports); without it tasks are no-ops (pip install nai-security[celery])
Axes Integration
Enable brute-force protection with dynamic settings controlled from the admin panel:
# settings.py
INSTALLED_APPS = [
...
"axes",
"nai_security",
]
AXES_HANDLER = 'nai_security.handlers.axes_integration.DynamicAxesHandler'
AUTHENTICATION_BACKENDS = [
'axes.backends.AxesStandaloneBackend',
'django.contrib.auth.backends.ModelBackend',
]
This gives you admin-configurable control over:
| Setting | Description |
|---|---|
| Max login attempts | Failed attempts before lockout (default: 5) |
| Cooloff time | Minutes before locked accounts auto-unlock (0 = permanent) |
| Attempt expiry | Each failed attempt expires independently — requires cooloff > 0 |
All changes take effect immediately — no server restart required.
Validation: Enabling attempt expiry with cooloff set to 0 will raise a validation error in the admin panel.
Whitelist bypass
DynamicAxesHandler short-circuits all axes checks (is_allowed, is_locked, user_login_failed) when the request matches any active whitelist:
- The client IP (
REMOTE_ADDR, orX-Forwarded-For/X-Real-IPwhenNAI_SECURITY_TRUST_PROXY_HEADERS=True) appears inWhitelistedIP— bypass works even when the request has no credentials (e.g.GET /login/). - The login username/email resolves to a user with an active
WhitelistedUserrow — regardless ofexemption_type. Theexemption_typefield still controls middleware-level bypasses (IP, country, rate-limit); for axes lockout the rule is binary. - Username lookup tolerates
USERNAME_FIELD='username'deployments where the login form posts an email — falls back toemail__iexactautomatically.
When a whitelisted request fails authentication, no AccessAttempt row is recorded — the table stays clean for whitelisted admins.
Management Commands
# Download GeoIP database
python manage.py download_geoip
# Sync disposable email domains and bad bot lists
python manage.py sync_security_lists
python manage.py sync_security_lists --domains-only
python manage.py sync_security_lists --bots-only
Celery Tasks
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
'security-auto-blocks': {
'task': 'security.process_auto_blocks',
'schedule': crontab(minute='*/5'),
},
'security-cleanup-expired': {
'task': 'security.cleanup_expired_blocks',
'schedule': crontab(minute=0, hour='*'),
},
'security-sync-lists': {
'task': 'security.sync_security_lists',
'schedule': crontab(minute=0, hour=0, day_of_week=0),
},
'security-daily-report': {
'task': 'security.generate_security_report',
'schedule': crontab(minute=0, hour=6),
},
}
Models
| Model | Description |
|---|---|
BlockedIP |
Blocked IP addresses |
BlockedCountry |
Blocked countries |
AllowedCountry |
Allowed countries (whitelist mode) |
BlockedEmail |
Blocked email addresses (helpers for signup/login in your app — not request middleware) |
BlockedDomain |
Blocked email domains (helpers for signup/login in your app — not request middleware) |
BlockedUserAgent |
Blocked user agents |
WhitelistedIP |
IPs that bypass all checks |
WhitelistedUser |
Users exempted from security checks (see exemption types below) |
RateLimitRule |
Storage for custom rate rules — this package does not enforce them (use django-ratelimit) |
LoginHistory |
User login tracking |
SecurityLog |
Security event logs |
SecuritySettings |
Global settings (singleton) |
User Exemptions (WhitelistedUser)
Exempt specific users from security checks via the admin panel or ORM:
| Exemption Type | Bypasses |
|---|---|
all |
Entire security middleware — IP, country, user-agent |
ip_block |
IP blocking only |
geo_block |
Country/geo blocking only |
rate_limit |
Rate limit logging only |
Exemptions support optional expiration (expires_at) and can be toggled via is_active.
Axes lockout note: any active
WhitelistedUserrow exempts the user from django-axes lockout, regardless ofexemption_type. Theexemption_typefield controls only theSecurityMiddlewarechecks (IP/country/rate-limit). See Axes Integration → Whitelist bypass.
Upgrading to 1.14.0
New Path Blocking check (two migrations). Run python manage.py migrate — 0006 adds
SecuritySettings.path_blocking_enabled, default on. Dotfile paths (/.git, /.env, /.ssh, …)
and /server-status, /server-info, /phpinfo, /wp-config.php, /web.config, /id_rsa now
return 403 and log a PATH_BLOCK event; 0007 adds PATH_BLOCK to the SecurityLog.action choices. /.well-known/ stays reachable (ACME renewal is
unaffected). Disable it in admin → Security Settings.
Upgrading to 1.13.0
Install-time / support matrix (no app API change):
- Required Django floor is now 5.2 (4.2 and 5.0 are past end of support).
- Required
requestsfloor is now 2.32.4. - New optional extra:
pip install nai-security[celery](celery>=5.3,<6).[all]is unchanged (axes, ratelimit, import-export, unfold). - Dev extra adds pytest-cov, hypothesis, time-machine, responses, fakeredis, model-bakery, mypy, and pip-audit.
Upgrading to 1.12.2
Metadata only: NEMATI AI links (https://nemati.ai) on PyPI, docs, and wiki. No app API change.
Upgrading to 1.12.1
Docs links: PyPI Documentation now opens https://nematiai.github.io/nai-security/ (no app API change).
Upgrading to 1.12.0
Security / behavior fix (no migrations):
- If you run behind a reverse proxy, set
NAI_SECURITY_TRUST_PROXY_HEADERS = True. Forwarded headers are ignored by default so clients cannot spoof IP to bypass blocks. - Axes cooloff and attempt-expiry now apply on the next request in every worker (not only the process that saved admin).
- Whitelisting a user with any
exemption_typenow clears an active axes lockout. - Default bad-bot sync no longer blocks
python-requests/curl/wget/Go-http-client. GEOIP_PATHaccepts a directory or the.mmdbfile. Classifiers add Django 6.1 and Python 3.14.
Upgrading to 1.11.0
Dependency pin updates (install-time):
- Declared
requests>=2.28as a required dependency (used by sync services). - Raised/capped floors:
geoip2>=5.0,<6,redis>=5.0,<9. - Optional extras:
django-import-export>=4.0,<5,django-unfold>=0.90,django-ratelimit>=4.1. django-axesremains>=8.3.1,<9.0.- No application API changes in this release.
Upgrading to 1.10.1
Bug fixes (no breaking changes):
- Whitelisted users were still being locked out by django-axes. Three independent paths caused this:
WhitelistedIPwas not consulted by the axes handler — whitelisted IPs could still be locked, especially withAXES_LOCKOUT_PARAMETERS=['ip_address'].- The handler's whitelist check was hard-coded to
exemption_type='all'— users with'ip_block','rate_limit', or'geo_block'were still locked. - User lookup used
USERNAME_FIELDonly and silently failed when the login form posts an email butUSERNAME_FIELD='username'.
- All three are fixed in
nai_security.handlers.axes_integration.DynamicAxesHandlervia a unified_is_request_whitelisted()helper. Failed login attempts from whitelisted requests are no longer recorded inAccessAttempt. - Verified end-to-end with 100x failed-login smoke test (
scripts/smoke_100x_lockout.py) and 9 regression tests intests/test_axes_integration.py.
Upgrading to 1.9.1
Breaking changes:
SecurityMiddlewarenow requiresAuthenticationMiddlewareto be placed before it inMIDDLEWARE. If misordered, the app raisesImproperlyConfiguredat startup. Previously the middleware silently failed to resolve users.NAI_SECURITY_USER_RESOLVERsetting has been removed. User resolution now usesrequest.userdirectly (guaranteed by middleware ordering).parse_user_agentnow correctly detects Android, iOS, and Opera (previously misidentified as Linux, macOS, and Chrome respectively).
Testing
pip install -e ".[dev,all]"
python -m pytest
python -m pytest --cov=nai_security --cov-report=term-missing
pip-audit
License
MIT License
Author
Ali Nemati - NEMATI AI
Metadata
Release files for nai-security 1.14.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nai_security-1.14.0.tar.gz | 59.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nai_security-1.14.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 111.7 kB
Release files / nai_security-1.14.0.tar.gz
| Download URL | nai_security-1.14.0.tar.gz |
|---|---|
| Size | 59.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
70e28cb06fcd69207d1e1a9a9ca33c10e6efaac5a74975a68ca1d8a3d04203a1
|
|
BLAKE2b-256 checksum How to use checksums |
0cb9c83aa0473f986cf101e213c9cc4f98e05b6f392d401661fac28f250e6478
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / nai_security-1.14.0-py3-none-any.whl
| Download URL | nai_security-1.14.0-py3-none-any.whl |
|---|---|
| Size | 52.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2f1f14931315c74c6ab909f3f8ff53780e83df8b017d9966a809296d79f20e27
|
|
BLAKE2b-256 checksum How to use checksums |
881d2cf23ab0483afc032682edd43f9279ed792287d7aa5807535d62b18ddbe3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|