SECURE_PROXY_SSL_HEADER puede corregir la detección de HTTPS o, si tu proxy transmite los encabezados del cliente, hacer que un encabezado controlado por un atacante forme parte de tu modelo de seguridad.
Pero ese límite es donde la mayoría de las listas de comprobación de Django se quedan cortas. DEBUG = False es necesario, pero no es un plan de seguridad; un proxy defectuoso, una ruta de media o un flujo de secretos mal configurado pueden dejar expuesta la aplicación desplegada. Django ofrece valores predeterminados sólidos para ataques comunes. Tu despliegue debe preservarlos.
Por eso, sigue esta lista de comprobación de seguridad de Django en orden: audita la configuración de producción, protege el tráfico del navegador y la identidad, revisa las vías de escape de la aplicación y, después, prueba nginx, Gunicorn, los media subidos, los servicios privados, las alertas y las copias de seguridad.
En este artículo
- Antes de cambiar código, ejecuta la auditoría de despliegue de Django
- Mantén los secretos de producción fuera del repositorio
- Haz que Django falle de forma segura en producción
- Trata los encabezados Host como un límite de entrada
- Fuerza HTTPS para todo el sitio
- No confíes ciegamente en
SECURE_PROXY_SSL_HEADER - Mantén intacta la protección CSRF en formularios y AJAX
- Refuerza la autenticación donde los atacantes ejercen presión
- Deja que los valores predeterminados de Django gestionen la inyección SQL hasta que los evadas
- Conserva el escape de plantillas en lugar de corregir la salida manualmente
- Añade los encabezados que Django puede proporcionar y conoce sus límites
- Los media subidos no son confiables y no deben ejecutarse
- Haz que el servidor web rechace los hosts desconocidos antes de que Django los vea
- Comprueba que los servicios de producción que Django supone privados lo sean
- Configura los informes de fallos sin filtrar detalles de los fallos
- Verifica la aplicación desplegada el día del lanzamiento
Antes de cambiar código, ejecuta la auditoría de despliegue de Django
Así que ejecuta la comprobación de despliegue de Django contra el módulo de configuración que utilizará tu proceso de producción:
python manage.py check --deploy --settings=config.settings.production
check --deploy es una alarma de humo, no una inspección contra incendios. Detecta muchas advertencias de configuración del despliegue, incluidos el modo de depuración, la configuración de la clave, las cookies inseguras, la ausencia de redirecciones HTTPS, HSTS y las deficiencias de configuración de hosts. No puede inspeccionar si tu balanceador de carga elimina el X-Forwarded-Proto proporcionado por el cliente, si nginx rechaza los hosts desconocidos, si PostgreSQL es accesible públicamente o si /media/ puede ejecutar un archivo subido.
Tampoco puede determinar si una vista de detalles de un problema olvidó una comprobación de autorización a nivel de objeto. Django puede inspeccionar la configuración; no puede inferir la intención de seguridad de tu aplicación.
Usa el comando como primera barrera:
- Configura: corrige cada advertencia que se aplique a producción.
- Inspecciona: revisa la configuración del proxy, el servidor web, la base de datos, la caché, el almacenamiento y las alertas.
- Verifica: envía solicitudes al servicio desplegado y registra sus respuestas.
Y la lista de comprobación de despliegue de Django te proporciona la base. El trabajo útil comienza donde terminan sus comprobaciones automatizadas.
Mantén los secretos de producción fuera del repositorio
Un SECRET_KEY filtrado puede comprometer las sesiones firmadas, los tokens de restablecimiento de contraseña u otros datos protegidos con esa clave, dependiendo de cómo la utilice tu aplicación. Las credenciales de la base de datos merecen el mismo tratamiento y solo deberían poder utilizarse desde la infraestructura de aplicaciones aprobada.
Django requiere una clave secreta grande y aleatoria. Genera una con Django:
python -m django shell -c \
"from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
Y léela desde el entorno o desde un archivo protegido:
# config/settings/production.py
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["POSTGRES_DB"],
"USER": os.environ["POSTGRES_USER"],
"PASSWORD": os.environ["POSTGRES_PASSWORD"],
"HOST": os.environ["POSTGRES_HOST"],
"PORT": os.environ.get("POSTGRES_PORT", "5432"),
}
}
La ausencia de un secreto obligatorio debería detener el arranque. Un servidor que recurre silenciosamente a una clave de desarrollo convierte un error de despliegue en una vulnerabilidad activa.
Para una rotación escalonada, usa SECRET_KEY_FALLBACKS:
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
SECRET_KEY_FALLBACKS = [
key.strip()
for key in os.environ.get("DJANGO_SECRET_KEY_FALLBACKS", "").split(",")
if key.strip()
]
Usa los valores alternativos durante una breve ventana de migración y después elimina rápidamente las claves antiguas. Conservar para siempre todas las claves históricas convierte la rotación en una mera apariencia.
| Revisión | Acción |
|---|---|
| Repositorio y manifiestos | Busca claves, contraseñas, tokens y URLs de bases de datos |
| Proceso de despliegue | Confirma que los secretos llegan a través de la plataforma o de archivos protegidos |
| Rotación | Documenta el reemplazo, la duración del valor alternativo y la revocación |
| Comportamiento ante fallos | Haz que la ausencia de secretos obligatorios detenga el arranque |
git grep -nE "SECRET_KEY|PASSWORD|DATABASE_URL|TOKEN|API_KEY"
Así que despliega una vez con un secreto ausente deliberadamente en un entorno que no sea de producción. El proceso debería fallar antes de servir tráfico. Comprueba los registros de arranque. Confirma que el valor del secreto previsto nunca aparezca.
Haz que Django falle de forma segura en producción
Pero la configuración de producción debería ser explícita. No dependas de que un archivo de desarrollo quede “mayormente sobrescrito”.
| Configuración | Dirección en producción | Riesgo reducido |
|---|---|---|
DEBUG | False | Páginas de depuración que exponen código fuente, configuración y variables locales |
ALLOWED_HOSTS | Nombres de host exactos del despliegue | Ataques mediante el encabezado Host y aceptación de hosts no previstos |
SECRET_KEY | Valor secreto, aleatorio y cargado desde el entorno | Falsificación de datos firmados |
SESSION_COOKIE_SECURE | True | Transmisión de sesiones mediante HTTP |
CSRF_COOKIE_SECURE | True | Transmisión de la cookie CSRF mediante HTTP |
SECURE_SSL_REDIRECT | True cuando Django ve HTTP | Acceso HTTP accidental |
Así que, para un pequeño gestor de incidencias en app.example.com, nginx puede terminar HTTPS y hacer proxy hacia Gunicorn:
# config/settings/production.py
DEBUG = False
ALLOWED_HOSTS = [
"app.example.com",
]
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True
Sustituye el nombre de host por los nombres exactos a los que accederán tus usuarios. ALLOWED_HOSTS = ["*"] es un atajo que traslada el riesgo a otro lugar. Si no puedes nombrar y probar ese otro validador, no lo uses.
Con DEBUG=False, Django requiere un valor adecuado para ALLOWED_HOSTS. Las respuestas de depuración pueden exponer fragmentos de código fuente, configuración, variables locales y detalles de bibliotecas.
Comprueba el comando del proceso y el archivo que utiliza:
ps aux | grep '[g]unicorn'
Después solicita un host no válido a través de la ruta pública. Una solicitud válida debería llegar a la aplicación; un host desconocido debería recibir un rechazo en lugar de una respuesta normal de la aplicación.
Trata los encabezados Host como un límite de entrada
Django valida el encabezado Host mediante request.get_host() contra ALLOWED_HOSTS. El acceso directo a los metadatos sin procesar evita esa protección.
# Validado por Django
host = request.get_host()
# Entrada sin procesar de la solicitud
host = request.META["HTTP_HOST"]
Busca ambos patrones:
git grep -nE "HTTP_HOST|USE_X_FORWARDED_HOST|get_host"
Usa request.get_host() para validar el host y, después, evita utilizar la entrada del host para seleccionar el tenant o crear redirecciones, a menos que el valor resultante esté permitido por una lista separada. Los valores de host sin procesar pueden influir en las URLs absolutas y los destinos de redirección. Pueden afectar a los enlaces de restablecimiento de contraseña, al enrutamiento de tenants y a las comprobaciones de origen.
USE_X_FORWARDED_HOST = True cambia el origen del host que utiliza Django. Actívalo únicamente cuando un proxy conocido establezca ese encabezado y elimine los valores no confiables proporcionados por el cliente.
Prueba el extremo y la aplicación por separado. Para HTTPS, el SNI coincidente es tan importante como el encabezado Host:
curl -i --resolve app.example.com:443:SERVER_IP \
https://app.example.com/
Repite la prueba con un encabezado Host inesperado contra la IP pública y, cuando esté permitido, contra el origen. El host desconocido debería llegar a la ruta de rechazo.
Fuerza HTTPS para todo el sitio
Cualquier sitio que acepte inicios de sesión debería forzar HTTPS en todas partes. Las cookies de sesión, los tokens de restablecimiento de contraseña y los tokens de acceso transportan credenciales. Proteger únicamente /login/ o /admin/ sigue dejando disponible una sesión autenticada del navegador para solicitudes HTTP en otros lugares.
Configura Django y el servidor web externo:
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Valor inicial de despliegue mientras se verifica la cobertura HTTPS.
SECURE_HSTS_SECONDS = 0
SECURE_HSTS_INCLUDE_SUBDOMAINS = False
SECURE_HSTS_PRELOAD = False
Mantén HSTS en staging mientras SECURE_HSTS_SECONDS siga siendo 0; esta configuración es para staging, mientras que la configuración final utiliza una duración mayor. Auméntala únicamente después de que cada nombre de host relevante funcione mediante HTTPS. includeSubDomains cubre los subdominios. La precarga crea un compromiso de mayor duración.
El servidor web debería redirigir cada solicitud HTTP a HTTPS antes de enviar contenido de la aplicación mediante proxy. Comprueba la cadena de redirección:
curl -I http://app.example.com/
curl -I https://app.example.com/
La respuesta HTTP debería redirigir sin representar contenido autenticado. La respuesta HTTPS debería incluir HSTS una vez activado. HSTS enviado mediante HTTP no establece la política.
En un navegador, inicia sesión mediante HTTPS e inspecciona las cookies de sesión y CSRF. Ambas deberían incluir Secure. Registra también los valores HttpOnly y SameSite de la cookie de sesión. Las cookies personalizadas creadas con set_cookie() también necesitan las marcas adecuadas.
No confíes ciegamente en SECURE_PROXY_SSL_HEADER
No pegues SECURE_PROXY_SSL_HEADER en la configuración de producción porque te lo haya dicho un blog. Puedo decirte qué debes verificar aquí; no puedo inferir el límite de confianza de tu proxy únicamente a partir de la configuración de Django.
Un proxy inverso crea un contrato de confianza:
navegador
│ HTTPS
▼
proxy que termina TLS
│ X-Forwarded-Proto: https
▼
Gunicorn → Django
Esta configuración solo es adecuada cuando:
- El proxy externo termina TLS.
- Establece
X-Forwarded-Proto: httpspara las solicitudes HTTPS. - Elimina o reemplaza cualquier
X-Forwarded-Protoproporcionado por el cliente. - Gunicorn solo es accesible a través de esa ruta de confianza.
- Cada proxy situado delante de Django sigue el mismo contrato.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
Si el proxy no establece el encabezado, SECURE_SSL_REDIRECT puede redirigir repetidamente una solicitud del navegador que ya es segura. Las comprobaciones de referer CSRF para solicitudes seguras también pueden fallar porque Django cree que la solicitud es HTTP.
Un diagnóstico práctico debería mostrar el esquema efectivo únicamente a un operador autorizado:
registro de acceso del proxy: forwarded_proto=https
diagnóstico de Django: request.is_secure()=True
respuesta: sin redirección
Registra temporalmente y de forma segura el valor del encabezado recibido en el límite del proxy. Después ejecuta la prueba de suplantación contra el extremo público:
curl -i -H 'X-Forwarded-Proto: http' https://app.example.com/
Compara el registro del encabezado recibido por el proxy con el valor enviado a Gunicorn. Comprueba también el resultado de request.is_secure() en el diagnóstico. El valor proporcionado por el cliente debe eliminarse o reemplazarse antes de que Django lo vea. Cuando esté permitido, repite la prueba contra el origen. Gunicorn debería ser accesible únicamente a través de la ruta de red de confianza.
La referencia del middleware de Django documenta la salvedad del proxy. En muchos despliegues, redirigir HTTP en el servidor web principal es más fácil y seguro porque ese componente ya conoce el estado de TLS.
Mantén intacta la protección CSRF en formularios y AJAX
El CsrfViewMiddleware de Django protege métodos inseguros como POST, PUT, PATCH y DELETE. Mantenlo en MIDDLEWARE:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
# ...
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
Incluye un token en los formularios internos:
<form method="post" action="{% url 'issues:create' %}">
{% csrf_token %}
<label>
Title
<input name="title">
</label>
<button type="submit">Create issue</button>
</form>
El siguiente ejemplo de AJAX supone que un formulario renderizado proporciona el token del DOM:
const token = document.querySelector(
"[name=csrfmiddlewaretoken]"
).value;
fetch("/issues/42/comment/", {
method: "POST",
credentials: "same-origin",
headers: {
"X-CSRFToken": token,
"Content-Type": "application/json"
},
body: JSON.stringify({ body: "Needs reproduction steps." })
});
Si tu página no renderiza un formulario, utiliza el patrón documentado por Django para leer la cookie cuando la configuración de cookies lo permita. CSRF_COOKIE_HTTPONLY=True cambia ese procedimiento: JavaScript no puede leer la cookie, por lo que se necesita un token del DOM renderizado por el servidor u otra vía de entrega deliberada.
Las solicitudes seguras también reciben una comprobación de referer HTTPS. Por tanto, una configuración incorrecta del esquema del proxy o del host puede parecer un defecto de CSRF.
| Solicitud | Resultado esperado |
|---|---|
| POST sin token | Rechazo CSRF |
| POST con token de formulario válido | Éxito |
| POST AJAX con token y credenciales del mismo origen | Éxito |
| PUT, PATCH o DELETE sin token | Rechazo CSRF |
| Solicitud entre orígenes que cambia el estado | Rechazada salvo que esté diseñada y protegida deliberadamente |
Endpoint csrf_exempt | Prueba independiente de autenticación e integridad |
Revisa cada aparición de csrf_exempt. Cada excepción necesita un motivo limitado y una prueba específica.
Refuerza la autenticación donde los atacantes ejercen presión
Usa django.contrib.auth, las sesiones de Django y los validadores de contraseñas en lugar de inventar reemplazos:
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": (
"django.contrib.auth.password_validation."
"UserAttributeSimilarityValidator"
),
"OPTIONS": {"max_similarity": 0.7},
},
{
"NAME": (
"django.contrib.auth.password_validation."
"MinimumLengthValidator"
),
"OPTIONS": {"min_length": 8},
},
{
"NAME": (
"django.contrib.auth.password_validation."
"CommonPasswordValidator"
),
},
{
"NAME": (
"django.contrib.auth.password_validation."
"NumericPasswordValidator"
),
},
]
Las funciones auxiliares de contraseñas de Django aplican hash y verifican las contraseñas:
from django.contrib.auth.hashers import check_password, make_password
stored = make_password(raw_password)
valid = check_password(raw_password, stored)
Mantén las contraseñas sin procesar fuera de los registros, correos electrónicos, analíticas y mensajes de excepción.
Inspecciona las vistas protegidas y los patrones de URL para comprobar la presencia de @login_required o un control de acceso equivalente. La autenticación demuestra la identidad. La autorización a nivel de objeto debe confirmar que el usuario puede acceder al problema o archivo adjunto solicitado.
Los validadores de contraseñas no ralentizan miles de intentos de inicio de sesión. Añade un control mantenido, como django-ratelimit o django-axes, al inicio de sesión, al restablecimiento de contraseña y a otros flujos sensibles. Elige uno y verifica su comportamiento real en lugar de suponer que la respuesta predeterminada del paquete es adecuada.
La hoja de trucos de seguridad de Django de OWASP recomienda cambiar la URL de administración predeterminada para reducir las sondas automatizadas. Cambiar /admin/ reduce las sondas automatizadas, mientras que la autenticación sigue dependiendo de controles de acceso reales. Trátalo como el último tramo de la defensa.
MFA es mi recomendación para las cuentas sensibles y de administrador. DjangoZen también presenta MFA como una medida de refuerzo de la autenticación, pero el núcleo de Django no proporciona una política MFA completa. Fuérzala mediante un paquete de aplicación o un proveedor de identidad y, después, prueba el registro, la recuperación, la invalidación de sesiones y la aplicación para administradores.
Después del umbral configurado, el flujo de inicio de sesión debe dejar de aceptar intentos ilimitados. Registra el estado, la respuesta y el comportamiento de restablecimiento del control elegido. Intenta también acceder a una ruta administrativa protegida como usuario normal y confirma el rechazo de autorización.
Deja que los valores predeterminados de Django gestionen la inyección SQL hasta que los evadas
Un queryset normal mantiene la entrada del usuario como valor de búsqueda:
def find_issues(term):
return Issue.objects.filter(title__icontains=term)
La API de queryset de Django parametriza los valores de búsqueda. El SQL sin procesar sigue siendo posible, pero pasa los valores mediante el argumento de parámetros del controlador.
Este es un ejemplo de PostgreSQL porque ILIKE es sintaxis de PostgreSQL:
from django.db import connection
def find_issues(term):
with connection.cursor() as cursor:
cursor.execute(
"""
SELECT id, title
FROM app_issue
WHERE title ILIKE %s
""",
[f"%{term}%"],
)
return cursor.fetchall()
Los parámetros protegen los valores. No hacen seguro un nombre de tabla o columna proporcionado por el usuario. Las cláusulas de ordenación y las palabras clave SQL necesitan el mismo tratamiento. Toma esas opciones de listas permitidas fijas:
ORDERING = {
"newest": "created_at DESC",
"oldest": "created_at ASC",
}
order_sql = ORDERING.get(request.GET.get("order"), "created_at DESC")
Revisa raw(), RawSQL, extra(), los managers personalizados, las consultas de informes, cursor.execute() y las funciones de base de datos:
git grep -nE "raw\(|RawSQL|extra\(|cursor\(|execute\("
Para cada resultado, confirma que los valores utilizan parámetros y que los identificadores proceden de código controlado. La autorización es una revisión independiente.
Conserva el escape de plantillas en lugar de corregir la salida manualmente
Las plantillas de Django escapan por defecto los caracteres HTML peligrosos, pero la salida específica de cada contexto aún necesita revisión. Mantén los valores no confiables fuera de JavaScript y CSS. Gestiona los atributos HTML y otros contextos con el escape adecuado.
Revisa cada vía de escape deliberada:
|safemark_safe()is_safe{% autoescape off %}
Pueden ser correctas para marcado confiable y construido deliberadamente. Son peligrosas para títulos de problemas, comentarios, nombres de usuario, metadatos subidos y otros valores influenciados por el usuario.
Usa json_script al pasar datos estructurados a JavaScript:
{{ issue_data|json_script:"issue-data" }}
<script>
const issue = JSON.parse(
document.getElementById("issue-data").textContent
);
</script>
Prueba un comentario de problema que contenga corchetes angulares, comillas y saltos de línea. Debería representarse como texto, y la ruta de datos de JavaScript debería analizarse sin crear marcado ejecutable.
CSP merece su lugar después de que funcionen los controles aburridos. Publicar una política complicada antes de comprender tus scripts crea una falsa sensación de seguridad. Django no incluye CSP; añade un paquete mantenido como django-csp o configura tú mismo el encabezado de respuesta.
Mi despliegue recomendado es:
- Envía
Content-Security-Policy-Report-Onlyy filtra los informes legítimos. - Corrige las infracciones restantes.
- Aplica
Content-Security-Policy.
Verifica la respuesta desplegada y el comportamiento del navegador:
curl -sS -D - -o /dev/null https://app.example.com/
Después confirma que un script deliberadamente no permitido se bloquea una vez que comience la aplicación de la política.
Añade los encabezados que Django puede proporcionar y conoce sus límites
Riesgo: los encabezados de respuesta reducen el clickjacking, la confusión de tipos MIME, la filtración de referers y los riesgos de ventanas entre orígenes. Son defensa en profundidad, no un sustituto de una salida segura o HTTPS.
La documentación de seguridad de Django y la guía de Django de OWASP cubren el middleware del framework y los controles de encabezados. Registra la respuesta real de cada capa que sirve contenido.
| Encabezado | Configuración | Amenaza reducida | Verificación |
|---|---|---|---|
X-Content-Type-Options: nosniff | SecurityMiddleware; SECURE_CONTENT_TYPE_NOSNIFF = True | Confusión del tipo MIME | curl -I |
X-Frame-Options | XFrameOptionsMiddleware; X_FRAME_OPTIONS = "DENY" o "SAMEORIGIN" | Clickjacking | Prueba de encabezado y frame |
Referrer-Policy | Establece este encabezado de respuesta en Django o nginx | Filtración de información del referer | Inspeccionar la respuesta desplegada |
Cross-Origin-Opener-Policy | Establece este encabezado de respuesta en Django o nginx | Aislamiento de ventanas entre orígenes | Inspeccionar la respuesta desplegada |
Content-Security-Policy | Paquete independiente o encabezado de respuesta deliberado | Limita parte del impacto de XSS | Consola del navegador e informes de política |
Una base podría verse así:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
# ...
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
Elige SAMEORIGIN si la aplicación inserta deliberadamente páginas en frames del mismo origen. Los archivos estáticos servidos directamente por nginx nunca pasan por el middleware de Django, así que inspecciona esas respuestas por separado:
curl -sS -D - -o /dev/null https://app.example.com/
curl -sS -D - -o /dev/null https://app.example.com/static/app.css
curl -sS -D - -o /dev/null https://app.example.com/missing-page
Los media subidos no son confiables y no deben ejecutarse
Riesgo: una carga de archivos puede convertirse en una ruta de ejecución en el servidor o exponer datos privados de problemas si el servidor web y el límite de autorización están mal configurados.
Mantén separados el código de la aplicación, los archivos estáticos recopilados y los media de los usuarios:
STATIC_ROOT = "/srv/app/static/"
MEDIA_ROOT = "/srv/app/media/"
STATIC_URL = "/static/"
MEDIA_URL = "/media/"
En el gestor de incidencias, los archivos adjuntos contienen datos de problemas potencialmente privados. Para los archivos adjuntos privados, autoriza cada descarga antes de servirla. No puedo saber por esta lista de comprobación si la autorización de tus archivos adjuntos es correcta; pruébala con dos usuarios reales y un archivo privado.
nginx debería servir /static/ y /media/ como archivos, mientras Gunicorn sirve las rutas de la aplicación. La ruta de media no debe solaparse con el árbol de código Python ni con ninguna raíz de documentos capaz de ejecutar scripts en la configuración de servidor web elegida. Revisa los handlers del servidor:
sudo nginx -T
Inspecciona la ubicación /media/ y verifica que no tenga ningún handler de intérprete FastCGI, CGI, uWSGI, proxy o equivalente. Después sube un archivo de prueba de texto plano en staging y solicítalo mediante la ruta de media desplegada. La respuesta debería ser una respuesta de archivo ordinaria, nunca una ruta de ejecución de la aplicación.
Si los archivos adjuntos privados deberían descargarse en lugar de representarse, verifica el Content-Disposition elegido; la configuración de media de Django no lo impone.
Haz que el servidor web rechace los hosts desconocidos antes de que Django los vea
Riesgo: un virtual host predeterminado explícito evita que los valores Host desconocidos lleguen a la ruta de enrutamiento normal de la aplicación.
ALLOWED_HOSTS de Django sigue siendo obligatorio. nginx también debería tener un comportamiento predeterminado explícito para los virtual hosts que no coincidan. El patrón HTTP oficial es:
server {
listen 80 default_server;
server_name _;
return 444;
}
Para HTTPS, configura un servidor TLS predeterminado explícito según tu versión de nginx, la disposición de certificados y los requisitos de selección de certificados. No copies las rutas de certificados de este artículo como valores universales. La propiedad importante es que un virtual host TLS sin coincidencia tenga un comportamiento deliberado y no envíe tráfico arbitrario mediante proxy a la aplicación.
Prueba HTTP y HTTPS por separado. Para HTTPS, utiliza un SNI coincidente:
curl -i -H 'Host: unknown.example' http://SERVER_IP/
curl -i --resolve app.example.com:443:SERVER_IP \
https://app.example.com/
El host HTTP desconocido debería llegar a la ruta de rechazo de nginx. El nombre de host HTTPS válido debería llegar al virtual host TLS previsto.
Comprueba que los servicios de producción que Django supone privados lo sean
Riesgo: una aplicación orientada al navegador aún puede verse comprometida mediante una base de datos expuesta, una caché, una sesión obsoleta, una recopilación de estáticos ausente o una copia de seguridad no probada.
| Servicio | Configurar e inspeccionar | Verificación |
|---|---|---|
| PostgreSQL | Restringir el acceso a los servidores de aplicaciones; proteger la contraseña | Conectarse desde una red no autorizada y esperar un rechazo |
| Redis o caché | Restringir el acceso a los servidores de aplicaciones; revisar la autenticación | Intentar una conexión no autorizada y esperar un rechazo |
| Archivos estáticos | Establecer STATIC_ROOT; ejecutar collectstatic | Solicitar un recurso conocido e inspeccionar su capa de servicio |
| Sesiones | Identificar el backend y el comportamiento de expiración | Cerrar sesión y solicitar después un recurso protegido |
| Copias de seguridad | Definir la retención y un responsable | Restaurar en aislamiento y registrar el resultado |
Para el gestor de incidencias, Redis puede contener sesiones y datos de caché, mientras PostgreSQL almacena problemas y usuarios. Inspecciona la dirección de enlace de Gunicorn y los permisos del socket Unix. Revisa los grupos de seguridad y las reglas del firewall y, después, prueba desde una red no autorizada. “El puerto no está en la configuración de la aplicación” no es una prueba.
Si utilizas sesiones respaldadas por la base de datos, identifica el trabajo de limpieza requerido por tu despliegue y prográmalo. Para otros backends, verifica por separado su expiración y comportamiento de expulsión. Las sesiones en caché, las conexiones persistentes a la base de datos y el cargador de plantillas en caché de Django cuando DEBUG=False son comprobaciones operativas; no sustituyen las restricciones de red.
Restaura la copia de seguridad más reciente en una base de datos aislada, ejecuta una consulta conocida o una comprobación de migración y registra la duración, la marca de tiempo y el resultado de la restauración.
Configura los informes de fallos sin filtrar detalles de los fallos
Riesgo: los usuarios deberían recibir errores de producción genéricos, mientras que los responsables del mantenimiento reciben información suficiente para responder.
ADMINS = [
("Operations", "ops@example.com"),
]
MANAGERS = ADMINS
DEFAULT_FROM_EMAIL = "app@example.com"
SERVER_EMAIL = "server@example.com"
IGNORABLE_404_URLS = [
# Añade solo patrones ruidosos conocidos, como una ruta de escáner documentada.
]
ADMINS recibe notificaciones de errores del servidor. MANAGERS recibe notificaciones de errores 404. Los informes por correo electrónico no escalan bien. Usa un servicio de monitorización de errores escalable cuando el volumen o el tiempo de respuesta lo requieran.
Elimina Authorization, las cookies y las contraseñas antes de reenviar eventos. Elimina también los tokens de restablecimiento, las credenciales de la base de datos y el contenido subido. Mantén esa revisión en la configuración de monitorización en lugar de suponer que un recopilador es seguro para los cuerpos de solicitud sin procesar.
Prueba en staging o mediante un mecanismo temporal de mantenimiento autenticado que se elimine antes del lanzamiento. Provoca un error 500 conocido y solicita una URL ausente conocida. Las respuestas visibles para el usuario no deberían contener ninguna página de depuración; las alertas deberían llegar al canal previsto.
Verifica la aplicación desplegada el día del lanzamiento
La revisión final debería seguir la ruta desplegada, no volver a leer el archivo de configuración.
Ejecuta la barrera automatizada:
python manage.py check --deploy --settings=config.settings.production
Después registra las respuestas desplegadas:
curl -I http://app.example.com/
curl -sS -D - -o /dev/null https://app.example.com/
curl -i -H 'Host: unexpected.example' http://SERVER_IP/
curl -i --resolve app.example.com:443:SERVER_IP \
https://app.example.com/
Usa los registros para marcar estas comprobaciones:
[ ] El módulo de configuración de producción lo utilizan los comandos web, worker, de migración y de auditoría.
[ ] Los secretos son externos, están ausentes del código fuente y los registros, y la rotación está documentada.
[ ] DEBUG=False y los ALLOWED_HOSTS exactos están activos.
[ ] HTTP redirige a HTTPS; las cookies de sesión muestran Secure, HttpOnly y SameSite.
[ ] Los fallos CSRF y las solicitudes válidas de formularios/AJAX se comportan como se espera.
[ ] La limitación de inicios de sesión detiene los intentos ilimitados; MFA se aplica a las cuentas designadas.
[ ] El SQL sin procesar y las vías de escape de plantillas tienen responsables identificados y pruebas aprobadas.
[ ] Las respuestas HTTPS incluyen HSTS cuando está habilitado, nosniff, X-Frame-Options, Referrer-Policy y COOP según la configuración.
[ ] nginx rechaza hosts desconocidos; Gunicorn es privado; media no tiene ningún handler de intérprete.
[ ] Los archivos adjuntos privados rechazan a un usuario no autorizado.
[ ] PostgreSQL y Redis rechazan clientes de red no autorizados.
[ ] Los archivos estáticos, las alertas de errores y el filtrado 404 de alcance limitado funcionan.
[ ] Se completó y registró una restauración de la base de datos.
[ ] Las versiones desplegadas de Django y sus dependencias tienen soporte y se revisaron las vulnerabilidades conocidas.
Pospone el lanzamiento si alguna comprobación de límites carece de evidencia registrada. Asigna un responsable. Un diff de configuración en verde no es una prueba.