Protect the Django Admin With Cloudflare Access
Put the Django admin behind Cloudflare Access (Zero Trust): create a self-hosted application for /admin, require SSO or email one-time codes, validate the Cf-Access-Jwt-Assertion header in Django, and block direct origin access so nobody can bypass it.
Problem statement
The Django admin is a high-value target: password spraying, credential stuffing, and vulnerability scans hit /admin/ on every public Django site. If the site already sits behind Cloudflare, Cloudflare Access (part of Cloudflare Zero Trust) can require staff to authenticate with your identity provider or a one-time code before any request to /admin/ reaches Django. It is only effective if attackers cannot reach the origin directly, and Django should verify that requests really passed through Access.
- In Cloudflare Zero Trust, add a self-hosted application for
shop.example.com/admin. - Create a policy allowing your staff (identity provider group or specific emails).
- In Django, verify the
Cf-Access-Jwt-Assertionheader on admin requests against your team's public keys and the application's AUD tag. - Lock the origin so only Cloudflare can reach it: Cloudflare Tunnel, or firewall rules allowing only Cloudflare IP ranges plus authenticated origin pulls.
- Keep Django's own admin login and 2FA; Access is an outer layer.
Step-by-step solution
1. Create the Access application
In the Zero Trust dashboard, go to Access, Applications, add a Self-hosted application:
- Domain:
shop.example.com, pathadmin - Session duration: for example 8 hours
- Identity providers: Google Workspace, Microsoft Entra ID, GitHub, or one-time PIN by email
Note the Application Audience (AUD) tag; Django needs it.
2. Define the policy
Add an Allow policy with rules such as "Emails ending in @example.com" or "Members of the staff group" from your identity provider. Optionally require device posture or country. Everyone else gets Cloudflare's login page and never reaches Django.
3. Verify the Access token in Django
Cloudflare adds a signed JWT to requests that pass Access. Verify it in middleware for admin paths:
import jwt
from jwt import PyJWKClient
from django.conf import settings
from django.http import HttpResponseForbidden
_jwks = PyJWKClient(f"https://{settings.CF_ACCESS_TEAM}.cloudflareaccess.com/cdn-cgi/access/certs")
class CloudflareAccessMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.path.startswith("/admin/"):
token = request.headers.get("Cf-Access-Jwt-Assertion")
if not token:
return HttpResponseForbidden("Access token missing")
try:
key = _jwks.get_signing_key_from_jwt(token).key
claims = jwt.decode(token, key, algorithms=["RS256"], audience=settings.CF_ACCESS_AUD)
except jwt.PyJWTError:
return HttpResponseForbidden("Invalid access token")
request.cf_access_email = claims.get("email")
return self.get_response(request)
Install PyJWT[crypto]. The JWKS client caches keys. You can also compare cf_access_email with the logged-in staff user's email for extra assurance.
4. Lock down the origin
If the server accepts traffic from anywhere, attackers can connect to its IP with a Host: shop.example.com header and skip Access. Options:
- Cloudflare Tunnel: the server makes outbound connections to Cloudflare and accepts no inbound HTTP at all.
- Firewall: allow ports 80 and 443 only from Cloudflare's published IP ranges, and enable authenticated origin pulls so Nginx requires Cloudflare's client certificate.
The JWT check in step 3 also defeats direct access, because attackers cannot forge the token.
5. Keep Django's protections
Access verifies identity at the edge, but staff still log in to Django. Keep strong passwords, 2FA (for example django-otp), and django-axes style lockouts. Access sessions also help: when someone leaves the company, removing them from the identity provider group cuts admin access immediately.
Explanation
Cloudflare Access is an identity-aware proxy at Cloudflare's edge. Requests to a protected application are redirected to a login flow; after authentication, Cloudflare sets a cookie for the session and forwards requests with a signed JWT header. Because the check happens before the request reaches your server, brute-force and scanning traffic against the admin never touches Django. Verifying the JWT in Django turns the edge check into a cryptographic guarantee rather than a network assumption.
Edge cases / notes
- Custom admin URL. If you moved the admin to another path, configure that path in the Access application and in the middleware.
- APIs and automation. Scripts accessing protected paths need Access service tokens, sent as headers.
- Plans. Cloudflare Zero Trust has a free tier for small teams; check current seat limits.
Next steps
- Review the rest of the Django Admin Security Checklist.
- Set up the proxy in Deploy Django Behind Cloudflare.
- Add another factor with Add Two-Factor Authentication to the Django Admin.
FAQ
Do I still need Django admin passwords with Cloudflare Access?
Yes. Access is an extra layer; Django accounts still control permissions inside the admin.
Can attackers bypass Access?
Only if they can reach the origin directly. Lock the origin to Cloudflare and verify the JWT in Django.
Which identity providers work?
Common ones include Google Workspace, Microsoft Entra ID, Okta, GitHub, and email one-time codes.
Does this affect the public site?
No. The Access application can cover only the admin path.