Whitelist and Ban-List Bootstrap for New Game Servers: UUID Resolution, Atomic Apply, Verification

A whitelisted server opened to 500 people through a Discord signup will have roughly 12% of its accepted members fail their first connection, and most of those failures share three causes: a UUID mismatch from a cracked or migrated account, a Mojang name change nobody recorded, and a name that resolved to a totally different account because the admin typed it by hand. All three are avoidable with a bootstrap step that resolves names to UUIDs once, stores them in version control, and pushes them with a single command.

This is the operational procedure we run before opening a private or event server: build the list, validate it, apply it atomically, and keep the ban list as reviewed data rather than a chat log. Numbers here come from running this on Paper and Fabric servers provisioned from our dedicated platform.

Resolve every name to a UUID before you open the door

Every whitelist entry should be a UUID, never a name. Names change, names get reused, and on offline-mode servers two players can claim the same name on different days. The Mojang lookup endpoint returns a canonical name for a UUID and the current name for a lookup by name, so the safe direction is: collect names, resolve once, store UUIDs, verify names against UUIDs at apply time.

#!/usr/bin/env bash
# resolve.sh -- names on stdin, JSON on stdout
set -euo pipefail
while read -r name; do
  [ -z "$name" ] && continue
  resp=$(curl -s --max-time 10 \
    "https://api.mojang.com/users/profiles/minecraft/${name}") || {
      echo "{\"name\":\"${name}\",\"error\":\"lookup-failed\"}" >&2; continue; }
  [ -z "$resp" ] && { echo "{\"name\":\"${name}\",\"error\":\"not-found\"}" >&2; continue; }
  echo "$resp" | jq -c \
    --arg src "$name" '{name:.name, id:.id, requested:$src}'
done

Rate-limit the loop to one request per 200 ms; the endpoint tolerates bursts but answers 429 if you push 500 names at once. For lists above 200, batch ten names per request against the session server profiles endpoint instead. Record the raw response alongside the parsed result, because a name that resolves differently three months later is the single most common source of “whitelist broke” tickets.

Store the list as reviewed data, not as a live file

A whitelist that exists only on the running server is unrecoverable after a disk failure, and a whitelist edited directly in the game is unauditable. Keep a canonical CSV in git with five columns, and generate the server’s whitelist.json from it. Then every change has an author, a timestamp and a diff.

ColumnExampleSourceWhy it exists
uuid069a79f444e94726a5befca90e38aaf5Mojang lookupThe only stable identifier
nameNotchLookup at apply timeFor humans; never used for matching
added2026-10-01ManualAge out stale entries after a season
tiermember | staff | guestManualMaps to permission groups, not to trust
noteevent-2026-q4ManualLets you revoke a whole cohort in one expression

Generating whitelist.json from CSV takes a handful of lines and removes the class of error where a trailing comma or a BOM makes Paper silently load zero entries. Validate the output with jq before pushing; an invalid whitelist.json is ignored rather than rejected, which is how servers end up wide open after a “successful” reload.

import csv, json, sys
rows = list(csv.DictReader(open(sys.argv[1], encoding='utf-8')))
out = {"whitelist": [{"uuid": r["uuid"].strip(), "name": r["name"].strip()}
                     for r in rows if r["tier"] in {"member", "staff"}]}
assert all(len(e["uuid"]) == 32 for e in out["whitelist"]), "malformed uuid"
json.dump(out, open(sys.argv[2], "w"), indent=2)
print(f"wrote {len(out['whitelist'])} entries")

Apply atomically and verify the count from inside the server

Never edit whitelist.json while the server is running: Paper holds the parsed list in memory and writes it back on the next whitelist command, silently discarding your edit. Write to a temp file, move it into place, then reload and read the count back from the console rather than trusting the file.

# on the game host, with the console reachable via RCON or stdin
cp whitelist.json /srv/minecraft/whitelist.json.tmp
jq empty /srv/minecraft/whitelist.json.tmp   # fail fast on bad JSON
mv /srv/minecraft/whitelist.json.tmp /srv/minecraft/whitelist.json
chown minecraft:minecraft /srv/minecraft/whitelist.json

# reload and confirm the server's own view
rcon-cli whitelist reload
rcon-cli whitelist list | tail -1
# expect: There are N whitelisted players: ...

The count check catches the two silent failures: a file the server cannot read (permissions) and a JSON document with the wrong top-level key. Both result in an empty list and an open server. Alert if the applied count differs from the CSV count by more than zero — a mismatch means the generation step was skipped.

Choose the enforcement mode deliberately

Whitelisting is a switch with three meaningful states, and picking the wrong one is an outage rather than a security hole. Set it explicitly in server.properties and in the startup check, and treat a change of mode as a change requiring a restart.

Modeserver.propertiesBehaviourCorrect use
Openwhite-list=false, enforce-whitelist=falseAnyone connectsPublic hub, event overflow servers
Softwhite-list=true, enforce-whitelist=falseWhitelist used for ops mapping onlyAlmost never correct; avoid
Enforcedwhite-list=true, enforce-whitelist=trueConnection refused unless listedPrivate survival, staff, tournament
enforce-whitelist during startenforce-whitelist=trueRemoves non-whitelisted players on bootAfter a season reset, deliberately

The soft mode is the dangerous one because it looks like protection. enforce-whitelist=false with white-list=true does not block connections on modern Paper; it only affects the ops/permission defaults. Confirm the effective policy by attempting a connection from an unlisted test account after every policy change — the check takes 20 seconds and is the only evidence that matters.

Ban lists: separate the three kinds of ban

Most “we got raided” incidents trace back to a single conflated ban list. There are three distinct mechanisms and they should never be merged: the game’s native ban (UUID-based, persists in banned-players.json), IP bans (weak, punish shared connections), and account-level moderator bans enforced by a proxy or plugin. Keep them in separate files with separate review processes.

  • banned-players.json: canonical, UUID-keyed, with a reason, a source and an expiry. Never hand-edit while running.
  • IP bans: only for a confirmed single-actor attack from a static address. A /24 ban on a residential ISP takes out whole neighbourhoods.
  • Proxy-level bans (Velocity, BungeeCord): the correct place for network-wide bans, since they apply before the player reaches a backend server.
  • Allow-list overrides: a player with a staff tier should be removable from one file, not four.

Set real expiry dates. A permanent ban list that has grown for two years is a maintenance problem and a false-positive generator; a quarterly review that expires anything older than 12 months with no repeat offence keeps the list under 200 entries on a typical community server. Encode the review date in the reason field so the audit is mechanical.

For litigation-grade evidence, the ban record should reference a log line rather than a chat quote. Pair the ban action with the player’s join time, IP at connect, and the server log offset — that triad is enough to reconstruct the incident months later without storing chat at volume.

Pre-open checklist

StepCheckFail consequence
1All names resolved to 32-hex UUIDs, no lookup-failed rowsSilent partial whitelist
2CSV committed, JSON generated, jq validation passesServer loads empty list
3Applied count equals CSV member+staff countOpen server, undetected
4enforce-whitelist=true confirmed in server.propertiesAnyone connects
5Unlisted test account refused within 30 s of reloadPolicy assumed, not verified
6Ban list has expiry on every entry added this cycleIndefinite bans accumulate
7Backup of both JSON files taken to off-server storageUnrecoverable after disk loss

Run this checklist as a script, exit non-zero on any failure, and wire it into the deployment job so a server cannot be marked ready for players while check 5 fails. The whole sequence runs in under two minutes on a warm DNS cache.

Where this fits in a larger setup

The same pattern — generate config from reviewed data, validate, apply atomically, verify from inside the service — applies to ops lists, permission groups and mod lists. On a multi-server network, put the CSV in one repository and have each server’s provisioning pull and generate at boot, so a rebuilt node comes up with the correct allow-list before it accepts a connection.

If you are opening a whitelisted server for an event and need a machine that can be rebuilt from scratch in minutes, the configurations on our main site include IPMI access and NVMe RAID1, which is what makes a clean rebuild at 02:00 practical rather than theoretical.

Leave a Reply