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_secretandoauth_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 file | Enable it | |
|---|---|---|
| Debian/Ubuntu | /etc/nginx/sites-available/bot.example.org | sudo 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 teecopies the site from the repository with your host name in it.nginx -tprintssyntax is okandtest 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;, andnginx -treportsunknown directive "http2". Delete that line and write the twolistenlines aslisten 443 ssl http2;andlisten [::]: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:
- under Redirects, add
https://bot.example.org/oauth/callback. It must equal<public_url>/oauth/callbackexactly, with thepublic_urlfrom your config file (Install, step 6); - 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_keyis 32 random bytes written as 64 hex characters, generated here and never shown. It signs the/perms authorizelinks and encrypts the stored refresh tokens. Generating a new one later makes every server run/perms authorizeagain.
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. curlprintsok. (From the server itself,curl -sS http://127.0.0.1:8787/healthzchecks 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.