Put the Django Admin on a Private Tailscale Network

Remove the Django admin from the public internet by serving it only over Tailscale: install Tailscale on the server, route /admin through a tailnet-only Nginx server block or a separate admin hostname, and keep staff access simple.

Problem statement

The Django admin at /admin/ is a well-known URL that bots probe constantly for weak passwords, and any admin vulnerability is reachable by the whole internet. Renaming the URL only slows scanners. A stronger approach is to make the admin reachable only from a private network your staff are on. Tailscale builds that network with WireGuard and identity-based access: staff install the client, sign in with your identity provider, and reach the admin at a private address, while the public site stays exactly as it is.

Admin only over the tailnetStaff deviceTailscale clientTailnetWireGuard, SSOAdmin vhostlistens on tailnet IPPublic siteno /admin routeThe public server block has no admin route; only devices on the tailnet can reach it.
  1. Install Tailscale on the Django server and join it to your tailnet (tailscale up), ideally with a tag such as tag:prod-web.
  2. Add an Nginx server block that listens only on the server's Tailscale IP (or uses MagicDNS name admin-prod) and proxies everything to Django.
  3. In the public server block, return 404 for /admin/.
  4. Add the admin host to ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS.
  5. Restrict which users reach the server with Tailscale ACLs or grants.
  6. Keep strong passwords and two-factor authentication anyway.

Step-by-step solution

1. Join the server to the tailnet

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --advertise-tags=tag:prod-web --ssh=false
tailscale ip -4      # e.g. 100.101.102.103

Use an auth key from the admin console for unattended setup, and enable MagicDNS so the server gets a name such as prod-web.tailnet-name.ts.net.

2. Serve the admin only on the tailnet address

# Private admin: listens only on the Tailscale interface
server {
    listen 100.101.102.103:443 ssl;
    server_name prod-web.tailnet-name.ts.net;
    ssl_certificate     /etc/ssl/tailscale/prod-web.crt;
    ssl_certificate_key /etc/ssl/tailscale/prod-web.key;

    location / {
        proxy_pass http://unix:/run/gunicorn/gunicorn.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
    }
}

tailscale cert prod-web.tailnet-name.ts.net issues a valid Let's Encrypt certificate for the MagicDNS name, so HTTPS works without warnings. Because Nginx binds only the 100.x address, nothing on the public interface answers for this block.

3. Remove the admin from the public site

server {
    listen 443 ssl;
    server_name example.com;
    location /admin/ { return 404; }
    location / { proxy_pass http://unix:/run/gunicorn/gunicorn.sock; include proxy_params; }
}

Returning 404 hides the admin entirely from public visitors and scanners.

who can reach /admin/?Public /admin/✕scanned and brute-forced daily✕any admin bug is internet-facing✕relies on passwords aloneTailnet-only admin✓unreachable without the tailnet✓access tied to SSO identity✓offboarding removes accessNetwork-level access control removes the admin from the attack surfacedjango-deployment.com
Keep 2FA on the admin as defence in depth.

4. Django settings

ALLOWED_HOSTS = ["example.com", "prod-web.tailnet-name.ts.net"]
CSRF_TRUSTED_ORIGINS = ["https://example.com", "https://prod-web.tailnet-name.ts.net"]

Optionally enforce the restriction in Django too, so a misconfigured proxy cannot expose it:

# middleware.py
from django.http import Http404

class AdminHostOnlyMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    def __call__(self, request):
        if request.path.startswith("/admin/") and request.get_host() != "prod-web.tailnet-name.ts.net":
            raise Http404
        return self.get_response(request)

5. Restrict who can reach it

In the Tailscale policy file, allow only the staff group to reach the server on port 443:

{
  "tagOwners": { "tag:prod-web": ["group:ops"] },
  "acls": [
    { "action": "accept", "src": ["group:staff"], "dst": ["tag:prod-web:443"] }
  ]
}

Group membership comes from your identity provider, so offboarding someone in Google Workspace or Okta removes their access.

private admin setupServer tagged and on the tailnetauth key, MagicDNS nameAdmin vhost bound to the 100.x IPtailscale cert for HTTPSPublic vhost returns 404 for /admin/hidden from scannersACL limits port 443 to staffgroups from your IdPAdmin host check in Django toodefence in depthTwo independent checks keep the admin private if one is misconfigureddjango-deployment.com
Test from a device off the tailnet to confirm the admin is unreachable.

Verification check: from a phone off the tailnet, https://example.com/admin/ returns 404; from a staff laptop on the tailnet, https://prod-web.tailnet-name.ts.net/admin/ shows the login page with a valid certificate; a user outside group:staff cannot connect.

Explanation

Tailscale creates a mesh of WireGuard tunnels between devices, with keys distributed by a coordination server that authenticates users through your identity provider. Each device gets a stable private 100.x address, and ACLs decide which identities can reach which devices and ports. Binding the admin's server block to that address makes it a private service: there is no public route to it at all, which is stronger than IP allow-lists because access follows identity rather than network location.

Edge cases / notes

  • Several app servers each join the tailnet; put a small internal load balancer or use Tailscale Serve on one admin node.
  • Containers. Run Tailscale as a sidecar or on the host and proxy into the container network.
  • Headscale is a self-hosted coordination server if you cannot use the hosted service.
  • Emergency access. Keep a documented way in (SSH over Tailscale, or a provider console) if Tailscale is unavailable.

Next steps

FAQ

How do I make the Django admin private?

Serve it only on a private network such as a Tailscale tailnet, and return 404 for /admin/ on the public site.

Do staff need a VPN client?

Yes, the Tailscale client, signed in with your company identity provider.

Is HTTPS needed inside the tailnet?

Traffic is already encrypted by WireGuard, but tailscale cert makes HTTPS easy and keeps secure cookies working.

Is changing the admin URL enough instead?

It reduces noise but does not protect against targeted attacks. Network restriction is much stronger.

Keep going