MOTD Messages after you log in (debian)

Written by

in

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:

  1. Static Base Notice: Read from /etc/motd (e.g., standard OS version and copyright information).
  2. Dynamic Scripts: Executed numerically from /etc/update-motd.d/.
  3. 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

Comments

Leave a Reply