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.
- Install Tailscale on the Django server and join it to your tailnet (
tailscale up), ideally with a tag such astag:prod-web. - 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. - In the public server block, return 404 for
/admin/. - Add the admin host to
ALLOWED_HOSTSandCSRF_TRUSTED_ORIGINS. - Restrict which users reach the server with Tailscale ACLs or grants.
- 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.
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.
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
- Review all admin protections with the Django Admin Security Checklist.
- Add a second factor with Add Two-Factor Authentication to the Django Admin.
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.