Thumbnail image

Setting up a Python Discord bot service using systemd (and securing your VPS!)

Sun, May 12, 2024 9-minute read

A friend of mine recently created a chatbot for his Discord server, and he asked if I could host it for him. He didn’t want to leave his PC running 24/7, and he knows I enjoy tinkering with Linux servers. Join me as I set up a cozy little home for the Discord bot I adopted.

Welcome to server admin 101

If you’re planning on following along, please know I won’t be getting into the Discord side of things because there are already plenty of guides out there explaining that part. This blogpost focuses more on teaching newbies a little bit about Linux server administration, things that might be overlooked by those guides that teach how to get a bot up and running as quickly as possible. Now let’s dive in.

You need a VPS

Well, you actually don’t, but having a virtual private server (VPS) is great for projects like this one. Very reliable uptime provided by a data center with all their safeguards? Sign me up.

I haven’t set up a Linux server in a hot minute. I usually use Debian for servers, but I wanted to try something new, so I went with an AlmaLinux VPS provided by RackNerd. Not an affiliate link, and I wasn’t sponsored to say this. I just like them.

If you purchase a VPS, you should be able to find its credentials (IP address and root password) somewhere in your user dashboard.

You can then log into it by running the ssh command in your favorite terminal (Linux/macOS), or Windows PowerShell:

ssh root@12.345.678.90 (substitute the IP address of your VPS)

Once you’ve logged in, all commands you type into that terminal window will be executed on your VPS, not on your local machine.

Initial setup

I didn’t want the root (admin) user to be running the Discord bot, because running everything with elevated privileges has security implications. So the first thing I did was add a new user named bots and set the password:

useradd bots
passwd bots

The bot my friend sent me was written in Python. As of this writing, Discord’s Python API is compatible with Python versions 3.8, 3.9, and 3.10, so I went ahead and installed version 3.9, switched to my new non-root user, and installed the Discord package.

dnf install python39 python39-pip
su bots
python3.9 -m pip install -U discord.py

At this point, you can run your Discord bot and expect it to work, assuming you’ve uploaded it to your VPS:

python3.9 /path/to/your/bot.py

I highly recommend WinSCP to any Windows users looking for a nice easy way to manage files on their VPS. Authentication works very similarly to the ssh command described earlier, where you provide the name of the user you want to log in as, the IP address, and the password. It also lets you directly edit files on your VPS in your preferred code editor.

If you’re a Linux user, the default file manager in most desktop environments has similar functionality builtin, so there’s no need to install a separate program for this.

Adding a systemd service

Occasionally, your VPS might get restarted without your knowledge, such as during data center maintenance, so manually logging in and restarting the bot every time this happens is impractical, especially for a bot that manages mission-critical actions such as user registrations in a popular Discord server.

For this reason, you want the bot to auto-start on bootup, minimizing downtime. We’ll set that up now. Switch back to the root user and navigate to the directory:

su root
cd /etc/systemd/system
nano welcome-bot.service

And then paste this template in there (adjust your paths if they’re different from mine!):

[Unit]
Description=WelcomeBot Discord bot service
After=network.target

[Service]
ExecStart=/usr/bin/python3.9 bot.py # the command to run the bot
WorkingDirectory=/home/bots/welcome-bot # the directory where the bot lives
StandardOutput=inherit
StandardError=inherit
Restart=always
User=bots # the non-root user that will run the bot

[Install]
WantedBy=multi-user.target

Make sure you save the file as you exit nano. Then run these commands to enable the service and reboot the server. You will have to ssh back in after the reboot.

systemctl enable welcome-bot
reboot

At any time while the service is running, you can peek at the current status and any errors it may have thrown by using this command:

systemctl status welcome-bot

And when you want to restart the bot after modifying its code, you can do so by running the following command:

systemctl restart welcome-bot

You can do this for any services you want to run, not just chat bots, and not just in Python. Any program you write, and in any language!

Automatic updates

While we’re automating things, I also recommend setting up automatic updates. Completely optional, but a nice time-saver. Especially if you’re maintaining multiple servers.

I’m only very briefly touching on this topic because the way you do it depends on what distro you’re running and what package manager it uses.

Here are some useful resources:

If you take a moment to skim those articles, you might notice that these are managed using the systemctl command. Indeed, they are systemd services just like our Discord bot!

Mitigating brute-force attacks

I wasn’t too concerned about this during the initial setup (was feeling lazy), but when I logged into my VPS a couple of months later, I was greeted with the following message:

Last failed login: Mon Apr 29 00:31:28 EDT 2024 from 213.109.202.127 on ssh:notty
There were 177514 failed login attempts since the last successful login.
Last login: Tue Apr  2 07:51:43 2024

Over 150,000 failed login attempts? What was going on? Out of curiosity, I traced the IP address:

This was a brute-force attack, unsurprisingly originating from Russia. And this sort of thing is super common. I was tempted to ignore it and leave the server unsecured so that, in the event that they did manage to get into it, all they would find is a Discord bot. That would have been a funny way to waste their time, but it’s better to be safe than sorry. I also want to use the VPS for more projects in the future, so let’s lock it down!

The easiest way to do this is with an SSH key. It is a key in the most literal sense, like the key to any door or lock. So keep it safe, and don’t lose it!

In Windows PowerShell (note: we’re doing this part locally, not on our VPS!), generate a key with the ssh-keygen command by simply running:

ssh-keygen

Follow the prompts. Assuming you haven’t done this before, you can press the enter key on each prompt to use the defaults. Protecting your SSH key with a password is not necessary, but the option is provided. A password adds an extra layer of security in case someone manages to steal the key, because then they still need the password in order to use it. If you don’t want a password, press the enter key without typing one, when prompted.

And then tell the server to recognize your new key by running this command (on Linux or macOS):

ssh-copy-id root@12.345.678.90 (substitute the IP address of your VPS)

Note: at the time of this writing, Windows PowerShell doesn’t have the ssh-copy-id command, but you can work around it like so (tweaked this snippet from Christopher Hart’s blog, read more here: Windows 10 OpenSSH Equivalent of ssh-copy-id):

type $env:USERPROFILE\.ssh\id_rsa.pub | ssh root@12.345.678.90 "mkdir -p -m 700 ~/.ssh && cat >> ~/.ssh/authorized_keys"

And if that doesn’t work, you could try copying it over using the scp (secure copy) command:

scp $env:USERPROFILE\.ssh\id_rsa.pub root@12.345.678.90:/
ssh root@12.345.678.90
mkdir -m 700 ~/.ssh
cat /id_rsa.pub >> ~/.ssh/authorized_keys
chmod 644 -f ~/.ssh/authorized_keys
rm -f /id_rsa.pub

See? There are always workarounds if you have to get creative!

Also worth noting is that SSH authentication can be very picky about file permissions, so if you’re still having trouble, try the solution from this gist:

chmod 700 ~/.ssh && \
chmod 600 ~/.ssh/* && \
chmod 644 -f ~/.ssh/*.pub ~/.ssh/authorized_keys ~/.ssh/known_hosts ~/.ssh/config

At this point, you should be able to ssh into your server without being prompted for a password: very cool!

We’re almost there. Next, we need to disable password authentication altogether so that the only way to log in is with the key. Log into your server if you haven’t already done so, and run this command:

nano /etc/ssh/sshd_config

Then edit each of the following attributes to be set to no, like so:

PasswordAuthentication no
ChallengeResponseAuthentication no

Note: also make sure those two attributes aren’t commented out (aka if there’s a # sign to the left of one of them, delete the # sign). For any attributes that don’t exist, add them somewhere (at the end of the file is fine). Remember to save your changes as you exit nano.

Lastly, reload the SSH daemon with the following command and you should be good to go. Make sure you test it on a device that doesn’t have the SSH key!

systemctl reload sshd

Now those big bad hackers wouldn’t be able to get in even if they knew the password!

But that doesn’t mean the server is totally bulletproof. For example: there was a recent exploit discovered that was trying to steal SSH keys, which you can read more about below. Stay vigilant!

Using your SSH key with WinSCP

With password authentication turned off, you won’t be able to connect over WinSCP using the old method from before, so let’s fix that. Relevant parts of the screenshot below have been highlighted in pink.

Yep, it’s that simple! You’ll still need to enter the IP address (where it asks for hostname) and username. And you can click the Save button to remember the key/IP/username so you don’t have to enter them each time you connect.

That’s everything!

As much as I would like to write, “and the Discord bot lived happily ever after”, projects involving web servers require maintenance, which continues ad infinitum. Anything that happens to break over time will need fixing, and I’ll probably have to switch to a newer version of Python eventually. It’s a living, breathing, ever-evolving system, and there’s some beauty in that.

Exercises for the reader

I wanted to include more sections in this blogpost, but it’s already a bit of an infodump, so I’ve attached everything else below as exercises for the reader (assuming anyone out there decides to follow along). Have fun!

  1. [Beginner] Find out how to copy your SSH key to other devices, such as a second computer or a virtual machine, and log into your VPS from those devices.

  2. [Intermediate] Create an additional bot service for prototyping new features before merging them into the main (stable) bot.

  3. [Advanced] Use continuous delivery to update and restart your main bot automatically as code is merged into its GitHub repo. How to build a CI/CD pipeline with GitHub Actions in four simple steps - The GitHub Blog