skip to content

How would you reduce the attack surface of a production Django admin, including its URL and login page, and what does Django not do for you?

level: seniorimportance: should knowfreq 40%

answer

  1. obscurity cuts noise, not risk
  2. no built-in login throttling
  3. the site's own permission gate
  4. fewer staff, fewer superusers

basics

~20 s

Mount the Django admin at a non-default path, keep staff and superuser accounts few, add a second factor, and restrict who can reach it. Django does not throttle admin logins; add rate limiting or override AdminSite.has_permission for extra conditions.

solid answer

~50 s

The admin is a privileged entry point, so shrink who can reach it and what a stolen password buys. Mount it at a non-default path, e.g. `path("ops-console/", admin.site.urls)`: internal links use the `admin:` namespace, so nothing breaks, and automated scans of `/admin/` stop — but that is obscurity, not protection. Django does **not** throttle login attempts: `ModelBackend` has no rate limiting, and the docs point you to a plugin or web-server module. Add a second factor through an authentication package, limit network reachability where you can, and keep `is_staff` and especially `is_superuser` accounts few. For conditions the flags cannot express, subclass `AdminSite` and override `has_permission()` — e.g. require membership in an operations group — remembering that it gates every admin view but not the login form. The login form already rejects non-staff users without revealing which accounts exist, and logout is POST-only since Django 5.0.

code

python · 25 lines
python
# ops/admin_site.py
from django.contrib import admin


class OpsAdminSite(admin.AdminSite):
    site_header = "Operations console"

    def has_permission(self, request):
        user = request.user
        return (
            super().has_permission(request)
            and (user.is_superuser or user.groups.filter(name="operations").exists())
        )


# urls.py
from django.urls import path

from ops.admin_site import OpsAdminSite

ops_site = OpsAdminSite(name="admin")

urlpatterns = [
    path("ops-console/", ops_site.urls),
]

go deeper

for a junior

Recall that the admin should not stay at an obvious path with weak passwords, and that only trusted staff should have accounts.

for a middle

Explain has_permission, the admin login form's staff check, and that Django has no built-in login throttling.

for a senior

Layer throttling, a second factor, network limits and minimal privileged accounts, and extend has_permission for rules the flags cannot express.

for a principal

Decide whether the admin is reachable from the internet at all, and who owns reviewing staff access over time.

## Why the admin deserves special treatment `django.contrib.admin` gives its users broad reach: bulk actions, cascading deletes, every registered model. A compromised staff account is therefore worth more than a customer account, and the admin login page is a predictable target. Hardening it has three goals: **fewer people can reach it**, **guessing passwords is slow and insufficient**, and **each account can do less**. ## What Django already does - **The door is `is_staff`.** `AdminSite.has_permission()` requires `is_active` and `is_staff`; every admin view is wrapped by `admin_view()`, which redirects to the admin login otherwise. - **The login form does not enumerate staff.** `AdminAuthenticationForm` rejects non-staff users with the same "correct username and password for a staff account" error as a wrong password. - **CSRF protection and POST-only logout.** Admin views are CSRF-protected, and logging out via GET was removed in Django 5.0. - **Audit history.** `LogEntry` records admin additions, changes and deletions. ## What Django does not do 1. **No throttling of login attempts.** The security docs say Django does not throttle authentication requests and suggest a plugin or web-server module; the `ModelBackend` documentation says the same. 2. **No second factor.** Multi-factor login comes from an authentication package. 3. **No network restriction.** Whether the admin is reachable from the internet is decided outside Django. 4. **No secret URL.** `/admin/` is only the conventional mount point in the project template. ## The hardening checklist | Measure | Where | What it buys | |---|---|---| | Non-default mount path | project `urls.py` | Removes drive-by scans of `/admin/`; not a security boundary | | Login throttling | web server, plugin or custom auth backend | Makes online password guessing slow | | Second factor | authentication package | A leaked password alone is not enough | | Network restriction | proxy, VPN, allow-list | Outsiders never see the login page | | Few staff, very few superusers | user management | Less to steal; rights reviewed per group | | Extra gate in `has_permission()` | `AdminSite` subclass | Encodes rules the flags cannot, per request | | HTTPS and secure cookies | settings and proxy | Session cookies cannot be sniffed | ## Overriding `has_permission()` A subclass of `AdminSite` can require more than `is_staff`, for example membership in an "operations" group, or a flag your second-factor package sets on the session. It runs on every admin view wrapped by `admin_view()` — everything except the login page — so the rule applies consistently. Two cautions: - It does not change the login form: a staff user who fails the extra rule authenticates, then lands back on the login page, which says they are authenticated but not authorized. Pair it with a custom `login_form` whose `confirm_login_allowed()` gives a clear message. - Making the project use the subclass by default is the admin-branding topic's `default_site` mechanism. ## Moving the URL, correctly - Replace `path("admin/", admin.site.urls)` with an unguessable-looking path, and never hard-code `/admin/` in templates; `reverse("admin:index")` follows the move. - Do not rely on it: the path leaks through browser history, referrers and staff screenshots. - Some teams keep a decoy at `/admin/` that just logs attempts; that is monitoring, not protection. ## Operating it - Review staff and superuser lists regularly; remove `is_staff` when roles change rather than deactivating permissions one by one. - Alert on failed-login bursts at the proxy. - Keep the admin on the same Django security releases as the rest of the site; it is part of the attack surface, not an add-on. HTTPS, HSTS and secure cookie settings, and the details of MFA packages, belong to their own topics; the admin-specific parts are the mount point, `has_permission()`, the login form and who holds the flags. ## A test that keeps it honest Hardening decays when nobody checks it. A few cheap tests guard the Django-side parts: 1. `GET /admin/` returns 404 once the mount point moves, so the old path is really gone. 2. A staff user outside the operations group is redirected to the login page from the admin index. 3. A non-staff user with valid credentials gets the admin login form's error, not a session. 4. A GET to the admin logout URL does not log the user out. Throttling, the second factor and network rules live outside these tests; verify them in the environment where they run.

  • Does moving the Django admin from /admin/ to a custom path make it secure?
    No. It removes noise from automated scans that probe `/admin/`, which makes logs quieter and real attacks easier to spot, but the path leaks through history, referrers and people. Throttling, a second factor, network restriction and few privileged accounts are what reduce risk.
  • In Django, where can login throttling for the admin be implemented if the framework does not provide it?
    The docs name three places: a web-server module in front of Django, a third-party Django plugin, or your own rate limiting inside a custom authentication backend. Throttle per account and per client address, and make sure the proxy passes the real client address so the limit is not shared by everyone.

saying these in an interview costs you the question

  • Moving the admin URL is enough to secure it
  • Django locks accounts after repeated failed admin logins
  • The admin login page tells attackers which accounts are staff
  • Overriding has_permission also blocks the user at the login form
  • Internal admins need no hardening because only staff use them