Real-Time Server Monitoring Through Discord
Published on May 28, 2025
My server sees hundreds of malicious scans a day. Logging's coveredโI use GoAccess for NGINX traffic and keep an eye on /var/log/auth.log for SSH. Fail2ban handles brute force attempts and scans well enough. But I wanted a way to know what's happening without having to SSH in and check manually. Just something simple that tells me who's getting banned or when someone logs in.
So I hooked it into Discord.
How I Did It (and Why)
The first thing I wanted was SSH login alerts. I'm already logging logins in /var/log/auth.log, so instead of setting up something bloated like an email server or PAM modules that barely work under WSL, I wrote a simple shell script that tails the auth log and sends a Discord message when someone logs in successfully over SSH.
Here's the complete SSH monitoring script at /usr/local/bin/ssh-log-watcher.sh:
bash#!/bin/bash
LOGFILE="/var/log/auth.log"
WEBHOOK_URL="https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN"
tail -n0 -F "$LOGFILE" | while read LINE; do
echo "$LINE" | grep "Accepted" &> /dev/null
if [ $? -eq 0 ]; then
USER=$(echo "$LINE" | awk '{print $4}')
IP=$(echo "$LINE" | awk '{print $6}')
HOST=$(hostname)
TIME=$(date)
curl -H "Content-Type: application/json" -X POST -d "{\\"content\\": \\"๐ SSH Login Detected\\\\n๐ค User: $USER\\\\n๐ IP: $IP\\\\n๐ป Host: $HOST\\\\n๐ Time: $TIME\\"}" "$WEBHOOK_URL"
fi
done
The script grabs the username, IP, current time, and hostname, then uses curl
to POST a message to the Discord webhook. I made it a background systemd service so it starts on boot and stays up:
[Unit]
Description=SSH Login Monitor
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/ssh-log-watcher.sh
Restart=always
RestartSec=5
User=root
Environment=DISCORD_WEBHOOK_URL_FILE=/etc/ssh-monitor/webhook.conf
[Install]
WantedBy=multi-user.target
For the SSH login alerts, the full script lives at: /usr/local/bin/ssh-log-watcher.sh. I made it executable and then created a systemd service at: /etc/systemd/system/ssh-log-watcher.service
That service just calls the script on boot and restarts it if it ever dies. You'll also want to run:
bashsudo systemctl daemon-reload # Reloads systemd to pull in new service files
sudo systemctl enable ssh-log-watcher # Enables the service to start on boot
sudo systemctl start ssh-log-watcher # Starts the service right away, no reboot
This means every successful login, from anywhere, immediately alerts me on Discord with something like:
๐ SSH Login Detected
๐ค User: chris
๐ IP: 192.168.1.10
๐ป Host: ehchris
๐ Time: Thu May 29 08:42:17 CDT 2025
No delay, no need to manually read a log, no guessing. It works even if I'm not SSH'd in, which is the whole point.
Fail2ban Integration
Then I wired up Fail2ban to do the same. I already had a jail for SSH brute-force attempts and one for NGINX 404 spam. I created a custom Fail2ban action file at /etc/fail2ban/action.d/discord-ban.conf that sends a webhook when a ban happens. It uses placeholders like <ip>
and <name>
from the jail context:
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = curl -H "Content-Type: application/json" -X POST \\
-d "{\\"content\\": \\"๐ซ IP Banned\\\\n๐ IP: <ip>\\\\n๐ Jail: <name>\\\\n๐ Time: $(date)\\\\\\n๐ป Host: $(hostname)\\\\\\n๐
Unban: $(date -d '+86400 seconds')\\"}" \\
$(cat /etc/fail2ban/discord-webhook.conf 2>/dev/null || echo "WEBHOOK_URL_NOT_CONFIGURED")
actionunban =
The actionban
line does the heavy lifting by firing the webhook with the IP, jail name, and calculated unban time.
To wire that into my actual jails, I dropped two override files into:
/etc/fail2ban/jail.d/sshd.conf
/etc/fail2ban/jail.d/nginx-404.conf
Each one just contains the basic jail settings (enabled
, port
, logpath
, etc.) plus:
action = discord-ban
Fail2ban triggers that hook every time someone gets banned. I get a nice clean log of blocked bots in a second Discord channel like:
๐ซ IP Banned
๐ IP: 185.232.67.98
๐ Jail: nginx-404
๐ Time: Thu May 29 09:14:32 CDT 2025
๐ป Host: ehchris
๐
Unban: Fri May 30 09:14:32 CDT 2025
And yeah, I split the webhook URLs so SSH logins and Fail2ban bans go to different Discord channels. Way easier to keep high-signal alerts (like logins) separate from the noise of constant bot bans. That just means generating two Discord webhooks, one per channel, which takes five seconds and makes life significantly cleaner.
Security Considerations
Webhook URL Protection: Keep your Discord webhook URLs in separate config files with restricted permissions rather than hardcoding them in scripts:
bashsudo chmod 600 /etc/ssh-monitor/webhook.conf
sudo chmod 600 /etc/fail2ban/discord-webhook.conf
Error Handling: The scripts include basic fallbacksโif Discord is unreachable or the webhook fails, the monitoring continues without crashing. Failed webhook calls don't interrupt log processing, though you might want to add local logging for webhook failures if uptime is critical.
Performance Under Load: On busy servers, the tail -F
approach handles moderate traffic well. If you're seeing thousands of login attempts per minute, consider switching to a log aggregation tool, but for typical server loads this approach is lightweight and reliable.
Results

After running this setup for several weeks, it's completely changed how I monitor server activity. Instead of periodic SSH check-ins, I get real-time awareness of what's happening. The separate channels mean I can mute the ban notifications when I don't need the constant bot chatter, but keep login alerts at full volume.
The system has caught several legitimate security events I would have missed with manual log checking, and the immediate notification means I can respond to issues in minutes rather than hours. Plus, having the historical record in Discord makes it easy to spot patterns or reference past incidents.

For something that took maybe an hour to set up, the operational awareness it provides is invaluable. No complex monitoring stack requiredโjust Discord, some shell scripting, and systemd doing what it does best.