A custom Message of the Day (MOTD) provides useful system metrics every time you log in via SSH.
While static text files in /etc/motd or /etc/motd.d/ print raw text, scripts placed in /etc/update-motd.d/ execute dynamically, allowing for real-time resource tracking and colorized formatting.
1. How MOTD Processing Works in Debian
On Debian and Ubuntu, PAM (Pluggable Authentication Modules) constructs the login screen in sequential order:
- Static Base Notice: Read from
/etc/motd(e.g., standard OS version and copyright information). - Dynamic Scripts: Executed numerically from
/etc/update-motd.d/. - Drop-in Static Notices: Appended from
/etc/motd.d/(such as the Cockpit web console notice).
Key Rule: Scripts in
/etc/update-motd.d/must be executable (chmod +x), whereas files in/etc/motd.d/are treated strictly as plain text.
2. Setting Up the Dynamic Stats Script
Step 1: Create the Script
Create a new script file in /etc/update-motd.d/. Using the prefix 50- ensures it runs in the middle—after system notices and before Cockpit or custom app notices:
Bash
sudo nano /etc/update-motd.d/50-admin-stats
Step 2: Add the Script Content
Paste the following complete script. It includes standard system metrics alongside optional advanced features like Wazuh Agent status check, updates pending check, and failed SSH login tracking.
Basic Version
#!/usr/bin/env bash
# Color definitions
GREEN='\033[0;32m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# System Stats
HOSTNAME=$(hostname -f)
KERNEL=$(uname -r)
UPTIME=$(uptime -p | sed 's/up //' 2>/dev/null || uptime)
LOAD=$(cut -d' ' -f1-3 /proc/loadavg)
MEM_USAGE=$(free -h | awk '/Mem:/ {print $3 "/" $2}')
DISK_USAGE=$(df -h / | awk 'NR==2 {print $3 "/" $2 " (" $5 ")"}')
echo -e "${BLUE}============================================================${NC}"
echo -e " Welcome to ${GREEN}${HOSTNAME}${NC}"
echo -e " Kernel: ${KERNEL}"
echo -e " Uptime: ${UPTIME}"
echo -e " Load: ${LOAD}"
echo -e " Memory: ${MEM_USAGE}"
echo -e " Disk /: ${DISK_USAGE}"
echo -e "${BLUE}============================================================${NC}"
Advanced Version
#!/usr/bin/env bash
# ==============================================================================
# Custom Dynamic MOTD Script for Debian
# Place in: /etc/update-motd.d/50-admin-stats
# Permissions: chmod +x /etc/update-motd.d/50-admin-stats
# ==============================================================================
# ANSI Color Codes
BOLD='\033[1m'
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# ------------------------------------------------------------------------------
# 1. CORE SYSTEM METRICS
# ------------------------------------------------------------------------------
HOSTNAME=$(hostname -f)
KERNEL=$(uname -r)
UPTIME=$(uptime -p | sed 's/up //' 2>/dev/null || uptime)
LOAD=$(cut -d' ' -f1-3 /proc/loadavg)
# Memory & Swap Usage
MEM_USED=$(free -m | awk '/Mem:/ {print $3}')
MEM_TOTAL=$(free -m | awk '/Mem:/ {print $2}')
MEM_PCT=$(awk "BEGIN {printf \"%.1f\", ($MEM_USED/$MEM_TOTAL)*100}")
# Disk Usage (Root Partition)
DISK_USED=$(df -h / | awk 'NR==2 {print $3}')
DISK_TOTAL=$(df -h / | awk 'NR==2 {print $2}')
DISK_PCT=$(df -h / | awk 'NR==2 {print $5}')
# IP Addresses
IP_LOCAL=$(hostname -I | awk '{print $1}')
# ------------------------------------------------------------------------------
# 2. OPTIONAL ADVANCED CHECKS
# ------------------------------------------------------------------------------
# Feature A: Check Wazuh Agent Service Status
if systemctl is-active --quiet wazuh-agent 2>/dev/null; then
WAZUH_STATUS="${GREEN}Active (Running)${NC}"
elif systemctl is-active --quiet wazuh-manager 2>/dev/null; then
WAZUH_STATUS="${GREEN}Active (Manager - Agent 000)${NC}"
else
WAZUH_STATUS="${RED}Inactive / Not Installed${NC}"
fi
# Feature B: Failed SSH Login Attempts (Last 24 Hours)
FAILED_LOGINS=$(journalctl _SYSTEMD_UNIT=ssh.service --since "24 hours ago" 2>/dev/null | grep -c "Failed password")
if [ "$FAILED_LOGINS" -gt 50 ]; then
FAILED_STR="${RED}${FAILED_LOGINS} attempts!${NC}"
elif [ "$FAILED_LOGINS" -gt 0 ]; then
FAILED_STR="${YELLOW}${FAILED_LOGINS} attempts${NC}"
else
FAILED_STR="${GREEN}0 attempts${NC}"
fi
# ------------------------------------------------------------------------------
# 3. DISPLAY OUTPUT
# ------------------------------------------------------------------------------
echo -e "${BLUE}============================================================${NC}"
echo -e " ${BOLD}System Status for:${NC} ${CYAN}${HOSTNAME}${NC} (${IP_LOCAL})"
echo -e "${BLUE}------------------------------------------------------------${NC}"
echo -e " OS Kernel : ${KERNEL}"
echo -e " Uptime : ${UPTIME}"
echo -e " Load Avg : ${LOAD}"
echo -e " Memory : ${MEM_USED}MB / ${MEM_TOTAL}MB (${MEM_PCT}%)"
echo -e " Disk (/) : ${DISK_USED} / ${DISK_TOTAL} (${DISK_PCT})"
echo -e " Wazuh : ${WAZUH_STATUS}"
echo -e " SSH Security (24h) : ${FAILED_STR} failed logins"
echo -e " DO NOT FORGET TO DO MANUAL WORDPRESS UPDATES"
echo -e "https://fjgraf.fbx.one/2026/01/02/wp-cli/"
echo -e "${BLUE}============================================================${NC}"
More Advanced versions checking a small WordPress security scan:
#!/usr/bin/env bash
# ==============================================================================
# Custom Dynamic MOTD Script for Debian
# Place in: /etc/update-motd.d/50-admin-stats
# Permissions: chmod +x /etc/update-motd.d/50-admin-stats
# ==============================================================================
# ANSI Color Codes
BOLD='\033[1m'
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
BASE_DIR="/var/www/"
# ------------------------------------------------------------------------------
# 1. CORE SYSTEM METRICS
# ------------------------------------------------------------------------------
HOSTNAME=$(hostname -f)
KERNEL=$(uname -r)
UPTIME=$(uptime -p | sed 's/up //' 2>/dev/null || uptime)
LOAD=$(cut -d' ' -f1-3 /proc/loadavg)
# Memory & Swap Usage
MEM_USED=$(free -m | awk '/Mem:/ {print $3}')
MEM_TOTAL=$(free -m | awk '/Mem:/ {print $2}')
MEM_PCT=$(awk "BEGIN {printf \"%.1f\", ($MEM_USED/$MEM_TOTAL)*100}")
# Disk Usage (Root Partition)
DISK_USED=$(df -h / | awk 'NR==2 {print $3}')
DISK_TOTAL=$(df -h / | awk 'NR==2 {print $2}')
DISK_PCT=$(df -h / | awk 'NR==2 {print $5}')
# IP Addresses
IP_LOCAL=$(hostname -I | awk '{print $1}')
# ------------------------------------------------------------------------------
# 2. OPTIONAL ADVANCED CHECKS
# ------------------------------------------------------------------------------
# Feature A: Check Wazuh Agent Service Status
if systemctl is-active --quiet wazuh-agent 2>/dev/null; then
WAZUH_STATUS="${GREEN}Active (Running)${NC}"
elif systemctl is-active --quiet wazuh-manager 2>/dev/null; then
WAZUH_STATUS="${GREEN}Active (Manager - Agent 000)${NC}"
else
WAZUH_STATUS="${RED}Inactive / Not Installed${NC}"
fi
# Feature B: Failed SSH Login Attempts (Last 24 Hours)
FAILED_LOGINS=$(journalctl _SYSTEMD_UNIT=ssh.service --since "24 hours ago" 2>/dev/null | grep -c "Failed password")
if [ "$FAILED_LOGINS" -gt 50 ]; then
FAILED_STR="${RED}${FAILED_LOGINS} attempts!${NC}"
elif [ "$FAILED_LOGINS" -gt 0 ]; then
FAILED_STR="${YELLOW}${FAILED_LOGINS} attempts${NC}"
else
FAILED_STR="${GREEN}0 attempts${NC}"
fi
# ------------------------------------------------------------------------------
# 3. DISPLAY OUTPUT
# ------------------------------------------------------------------------------
echo -e "${BLUE}============================================================${NC}"
echo -e " ${BOLD}System Status for:${NC} ${CYAN}${HOSTNAME}${NC} (${IP_LOCAL})"
echo -e "${BLUE}------------------------------------------------------------${NC}"
echo -e " OS Kernel : ${KERNEL}"
echo -e " Uptime : ${UPTIME}"
echo -e " Load Avg : ${LOAD}"
echo -e " Memory : ${MEM_USED}MB / ${MEM_TOTAL}MB (${MEM_PCT}%)"
echo -e " Disk (/) : ${DISK_USED} / ${DISK_TOTAL} (${DISK_PCT})"
echo -e "${BLUE}------------------------------------------------------------${NC}"
echo -e " Wazuh : ${WAZUH_STATUS}"
echo -e " SSH Security (24h) : ${FAILED_STR} failed logins"
echo -e " ${YELLOW}${BOLD}REMINDER:${NC}DO NOT FORGET TO DO MANUAL WORDPRESS UPDATES"
echo -e " https://fjgraf.fbx.one/2026/01/02/wp-cli/"
echo -e "${BLUE}============================================================${NC}"
echo -e " ${BOLD}WordPress Sites Scan (/var/www):${NC}"
# Find all directory paths containing a wp-config.php file
mapfile -t WP_SITES < <(find "$BASE_DIR" -maxdepth 3 -type f -name "wp-config.php" -exec dirname {} \;)
if [ ${#WP_SITES[@]} -eq 0 ]; then
echo -e " ${YELLOW}No WordPress installations found under ${BASE_DIR}${NC}"
else
for SITE_PATH in "${WP_SITES[@]}"; do
SITE_NAME=$(basename "$SITE_PATH")
# 1. Search for hidden PHP backdoors (e.g., .m982abd89.php, .ico.php) in root and subfolders
HIDDEN_SHELLS=$(find "$SITE_PATH" -maxdepth 4 -type f -name ".*.php" 2>/dev/null | wc -l)
# 2. Search for illicit executable PHP files inside wp-content/uploads/
UPLOADS_PHP=0
if [ -d "$SITE_PATH/wp-content/uploads" ]; then
UPLOADS_PHP=$(find "$SITE_PATH/wp-content/uploads" -type f -name "*.php" 2>/dev/null | wc -l)
fi
# Format output per site
echo -e " ${BOLD}Site:${NC} ${CYAN}${SITE_NAME}${NC}"
# Hidden Shell Status
if [ "$HIDDEN_SHELLS" -gt 0 ]; then
echo -e " ├─ Backdoors : ${RED}CRITICAL! ${HIDDEN_SHELLS} hidden .php file(s) found!${NC}"
else
echo -e " ├─ Backdoors : ${GREEN}Clean (No hidden .php files)${NC}"
fi
# Uploads PHP Status
if [ "$UPLOADS_PHP" -gt 0 ]; then
echo -e " └─ Uploads : ${RED}ALERT! ${UPLOADS_PHP} executable .php file(s) in uploads!${NC}"
else
echo -e " └─ Uploads : ${GREEN}Clean (No .php in uploads/)${NC}"
fi
done
fi
echo -e "${BLUE}============================================================${NC}"
Step 3: Make the Script Executable
Grant execution rights so PAM can run the script during authentication:
Bash
sudo chmod +x /etc/update-motd.d/50-admin-stats
3. Testing and Verification
You can test the entire MOTD assembly directly from the command line without opening a new SSH session:
Bash
sudo run-parts /etc/update-motd.d/
Expected Output Example:
Plaintext
Linux webserver 6.1.0-51-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.177-1 (2026-07-16) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
============================================================
System Status for: ap-webserver.local (192.168.1.100)
------------------------------------------------------------
OS Kernel : 6.1.0-51-amd64
Uptime : 2 days, 4 hours
Load Avg : 0.08 0.03 0.01
Memory : 1240MB / 7912MB (15.7%)
Disk (/) : 14G / 50G (28%)
Wazuh : Active (Running)
SSH Security (24h) : 0 attempts failed logins
============================================================
Web console: https://ap-webserver:9090/ or https://192.168.1.100:9090/
Last login: Fri Jul 31 02:05:11 2026 from 192.168.15.236
4. SSH Configuration Check
If the MOTD output does not display during login, verify that /etc/ssh/sshd_config contains the following active settings:
Ini, TOML
PrintMotd yes
UsePAM yes
If you make any changes to sshd_config, reload the SSH daemon:
Bash
sudo systemctl restart ssh
Leave a Reply
You must be logged in to post a comment.