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.

Layers protecting the adminNetworkIP allowlist, VPNURLnon-default pathAuthenticationstrong, two-factorPermissionsleast privilegeNo single layer is enough; each one narrows what an attacker can reach.

Quick answer

  1. Restrict network access: IP allowlist, VPN, or an identity-aware proxy in front of /admin/.
  2. Require strong authentication: unique strong passwords and multi-factor authentication for every staff account.
  3. Limit accounts: few superusers, staff accounts with only the permissions their role needs, and prompt removal of departed users.
  4. Rate limit and lock out: throttle login attempts at the proxy and in the application.
  5. Harden sessions and cookies: HTTPS-only, secure cookies, short session lifetime for staff.
  6. Log and review: admin access logs, Django's LogEntry history, and alerts on unusual activity.
  7. 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.

defence in depth for /admin/NetworkVPN or IP allowlist in the proxyLoginstrong passwords, 2FA, rate limitsSessionshort, secure, HttpOnly cookiesPermissionsgroups, not superusersAuditLogEntry and login logs reviewedEach layer limits what a failure in the one above can dodjango-deployment.com
Network restriction alone stops almost all automated attacks.

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.

give staff only what their job needsEveryone is a superuser✕every model, every action✕one phished account = everything✕no record of intended scopeGroups with model permissions✓editors, support, finance groups✓view-only where possible✓one or two break-glass superusersReview group membership whenever someone changes roledjango-deployment.com
Custom ModelAdmin permission methods can narrow access to specific rows.

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, delete are separate)
  • restrict querysets and fields in ModelAdmin where 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
session settings for staffValueSESSION_COOKIE_SECURE✓TrueSESSION_COOKIE_AGE✓hours, not weeksSESSION_EXPIRE_AT_BROWSER_CLOSETrue for admin-heavy sitesCSRF_COOKIE_SECURE✓TrueRotate sessions on login and on password changedjango-deployment.com
Django already rotates the session key at login; custom login views must keep that behaviour.

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.auth session 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_page is 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
does the admin belong in production?Who uses the admin, and from where?a few developersSeparate internal hostVPN only, or disabledoffice staff dailyKeep, fully hardenedevery layer abovecustomersBuild real viewsthe admin is not a product UIAn admin nobody needs in production is the easiest one to securedjango-deployment.com
An internal deployment can share the database while serving only the admin URLs.

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

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.

Learning path · step 6 of 15

Hardening a Django deployment

Keep going