Django는 강력한 보안 기능을 내장한 훌륭한 프레임워크입니다. 하지만 많은 개발자가, 특히 프로젝트 초기에 공식 문서나 튜토리얼을 따라가며 만든 /admin URL을 그대로 방치하는 실수를 범합니다.

이것은 "우리 집 현관문은 여기에 있습니다"라고 크게 광고판을 붙여 놓는 것과 같습니다. 전 세계의 자동화된 스캐너와 공격 봇은 웹사이트를 발견하면 가장 먼저 example.com/admin/을 스캔합니다.

이 글에서는 단순한 URL 변경부터 침입자 IP를 즉시 차단하는 적극적인 방어까지, Django 관리자 페이지를 안전하게 보호하는 몇 가지 핵심 방법을 소개합니다. 그렇게 해야 하는지에 초점을 맞춰 설명합니다.


1. 가장 기본: 관리자 URL 변경 (환경 변수 사용)



가장 쉽고, 가장 빠르며, 가장 효과적인 첫 번째 방어선입니다.

🤔 WHY: 왜 URL을 숨겨야 할까?

공격자는 URL을 모르고서는 브루트 포스(Brute-force) 공격이나 자격 증명 탈취를 시도조차 할 수 없습니다. /admin이라는 모두가 아는 경로 대신, my-super-secret-admin-path/처럼 아무도 추측할 수 없는 경로를 사용하면 99%의 자동화된 공격을 차단할 수 있습니다.

'숨기는 것(Obscurity)'이 '보안(Security)'의 전부는 아니지만, 가장 비용 대비 효과가 높은 방어막입니다.

🚀 HOW: 환경 변수로 주입하기

URL을 코드에 하드코딩하지 않고 환경 변수(Environment Variable)로 주입하는 것이 모범 사례입니다.

  1. .env 파일 (또는 서버 환경 변수 설정)
# .env
# 아무도 추측할 수 없는 복잡한 문자열을 사용하세요.
DJANGO_ADMIN_URL=my-secret-admin-portal-b7x9z/
  1. settings.py
# settings.py
import os

# 기본값을 두되, 환경 변수에서 읽어오도록 설정
ADMIN_URL = os.environ.get('DJANGO_ADMIN_URL', 'admin/')
  1. urls.py (메인 프로젝트)
# urls.py
from django.contrib import admin
from django.urls import path
from django.conf import settings # settings 모듈 import

urlpatterns = [
    # admin/ 대신 settings.ADMIN_URL 값을 사용
    path(settings.ADMIN_URL, admin.site.urls),
    # ... other urls
]

이제 개발 환경에서는 admin/을 쓰더라도, 운영 서버에서는 환경 변수만 변경하여 실제 관리자 경로를 숨길 수 있습니다.


2. 성벽 쌓기: Nginx에서 IP로 접근 제한

URL이 혹시나 노출되더라도, 아예 허용된 IP가 아니면 관리자 페이지 접속 자체를 차단하는 강력한 방법입니다.

🤔 WHY: 왜 Nginx에서 막아야 할까?

이 방식은 공격 트래픽이 Django(애플리케이션)에 도달하기 전에 웹 서버(Nginx)단에서 차단합니다. 즉, Django는 공격이 시도되었는지조차 모르게 되며, 불필요한 리소스를 낭비하지 않습니다. 관리자가 특정 IP(사무실, VPN 등)에서만 접속한다면 가장 확실한 방법입니다.

HOW: Nginx 설정 예제

Nginx 설정 파일(sites-available의 해당 사이트 설정)에 location 블록을 추가합니다.

server {
    # ... (기존 설정)

    # ADMIN_URL 환경변수와 동일한 경로를 지정
    location /my-secret-admin-portal-b7x9z/ {
        # 1. 허용할 IP 주소 (예: 사무실 고정 IP)
        allow 192.168.0.10;
        # 2. 허용할 IP 대역 (예: VPN 대역)
        allow 10.0.0.0/24;
        # 3. 로컬 호스트 (서버 내부)
        allow 127.0.0.1;

        # 4. 위에 명시된 IP 외 모든 접근 차단
        deny all;

        # 5. 모든 처리를 uwsgi/gunicorn 프록시로 전달
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }

    # ... (기타 location 설정)
}

이제 허용된 IP 외의 모든 사용자가 해당 URL로 접근하면 Django는 응답조차 하지 않고, Nginx가 403 Forbidden 오류를 즉시 반환합니다.


3. 문지기 배치: 로그인 시도 횟수 제한 (django-axes)



공격자가 어떻게든 URL을 알아내고 IP 제한도 우회했다면, 이제 브루트 포스 공격을 막아야 합니다.

🤔 WHY: 왜 시도 횟수를 제한해야 할까?

브루트 포스 공격은 'admin' 같은 흔한 계정 ID에 대해 수천, 수만 개의 비밀번호를 자동으로 대입합니다. django-axes 같은 패키지는 "짧은 시간 내에 5회 이상 로그인 실패 시, 해당 IP 또는 계정을 일정 시간 동안 잠금"과 같은 규칙을 만듭니다.

이는 자동화된 스크립트를 거의 무용지물로 만듭니다.

HOW: django-axes 사용하기

django-axes는 이 작업을 위한 가장 표준적인 패키지입니다.

  1. 설치: pip install django-axes

  2. settings.py 등록:

INSTALLED_APPS = [
    # ...
    'axes', # 다른 앱들보다 위에 두는 것을 권장
    # ...
    'django.contrib.admin',
]

AUTHENTICATION_BACKENDS = [
    # AxesBackend는 맨 앞에 위치해야 합니다.
    'axes.backends.AxesBackend',
    # 기본 Django 인증 백엔드
    'django.contrib.auth.backends.ModelBackend',
]

# 5회 실패 시 10분간 잠금 (기본값)
AXES_FAILURE_LIMIT = 5
AXES_COOLOFF_TIME = 0.166 # 0.166 * 60 = 약 10분
  1. 마이그레이션: python manage.py migrate

이제 누군가 로그인 시도를 5회 연속 실패하면, axes가 해당 시도를 기록하고 정해진 시간 동안 해당 IP/계정의 로그인을 차단합니다.


4. 이중 잠금: 2단계 인증 (2FA)

비밀번호가 뚫렸을 때를 대비한 최후의 보루입니다.

🤔 WHY: 왜 2FA가 필요한가?

관리자 계정의 비밀번호가 유출되거나, 너무 쉬운 비밀번호를 사용했을 수 있습니다. 2단계 인증(Two-Factor Authentication)은 "내가 아는 것(비밀번호)""내가 가진 것(스마트폰 OTP)"을 모두 요구합니다.

해커가 비밀번호를 훔쳐도, 관리자의 스마트폰 없이는 절대 로그인할 수 없습니다.

HOW: django-otp 사용하기

django-otp는 Django에 2FA를 통합하는 핵심 패키지입니다.

  1. 설치: pip install django-otp

  2. settings.py 등록:

INSTALLED_APPS = [
    # ...
    'django_otp',
    'django_otp.plugins.otp_totp', # Google Authenticator 등 지원
    # ...
]

MIDDLEWARE = [
    # ...
    'django_otp.middleware.OTPMiddleware', # SessionMiddleware 다음에
    # ...
]

django-otp는 기본 골격이며, 이를 어드민에 쉽게 통합해주는 django-two-factor-auth 같은 패키지를 함께 사용하면 사용자가 직접 QR코드를 스캔하고 등록하는 전체 프로세스를 손쉽게 구현할 수 있습니다.


5. 함정 설치: 허니팟(Honeypot)과 Fail2Ban 연동

가장 적극적인 방어책입니다. 공격자의 /admin 스캔 시도를 역으로 이용해 서버에서 영구 추방합니다.

🤔 WHY: 왜 함정을 파야 할까?

어차피 공격자들은 /admin을 계속 스캔할 것입니다. 그렇다면 이 경로를 가짜 함정(Honeypot) 으로 만들어서, 여기에 한 번이라도 접근을 시도하는 IP는 무조건 악의적인 것으로 간주하고 즉시 차단해버리는 것입니다.

HOW: 가짜 Admin + Fail2Ban

이 방법은 다소 복잡하지만 매우 효과적입니다.

  1. 가짜 Admin 뷰 생성: 실제 관리자 URL은 1번처럼 my-secret-admin-portal-b7x9z/로 숨깁니다. 그리고 버려진 /admin/ 경로에 가짜 뷰를 연결합니다.
# urls.py
from django.urls import path
from . import views # 가짜 뷰 import

urlpatterns = [
    path('my-secret-admin-portal-b7x9z/', admin.site.urls), # 진짜
    path('admin/', views.admin_honeypot), # 가짜 (함정)
]

# views.py
import logging
from django.http import HttpResponseForbidden

# honeypot 전용 로거 설정 (settings.py에 'honeypot' 로거 정의 필요)
honeypot_logger = logging.getLogger('honeypot')

def admin_honeypot(request):
    # 접근 시도자의 IP를 'honeypot' 로그에 기록
    ip = request.META.get('REMOTE_ADDR')
    honeypot_logger.warning(f"HONEYPOT: Admin access attempt from {ip}")

    # 공격자에게는 그냥 403 오류를 보여줌
    return HttpResponseForbidden()
  1. Fail2Ban 설정: Fail2Ban은 서버 로그 파일을 실시간으로 감시하다가 특정 패턴(예: "HONEYPOT: ...")이 감지되면, 해당 로그를 생성한 IP를 iptables(리눅스 방화벽)에 추가하여 차단하는 도구입니다.

    • Django가 honeypot.log 파일에 로그를 남기도록 설정합니다.

    • Fail2Ban이 honeypot.log를 감시하도록 설정합니다.

    • 누군가 /admin/에 접근하면 views.py가 로그를 남기고, Fail2Ban이 이를 감지하여 해당 IP의 모든 접속(SSH, HTTP 등)을 즉시 차단합니다.

요약

Django 관리자 페이지 보안은 여러 겹의 방어막을 치는 것이 핵심입니다.

  • (필수) 1. URL 변경: 지금 당장 5분 투자해서 하세요.

  • (권장) 2. IP 제한: 고정 IP가 있다면 가장 강력합니다.

  • (권장) 3. django-axes: 브루트 포스 공격을 막습니다.

  • (강력 권장) 4. 2FA: 관리자 계정 탈취를 원천 차단합니다.

  • (고급) 5. 허니팟: 공격적인 방어로 서버 전체를 보호합니다.

/admin을 그대로 방치하는 것은 보안에 대한 태만입니다. 지금 바로 여러분의 urls.py를 확인해 보세요.