diff --git a/du_setup.sh b/du_setup.sh index d2f28f9..f210136 100644 --- a/du_setup.sh +++ b/du_setup.sh @@ -1,8 +1,10 @@ #!/bin/bash # Debian and Ubuntu Server Hardening Interactive Script -# Version: 0.73 | 2025-10-22 +# Version: 0.74 | 2025-11-06 # Changelog: +# - v0.74: Add optional dtop (https://github.com/amir20/dtop) after docker installation. +#. Update .bashrc # - v0.73: Revised/improved logic in .bashrc for memory and system updates. # - v0.72: Added configure_custom_bashrc() function that creates and installs a feature-rich .bashrc file during user creation. # - v0.71: Simplify test backup function to work reliably with Hetzner storagebox @@ -77,7 +79,7 @@ set -euo pipefail # --- Update Configuration --- -CURRENT_VERSION="0.73" +CURRENT_VERSION="0.74" SCRIPT_URL="https://raw.githubusercontent.com/buildplan/du_setup/refs/heads/main/du_setup.sh" CHECKSUM_URL="${SCRIPT_URL}.sha256" @@ -228,7 +230,7 @@ print_header() { printf '%s\n' "${CYAN}╔═════════════════════════════════════════════════════════════════╗${NC}" printf '%s\n' "${CYAN}║ ║${NC}" printf '%s\n' "${CYAN}║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║${NC}" - printf '%s\n' "${CYAN}║ v0.73 | 2025-10-22 ║${NC}" + printf '%s\n' "${CYAN}║ v0.74 | 2025-11-06 ║${NC}" printf '%s\n' "${CYAN}║ ║${NC}" printf '%s\n' "${CYAN}╚═════════════════════════════════════════════════════════════════╝${NC}" printf '\n' @@ -1238,16 +1240,15 @@ __bash_prompt_command() { PROMPT_COMMAND=__bash_prompt_command # --- Editor Configuration --- -# Set default editor with fallback chain. if command -v vim &>/dev/null; then export EDITOR=vim export VISUAL=vim -elif command -v vi &>/dev/null; then - export EDITOR=vi - export VISUAL=vi -else +elif command -v nano &>/dev/null; then export EDITOR=nano export VISUAL=nano +else + export EDITOR=vi + export VISUAL=vi fi # --- Additional Environment Variables --- @@ -1265,7 +1266,7 @@ mkcd() { # Create a backup of a file with timestamp. backup() { if [ -f "$1" ]; then - local backup_file="$1.backup-$(date +%Y%m%d-%H%M%S)" + local backup_file; backup_file="$1.backup-$(date +%Y%m%d-%H%M%S)" cp "$1" "$backup_file" echo "Backup created: $backup_file" else @@ -1278,19 +1279,19 @@ backup() { extract() { if [ -f "$1" ]; then case "$1" in - *.tar.bz2) tar xjf "$1" ;; - *.tar.gz) tar xzf "$1" ;; - *.tar.xz) tar xJf "$1" ;; - *.bz2) bunzip2 "$1" ;; - *.rar) unrar x "$1" ;; - *.gz) gunzip "$1" ;; - *.tar) tar xf "$1" ;; - *.tbz2) tar xjf "$1" ;; - *.tgz) tar xzf "$1" ;; - *.zip) unzip "$1" ;; - *.Z) uncompress "$1" ;; - *.7z) 7z x "$1" ;; - *.deb) ar x "$1" ;; + *.tar.bz2) tar xjf "$1" ;; + *.tar.gz) tar xzf "$1" ;; + *.tar.xz) tar xJf "$1" ;; + *.bz2) bunzip2 "$1" ;; + *.rar) unrar x "$1" ;; + *.gz) gunzip "$1" ;; + *.tar) tar xf "$1" ;; + *.tbz2) tar xjf "$1" ;; + *.tgz) tar xzf "$1" ;; + *.zip) unzip "$1" ;; + *.Z) uncompress "$1" ;; + *.7z) 7z x "$1" ;; + *.deb) ar x "$1" ;; *.tar.zst) if command -v zstd &>/dev/null; then zstd -dc "$1" | tar xf - @@ -1300,7 +1301,7 @@ extract() { ;; *) echo "'$1' cannot be extracted via extract()" >&2 - return 1 + return 1 # Add return 1 for consistency ;; esac else @@ -1334,6 +1335,9 @@ ftext() { grep -rnw . -e "$1" 2>/dev/null } +# Search history easily +hgrep() { history | grep -i --color=auto "$@"; } + # Create a tarball of a directory. targz() { if [ -d "$1" ]; then @@ -1357,7 +1361,36 @@ sizeof() { # Show most used commands from history. histop() { - history | awk '{CMD[$2]++;count++;}END { for (a in CMD)print CMD[a] " " CMD[a]/count*100 "% " a;}' | grep -v "./" | column -c3 -s " " -t | sort -nr | nl | head -n20 + history | awk -v ig="$HISTIGNORE" 'BEGIN{OFS="\t";gsub(/:/,"|",ig);ir="^("ig")($| )";sr="(^|\\s)\\./"} + {cmd=$4;for(i=5;i<=NF;i++)cmd=cmd" "$i} + (cmd==""||cmd~ir||cmd~sr){next} + {C[cmd]++;t++} + END{if(t>0)for(a in C)printf"%d\t%.2f%%\t%s\n",C[a],(C[a]/t*100),a}' | + sort -nr | head -n20 | + awk 'BEGIN{ + FS="\t"; + maxc=length("COUNT"); + maxp=length("PERCENT"); + } + { + data[NR]=$0; + len1=length($1); + len2=length($2); + if(len1>maxc)maxc=len1; + if(len2>maxp)maxp=len2; + } + END{ + fmt=" %-4s %-*s %-*s %s\n"; + printf fmt,"RANK",maxc,"COUNT",maxp,"PERCENT","COMMAND"; + sep_c=sep_p=""; + for(i=1;i<=maxc;i++)sep_c=sep_c"-"; + for(i=1;i<=maxp;i++)sep_p=sep_p"-"; + printf fmt,"----",maxc,sep_c,maxp,sep_p,"-------"; + for(i=1;i<=NR;i++){ + split(data[i],f,"\t"); + printf fmt,i".",maxc,f[1],maxp,f[2],f[3] + } + }' } # Quick server info display @@ -1392,18 +1425,45 @@ sysinfo() { cpu_info=$(lscpu | awk -F: '/Model name/ {print $2; exit}' | xargs || grep -m1 'model name' /proc/cpuinfo | cut -d ':' -f2 | xargs) [ -z "$cpu_info" ] && cpu_info="Unknown" - # --- IP Detection (preferred interfaces first) --- - local ip_addr - for iface in eth0 wlan0 ens33 eno1 enp0s3 enp3s0; do + # --- IP Detection --- + local ip_addr public_ipv4 public_ipv6 + + # Try to get public IPv4 first + public_ipv4=$(curl -4 -s -m 2 --connect-timeout 1 https://checkip.amazonaws.com 2>/dev/null || \ + curl -4 -s -m 2 --connect-timeout 1 https://ipconfig.io 2>/dev/null || \ + curl -4 -s -m 2 --connect-timeout 1 https://api.ipify.org 2>/dev/null) + # If no IPv4, try IPv6 + if [ -z "$public_ipv4" ]; then + public_ipv6=$(curl -6 -s -m 2 --connect-timeout 1 https://ipconfig.io 2>/dev/null || \ + curl -6 -s -m 2 --connect-timeout 1 https://icanhazip.co 2>/dev/null || \ + curl -6 -s -m 2 --connect-timeout 1 https://api64.ipify.org 2>/dev/null) + fi + # Get local/internal IP as fallback + for iface in eth0 ens3 enp0s3 enp0s6 wlan0 ens33 eno1; do ip_addr=$(ip -4 addr show "$iface" 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1) [ -n "$ip_addr" ] && break done - [ -z "$ip_addr" ] && ip_addr=$(ip -4 addr show scope global | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1) + [ -z "$ip_addr" ] && ip_addr=$(ip -4 addr show scope global 2>/dev/null | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1) # --- System Info --- - if [ -n "$ip_addr" ]; then + if [ -n "$public_ipv4" ]; then + # Show public IPv4 (preferred) + printf "${CYAN}%-15s${RESET} %s ${YELLOW}[%s]${RESET}" "Hostname:" "$(hostname)" "$public_ipv4" + # Show local IP if different from public + if [ -n "$ip_addr" ] && [ "$ip_addr" != "$public_ipv4" ]; then + printf " ${DIM}(local: %s)${RESET}\n" "$ip_addr" + else + printf "\n" + fi + elif [ -n "$public_ipv6" ]; then + # Show public IPv6 if no IPv4 + printf "${CYAN}%-15s${RESET} %s ${YELLOW}[%s]${RESET}" "Hostname:" "$(hostname)" "$public_ipv6" + [ -n "$ip_addr" ] && printf " ${DIM}(local: %s)${RESET}\n" "$ip_addr" || printf "\n" + elif [ -n "$ip_addr" ]; then + # Show local IP only printf "${CYAN}%-15s${RESET} %s ${YELLOW}[%s]${RESET}\n" "Hostname:" "$(hostname)" "$ip_addr" else + # No IP detected printf "${CYAN}%-15s${RESET} %s\n" "Hostname:" "$(hostname)" fi printf "${CYAN}%-15s${RESET} %s\n" "OS:" "$(grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d'"' -f2 || echo 'Unknown')" @@ -1424,7 +1484,7 @@ sysinfo() { if [ -f /var/run/reboot-required ]; then printf "${CYAN}%-15s${RESET} ${BOLD_RED}⚠ REBOOT REQUIRED${RESET}\n" "System:" [ -s /var/run/reboot-required.pkgs ] && \ - printf " ${DIM}Reason:${RESET} %s\n" "$(paste -sd ' ' /var/run/reboot-required.pkgs)" + printf " ${DIM}Reason:${RESET} %s\n" "$(paste -sd ' ' /var/run/reboot-required.pkgs)" fi # --- Available Updates (APT) --- @@ -1439,19 +1499,23 @@ sysinfo() { security="${apt_check_output##*;}" fi fi + # Fallback if apt-check didn't provide values if [ -z "$total" ] && [ -r /var/lib/update-notifier/updates-available ]; then total=$(awk '/[0-9]+ (update|package)s? can be (updated|applied|installed)/ {print $1; exit}' /var/lib/update-notifier/updates-available 2>/dev/null) security=$(awk '/[0-9]+ (update|package)s? .*security/ {print $1; exit}' /var/lib/update-notifier/updates-available 2>/dev/null) fi + # Final fallback if [ -z "$total" ]; then total=$(apt list --upgradable 2>/dev/null | grep -c upgradable) security=$(apt list --upgradable 2>/dev/null | grep -ci security) fi + total="${total:-0}" security="${security:-0}" + # Display updates if available if [ -n "$total" ] && [ "$total" -gt 0 ] 2>/dev/null; then printf "${CYAN}%-15s${RESET} " "Updates:" if [ -n "$security" ] && [ "$security" -gt 0 ] 2>/dev/null; then @@ -1466,11 +1530,11 @@ sysinfo() { security_list=$(printf "%s\n" "${upgradable_all[@]}" | grep -i security | head -n5 | awk -F/ '{print $1}') [ -n "$upgradable_list" ] && \ - printf " ${DIM}Upgradable:${RESET} %s" "$(echo "$upgradable_list" | paste -sd ', ')" + printf " ${DIM}Upgradable:${RESET} %s" "$(echo "$upgradable_list" | paste -sd ', ')" [ "$total" -gt 5 ] && printf " ... (+%s more)\n" $((total - 5)) || printf "\n" [ -n "$security_list" ] && \ - printf " ${YELLOW}Security:${RESET} %s" "$(echo "$security_list" | paste -sd ', ')" + printf " ${YELLOW}Security:${RESET} %s" "$(echo "$security_list" | paste -sd ', ')" [ "$security" -gt 5 ] && printf " ... (+%s more)\n" $((security - 5)) || printf "\n" fi fi @@ -1485,6 +1549,28 @@ sysinfo() { fi fi + # --- Tailscale Info (if installed and connected) --- + if command -v tailscale &>/dev/null; then + local ts_ipv4 ts_ipv6 ts_hostname + # Get Tailscale IPs + ts_ipv4=$(tailscale ip -4 2>/dev/null) + ts_ipv6=$(tailscale ip -6 2>/dev/null) + # Only show if connected + if [ -n "$ts_ipv4" ] || [ -n "$ts_ipv6" ]; then + # Get hostname from status (FIXED: use head -n1 to get only first line) + ts_hostname=$(tailscale status --self --peers=false 2>/dev/null | head -n1 | awk '{print $2}') + printf "${CYAN}%-15s${RESET} " "Tailscale:" + printf "${GREEN}Connected${RESET}" + [ -n "$ts_ipv4" ] && printf " - %s" "$ts_ipv4" + [ -n "$ts_hostname" ] && printf " ${DIM}(%s)${RESET}" "$ts_hostname" + printf "\n" + # Optional: Show IPv6 on second line if available + if [ -n "$ts_ipv6" ]; then + printf " ${DIM}IPv6: %s${RESET}\n" "$ts_ipv6" + fi + fi + fi + printf "\n" } @@ -1501,6 +1587,47 @@ checkupdates() { fi } +# Disk space alert (warns if any partition > 80%) +diskcheck() { + df -h | awk ' + NR > 1 { + usage = $5 + gsub(/%/, "", usage) + if (usage > 80) { + printf "⚠️ %s\n", $0 + found = 1 + } + } + END { + if (!found) print "✓ All disks below 80%" + } + ' +} + +# Directory bookmarks +export MARKPATH=$HOME/.marks +[ -d "$MARKPATH" ] || mkdir -p "$MARKPATH" +mark() { ln -sfn "$(pwd)" "$MARKPATH/${1:-$(basename "$PWD")}"; } +jump() { cd -P "$MARKPATH/$1" 2>/dev/null || ls -l "$MARKPATH"; } + +# Service status shortcut (cleaner output) +svc() { sudo systemctl status "$1" --no-pager -l | head -20; } +alias failed='systemctl --failed --no-pager' + +# Show top 10 processes by CPU +topcpu() { ps aux --sort=-%cpu | head -11; } + +# Show top 10 processes by memory +topmem() { ps aux --sort=-%mem | head -11; } + +# Network connections summary +netsum() { + echo "=== Active Connections ===" + ss -s + echo -e "\n=== Listening Ports ===" + sudo ss -tulnp | grep LISTEN | awk '{print $5, $7}' | sort -u +} + # --- Aliases --- # Enable color support for common commands. if [ -x /usr/bin/dircolors ]; then @@ -1519,9 +1646,12 @@ fi alias ll='ls -alFh' alias la='ls -A' alias l='ls -CF' -alias lt='ls -alFht' # Sort by modification time, newest first -alias ltr='ls -alFhtr' # Sort by modification time, oldest first -alias lS='ls -alFhS' # Sort by size, largest first +alias lt='ls -alFht' # Sort by modification time, newest first +alias ltr='ls -alFhtr' # Sort by modification time, oldest first +alias lS='ls -alFhS' # Sort by size, largest first + +# Last command with sudo +alias please='sudo $(history -p !!)' # Safety aliases to prompt before overwriting. alias rm='rm -i' @@ -1534,7 +1664,7 @@ alias ..='cd ..' alias ...='cd ../..' alias ....='cd ../../..' alias .....='cd ../../../..' -alias -- -='cd -' # Go to previous directory +alias -- -='cd -' # Go to previous directory alias ~='cd ~' alias h='history' alias c='clear' @@ -1582,6 +1712,7 @@ alias myip='curl -s ifconfig.me || curl -s icanhazip.com' # Alternatives: api.ip localip() { ip -4 addr | awk '/inet/ {print $2}' | cut -d/ -f1 | grep -v '127.0.0.1' } + alias netstat='ss' alias ping='ping -c 5' alias fastping='ping -c 100 -i 0.2' @@ -1635,12 +1766,14 @@ if command -v docker &>/dev/null; then # Docker stats alias dstats='docker stats --no-stream' alias dstatsa='docker stats' - dtop() { + dst() { docker stats --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}' } # Safe stop all (shows command instead of executing) - alias dstopall='echo "To stop all containers, run: docker stop \$(docker ps -q)"' + alias dstopa='echo "To stop all containers, run: docker stop \$(docker ps -q)"' + # Start all stopped containers + alias dstarta='docker start $(docker ps -aq)' # Docker Compose v2 aliases (check if the compose plugin exists) if docker compose version &>/dev/null; then @@ -1816,11 +1949,11 @@ fi # Enable programmable completion features. if ! shopt -oq posix; then if [ -f /usr/share/bash-completion/bash_completion ]; then - # shellcheck disable=SC1091 - . /usr/share/bash-completion/bash_completion + # shellcheck disable=SC1091 + . /usr/share/bash-completion/bash_completion elif [ -f /etc/bash_completion ]; then - # shellcheck disable=SC1091 - . /etc/bash_completion + # shellcheck disable=SC1091 + . /etc/bash_completion fi fi @@ -1861,7 +1994,7 @@ bashhelp() { cat << 'HELPTEXT' ╔════════════════════════════════════════════════════════╗ -║ .bashrc - Quick Reference ║ +║ .bashrc - Quick Reference ║ ╚════════════════════════════════════════════════════════╝ Usage: bashhelp [category] @@ -1870,179 +2003,194 @@ Categories: navigation, files, system, docker, git, network ═══════════════════════════════════════════════════════════════════ 📁 NAVIGATION & DIRECTORY ═══════════════════════════════════════════════════════════════════ - .. Go up one directory - ... Go up two directories - .... Go up three directories - ..... Go up four directories - - Go to previous directory - ~ Go to home directory + .. Go up one directory + ... Go up two directories + .... Go up three directories + ..... Go up four directories + - Go to previous directory + ~ Go to home directory - mkcd
Filter logs
- dcvalidate Validate compose file
+ dcstatus Status & resource usage
+ dcreload Filter logs
+ dcvalidate Validate compose file
Examples:
dsh mycontainer
@@ -2182,20 +2337,20 @@ HELPTEXT
═══ GIT SHORTCUTS ═══
- gs git status
- ga git add
- gc git commit
- gp git push
- gl git log (graph)
- gd git diff
- gb git branch
- gco git checkout
+ gs git status
+ ga git add
+ gc git commit
+ gp git push
+ gl git log (graph)
+ gd git diff
+ gb git branch
+ gco git checkout
Examples:
- gs # Check status
- ga . # Add all changes
- gc -m "Update docs" # Commit
- gp # Push to remote
+ gs # Check status
+ ga . # Add all changes
+ gc -m "Update docs" # Commit
+ gp # Push to remote
HELPTEXT
;;
@@ -2205,16 +2360,18 @@ HELPTEXT
═══ NETWORK COMMANDS ═══
- myip Show external IP
- localip Show local IP(s)
- ports Show listening ports
- listening Ports with process info
- ping Ping (5 packets)
- fastping Fast ping (100 packets)
- netstat Network connections
+ myip Show external IP
+ localip Show local IP(s)
+ netsum Network connection summary
+ kssh SSH wrapper for kitty
+ ports Show listening ports
+ listening Ports with process info
+ ping Ping (5 packets)
+ fastping Fast ping (100 packets)
+ netstat Network connections (ss)
Examples:
- myip # Get public IP
+ myip # Get public IP
listening | grep 80
ping google.com
@@ -2231,6 +2388,7 @@ HELPTEXT
}
# Preserve Bash's builtin `help` while integrating bashhelp
+# This wrapper routes custom help to bashhelp, bash builtins to builtin help
help() {
case "${1:-}" in
""|all|navigation|files|system|docker|git|network)
@@ -3680,6 +3838,68 @@ EOF
fi
print_warning "NOTE: '$USERNAME' must log out and back in to use Docker without sudo."
log "Docker installation completed."
+ # Offer dtop installation
+ install_dtop_optional
+}
+
+install_dtop_optional() {
+ if sudo sh -c 'command -v dtop' >/dev/null 2>&1 || command -v dtop >/dev/null 2>&1; then
+ print_info "dtop is already installed."
+ return 0
+ fi
+ if ! confirm "Install 'dtop' (Docker container monitoring TUI)?"; then
+ print_info "Skipping dtop installation."
+ return 0
+ fi
+ print_info "Installing dtop for user '$USERNAME'..."
+ local DTOP_INSTALLER="/tmp/dtop-installer.sh"
+ if ! curl -fsSL "https://github.com/amir20/dtop/releases/latest/download/dtop-installer.sh" -o "$DTOP_INSTALLER"; then
+ print_warning "Failed to download dtop installer. Continuing setup..."
+ log "Failed to download dtop installer."
+ return 0
+ fi
+ chmod +x "$DTOP_INSTALLER"
+ trap 'rm -f "$DTOP_INSTALLER"' RETURN
+ local USER_HOME
+ USER_HOME=$(getent passwd "$USERNAME" | cut -d: -f6)
+ local USER_LOCAL_BIN="$USER_HOME/.local/bin"
+ if [[ ! -d "$USER_LOCAL_BIN" ]]; then
+ print_info "Creating $USER_LOCAL_BIN..."
+ if ! sudo -u "$USERNAME" mkdir -p "$USER_LOCAL_BIN"; then
+ print_warning "Failed to create $USER_LOCAL_BIN. Skipping dtop."
+ return 0
+ fi
+ fi
+ if sudo -u "$USERNAME" bash "$DTOP_INSTALLER" < /dev/null >> "$LOG_FILE" 2>&1; then
+ # Verify installation
+ if [[ -f "$USER_LOCAL_BIN/dtop" ]]; then
+ sudo -u "$USERNAME" chmod +x "$USER_LOCAL_BIN/dtop"
+ local BASHRC="$USER_HOME/.bashrc"
+ if [[ -f "$BASHRC" ]] && ! grep -q "\.local/bin" "$BASHRC"; then
+ print_info "Adding ~/.local/bin to PATH in $BASHRC..."
+ {
+ echo ''
+ echo '# Add local bin to PATH'
+ # shellcheck disable=SC2016
+ echo 'if [ -d "$HOME/.local/bin" ]; then PATH="$HOME/.local/bin:$PATH"; fi'
+ } >> "$BASHRC"
+ chown "$USERNAME:$USERNAME" "$BASHRC"
+ if grep -q "\.local/bin" "$BASHRC"; then
+ print_info "PATH configuration updated successfully."
+ else
+ print_warning "Failed to update PATH, but dtop is still installed."
+ fi
+ fi
+ print_success "dtop installed successfully to $USER_LOCAL_BIN."
+ log "dtop installed to $USER_LOCAL_BIN for user $USERNAME"
+ else
+ print_warning "dtop installer finished, but binary not found at $USER_LOCAL_BIN/dtop"
+ log "dtop binary missing after user installation attempt."
+ fi
+ else
+ print_warning "dtop installation script failed. Continuing setup..."
+ log "dtop installation script failed."
+ fi
}
install_tailscale() {