Защита Django Admin в продакшне: URL, IP, django-axes и 2FA
Как защитить Django Admin в продакшне: сменить URL, ограничить доступ по IP на nginx, блокировать перебор паролей django-axes, включить 2FA и HTTPS.
Содержание · 10 разделов
- Смените URL админки
- Ограничьте доступ по IP или VPN на nginx
- Блокируйте перебор паролей с django-axes
- Включите двухфакторную аутентификацию (django-otp)
- Как не запереть самого себя
- Включите HTTPS и защищённые cookie
- Сессии и пароли сотрудников
- Журнал действий: LogEntry
- Чек-лист безопасности Django Admin
- Частые вопросы
- Достаточно ли просто сменить /admin/?
- Я заблокировал себя в django-axes. Что делать?
- После включения django-otp не могу войти в админку
- Можно ли отключить админку на публичном сервере?
- Как узнать о каждом входе в админку?
- Итог
Админку Django в продакшне защищают в несколько слоёв: неочевидный URL отсекает сканеры, ограничение по IP или VPN на nginx закрывает её от интернета, django-axes блокирует перебор паролей, django-otp добавляет второй фактор, а настройки SECURE_* гарантируют HTTPS и защищённые cookie. Ни одна мера не работает в одиночку: смена /admin/ без остальных слоёв почти ничего не даёт.
Ниже — настройка каждого слоя с конфигурациями, проверенными на Django 5.2 LTS и 6.x, и чек-лист в конце. Если вы только знакомитесь с админкой, начните с полного руководства по Django Admin.
Смените URL админки
Путь admin/ проверяют все автоматические сканеры. Вынесите его в переменную окружения, чтобы он не попадал в репозиторий:
# config/urls.py
import os
from django.contrib import admin
from django.urls import path
ADMIN_URL = os.environ.get("DJANGO_ADMIN_URL", "admin/")
urlpatterns = [
path(ADMIN_URL, admin.site.urls),
]
# .env на сервере — путь обязательно со слешем в конце
DJANGO_ADMIN_URL=control-7f3a9c/
Что важно учесть:
- Не упоминайте путь в
robots.txtи sitemap —Disallow: /control-7f3a9c/сразу выдаёт адрес. - Ссылки на админку в шаблонах стройте через
{% url 'admin:index' %}, а не хардкодом: тогда смена пути ничего не сломает. - Смена пути только снижает шум. Целенаправленный атакующий может найти путь в утёкших логах, JS-бандле или истории git. Поэтому дальше — настоящие барьеры.
Ограничьте доступ по IP или VPN на nginx
Самая надёжная мера — не пускать к админке никого, кроме офиса и VPN. Делайте это на уровне веб-сервера: запрос отсекается ещё до Django. Как настроить связку целиком, описано в статье про деплой Django с Gunicorn и Nginx.
# /etc/nginx/conf.d/example.conf
limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/m;
server {
listen 443 ssl;
server_name example.org;
# Вся админка — только из офиса и VPN
location /control-7f3a9c/ {
allow 203.0.113.10; # офис
allow 10.8.0.0/24; # подсеть VPN
deny all;
include /etc/nginx/proxy_params;
proxy_pass http://127.0.0.1:8000;
}
# Страница входа: те же правила + ограничение частоты запросов
location = /control-7f3a9c/login/ {
allow 203.0.113.10;
allow 10.8.0.0/24;
deny all;
limit_req zone=admin_login burst=5 nodelay;
limit_req_status 429;
include /etc/nginx/proxy_params;
proxy_pass http://127.0.0.1:8000;
}
location / {
include /etc/nginx/proxy_params;
proxy_pass http://127.0.0.1:8000;
}
}
Важно: директивы
allow/denyвнутриlocationне наследуются соседними блоками. Если для страницы входа заведён отдельныйlocation = ..., правила доступа нужно повторить в нём — иначе/login/окажется открыт всему интернету.
Файл /etc/nginx/proxy_params поставляется с пакетом nginx в Debian и Ubuntu: он передаёт приложению заголовки Host, X-Real-IP, X-Forwarded-For и X-Forwarded-Proto. В пакетах с nginx.org, на RHEL-совместимых системах и в Docker-образе nginx этого файла нет — вместо include пропишите заголовки в каждом location:
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
Проверьте конфигурацию и перечитайте её: sudo nginx -t && sudo systemctl reload nginx. Посторонние адреса получат 403 Forbidden.
Если перед nginx стоит CDN или балансировщик, $remote_addr будет адресом прокси, а не клиента. Тогда настройте модуль ngx_http_realip_module (set_real_ip_from + real_ip_header) — иначе правила allow сработают не так, как вы ожидаете.
Альтернатива для распределённой команды — открыть админку только во внутренней сети (WireGuard, Tailscale, корпоративный VPN) или вынести её на отдельный поддомен с собственным server-блоком и ограничениями.
Блокируйте перебор паролей с django-axes
django-axes считает неудачные попытки входа и временно блокирует источник атаки. Установка:
python -m pip install django-axes
# settings.py
from datetime import timedelta
INSTALLED_APPS += ["axes"]
AUTHENTICATION_BACKENDS = [
"axes.backends.AxesStandaloneBackend", # обязательно первым
"django.contrib.auth.backends.ModelBackend",
]
MIDDLEWARE += [
"axes.middleware.AxesMiddleware", # рекомендуется последним
]
AXES_FAILURE_LIMIT = 5 # попыток до блокировки
AXES_COOLOFF_TIME = timedelta(minutes=30) # без этого — бессрочная блокировка
AXES_LOCKOUT_PARAMETERS = [["username", "ip_address"]]
AXES_RESET_ON_SUCCESS = True # успешный вход обнуляет счётчик
python manage.py migrate
Ключевые моменты конфигурации:
AXES_COOLOFF_TIMEпринимаетtimedelta(нуженfrom datetime import timedelta), число часов илиNone. Со значением по умолчаниюNoneзаблокированный остаётся заблокированным, пока вы не сбросите его вручную.AXES_LOCKOUT_PARAMETERS = [["username", "ip_address"]]— блокировка по связке «логин + IP». Если блокировать только поusername, злоумышленник может намеренно запереть ваш аккаунт с любого адреса. Если только по IP — все пользователи за одним NAT пострадают вместе.- После лимита попыток axes отвечает кодом
429 Too Many Requests(настраивается черезAXES_HTTP_RESPONSE_CODE).
Если Django за прокси. По умолчанию axes берёт IP из REMOTE_ADDR, а за nginx это 127.0.0.1 — то есть все клиенты для axes выглядят одинаково. Установите python -m pip install "django-axes[ipware]" и укажите, из какого заголовка брать адрес. Когда перед Django стоит один nginx с proxy_params, надёжнее всего X-Real-IP: nginx перезаписывает его адресом клиента, и подделать его из браузера нельзя.
AXES_IPWARE_META_PRECEDENCE_ORDER = ["HTTP_X_REAL_IP", "REMOTE_ADDR"]
Связка AXES_IPWARE_PROXY_COUNT = 1 и HTTP_X_FORWARDED_FOR в такой схеме не подходит: nginx кладёт в X-Forwarded-For только адрес клиента, ipware не находит в цепочке ожидаемого прокси и возвращает None вместо IP. AXES_IPWARE_PROXY_COUNT нужен, когда перед nginx есть ещё балансировщик или CDN и в X-Forwarded-For несколько адресов.
Разблокировать пользователя можно командами:
python manage.py axes_reset_username admin # конкретный логин
python manage.py axes_reset_ip 203.0.113.10 # конкретный IP
python manage.py axes_reset # все блокировки
Журнал попыток (Access attempts, Access logs) появляется прямо в админке.
Включите двухфакторную аутентификацию (django-otp)
Даже сложный пароль может утечь. django-otp добавляет на страницу входа одноразовый код из приложения-аутентификатора (Google Authenticator, Aegis, 1Password).
python -m pip install "django-otp[qrcode]"
Экстра qrcode нужна, чтобы админка показывала QR-код для настройки приложения.
# settings.py
INSTALLED_APPS += [
"django_otp",
"django_otp.plugins.otp_totp", # коды из приложения (TOTP)
"django_otp.plugins.otp_static", # резервные одноразовые коды
]
MIDDLEWARE = [
# ...
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django_otp.middleware.OTPMiddleware", # сразу после AuthenticationMiddleware
# ...
]
# config/urls.py
from django.contrib import admin
from django_otp.admin import OTPAdminSite
admin.site.__class__ = OTPAdminSite
После python manage.py migrate админка пускает только пользователей, подтвердивших вход кодом с зарегистрированного устройства. Пользователь без устройства для неё всё равно что не is_staff.
Как не запереть самого себя
Возникает проблема курицы и яйца: чтобы добавить TOTP-устройство в админке, нужно в неё войти, а войти без устройства нельзя. Выпустите себе резервный код из консоли:
python manage.py addstatictoken admin
# → wqb4qi55
Дальше:
- На странице входа введите логин и пароль — форма попросит код и покажет список устройств.
- Выберите устройство Backup Code и введите код из консоли (он одноразовый).
- В разделе TOTP devices добавьте устройство для своего пользователя и откройте ссылку qrcode в списке устройств.
- Отсканируйте QR-код приложением. Следующий вход — уже с кодом из него.
Повторите эти действия для каждого сотрудника с доступом в админку. Создание первого администратора описано в статье про createsuperuser.
Совет: django-otp и django-axes совместимы — axes считает неудачные попытки на той же форме входа.
Включите HTTPS и защищённые cookie
Пароль и сессионная cookie не должны передаваться по HTTP. Минимальный набор настроек для продакшна за nginx:
# settings.py (продакшн)
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = ["https://example.org"]
SECURE_HSTS_SECONDS = 60 * 60 * 24 * 365
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
| Настройка | Что делает |
|---|---|
SECURE_PROXY_SSL_HEADER | Доверять заголовку прокси о том, что исходный запрос был по HTTPS |
SECURE_SSL_REDIRECT | Перенаправлять все HTTP-запросы на HTTPS |
SESSION_COOKIE_SECURE | Отправлять сессионную cookie только по HTTPS |
CSRF_COOKIE_SECURE | То же для CSRF-cookie |
CSRF_TRUSTED_ORIGINS | Доверенные origin со схемой — без этого вход за прокси может упасть |
SECURE_HSTS_SECONDS | Браузер запомнит, что сайт работает только по HTTPS, на заданный срок |
Важно:
SECURE_PROXY_SSL_HEADERвключайте, только если прокси всегда выставляетX-Forwarded-Protoи затирает пришедший от клиента заголовок (proxy_set_header X-Forwarded-Proto $scheme;). HSTS начинайте с малого значения (например, 3600) и увеличивайте, когда убедитесь, что HTTPS работает на всех поддоменах — откатить его быстро нельзя.
Проверьте конфигурацию встроенной командой — она перечислит пропущенные настройки безопасности:
python manage.py check --deploy
Заголовок X-Frame-Options: DENY против кликджекинга Django ставит по умолчанию через XFrameOptionsMiddleware. Политику CSP (Content Security Policy — список разрешённых источников скриптов, стилей и картинок) в Django 5.2 добавляют пакетом django-csp. С Django 6.0 поддержка встроена: подключите ContentSecurityPolicyMiddleware и опишите политику в SECURE_CSP:
# settings.py, Django 6.0+
from django.utils.csp import CSP
MIDDLEWARE += ["django.middleware.csp.ContentSecurityPolicyMiddleware"]
SECURE_CSP = {
"default-src": [CSP.SELF],
"img-src": [CSP.SELF, "data:"],
}
Стандартная админка работает с такой политикой, но сторонние темы и виджеты с внешними CDN могут потребовать дополнительных источников. Начните с SECURE_CSP_REPORT_ONLY: браузер будет сообщать о нарушениях, ничего не блокируя.
Сессии и пароли сотрудников
Длинные сессии превращают украденный ноутбук в открытую админку. Сократите срок жизни сессии:
SESSION_COOKIE_AGE = 60 * 60 * 8 # 8 часов вместо 2 недель по умолчанию
SESSION_EXPIRE_AT_BROWSER_CLOSE = True # сессия закончится с закрытием браузера
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {"min_length": 12},
},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
]
SESSION_COOKIE_HTTPONLY по умолчанию уже True — JavaScript не прочитает сессионную cookie. После смены пароля Django завершает остальные сессии пользователя. Просроченные сессии из базы периодически удаляйте командой python manage.py clearsessions (например, по cron).
Принцип минимальных прав тоже часть защиты: выдавайте сотрудникам is_staff и нужные права через группы, а is_superuser — только тем, кому он действительно нужен. Подробнее о группах и правах — в разделе аутентификация в Django.
Журнал действий: LogEntry
Каждое добавление, изменение и удаление через админку Django записывает в модель LogEntry: кто, когда, какой объект и какие поля изменились. По умолчанию журнал виден только в истории объекта и в блоке «Последние действия». Зарегистрируйте его в админке только для чтения:
# core/admin.py
from django.contrib import admin
from django.contrib.admin.models import LogEntry
@admin.register(LogEntry)
class LogEntryAdmin(admin.ModelAdmin):
list_display = ["action_time", "user", "content_type", "object_repr", "action_flag"]
list_filter = ["action_flag", "content_type"]
search_fields = ["object_repr", "change_message", "user__username"]
date_hierarchy = "action_time"
list_select_related = ["user", "content_type"]
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
Запрос к журналу из кода или shell:
from django.contrib.admin.models import LogEntry
for entry in LogEntry.objects.filter(user__username="admin")[:10]:
print(entry.action_time, entry.get_action_flag_display(), entry.object_repr,
entry.get_change_message())
# → 2026-10-09 10:12:03+00:00 Change Заказ A-17 Changed Status.
Учтите ограничения: LogEntry фиксирует только действия через страницы админки — формы и встроенное удаление. Массовые операции через queryset.update() в него не попадают — логируйте их сами (см. оптимизацию admin actions). Входы и неудачные попытки журналирует django-axes.
Чек-лист безопасности Django Admin
- путь админки вынесен в переменную окружения и не упоминается в
robots.txt - доступ к админке ограничен по IP, VPN или отдельному поддомену на nginx
- django-axes настроен с
AXES_COOLOFF_TIMEи блокировкой по «логин + IP» - за прокси axes видит реальный IP клиента (
django-axes[ipware]и заголовокX-Real-IP) - у каждого сотрудника настроена 2FA через django-otp
-
SECURE_SSL_REDIRECT, secure-cookie и HSTS включены,check --deployбез предупреждений - срок сессии сокращён, пароли проверяются валидаторами
-
is_superuser— только у тех, кому он нужен; остальным права через группы - журнал
LogEntryдоступен для аудита
Частые вопросы
Достаточно ли просто сменить /admin/?
Нет. Смена пути убирает шум от ботов, но от целенаправленной атаки не защищает. Минимум для продакшна — HTTPS, ограничение попыток входа и 2FA.
Я заблокировал себя в django-axes. Что делать?
Выполните на сервере python manage.py axes_reset_username <логин> или axes_reset_ip <адрес>. Если AXES_COOLOFF_TIME задан, блокировка снимется сама по истечении срока.
После включения django-otp не могу войти в админку
Админка OTPAdminSite пускает только пользователей с подтверждённым устройством. Выпустите резервный код командой addstatictoken и войдите с ним, выбрав устройство Backup Code, как описано в разделе о django-otp.
Можно ли отключить админку на публичном сервере?
Да. Подключайте маршрут по флагу из окружения, а админку запускайте отдельным экземпляром приложения во внутренней сети:
# config/urls.py
import os
from django.contrib import admin
from django.urls import path
urlpatterns = [
# публичные маршруты
]
if os.environ.get("DJANGO_ADMIN_ENABLED") == "1":
urlpatterns.append(path(os.environ.get("DJANGO_ADMIN_URL", "admin/"), admin.site.urls))
На публичном сервере без флага путь админки вернёт 404.
Как узнать о каждом входе в админку?
Подпишитесь на сигнал user_logged_in и пишите входы сотрудников в отдельный лог — оттуда их легко отправить в систему мониторинга:
# core/signals.py — импортируйте модуль в AppConfig.ready()
import logging
from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver
logger = logging.getLogger("security")
@receiver(user_logged_in)
def log_staff_login(sender, request, user, **kwargs):
if user.is_staff:
logger.warning("Вход в админку: %s, IP %s", user.get_username(), request.META.get("REMOTE_ADDR"))
За прокси в REMOTE_ADDR окажется адрес nginx. Если django-axes уже настроен на реальный IP, используйте его функцию axes.helpers.get_client_ip_address(request).
Итог
Надёжная защита админки складывается из слоёв: скрытый путь, сетевые ограничения, лимит попыток, второй фактор, HTTPS и аудит. Начните с IP-фильтра и 2FA — они закрывают большинство реальных атак. Остальные настройки админки собраны в руководстве по Django Admin, а все статьи кластера — в разделе Django Admin.
- #Django Admin
- #Безопасность
- #django-axes
- #2FA
- #nginx
- #Продакшн