This guide exists to fix one of the most common and most damaging mistakes on Minecraft networks: a proxy that is carefully locked down while its backend servers sit wide open to the internet. When a backend port is reachable from outside, anyone who finds the IP can connect straight to it, skip the proxy entirely, and arrive with whatever permissions their account holds. The proxy becomes decoration. This article walks through the firewall, binding, and forwarding work that closes that hole on both BungeeCord and Velocity, and it explains how to prove the hole is actually closed.
It is written for server owners and administrators who already have a proxy running, plus the staff who inherit one and are not sure what is exposed. The rules below were tested on Ubuntu 24.04 with UFW 0.36 and Velocity 3.3 in front of Paper 1.21 backends, then checked from outside the network with nmap to confirm nothing answered that should not. Every command is copy-pasteable, and every section explains what breaks if you skip it.
Understanding the Proxy Threat Model
A proxy network has three separate attack surfaces, and a single fix never covers all of them.
Direct backend access
Your backend servers listen on a port, usually 25565 on small networks. If that port is bound to 0.0.0.0 and the host has a public IP, the backend is a full Minecraft server that anyone can join. Whitelists help, but they do not stop a determined player from joining as an alted account, farming, testing exploits, or just loading the server down. Worse, any plugin that trusts “you came through the proxy” now trusts a stranger.
Proxy bypass and spoofed forwarding
Bypass is the act of reaching the backend directly. Spoofing is the next step up: a client connects to the backend and imitates the proxy’s handshake, claiming an arbitrary username and UUID. Legacy BungeeCord forwarding (the ip_forward style handshake) is trivially spoofable if the backend accepts connections from anywhere, which is exactly why the modern approach moves the trust anchor to a shared secret plus a firewall rule that only the proxy can pass.
If you are still deciding which proxy to run, the differences matter for how much of this work you do by hand. Our comparison of Velocity vs BungeeCord covers the tradeoffs; the firewall rules here apply to both.
Binding Backends to Localhost or a Private Interface
Firewalls are the second layer. The first layer is making sure the backend is not listening on a public interface at all. In server.properties, set server-ip=127.0.0.1 when the proxy runs on the same machine. When the proxy and backends live on different hosts, bind to the private address of the internal network instead, such as server-ip=10.0.0.12, and keep that network unreachable from the internet.
On Paper and its forks, you also want the backend to accept only proxy connections. Set settings.velocity.enabled=true in config/paper-global.yml with online-mode=true, or for BungeeCord set bungeecord: true in paper-global.yml and ip_forward: true in spigot.yml. Those flags tell the backend to expect forwarded player data; they do not stop a direct connection, which is why the bind and the firewall still matter.
| Port | Service | Who may reach it | Protocol |
|---|---|---|---|
| 25565 | Proxy (Velocity / BungeeCord) | Public internet | TCP |
| 25566-25570 | Backend servers | Proxy host only | TCP |
| 25565 | Query (if enabled) | Proxy only, or disabled | UDP |
| 25575 | RCON | Localhost or admin VPN only | TCP |
If your live setup does not match that table, everything below is the fix.
UFW Rules That Expose Only the Proxy Port
On the proxy host, the goal is simple: allow SSH and the proxy port, deny everything else. Always add SSH first so enabling the firewall cannot lock you out. The commands below assume a single proxy port on 25565 and backends on a private address, and they are safe to paste in order.
# Proxy host: Ubuntu 22.04/24.04 with UFW
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'ssh - add first'
sudo ufw allow 25565/tcp comment 'minecraft proxy'
sudo ufw enable
sudo ufw status verbose
# Backend host: only the proxy's private IP may connect
sudo ufw default deny incoming
sudo ufw allow 22/tcp comment 'ssh - add first'
sudo ufw allow from 10.0.0.5 to any port 25566 proto tcp comment 'proxy only'
sudo ufw deny 25566/tcp
sudo ufw enable
Note the ordering on the backend. UFW evaluates rules top to bottom, so the narrow rule from the proxy IP must come before any blanket deny. Also note what is not there: no rule allows 25565 on the backend host, because the backend is not listening there.
If players use Bedrock crossplay through Geyser, the UDP port (19132 by default) needs its own rule on the proxy host. If you run a status page or a map renderer, those belong behind the reverse proxy, not on an open Minecraft port. The Ubuntu UFW documentation covers the full syntax, and netfilter.org explains the packet filtering layer underneath if you ever need to debug a rule that looks correct but is not matching.
Modern Forwarding, Docker, and Pterodactyl
Firewall rules stop network access. Forwarding secrets stop identity spoofing if a rule is ever wrong, which is why you run both. Velocity’s modern forwarding signs player data with a secret shared between proxy and backend, so a client that reaches a backend directly still cannot convincingly claim to be someone else.
[forwarding]
# velocity.toml
mode = "modern"
secret-file = "/etc/velocity/forwarding.secret"
Generate the secret once with a long random value, copy the same file to every backend, and set secret in paper-global.yml to match. Never commit it to a public repository, and rotate it if a staff member with file access leaves. The Velocity documentation explains the forwarding modes, and the BungeeCord wiki covers the equivalent ip_forward and online-mode pairing for older setups.
Docker and Pterodactyl caveats
Docker publishes ports by writing its own NAT rules ahead of UFW’s chains, which means -p 25565:25565 exposes the port even when UFW says it is denied. Bind published ports to localhost instead, for example -p 127.0.0.1:25565:25565, and let the proxy be the only thing with a public mapping. Pterodactyl adds another layer: the Wings daemon allocates ports per server and Docker handles the mapping, so a backend allocation with a public IP is publicly reachable regardless of the host firewall. Keep backend allocations on the internal interface and verify from outside rather than trusting the panel’s display.
Verifying Exposure and Surviving a Bad Rule
Never assume. Test from a machine outside your network; a local scan tells you nothing about what the internet sees.
nmap -Pn -p 25565,25566,25575 203.0.113.10from an external VPS. Only the proxy port should report open; the rest should be filtered or closed.- An external port checker such as an online TCP scanner, run against the backend IP specifically, not the proxy IP.
ss -tulpn | grep javaon each host to confirm what is actually listening and on which interface.
Then harden against join floods. A botnet hitting your proxy port with connection spam costs almost nothing to run, so add connection-rate limits at the firewall or reverse proxy layer, and use fail2ban to ban sources that repeat failed handshakes. Roughly ten new connections per second from a single IP is already abnormal for a survival server; tune the threshold against your own peak player count.
Finally, plan your recovery before you need it. Keep an SSH session open while enabling rules, use ufw --dry-run mentally by reviewing ufw status numbered, and know that ufw disable over console is the fastest undo. If you rent a host, confirm the provider’s out-of-band console works before you touch iptables. If you are also fighting volumetric attacks, the layered approach in our guide to Minecraft server DDoS protection covers what the firewall cannot do alone.
Pros and Cons of a Strict Proxy Firewall
Locking backend ports is the highest-value security change most proxy networks can make. It also costs convenience.
- Pro: Direct-connect bypass stops working, so permissions and UUIDs stay trustworthy.
- Pro: Backends are no longer individually attackable, which shrinks your DDoS surface to one host.
- Pro: Plugin exploits that assume proxy-only traffic lose their easiest entry point.
- Con: Every backend move or new allocation needs a matching firewall change, so documentation is mandatory.
- Con: Docker and panel-based hosts can silently override your rules, adding debugging time.
- Con: A mistake can lock out your own staff, not just attackers.
Common Mistakes and Expert Tips
- Firewalling the proxy but not the backends. The proxy was never the weak point. Check the backend hosts first.
- Trusting the bind alone. A container or panel can rewrite the bind. Verify with
ss, not with the config file. - Using legacy forwarding because it “just works”. It works for attackers too. Move to modern forwarding and rotate the secret.
- Enabling UFW before allowing SSH. The classic self-lockout. Add port 22 first, every time.
- Forgetting UDP. UFW handles TCP and UDP separately; a denied TCP port says nothing about its UDP sibling.
- Skipping the external test. Internal scans pass while the public interface leaks, which is the worst possible false confidence.
The habit that separates stable networks from fragile ones is a written inventory: which host holds which port, who may reach it, and when it was last verified. Re-run the external scan after every host migration. If your staff permissions are also part of the blast radius, the audit trail advice in our LuckPerms guide pairs naturally with a tight firewall.
Frequently Asked Questions
Strictly no, because nothing outside the machine can reach a loopback bind. Keep the default-deny policy anyway as a safety net for the day a config change or container mapping exposes the port again.
Only if a backend port is reachable from the internet. Close that port and use modern forwarding with a shared secret, and a direct connection has nowhere to go.
25565 TCP is the default and the only port most networks need public. Put backends on 25566 and above, bound to localhost or a private interface.
Docker inserts its own NAT rules ahead of UFW, so published ports bypass it. Bind published ports to 127.0.0.1 or use a Docker-aware firewall helper.
Close the Hole This Week
A proxy firewall is not exotic. It is four ports, one bind, and one secret, and it takes about twenty minutes per host once you know what to check. Start with the backend hosts, because that is where the real exposure lives, then scan from outside and confirm only the proxy answers. Keep SSH allowed, keep the console handy, and write down what you changed so the next administrator inherits a map instead of a mystery.
When the ports are closed and forwarding is live, spend the next hour on permissions and monitoring.

Leave a Reply