Setting up a cracked Minecraft server, securely

I wanted to set up a minecraft server for some of my friends that couldn't afford minecraft. Online communities such as r/admincraft tend to be hostile towards doing this kind of thing, and guides freely available tend to be insufficient.

Part 1: Gathering Requirements

The easiest way to allow un-authenticated players is to set the server configuration option online to false. However, this just allows any user to connect with any username, and there is no additional verification. This behavior could lead to very easy griefing by impersonating any other user.

I needed a solution that provided the following:

Part 2: Implementing requirements

Part 2a: Setting up a Reverse Proxy

The first thing I decided to tackle was preventing players from joining the server unless they had the correct subdomain. Security by obscurity isn't sufficient, but reducing attack surface is a good idea. Plus, I kind of wanted to troll people a little bit.

The main options that I found were infrared and mc-router. I did some performance testing and ultimately settled on infrared. After it was set up, the architecture looked like this:

graph LR

player --*.my.site--> infrared
infrared --*.my.site--> potato[24w14potato server]
infrared --subdomain.my.site--> real[real server]

After this was done, I set up a CNAME record of *.my.site pointing to the server running infrared.

The problem I encountered was that there was no ability to "exclude" subdomains when matching. So, someone connecting on subdomain.my.site could end up on the 24w14potato server, and this behavior was non-deterministic. It was simple enough to fork infrared and make the change allowing exclusions in the configuration file. After making the change and adding some logging, it worked entirely!

Part 2b: Setting up Velocity

Next, I needed to create a "lobby" world where players would be sent for authentication, before being sent to the real game world. After doing research, I came to the conclusion that NanoLimbo would best suit my use case. It can also be run in a container by configuring docker-minecraft-server properly. I also needed to figure out a way to enable players to switch between the lobby world and the main world with commands. I settled on using velocity as there is also a docker image available for it.

After adding Velocity and NanoLimbo, the overall setup looked like this:

graph LR

player --*.my.site--> infrared
infrared --*.my.site--> potato[24w14potato server]
infrared --subdomain.my.site--> velocity[velocity]
velocity --> lobby["lobby (NanoLimbo)"]
velocity --> main

Part 2c: Implementing User Accounts

Next up was configuring the setup to allow people to "claim" cracked usernames. I did some digging and found the plugin LibreLogin, which did exactly that. It implemented passwords with accounts and even comes with the ability to store them in a Postgres database. This feature was especially enticing, as I already maintain a postgres deployment.

I was able to configure LibreLogin with no problems, and it worked exactly as expected!

Part 2d: Discord integration

The final task was to restrict players to one account, as best I could. I didn't aim to stamp out multilogging entirely, as I figured that would be too difficult, and I didn't want to apply blanket IP bans except in the absolute worst cases. A proxy measurement that I settled for was 1 discord account = 1 minecraft account. Especially with Discord's new feature of banning all of a person's known alts when they are banned from a discord server, I had high confidence that a discord account being in the server was a reasonable restriction.

I took a look at DiscordSRV but unfortunately, it didn't do what I needed it to do. Ultimately, I decided to roll my own authentication bridge between discord and minecraft. After about a day of work, I had a minecraft plugin and associated discord bot that interacted in the following way:

sequenceDiagram

    participant user as user
    participant mcServer as lobby server
    participant discordBot as discord bot
    participant postgres as link database

    user ->> mcServer : /link command
    mcServer ->> user: verification code
    user ->> discordBot : /link command + verification code
    discordBot ->> mcServer : discord user id + verification code
    alt validity check passed
        mcServer ->> discordBot: success message
        mcServer ->> postgres: discord id <-> minecraft uuid link
        mcServer ->> user: success message
    else validity check failed
        mcServer ->> discordBot: failure message
        mcServer ->> user: failure message
    end

And the validity check logic looks like this:

graph LR

discord-check{is discord account <br> already linked?}
code-check{is the code associated <br> with this minecraft account?}
minecraft-check{is minecraft account <br> already linked?}
fail-message[send failure message]

discord-check -- no --> code-check
code-check --yes--> minecraft-check 
code-check -- no --> fail-message


minecraft-check -- no --> associate[associate discord and minecraft accounts]

discord-check --yes--> fail-message
minecraft-check --yes--> fail-message

Part 2 Summary

Recapping, my goals were:

I changed the main world to whitelist-only, and players would get stuck in the lobby until they were whitelisted. This was good, but there were still a few issues:

So, I didn't consider this completely done, I still needed to refine the user experience.

Part 3: Implementing Permissions

I did a lot of digging to find solutions to the problems outlined in the previous part. I found that restrictions on running the /server command to switch worlds could be controlled via the ServerPermissions plugin, and that permission groups could be set up via LuckPerms.

I then created the following permissions groups:

group name permissions
default serverpermissions.server.lobby, librepremium.user.login
authenticated discordlinker.link
linked serverpermissions.server.main, discordlinker.unlink

Because of the very linear flow of default -> authenticated -> linked, there were no gotchas with LuckPerms and the permission inheritance from the previous roles behaved exactly as expected. Additionally, by restricting the /link command to only authenticated players, it also removed the attack vector of trying to link a cracked account that hasn't yet been logged in.

After some testing, my confidence in the LuckPerms integration was high enough that I considered all of my requirements met!

Part 4: Summary

This whole project took about a month of research and testing. I was very satisfied with the end result! The scope creep was much higher than anticipated; I wasn't expecting to have to write my own plugin. All in all, it took about two months for me to comprehend the roles of the components and set up the server.