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:
- a seamless experience for players using authenticated accounts
- a method for players on cracked clients to preserve ownership over an account
- a method to prevent random people from joining the server via IP
- a method of associating players accounts with minecraft accounts, to prevent multilogging
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:
- [ ] a seamless experience for players using authenticated accounts
- [x] a method for players on cracked clients to preserve ownership over an account
- [x] a method to prevent random people from joining the server via IP
- [x] a method of associating players accounts with minecraft accounts, to prevent multilogging
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:
- Players could attempt to switch to worlds they were not whitelisted to. I didn't want to expose knowledge of worlds that they weren't privileged to access.
- Adding to / removing from the whitelist was a manual, high-friction process. I didn't feel that it would be good for the long-term health of the server to require a manual intervention when a new player wanted to join.
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.