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.

Admin behind AccessRequest /admin/via CloudflareAccess policySSO or email codeJWT headersigned by CloudflareDjango verifiesthen normal loginAccess blocks anonymous traffic at the edge; Django checks the signed token as a second lock.
  1. In Cloudflare Zero Trust, add a self-hosted application for shop.example.com/admin.
  2. Create a policy allowing your staff (identity provider group or specific emails).
  3. In Django, verify the Cf-Access-Jwt-Assertion header on admin requests against your team's public keys and the application's AUD tag.
  4. Lock the origin so only Cloudflare can reach it: Cloudflare Tunnel, or firewall rules allowing only Cloudflare IP ranges plus authenticated origin pulls.
  5. 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, path admin
  • 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.

from edge to appCloudflare Access policySSO or one-time codeOrigin locked to Cloudflaretunnel or IP rulesDjango JWT checkCf-Access-Jwt-AssertionDjango admin login + 2FAstaff accountsEach layer covers a failure of the one above itdjango-deployment.com
Without the origin lock, attackers can skip Access by hitting the server IP.

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.

gap and fixGapFixorigin reachable by IPtunnel or allow Cloudflare IPs onlyother host names proxiedcover every host that serves adminno check in Djangoverify Cf-Access-Jwt-Assertionadmin on another pathmatch the custom admin URLAccess protects only the paths and hosts it is configured fordjango-deployment.com
Test bypasses yourself with curl against the origin IP.

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

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.

Keep going