Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Command visibility: OAuth and nginx

Optional but recommended. Without it, every staff command is visible only to members with the Administrator permission. With it, after an Owner runs /perms authorize once, each role sees exactly the commands its /perms level allows. (The bot checks the level itself either way; this is about what Discord shows.) How it works explains the mechanism, the security model the OAuth details.

What you set up here:

  • a host name, e.g. bot.example.org, pointing at your server;
  • nginx with a Let’s Encrypt certificate, forwarding exactly two paths to the bot on 127.0.0.1:8787;
  • the redirect URL and the client secret in the Discord Developer Portal;
  • two more encrypted credentials: discord_client_secret and oauth_key.

Replace bot.example.org with your own host name everywhere below.

1. DNS

Create an A (and, if your server has IPv6, AAAA) record for bot.example.org pointing at your server, or a CNAME to a name that already does. If your DNS provider can proxy traffic (e.g. Cloudflare’s orange cloud), keep it DNS only until the first certificate exists, so Let’s Encrypt can reach the server over plain HTTP.

Check from any machine:

getent hosts bot.example.org

prints your server’s address.

2. nginx, certbot and the firewall

Debian/Ubuntu:

sudo apt install nginx certbot

Fedora:

sudo dnf install nginx certbot
sudo systemctl enable --now nginx certbot-renew.timer
sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload
sudo setsebool -P httpd_can_network_connect 1

On Fedora, SELinux by default forbids nginx to open network connections; the last command allows it to reach the bot on 127.0.0.1:8787. Debian and Ubuntu start nginx and certbot’s renewal timer on installation. If you run a firewall there, allow ports 80 and 443.

Where site files go differs:

Site fileEnable it
Debian/Ubuntu/etc/nginx/sites-available/bot.example.orgsudo ln -sf /etc/nginx/sites-available/bot.example.org /etc/nginx/sites-enabled/bot.example.org
Fedora/etc/nginx/conf.d/bot.example.org.conf(every *.conf there is loaded)

The commands below use the Debian path in a variable; on Fedora set SITE=/etc/nginx/conf.d/bot.example.org.conf instead and skip the ln lines.

3. The first certificate

The full site refers to the certificate files, so nginx -t would fail before they exist. Start with the HTTP-only bootstrap site, which serves only Let’s Encrypt’s challenge files:

SITE=/etc/nginx/sites-available/bot.example.org
sudo mkdir -p /var/www/letsencrypt
sed 's/bot\.polarsystems\.net/bot.example.org/g' deploy/nginx-bot.polarsystems.net.bootstrap.conf | sudo tee "$SITE" >/dev/null
sudo ln -sf "$SITE" /etc/nginx/sites-enabled/bot.example.org
sudo nginx -t && sudo systemctl reload nginx
sudo certbot certonly --webroot -w /var/www/letsencrypt -d bot.example.org --deploy-hook "systemctl reload nginx"
  • sed … | sudo tee copies the site from the repository with your host name in it.
  • nginx -t prints syntax is ok and test is successful.
  • certbot asks for an e-mail address and the terms on its first run, then prints Successfully received certificate. and the paths under /etc/letsencrypt/live/bot.example.org/. Its timer renews the certificate, and the deploy hook reloads nginx after each renewal.

4. The full site

sed 's/bot\.polarsystems\.net/bot.example.org/g' deploy/nginx-bot.polarsystems.net.conf | sudo tee "$SITE" >/dev/null
sudo nginx -t && sudo systemctl reload nginx

The site redirects HTTP to HTTPS, serves TLS 1.2/1.3 with HSTS and strict headers, forwards only GET /oauth/callback and GET /healthz to 127.0.0.1:8787, answers everything else with 404 and keeps no access log (the callback URL carries a one-time code).

nginx older than 1.25.1 (Debian 12, Ubuntu 24.04) does not know http2 on;, and nginx -t reports unknown directive "http2". Delete that line and write the two listen lines as listen 443 ssl http2; and listen [::]:443 ssl http2; instead.

If your DNS is proxied (Cloudflare), you can turn the proxy on now; use SSL mode Full (strict).

5. The Developer Portal

In https://discord.com/developers/applications, your application, OAuth2:

  1. under Redirects, add https://bot.example.org/oauth/callback. It must equal <public_url>/oauth/callback exactly, with the public_url from your config file (Install, step 6);
  2. choose Reset Secret and copy the client secret.

6. The two credentials

sudo -v
read -rsp "Client secret: " SECRET; echo
printf '%s' "$SECRET" | sudo systemd-creds encrypt --name=discord_client_secret - /etc/polarbot/discord_client_secret.cred
unset SECRET
od -An -N32 -tx1 /dev/urandom | tr -d ' \n' | sudo systemd-creds encrypt --name=oauth_key - /etc/polarbot/oauth_key.cred
sudo chmod 0600 /etc/polarbot/discord_client_secret.cred /etc/polarbot/oauth_key.cred
  • The client secret is read with hidden input, like the token.
  • oauth_key is 32 random bytes written as 64 hex characters, generated here and never shown. It signs the /perms authorize links and encrypts the stored refresh tokens. Generating a new one later makes every server run /perms authorize again.

If you removed the two LoadCredentialEncrypted= lines from the unit earlier, install the unit again: sudo install -m 0644 deploy/polarbot.service /etc/systemd/system/polarbot.service and sudo systemctl daemon-reload.

7. Restart and check

sudo systemctl restart polarbot
sudo journalctl -u polarbot -n 20 --no-pager | grep -i "web server"
curl -sS https://bot.example.org/healthz; echo
  • The log shows web server listening addr=127.0.0.1:8787.
  • curl prints ok. (From the server itself, curl -sS http://127.0.0.1:8787/healthz checks the bot without nginx.)

Then, in your Discord server, an Owner runs /perms authorize, opens the private link, approves, and sees “PolarBot is authorized”. Within seconds each role sees only the commands its level allows. /perms list shows the roles and levels.