Security Advanced

Private Networking for Django Infrastructure with WireGuard: Admin Behind a Tunnel, Not a Password

Take your admin, database and internal tooling off the public internet entirely: WireGuard peers and keys, binding services to the tunnel, restricting Django admin by network, revoking access, the MTU trap, and what a VPN does not solve.

DjangoZen Team Aug 07, 2026 16 min read 2 views

The strongest way to protect an administrative interface is not a better password. It is making the interface unreachable to anyone who is not already inside your network. An attacker cannot brute force a login page they cannot connect to, and cannot exploit a vulnerability in software they cannot reach.

This guide covers using WireGuard to put the private parts of a Django deployment — the admin, the database, internal dashboards, monitoring — behind a tunnel, so that the only thing exposed publicly is the application your users actually need.

Decide what does not belong on the public internet

Start with an inventory of what is currently reachable and ask, for each item, who legitimately needs it. The answers are usually clear once stated: your customers need the application; nobody outside your team needs the admin, the database, the metrics dashboard, the log viewer, or the staging environment.

Everything in that second group is attack surface maintained purely for convenience. A tunnel lets you keep the convenience and remove the exposure, and it removes an entire class of incident — the one that begins with a scanner finding an internal tool you forgot about.

Why WireGuard specifically

Older VPN software is powerful, configurable and complicated, and complexity in security software is how misconfigurations happen. WireGuard is deliberately small: a few thousand lines, a modern cryptographic design with no negotiable algorithms, and configuration files short enough to read in full.

It lives in the kernel on Linux, so throughput is close to native. And it is connectionless — there is no session to establish or lose, which means a laptop moving between networks simply continues working rather than reconnecting.

The mental model: peers, not clients and servers

WireGuard has no notion of a client or a server. There are peers, each with a key pair, and each configured with the public keys of the peers it will talk to. What makes one peer feel like a server is simply that it has a stable address and the others know where to find it.

Every peer declares which addresses it is allowed to send and receive for. That single mechanism handles both routing and authorisation: traffic for an address not listed against a peer is dropped. This is why the configuration is so short — there is no separate access control layer.

Generate keys, and treat the private ones properly

umask 077
wg genkey | tee server.key | wg pubkey > server.pub
wg genkey | tee laptop.key | wg pubkey > laptop.pub

The umask matters: without it the key files are readable by every account on the machine. Generate each peer's key on the device that will use it, so a private key never travels — you only ever exchange public keys.

The gateway configuration

# /etc/wireguard/wg0.conf
[Interface]
Address = 10.10.0.1/24
ListenPort = 51820
PrivateKey = <server private key>

[Peer]
# laptop
PublicKey = <laptop public key>
AllowedIPs = 10.10.0.2/32
sudo systemctl enable --now wg-quick@wg0
sudo wg show

Choose a private range that will not collide with the networks your team uses — home routers and café networks commonly use the popular ranges, and an overlap produces a laptop that cannot reach either its home printer or your servers.

The peer configuration

[Interface]
Address = 10.10.0.2/32
PrivateKey = <laptop private key>

[Peer]
PublicKey = <server public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 10.10.0.0/24
PersistentKeepalive = 25

The AllowedIPs value on this side decides what is routed through the tunnel. Listing only the private range gives a split tunnel: internal traffic goes over the VPN, everything else goes directly. Listing 0.0.0.0/0 routes everything, which is what you want on untrusted WiFi and unnecessary otherwise.

PersistentKeepalive exists because home and mobile routers forget mappings for idle connections. Without it, a peer behind such a router becomes unreachable from the other side after a minute or two of silence.

Expose one port, then close the rest

The tunnel is only a benefit if you actually remove the public exposure it replaces. That is the step people skip: the VPN gets set up, everyone uses it, and the admin interface stays reachable from the internet anyway.

sudo ufw allow 51820/udp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw delete allow 5432/tcp      # database no longer public
sudo ufw status verbose

You can go further and restrict SSH to the tunnel too, so that even administrative access requires being connected. Keep a provider console session available before you do — this is a change that locks you out if the tunnel misbehaves.

Bind internal services to the tunnel interface

A firewall rule is a filter in front of a service that is still listening everywhere. Binding the service to the tunnel address is stronger: even without the firewall, it accepts nothing from outside.

# PostgreSQL
listen_addresses = '10.10.0.1'

# An internal dashboard
gunicorn dashboard.wsgi:application --bind 10.10.0.1:9000

Now the service is genuinely private. A misconfigured firewall rule is an inconvenience rather than a disclosure, which is exactly the defence-in-depth property worth having.

Restricting the Django admin by network

Your public application must stay public, but its admin does not have to be. Restrict it in the web server, which keeps the check outside your application code entirely.

location /admin/ {
    allow 10.10.0.0/24;
    deny all;
    proxy_pass http://127.0.0.1:8000;
}

Anyone not on the tunnel receives a refusal before Django is ever consulted. Combined with strong authentication for those who do get through, this removes the single most common target on a Django deployment from the public internet.

If you prefer to enforce it in the application — for instance because the admin path varies — a small middleware checking the remote address works, but be careful to read the correct header when behind a proxy, and to trust it only from your own proxy.

Private links between servers

The same mechanism connects servers to each other. An application server and a database server in different locations can talk over the tunnel rather than across the public internet, with traffic encrypted and the database exposed to no one else.

Each server is simply another peer. The application connects to the database at its tunnel address, and the database listens only there — a topology that gives you the benefit of separated machines without the exposure of a publicly reachable database.

Revoking access

There is no revocation list, and that is a feature. Access is granted by a public key appearing in the configuration; remove the peer block and the key means nothing.

sudo wg set wg0 peer <public key> remove
sudo sed -i '/<public key>/,+2d' /etc/wireguard/wg0.conf

Do both: the first takes effect immediately, the second survives a reboot. Removing only the running configuration and forgetting the file is how a departed contractor's laptop regains access after the next restart.

The MTU trap

The classic WireGuard symptom: the tunnel connects, small requests work, and anything larger hangs. Encapsulation adds overhead, so packets sized for the underlying network no longer fit once wrapped. Small packets pass; large ones are dropped silently.

# In [Interface]
MTU = 1420

If pages load but file uploads stall, or SSH connects but a large paste freezes it, lower the MTU before suspecting anything else. It is almost always this, and it is almost never obvious.

Monitor the tunnel

sudo wg show wg0 latest-handshakes
sudo wg show wg0 transfer

A recent handshake means the peer is live; one that is hours old means it has gone away. Alert if a server-to-server tunnel has no recent handshake — a tunnel that dies quietly between two servers produces application errors that look like database problems and waste an afternoon.

What a tunnel does not solve

Be clear about the boundaries. WireGuard authenticates devices, not people: possession of the key is the credential, so a stolen laptop is an authorised peer until you remove it. It keeps no logs and has no concept of users, sessions or roles.

It also does not protect anything inside the tunnel from anything else inside it. If a peer is compromised, it reaches everything its allowed addresses permit. Keep those lists narrow, and do not treat "on the VPN" as equivalent to "trusted" — services should still authenticate their callers.

For teams needing per-user identity, audit trails and device posture checks, a managed overlay built on the same protocol adds those on top. For a small team protecting internal services, plain WireGuard with disciplined key handling is proportionate.

Onboarding a new device, repeatably

Access that is granted through an improvised exchange of files over chat tends to be revoked the same way — which is to say, not at all. Make onboarding a short, written procedure and offboarding becomes reliable too.

The person receiving access generates their own key pair and sends you only the public half. You add a peer block with the next free address in the range, apply it, and send back the configuration with the private key field left empty for them to fill in locally. At no point does a private key travel, and at no point do you hold one you would rather not be responsible for.

Keep a simple register: which address belongs to whom, which public key, when it was issued. Without it you end up with peer blocks nobody can identify, and nobody dares remove them — which quietly turns your access list into an append-only structure.

The machines you deploy from count as peers

If administrative interfaces are only reachable through the tunnel, then anything automating against them needs to be on it too. Continuous integration runners, backup jobs pulling from a database, monitoring collectors scraping internal metrics — each is a peer with its own key and its own narrow allowed addresses.

Resist the shortcut of giving automation a broad range because it is easier. A build runner that needs to reach one deployment endpoint should be permitted exactly that address. The whole benefit of this arrangement is that a compromised peer reaches only what it was explicitly allowed to reach, and that benefit disappears the moment you start granting the entire subnet for convenience.

A troubleshooting order that saves time

Tunnel problems are frustrating because everything looks correct. Work through them in a fixed order rather than changing settings hopefully.

# 1. Is the interface up at all?
sudo wg show wg0

# 2. Has a handshake ever happened?
sudo wg show wg0 latest-handshakes

# 3. Can the peer reach the endpoint port?
nc -vzu vpn.example.com 51820

# 4. Does traffic route into the tunnel?
ip route get 10.10.0.1

# 5. Small packets fine, large ones hang?  Lower the MTU.
ping -c3 -s 1400 -M do 10.10.0.1

No handshake at all points at the endpoint, the port or the firewall. A handshake followed by silence points at allowed addresses or routing. And a working tunnel where large transfers stall is the MTU, essentially every time.

Summary

Decide what genuinely needs to be public — usually only the application itself — and put everything else behind a tunnel. WireGuard's model is peers with key pairs and explicit allowed addresses, which is both its routing and its access control. Generate private keys on the device that will use them. Expose one UDP port and then actually remove the public exposure you replaced, including the database port. Bind internal services to the tunnel address so the firewall is your second line rather than your only one. Restrict the Django admin by network at the web server. Revoke by removing the peer from both the running configuration and the file. Lower the MTU when large requests hang. And remember that the tunnel authenticates devices, not people — keep authentication inside it as well.