Django Admin Security Checklist
Secure the Django admin in production: access restrictions, strong authentication, permissions, session policy, audit logging, and safe data handling.
Problem statement
The Django admin is a powerful interface: with the right permissions, a user can read and change almost any data in the application, including user accounts. It is also completely standard, so automated scanners find /admin/ on millions of sites and try common credentials against it constantly.
Most admin compromises do not exploit Django bugs. They use weak or reused passwords, staff accounts that were never removed, over-broad permissions, or a stolen session on an unprotected network. The damage is amplified because admin actions are often unlogged beyond Django's own change history, and because admin users frequently have access to personal data.
This checklist covers restricting access, strengthening authentication, limiting permissions, and making admin activity visible.
Quick answer
- Restrict network access: IP allowlist, VPN, or an identity-aware proxy in front of
/admin/. - Require strong authentication: unique strong passwords and multi-factor authentication for every staff account.
- Limit accounts: few superusers, staff accounts with only the permissions their role needs, and prompt removal of departed users.
- Rate limit and lock out: throttle login attempts at the proxy and in the application.
- Harden sessions and cookies: HTTPS-only, secure cookies, short session lifetime for staff.
- Log and review: admin access logs, Django's
LogEntryhistory, and alerts on unusual activity. - Handle data carefully: limit what the admin exposes and who can export it.
Checklist
1. Network access restrictions
The strongest control is not exposing the admin to the whole internet.
Nginx IP allowlist:
location /admin/ {
allow 203.0.113.10; # office
allow 198.51.100.0/24; # VPN range
deny all;
proxy_pass http://unix:/run/myapp/gunicorn.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Alternatives when staff work from changing locations:
- a VPN that everyone uses for admin access
- an identity-aware proxy (Cloudflare Access, Google IAP, or similar) that authenticates before requests reach Django
- a bastion or admin-only hostname not published in DNS zones shared publicly
Changing the admin URL (for example path("secret-admin/", admin.site.urls)) reduces automated noise but is not a security control on its own; combine it with real restrictions if you use it.
2. Authentication strength
What to check:
- every staff account uses a unique, strong password (a password manager, not a shared team password)
- multi-factor authentication is enforced for admin access, through a package such as django-otp with the admin site's OTP integration, or through the identity-aware proxy in front
- default or example accounts (
admin,test) do not exist:
python manage.py shell -c "
from django.contrib.auth import get_user_model
U = get_user_model()
for u in U.objects.filter(is_staff=True).order_by('username'):
print(u.username, u.email, 'super' if u.is_superuser else 'staff', 'active' if u.is_active else 'inactive', u.last_login)
"
- password validators are enabled (
AUTH_PASSWORD_VALIDATORS) with a reasonable minimum length - the password hasher is a current default (Django's default hasher, not a legacy one)
3. Account hygiene
What to check:
- the number of superusers is small (often one or two), and day-to-day work uses non-superuser staff accounts
- accounts of people who left are deactivated (
is_active = False) or removed - staff accounts are personal, never shared
- inactive accounts are reviewed periodically:
python manage.py shell -c "
from datetime import timedelta
from django.contrib.auth import get_user_model
from django.utils import timezone
U = get_user_model()
cutoff = timezone.now() - timedelta(days=90)
stale = U.objects.filter(is_staff=True).filter(last_login__lt=cutoff) | U.objects.filter(is_staff=True, last_login__isnull=True)
for u in stale.distinct():
print(u.username, u.last_login)
"
4. Permissions
Django's permission system is per model and per action. Use it instead of granting superuser status.
Guidelines:
- create groups per role (support, editors, finance) and assign model permissions to groups, not individual users
- grant only the actions a role needs (
view,add,change,deleteare separate) - restrict querysets and fields in
ModelAdminwhere a role should only see part of the data:
from django.contrib import admin
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ["number", "customer", "total", "status"]
readonly_fields = ["number", "customer", "total"]
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(region=request.user.profile.region)
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
- review which models are registered at all; models that never need manual editing do not belong in the admin
5. Brute-force protection
What to check:
- rate limiting on the admin login path at the proxy:
limit_req_zone $binary_remote_addr zone=adminlogin:10m rate=10r/m;
location = /admin/login/ {
limit_req zone=adminlogin burst=5 nodelay;
limit_req_status 429;
proxy_pass http://unix:/run/myapp/gunicorn.sock;
# proxy headers
}
- application-level lockouts for repeated failures, for example with django-axes, which can lock by username, IP, or both
- alerts on bursts of failed admin logins
6. Sessions and cookies
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_SECURE = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_COOKIE_AGE = 60 * 60 * 8
Guidelines:
- HTTPS everywhere, with HSTS, so session cookies never travel in clear text
- a session lifetime appropriate for staff access (for example a working day rather than two weeks)
django.contrib.authsession invalidation on password change works (avoid custom code that breaks it)- staff can log out of other sessions after a suspected compromise: clear their sessions from the session store or rotate their password
7. Audit and visibility
What to check:
- Django records add, change, and delete actions in
django_admin_log; review it for sensitive models:
python manage.py shell -c "
from django.contrib.admin.models import LogEntry
for e in LogEntry.objects.select_related('user', 'content_type').order_by('-action_time')[:20]:
print(e.action_time, e.user, e.content_type, e.object_repr, e.get_change_message())
"
- web server access logs show who reached
/admin/and when (with real client IPs) - successful and failed logins are logged and searchable
- alerts for unusual patterns: admin logins outside working hours, from new countries, or bulk deletions
For stricter requirements, add application-level audit logging for reads of sensitive data, which Django does not record by default.
8. Data exposure
Guidelines:
- admin list views and search fields do not expose more personal data than necessary
- export actions (CSV downloads, third-party admin export packages) are limited to roles that need them
- sensitive fields are read-only or masked where full values are not required
list_per_pageis modest, both for performance and to limit bulk scraping through the interface- file and image fields link to protected storage, not public URLs, for private documents
9. Admin-specific settings and deployment
- The admin is served over HTTPS only.
- Static files for the admin are collected and served correctly (an unstyled admin often means broader static file problems).
DEBUG = False, so admin errors do not render debug pages.- The admin site header and title are customized enough that staff can tell environments apart:
admin.site.site_header = "Example Admin (PRODUCTION)"
- Staging admins use separate credentials from production.
10. Consider whether the admin belongs in production at all
For some applications, the admin is an internal tool that does not need to be publicly reachable at all. Options:
- disable it in production settings and run it only through a private network or bastion
- keep it enabled but behind an identity-aware proxy
- replace common operational tasks with purpose-built views that have narrower permissions and better audit trails
Verify
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.com/admin/login/ # from an allowed network
curl -s -o /dev/null -w "%{http_code}\n" --interface eth1 https://www.example.com/admin/login/ # from elsewhere, expect 403
for i in $(seq 1 15); do curl -s -o /dev/null -w "%{http_code} " https://www.example.com/admin/login/; done; echo
Verification check:
- the admin is unreachable from networks that should not have access
- multi-factor authentication is required for every staff account
- superuser count is minimal and every staff account is personal and active
- repeated login attempts are rate limited
- admin actions appear in the admin log and are reviewable
Explanation
The admin combines broad data access with a predictable, well-known interface, which makes it the highest-value target on most Django sites. Defense works in layers: network restrictions remove most automated attacks, strong authentication and multi-factor protect against credential theft, least-privilege permissions limit what a compromised account can do, and audit logging makes misuse detectable. Each layer is straightforward to add, and together they turn the admin from a liability into a controlled tool.
Edge cases / notes
Customer-facing staff tools
If non-technical staff need daily access from anywhere, an identity-aware proxy with single sign-on is usually better than IP allowlists, which become unmanageable.
Third-party admin packages
Enhanced admin interfaces add features and their own security considerations (extra views, export endpoints, inline scripts affecting CSP). Review them like any dependency.
Superuser for automation
Automation should use dedicated service accounts with narrow permissions and API tokens, not superuser admin logins.
Common mistakes
- Relying only on a renamed admin URL for security.
- Granting superuser status because permissions are "complicated".
- Leaving staff accounts active after people leave.
- No rate limiting or lockout on admin login.
Next steps
- Set response headers with the Django Security Headers Checklist.
- Add proxy-level protection with Rate Limit Django Endpoints with Nginx.
- Confirm settings with Django check --deploy Explained.
- Review broader controls in Django Deployment Security Basics You Should Not Skip.
FAQ
Should I change the Django admin URL?
It reduces automated scanning noise but is not real protection. Use it alongside network restrictions, strong authentication, and rate limiting, not instead of them.
How do I add multi-factor authentication to the Django admin?
Use a package such as django-otp with its admin integration, or place the admin behind an identity-aware proxy that enforces single sign-on with MFA.
How can I restrict admin access by IP address?
Add allow and deny rules to the /admin/ location in Nginx, or restrict at the load balancer or cloud firewall. Make sure real client IPs reach the proxy when behind a CDN.
Does Django log admin changes?
Yes. Add, change, and delete actions performed in the admin are recorded in django_admin_log and visible in each object's history. Reads are not logged by default.