Compare commits

...

14 Commits

Author SHA1 Message Date
hykocx db3f5d6652 docs(readme): remove applications, tools, project structure, and release sections 2026-05-12 22:49:10 -04:00
hykocx 2a50724328 fix(benchmark): prevent disk exhaustion during benchmark runs
- add EXIT/INT/TERM trap to clean up /tmp artifacts on script exit or interrupt
- introduce DISK_SAFETY_MARGIN_MB constant to reserve 200MB free at all times
- build disk test configurations dynamically based on available space instead of a fixed list
- re-check free space before each test pass and skip configs that no longer fit
- return early with SCORE_DISK_WRITE=0 when no test fits in available space
2026-05-12 22:43:14 -04:00
hykocx 15c42e1f24 feat(lib): add require_disk_space helper and enforce pre-flight disk checks
- add `require_disk_space` function to lib/common.sh with dedup logic for shared filesystems
- gate cloudpanel, coolify, pterodactyl installs behind a 2–3 GB disk check
- gate uptime-kuma, proxmox update, harden, update-server, and server-benchmark behind 300–1024 MB disk checks
- fail early with a clear error before apt installs or config writes can leave the system in a partial state
2026-05-12 22:39:19 -04:00
hykocx 173b3bd581 refactor(nav): use exit code 75 to suppress redundant "Press Enter" prompt
- app submenus return 75 on back/exit instead of 0
- index menus capture child exit code and skip pause when rc is 75
- applies to apps (coolify, pterodactyl, uptime-kuma, cloudpanel, proxmox) and tools (server-benchmark, system-infos) indexes
2026-05-12 22:28:16 -04:00
hykocx c7dcaed0bf refactor(ui): replace neon palette and ad-hoc echo with minimal design system
- reduce color palette to red/cyan/white/gray; alias legacy vars onto CYAN
- add show_box_top/mid/bottom, show_separator, show_menu_item, show_prompt helpers to lib/common.sh
- update apps/index.sh and tools/index.sh to use new UI helpers instead of inline echo/ANSI
- drop BLINK, REV escape codes and remove hardcoded prompt strings from callers
2026-05-12 22:20:36 -04:00
hykocx ade8e76a68 style(ui): apply cyberpunk neon theme across all menus
- replace color palette with Cyberpunk 2077 neon variants in lib/common.sh
- add DIM, BLINK, REV attributes and show_title ui helper
- make show_separator terminal-width-aware via tput cols
- restyle menus with ◢ numbered items and ▸_ prompt indicator
- support two-digit input aliases (01-05, 00) alongside single-digit
- update status tokens: [✓]/[✗] → [OK]/[KO], PURPLE → MAGENTA
- apply consistent ui changes to lxs.sh and tools/index.sh
2026-05-12 22:01:57 -04:00
hykocx e0bd8e0a97 feat(lxs): reload script automatically after successful update 2026-05-12 21:50:21 -04:00
hykocx db6b349bc4 chore(tools): prompt user to reboot when running interactively after updates 2026-05-12 21:46:57 -04:00
hykocx cdcd89b5b2 feat(tools): add welcome message (MOTD) management tool
- add `tools/welcome-message.sh` with view, set, and reset actions
- register `lxs tool welcome|motd` command in `lxs.sh`
- add option 7 to interactive tools menu in `tools/index.sh`
- document `lxs tool welcome` in README
2026-05-12 21:44:05 -04:00
hykocx c3002ef274 feat(tools): add root-ssh-login tool to enable or disable root SSH password login
- add tools/root-ssh-login.sh with interactive menu, --enable/--disable/--status flags, and sshd drop-in config management
- register root-ssh-login in lxs.sh dispatch and tools/index.sh interactive menu
- document new tool in README.md command reference and project structure
2026-05-12 21:42:12 -04:00
hykocx fc50a71763 feat(tools): add root-password and update-server tools
- add `tools/root-password.sh` to change root password interactively or generate a random one
- add `tools/update-server.sh` to run apt update/upgrade/autoremove/autoclean
- register both tools in `lxs.sh` dispatcher and `tools/index.sh` interactive menu
- document new tools in README.md
2026-05-12 21:39:38 -04:00
hykocx 8383612fe8 refactor(harden): remove ssh key-only module and improve port detection
- drop DO_SSH flag, --no-ssh option, and setup_ssh() function entirely
- replace inline sshd_config port read with sshd_effective_port() that also scans sshd_config.d drop-ins
- replace raw DEBIAN_FRONTEND export with apt_noninteractive helper and add wait_for_apt guard
- replace hardcoded separator strings with show_separator calls
2026-05-12 21:35:25 -04:00
hykocx dc1475c64a refactor: extract interactive menus into dedicated index scripts
- move apps menu from inline lxs.sh function to apps/index.sh
- add tools/index.sh as entry point for tools menu
- rename system-tools.sh to system-infos.sh
- simplify help text to point users to the interactive menu
- update README directory tree to reflect new structure
2026-05-12 18:16:33 -04:00
hykocx dbe4b95190 refactor: rename system-tools.sh to system-infos.sh 2026-05-12 18:16:19 -04:00
17 changed files with 1200 additions and 353 deletions
-44
View File
@@ -33,50 +33,6 @@ lxs version # Show version
lxs help # Show help lxs help # Show help
``` ```
### Applications
| Command | Description |
|---|---|
| `lxs install coolify` | Self-hosted PaaS |
| `lxs install pterodactyl` | Game server management panel |
| `lxs install uptime-kuma` | Monitoring tool |
| `lxs install cloudpanel` | Web hosting control panel |
| `lxs install proxmox` | Proxmox VE management tools |
### Tools
| Command | Description |
|---|---|
| `lxs tool system` | System monitoring and diagnostics |
| `lxs tool benchmark` | Server benchmark (CPU / RAM / disk / network) |
| `lxs tool harden` | Baseline hardening: UFW + fail2ban + SSH key-only + unattended-upgrades |
## Project structure
```
lxs/
├── lxs.sh # Main entrypoint (menu + CLI dispatcher)
├── VERSION # Single source of truth for the version (bump on every release)
├── lib/
│ └── common.sh # Shared helpers (colors, loggers, spinner)
├── apps/ # Application installers
│ ├── coolify.sh
│ ├── pterodactyl.sh
│ ├── uptime-kuma.sh
│ ├── cloudpanel.sh
│ └── proxmox.sh
└── tools/ # System tools
├── system-tools.sh
├── server-benchmark.sh
└── harden.sh
```
After `lxs setup`, the full tree lives in `/usr/local/share/lxs/` and sub-scripts execute from disk. If `lxs.sh` is run without being installed (the one-liner mode), it falls back to downloading sub-scripts on demand.
### Releasing a new version
Bump only the [VERSION](VERSION) file — `lxs.sh` reads it at startup. Installed clients also fetch this file (cached 24 h in `~/.cache/lxs/`) to detect updates. In one-shot mode (`curl … | bash`) the file isn't on disk next to the script, so `lxs version` reports `dev`.
## Requirements ## Requirements
- Debian or Ubuntu (other distros may work but are not tested) - Debian or Ubuntu (other distros may work but are not tested)
+2 -1
View File
@@ -220,6 +220,7 @@ install_cloudpanel() {
return 0 return 0
fi fi
require_disk_space 2048 || return 1
check_requirements || return 1 check_requirements || return 1
echo "" echo ""
@@ -637,7 +638,7 @@ main() {
2) update_cloudpanel ;; 2) update_cloudpanel ;;
3) show_status ;; 3) show_status ;;
4) configure_rclone_backblaze ;; 4) configure_rclone_backblaze ;;
0) return 0 ;; 0) return 75 ;;
*) echo -e "${RED}Invalid option${NC}" ;; *) echo -e "${RED}Invalid option${NC}" ;;
esac esac
+2 -1
View File
@@ -67,6 +67,7 @@ install_coolify() {
return 0 return 0
fi fi
require_disk_space 3072 || return 1
check_requirements || return 1 check_requirements || return 1
echo "" echo ""
@@ -252,7 +253,7 @@ main() {
2) update_coolify ;; 2) update_coolify ;;
3) show_status ;; 3) show_status ;;
4) show_post_installation_guide ;; 4) show_post_installation_guide ;;
0) return 0 ;; 0) return 75 ;;
*) echo -e "${RED}Invalid option${NC}" ;; *) echo -e "${RED}Invalid option${NC}" ;;
esac esac
Executable
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
# LXS - Apps index
# Description: Interactive menu listing the application installers in apps/
# Author: LXS
# Date: 2025
# Load LXS common library (colors, separator, run_spinner, loggers)
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
eval "$_lib"
unset _lib
# Run a sibling app script. Prefers a file next to this script (installed
# layout); falls back to downloading from LXS_RAW_BASE.
run_sibling() {
local script_path=$1
shift
local script_name self_dir resolved src="${BASH_SOURCE[0]}"
script_name=$(basename "$script_path")
if [ -n "$src" ]; then
resolved=$(readlink -f "$src" 2>/dev/null) \
|| resolved=$(realpath "$src" 2>/dev/null) \
|| resolved="$src"
self_dir=$(dirname "$resolved")
fi
if [ -n "$self_dir" ] && [ -f "${self_dir}/${script_name}" ]; then
chmod +x "${self_dir}/${script_name}" 2>/dev/null || true
"${self_dir}/${script_name}" "$@"
return $?
fi
local temp_file exit_code
temp_file=$(mktemp "/tmp/lxs.${script_name%.*}.XXXXXX.sh")
echo -e "${CYAN}[..] Fetching ${BOLD}${script_name}${NC}${CYAN}...${NC}"
if curl -fsSL -H "Cache-Control: no-cache" -o "${temp_file}" "${LXS_RAW_BASE}/${script_path}"; then
echo -e "${GREEN}[OK] Payload acquired${NC}"
chmod +x "${temp_file}"
"${temp_file}" "$@"
exit_code=$?
rm -f "${temp_file}"
return $exit_code
else
echo -e "${RED}[KO] Failed to download ${script_path}${NC}"
rm -f "${temp_file}"
return 1
fi
}
menu_apps() {
while true; do
clear
show_box_top "APPLICATIONS" "APP_REPOSITORY"
echo ""
show_menu_item "01" "Coolify"
show_menu_item "02" "Pterodactyl Panel"
show_menu_item "03" "Uptime Kuma"
show_menu_item "04" "CloudPanel"
show_menu_item "05" "Proxmox VE Tools"
show_menu_item "00" "BACK" "" exit
echo ""
show_box_bottom
echo ""
show_prompt
read -r choice
child_rc=0
case $choice in
1|01) run_sibling "apps/coolify.sh"; child_rc=$? ;;
2|02) run_sibling "apps/pterodactyl.sh"; child_rc=$? ;;
3|03) run_sibling "apps/uptime-kuma.sh"; child_rc=$? ;;
4|04) run_sibling "apps/cloudpanel.sh"; child_rc=$? ;;
5|05) run_sibling "apps/proxmox.sh"; child_rc=$? ;;
0|00) return ;;
*) echo -e "${RED}[KO] Invalid protocol. Select 0-5.${NC}"; sleep 1; continue ;;
esac
# Exit code 75 from a child means it already paused on its own
# (its own "Back" or end-of-run prompt) — skip the redundant prompt.
if [ "$child_rc" -ne 75 ]; then
echo ""
read -r -p "Press Enter to continue..."
fi
done
}
menu_apps
+3 -1
View File
@@ -245,6 +245,8 @@ update_proxmox() {
check_proxmox || return 1 check_proxmox || return 1
require_disk_space 1024 || return 1
# Configure non-interactive mode # Configure non-interactive mode
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_MODE=a export NEEDRESTART_MODE=a
@@ -371,7 +373,7 @@ main() {
7) update_proxmox ;; 7) update_proxmox ;;
8) clear_cache ;; 8) clear_cache ;;
9) open_firewall_ports ;; 9) open_firewall_ports ;;
0) return 0 ;; 0) return 75 ;;
*) echo -e "${RED}Invalid option${NC}" ;; *) echo -e "${RED}Invalid option${NC}" ;;
esac esac
+3 -1
View File
@@ -212,6 +212,8 @@ install_pterodactyl() {
return 0 return 0
fi fi
require_disk_space 3072 || return 1
echo "[1/10] Installing dependencies..." echo "[1/10] Installing dependencies..."
install_dependencies && install_composer install_dependencies && install_composer
echo "" echo ""
@@ -323,7 +325,7 @@ main() {
case $choice in case $choice in
1) install_pterodactyl ;; 1) install_pterodactyl ;;
2) show_status ;; 2) show_status ;;
0) return 0 ;; 0) return 75 ;;
*) echo -e "${RED}Invalid option${NC}" ;; *) echo -e "${RED}Invalid option${NC}" ;;
esac esac
+3 -1
View File
@@ -94,6 +94,8 @@ install_uptime_kuma() {
return 0 return 0
fi fi
require_disk_space 1024 || return 1
echo "[1/4] Installing dependencies..." echo "[1/4] Installing dependencies..."
install_dependencies install_dependencies
echo "" echo ""
@@ -242,7 +244,7 @@ main() {
1) install_uptime_kuma ;; 1) install_uptime_kuma ;;
2) update_uptime_kuma ;; 2) update_uptime_kuma ;;
3) show_status ;; 3) show_status ;;
0) return 0 ;; 0) return 75 ;;
*) echo -e "${RED}Invalid option${NC}" ;; *) echo -e "${RED}Invalid option${NC}" ;;
esac esac
+148 -24
View File
@@ -7,31 +7,119 @@
# Repo: https://git.hyko.cx/hykocx/lxs # Repo: https://git.hyko.cx/hykocx/lxs
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# Colors # Colors — minimal palette: red, cyan, white (+ gray as a white shade)
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
RED='\033[0;31m' RED='\033[38;2;255;64;64m' # errors, destructive actions
GREEN='\033[0;32m' CYAN='\033[38;2;0;229;255m' # accents, titles, OK, info
YELLOW='\033[1;33m' WHITE='\033[38;2;240;240;240m' # primary text
PURPLE='\033[1;94m' GRAY='\033[38;2;140;140;140m' # secondary text, comments, separators
CYAN='\033[0;36m'
WHITE='\033[1;37m'
GRAY='\033[0;37m'
NC='\033[0m' NC='\033[0m'
BOLD='\033[1m' BOLD='\033[1m'
DIM='\033[2m'
# Legacy aliases — older sub-scripts still reference these. Map onto CYAN
# so the palette stays at red/cyan/white without breaking them.
YELLOW="$CYAN"
GREEN="$CYAN"
MAGENTA="$CYAN"
PURPLE="$CYAN"
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# UI helpers # UI helpers — title + horizontal rule, no closed box.
#
# Layout:
# show_box_top "TITLE" ["RIGHT_TAG"] → TITLE [ RIGHT ]
# ─────────────────────────────────
# show_box_mid "SECTION" → ─ SECTION ─────────────────────
# show_box_bottom → ───────────────────────────────
# show_separator → ─── (light gray divider)
# show_menu_item "01" "LABEL" "desc" → [01] LABEL // desc
# show_prompt → >
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
show_separator() { _lxs_term_cols() {
echo -e "${GRAY}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" local cols
cols=$(tput cols 2>/dev/null || echo 80)
[ "$cols" -gt 100 ] && cols=100
[ "$cols" -lt 60 ] && cols=60
echo "$cols"
} }
info() { echo -e "${PURPLE}[*]${NC} $*"; } show_box_top() {
ok() { echo -e "${GREEN}[✓]${NC} $*"; } local title="$1" right="${2:-}"
warn() { echo -e "${YELLOW}[!]${NC} $*"; } local cols pad_len pad
err() { echo -e "${RED}[✗]${NC} $*" >&2; } cols=$(_lxs_term_cols)
if [ -n "$right" ]; then
pad_len=$(( cols - 2 - ${#title} - ${#right} - 4 ))
[ "$pad_len" -lt 1 ] && pad_len=1
pad=$(printf ' %.0s' $(seq 1 "$pad_len"))
printf " ${CYAN}${BOLD}%s${NC}%s${GRAY}[ ${CYAN}%s${GRAY} ]${NC}\n" \
"$title" "$pad" "$right"
else
printf " ${CYAN}${BOLD}%s${NC}\n" "$title"
fi
_lxs_hr "$cols"
}
show_box_mid() {
local title="$1"
local cols fill_len fill
cols=$(_lxs_term_cols)
fill_len=$(( cols - ${#title} - 4 ))
[ "$fill_len" -lt 2 ] && fill_len=2
fill=$(printf '─%.0s' $(seq 1 "$fill_len"))
printf "${GRAY}${CYAN}${BOLD}%s${NC} ${GRAY}%s${NC}\n" "$title" "$fill"
}
show_box_bottom() {
_lxs_hr "$(_lxs_term_cols)"
}
show_separator() {
_lxs_hr "$(_lxs_term_cols)"
}
_lxs_hr() {
local cols=$1 fill
fill=$(printf '─%.0s' $(seq 1 "$cols"))
printf "${GRAY}%s${NC}\n" "$fill"
}
show_title() {
local title="$1"
local subtitle="${2:-}"
echo ""
if [ -n "$subtitle" ]; then
show_box_top "$title" "$subtitle"
else
show_box_top "$title"
fi
}
# Render a menu line. Pass "exit" as 4th arg to color the key red.
# show_menu_item "01" "APPLICATIONS" "deploy stacks"
# show_menu_item "00" "EXIT" "quit shell" exit
show_menu_item() {
local key="$1" label="$2" desc="${3:-}" kind="${4:-}"
local key_color="${CYAN}"
[ "$kind" = "exit" ] && key_color="${RED}"
if [ -n "$desc" ]; then
printf " ${key_color}[%s]${NC} ${WHITE}${BOLD}%-18s${NC} ${GRAY}// %s${NC}\n" \
"$key" "$label" "$desc"
else
printf " ${key_color}[%s]${NC} ${WHITE}${BOLD}%s${NC}\n" "$key" "$label"
fi
}
show_prompt() {
echo -e -n " ${CYAN}>${NC} "
}
info() { echo -e "${CYAN}[..]${NC} $*"; }
ok() { echo -e "${CYAN}[OK]${NC} $*"; }
warn() { echo -e "${CYAN}[!!]${NC} $*"; }
err() { echo -e "${RED}[KO]${NC} $*" >&2; }
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# Spinner — runs a shell command, redirects output to a log file, shows # Spinner — runs a shell command, redirects output to a log file, shows
@@ -51,13 +139,13 @@ run_spinner() {
local spinstr='|/-\' local spinstr='|/-\'
local log="${LXS_LOG_FILE:-/tmp/lxs.log}" local log="${LXS_LOG_FILE:-/tmp/lxs.log}"
echo -e "${PURPLE}[*] ${message}${NC}" echo -e "${CYAN}[..] ${message}${NC}"
eval "$command" > "$log" 2>&1 & eval "$command" > "$log" 2>&1 &
local pid=$! local pid=$!
while kill -0 "$pid" 2>/dev/null; do while kill -0 "$pid" 2>/dev/null; do
local temp=${spinstr#?} local temp=${spinstr#?}
printf "\r${PURPLE}[%c]${NC} ${message}" "$spinstr" printf "\r${CYAN}[%c.]${NC} ${message}" "$spinstr"
spinstr=$temp${spinstr%"$temp"} spinstr=$temp${spinstr%"$temp"}
sleep 0.15 sleep 0.15
done done
@@ -65,9 +153,9 @@ run_spinner() {
wait "$pid" wait "$pid"
local exit_code=$? local exit_code=$?
if [ $exit_code -eq 0 ]; then if [ $exit_code -eq 0 ]; then
printf "\r${GREEN}[]${NC} ${message}\n" printf "\r${CYAN}[OK]${NC} ${message}\n"
else else
printf "\r${RED}[]${NC} ${message}\n" printf "\r${RED}[KO]${NC} ${message}\n"
fi fi
return $exit_code return $exit_code
} }
@@ -83,6 +171,42 @@ require_debian_ubuntu() {
fi fi
} }
# Verify enough free disk space before doing apt installs or writing config.
# Fails early with a clear message so we don't get half-installed packages or
# files truncated mid-write when the disk is full.
#
# Usage:
# require_disk_space # 500MB on /var (apt cache + lists)
# require_disk_space 1024 # 1024MB on /var
# require_disk_space 1024 /var /opt # 1024MB on each listed path
#
# Duplicate filesystems (e.g. /var on the same fs as /) are checked once.
require_disk_space() {
local min_mb=${1:-500}
shift 2>/dev/null || true
local paths=("$@")
[ ${#paths[@]} -eq 0 ] && paths=(/var)
local path avail fs seen=" " rc=0
for path in "${paths[@]}"; do
fs=$(df -P "$path" 2>/dev/null | awk 'NR==2 {print $1}')
if [ -z "$fs" ]; then
err "Cannot read disk usage for ${path}"
rc=1
continue
fi
[[ "$seen" == *" $fs "* ]] && continue
seen="$seen$fs "
avail=$(df -Pm "$path" 2>/dev/null | awk 'NR==2 {print $4}')
if [ "${avail:-0}" -lt "$min_mb" ]; then
err "Not enough disk space on ${path} (${fs}): ${avail}MB free, ${min_mb}MB required."
rc=1
fi
done
return $rc
}
# Re-exec the current script via sudo if not already root. Preserves env and # Re-exec the current script via sudo if not already root. Preserves env and
# arguments. Call near the top of a sub-script: # arguments. Call near the top of a sub-script:
# require_root "$0" "$@" # require_root "$0" "$@"
@@ -138,7 +262,7 @@ ufw_allow() {
else else
ufw allow "$rule" >/dev/null ufw allow "$rule" >/dev/null
fi fi
ok "UFW: allowed ${rule}${comment:+ (${comment})}" ok "UFW :: allowed ${rule}${comment:+ (${comment})}"
} }
# Wait for other apt/dpkg processes to release their locks. Up to 120s. # Wait for other apt/dpkg processes to release their locks. Up to 120s.
@@ -154,17 +278,17 @@ wait_for_apt() {
fuser /var/lib/apt/lists/lock >/dev/null 2>&1; do fuser /var/lib/apt/lists/lock >/dev/null 2>&1; do
[ "$lock_detected" = false ] && lock_detected=true && \ [ "$lock_detected" = false ] && lock_detected=true && \
echo -e "${YELLOW}[!] Waiting for other package manager to finish...${NC}" echo -e "${YELLOW}[!!] Waiting for other package manager to finish...${NC}"
wait_count=$((wait_count + 1)) wait_count=$((wait_count + 1))
[ $wait_count -ge $max_wait ] && \ [ $wait_count -ge $max_wait ] && \
echo -e "${RED}[] Timeout waiting for package manager${NC}" && return 1 echo -e "${RED}[KO] Timeout waiting for package manager${NC}" && return 1
printf "\r${GRAY}[i] Waiting... (%ds/%ds)${NC}" $wait_count $max_wait printf "\r${GRAY}[..] Waiting... (%ds/%ds)${NC}" $wait_count $max_wait
sleep 1 sleep 1
done done
[ "$lock_detected" = true ] && \ [ "$lock_detected" = true ] && \
echo -e "\n${GREEN}[] Package manager is now available${NC}" echo -e "\n${GREEN}[OK] Package manager is now available${NC}"
return 0 return 0
} }
+62 -108
View File
@@ -120,18 +120,7 @@ check_remote_version() {
} }
show_lxs_logo() { show_lxs_logo() {
echo -e "${WHITE}${BOLD}" show_box_top "LXS // v${LXS_VERSION}" "ONLINE"
echo -e "╔═══════════════════════════════════════════════════════╗"
echo -e "║ ║"
echo -e "║ ██╗ ██╗ ██╗███████╗ ║"
echo -e "║ ██║ ╚██╗██╔╝██╔════╝ ║"
echo -e "║ ██║ ╚███╔╝ ███████╗ ║"
echo -e "║ ██║ ██╔██╗ ╚════██║ ║"
echo -e "║ ███████╗██╔╝ ██╗███████║ ║"
echo -e "║ ╚══════╝╚═╝ ╚═╝╚══════╝ v${LXS_VERSION}"
echo -e "║ ║"
echo -e "╚═══════════════════════════════════════════════════════╝"
echo -e "${NC}"
} }
show_header() { show_header() {
@@ -139,37 +128,32 @@ show_header() {
show_lxs_logo show_lxs_logo
if [ "${LXS_UPDATE_AVAILABLE:-0}" = "1" ]; then if [ "${LXS_UPDATE_AVAILABLE:-0}" = "1" ]; then
echo -e "${YELLOW}[!] Nouvelle version disponible: ${LXS_REMOTE_VERSION} (actuelle: ${LXS_VERSION})${NC}"
echo -e "${GRAY} Lance: lxs update${NC}"
echo "" echo ""
echo -e " ${CYAN}[!!] UPDATE_AVAILABLE${NC} ${WHITE}v${LXS_REMOTE_VERSION}${NC} ${GRAY}// run \`lxs update\`${NC}"
fi fi
local hostname os_version kernel uptime_info ip_address cpu_model cpu_cores total_mem used_mem disk_usage local hostname os_version kernel uptime_info ip_address cpu_cores total_mem used_mem disk_usage
hostname=$(hostname 2>/dev/null || cat /etc/hostname 2>/dev/null || echo "${HOSTNAME:-unknown}") hostname=$(hostname 2>/dev/null || cat /etc/hostname 2>/dev/null || echo "${HOSTNAME:-unknown}")
os_version=$(lsb_release -ds 2>/dev/null || grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d'"' -f2) os_version=$(lsb_release -ds 2>/dev/null || grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d'"' -f2)
kernel=$(uname -r) kernel=$(uname -r)
uptime_info=$(uptime -p 2>/dev/null || uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1}') uptime_info=$(uptime -p 2>/dev/null | sed 's/^up //' || uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1}')
ip_address=$(get_public_ip) ip_address=$(get_public_ip)
cpu_model=$(grep "model name" /proc/cpuinfo | head -1 | cut -d':' -f2 | xargs)
cpu_cores=$(nproc) cpu_cores=$(nproc)
total_mem=$(free -h | awk '/^Mem:/ {print $2}') total_mem=$(free -h | awk '/^Mem:/ {print $2}')
used_mem=$(free -h | awk '/^Mem:/ {print $3}') used_mem=$(free -h | awk '/^Mem:/ {print $3}')
disk_usage=$(df -h / | awk 'NR==2 {print $3 "/" $2 " (" $5 ")"}') disk_usage=$(df -h / | awk 'NR==2 {print $3 "/" $2 " (" $5 ")"}')
echo -e "${GRAY}Hostname:${NC} $hostname" echo ""
echo -e "${GRAY}IP Address:${NC} $ip_address" printf " ${CYAN}NODE${NC} ${WHITE}%-26s${NC} ${CYAN}KERN${NC} ${WHITE}%s${NC}\n" "$hostname" "$kernel"
echo -e "${GRAY}OS:${NC} $os_version" printf " ${CYAN}ADDR${NC} ${WHITE}%-26s${NC} ${CYAN}UP${NC} ${WHITE}%s${NC}\n" "$ip_address" "$uptime_info"
echo -e "${GRAY}Kernel:${NC} $kernel" printf " ${CYAN}OS${NC} ${WHITE}%-26s${NC} ${CYAN}CPU${NC} ${WHITE}%s cores${NC}\n" "$os_version" "$cpu_cores"
echo -e "${GRAY}Uptime:${NC} $uptime_info" printf " ${CYAN}DISK${NC} ${WHITE}%-26s${NC} ${CYAN}MEM${NC} ${WHITE}%s / %s${NC}\n" "$disk_usage" "$used_mem" "$total_mem"
echo -e "${GRAY}CPU:${NC} $cpu_model ($cpu_cores cores)"
echo -e "${GRAY}Memory:${NC} $used_mem / $total_mem"
echo -e "${GRAY}Disk:${NC} $disk_usage"
echo "" echo ""
} }
check_os() { check_os() {
if ! grep -qiE 'debian|ubuntu' /etc/os-release 2>/dev/null; then if ! grep -qiE 'debian|ubuntu' /etc/os-release 2>/dev/null; then
echo -e "${YELLOW}[!] LXS is tested on Debian/Ubuntu. Other distros may not work.${NC}" echo -e "${YELLOW}[!!] LXS is tested on Debian/Ubuntu. Other distros may not work.${NC}"
read -r -p "Continue anyway? [y/N] " reply read -r -p "Continue anyway? [y/N] " reply
case "$reply" in case "$reply" in
[yY]|[yY][eE][sS]) ;; [yY]|[yY][eE][sS]) ;;
@@ -195,9 +179,9 @@ download_and_run() {
echo "" echo ""
show_separator show_separator
if [ $exit_code -eq 0 ]; then if [ $exit_code -eq 0 ]; then
echo -e "${GREEN}[] Script completed successfully${NC}" echo -e "${GREEN}[OK] Script completed successfully${NC}"
else else
echo -e "${YELLOW}[!] Script exited with code: $exit_code${NC}" echo -e "${YELLOW}[!!] Script exited with code: $exit_code${NC}"
fi fi
return $exit_code return $exit_code
fi fi
@@ -206,12 +190,12 @@ download_and_run() {
temp_file=$(mktemp "/tmp/lxs.${script_name%.*}.XXXXXX.sh") temp_file=$(mktemp "/tmp/lxs.${script_name%.*}.XXXXXX.sh")
echo "" echo ""
echo -e "${PURPLE}[*] Downloading ${BOLD}${script_name}${NC}${PURPLE}...${NC}" echo -e "${CYAN}[..] Fetching ${BOLD}${script_name}${NC}${CYAN}...${NC}"
if curl -fsSL -H "Cache-Control: no-cache" \ if curl -fsSL -H "Cache-Control: no-cache" \
-o "${temp_file}" \ -o "${temp_file}" \
"${LXS_RAW_BASE}/${script_path}"; then "${LXS_RAW_BASE}/${script_path}"; then
echo -e "${GREEN}[✓] Downloaded${NC}" echo -e "${GREEN}[OK] Payload acquired${NC}"
chmod +x "${temp_file}" chmod +x "${temp_file}"
echo "" echo ""
@@ -226,14 +210,14 @@ download_and_run() {
echo "" echo ""
show_separator show_separator
if [ $exit_code -eq 0 ]; then if [ $exit_code -eq 0 ]; then
echo -e "${GREEN}[] Script completed successfully${NC}" echo -e "${GREEN}[OK] Script completed successfully${NC}"
else else
echo -e "${YELLOW}[!] Script exited with code: $exit_code${NC}" echo -e "${YELLOW}[!!] Script exited with code: $exit_code${NC}"
fi fi
return $exit_code return $exit_code
else else
echo -e "${RED}[] Failed to download ${script_path}${NC}" echo -e "${RED}[KO] Failed to download ${script_path}${NC}"
echo -e "${RED} URL: ${LXS_RAW_BASE}/${script_path}${NC}" echo -e "${RED} URL: ${LXS_RAW_BASE}/${script_path}${NC}"
rm -f "${temp_file}" rm -f "${temp_file}"
return 1 return 1
fi fi
@@ -252,7 +236,7 @@ cmd_help() {
LXS - Linux multi-tool (v${LXS_VERSION}) LXS - Linux multi-tool (v${LXS_VERSION})
Usage: Usage:
lxs Interactive menu lxs Interactive menu (browse apps and tools)
lxs setup Install lxs and all sub-scripts to ${LXS_INSTALL_DIR} lxs setup Install lxs and all sub-scripts to ${LXS_INSTALL_DIR}
lxs update Update all installed files to latest lxs update Update all installed files to latest
lxs install <app> Install an application lxs install <app> Install an application
@@ -261,17 +245,7 @@ Usage:
lxs version Show version lxs version Show version
lxs help Show this help lxs help Show this help
Applications (lxs install ...): Run \`lxs\` for the interactive menu to browse available applications and tools.
coolify Self-hosted PaaS
pterodactyl Game server management panel
uptime-kuma Monitoring tool
cloudpanel Web hosting control panel
proxmox Proxmox VE management tools
Tools (lxs tool ...):
system System monitoring and diagnostics
benchmark Server benchmark (CPU/RAM/disk/network)
harden Baseline hardening (UFW + fail2ban + SSH key-only + auto-updates)
Source: ${LXS_REPO_URL} Source: ${LXS_REPO_URL}
EOF EOF
@@ -285,8 +259,8 @@ cmd_install() {
uptime-kuma) download_and_run "apps/uptime-kuma.sh" ;; uptime-kuma) download_and_run "apps/uptime-kuma.sh" ;;
cloudpanel) download_and_run "apps/cloudpanel.sh" ;; cloudpanel) download_and_run "apps/cloudpanel.sh" ;;
proxmox) download_and_run "apps/proxmox.sh" ;; proxmox) download_and_run "apps/proxmox.sh" ;;
"") echo -e "${RED}[] Missing app name. Try: lxs help${NC}"; return 1 ;; "") echo -e "${RED}[KO] Missing app name. Try: lxs help${NC}"; return 1 ;;
*) echo -e "${RED}[] Unknown app: $app. Try: lxs help${NC}"; return 1 ;; *) echo -e "${RED}[KO] Unknown app: $app. Try: lxs help${NC}"; return 1 ;;
esac esac
} }
@@ -294,11 +268,15 @@ cmd_tool() {
local tool="${1:-}" local tool="${1:-}"
shift || true shift || true
case "$tool" in case "$tool" in
system) download_and_run "tools/system-tools.sh" "$@" ;; system) download_and_run "tools/system-infos.sh" "$@" ;;
benchmark) download_and_run "tools/server-benchmark.sh" "$@" ;; benchmark) download_and_run "tools/server-benchmark.sh" "$@" ;;
harden) download_and_run "tools/harden.sh" "$@" ;; harden) download_and_run "tools/harden.sh" "$@" ;;
"") echo -e "${RED}[✗] Missing tool name. Try: lxs help${NC}"; return 1 ;; root-password) download_and_run "tools/root-password.sh" "$@" ;;
*) echo -e "${RED}[✗] Unknown tool: $tool. Try: lxs help${NC}"; return 1 ;; update) download_and_run "tools/update-server.sh" "$@" ;;
root-ssh-login) download_and_run "tools/root-ssh-login.sh" "$@" ;;
welcome|motd) download_and_run "tools/welcome-message.sh" "$@" ;;
"") echo -e "${RED}[KO] Missing tool name. Try: lxs help${NC}"; return 1 ;;
*) echo -e "${RED}[KO] Unknown tool: $tool. Try: lxs help${NC}"; return 1 ;;
esac esac
} }
@@ -306,7 +284,7 @@ lxs_sync_install() {
local action="${1:-update}" local action="${1:-update}"
if lxs_need_root_for "$LXS_INSTALL_DIR" || lxs_need_root_for "$LXS_BIN_PATH"; then if lxs_need_root_for "$LXS_INSTALL_DIR" || lxs_need_root_for "$LXS_BIN_PATH"; then
echo -e "${YELLOW}[!] Need sudo to write ${LXS_INSTALL_DIR}; re-running with sudo...${NC}" echo -e "${YELLOW}[!!] Need sudo to write ${LXS_INSTALL_DIR}; re-running with sudo...${NC}"
exec sudo -E LXS_INSTALL_DIR="$LXS_INSTALL_DIR" LXS_BIN_PATH="$LXS_BIN_PATH" \ exec sudo -E LXS_INSTALL_DIR="$LXS_INSTALL_DIR" LXS_BIN_PATH="$LXS_BIN_PATH" \
LXS_BRANCH="$LXS_BRANCH" LXS_REPO_URL="$LXS_REPO_URL" \ LXS_BRANCH="$LXS_BRANCH" LXS_REPO_URL="$LXS_REPO_URL" \
bash "$(lxs_self_path)" "$action" bash "$(lxs_self_path)" "$action"
@@ -314,25 +292,25 @@ lxs_sync_install() {
local work local work
work=$(mktemp -d /tmp/lxs.install.XXXXXX) || { work=$(mktemp -d /tmp/lxs.install.XXXXXX) || {
echo -e "${RED}[] Failed to create temp dir${NC}" echo -e "${RED}[KO] Failed to create temp dir${NC}"
return 1 return 1
} }
trap 'rm -rf "$work"' RETURN trap 'rm -rf "$work"' RETURN
echo -e "${PURPLE}[*] Downloading ${LXS_TARBALL_URL}...${NC}" echo -e "${CYAN}[..] Fetching ${LXS_TARBALL_URL}...${NC}"
if ! curl -fsSL -H "Cache-Control: no-cache" -o "${work}/lxs.tgz" "$LXS_TARBALL_URL"; then if ! curl -fsSL -H "Cache-Control: no-cache" -o "${work}/lxs.tgz" "$LXS_TARBALL_URL"; then
echo -e "${RED}[] Download failed${NC}" echo -e "${RED}[KO] Download failed${NC}"
return 1 return 1
fi fi
mkdir -p "${work}/extracted" mkdir -p "${work}/extracted"
if ! tar -xzf "${work}/lxs.tgz" --strip-components=1 -C "${work}/extracted"; then if ! tar -xzf "${work}/lxs.tgz" --strip-components=1 -C "${work}/extracted"; then
echo -e "${RED}[] Extraction failed${NC}" echo -e "${RED}[KO] Extraction failed${NC}"
return 1 return 1
fi fi
if [ ! -f "${work}/extracted/lxs.sh" ] || [ ! -f "${work}/extracted/VERSION" ]; then if [ ! -f "${work}/extracted/lxs.sh" ] || [ ! -f "${work}/extracted/VERSION" ]; then
echo -e "${RED}[] Tarball is missing lxs.sh or VERSION${NC}" echo -e "${RED}[KO] Tarball is missing lxs.sh or VERSION${NC}"
return 1 return 1
fi fi
@@ -340,7 +318,7 @@ lxs_sync_install() {
if command -v rsync >/dev/null 2>&1; then if command -v rsync >/dev/null 2>&1; then
rsync -a --delete "${work}/extracted/" "${LXS_INSTALL_DIR}/" || { rsync -a --delete "${work}/extracted/" "${LXS_INSTALL_DIR}/" || {
echo -e "${RED}[] rsync failed${NC}" echo -e "${RED}[KO] rsync failed${NC}"
return 1 return 1
} }
else else
@@ -348,7 +326,7 @@ lxs_sync_install() {
[ -d "$LXS_INSTALL_DIR" ] && mv "$LXS_INSTALL_DIR" "${LXS_INSTALL_DIR}.old" [ -d "$LXS_INSTALL_DIR" ] && mv "$LXS_INSTALL_DIR" "${LXS_INSTALL_DIR}.old"
mkdir -p "$LXS_INSTALL_DIR" mkdir -p "$LXS_INSTALL_DIR"
if ! cp -a "${work}/extracted/." "${LXS_INSTALL_DIR}/"; then if ! cp -a "${work}/extracted/." "${LXS_INSTALL_DIR}/"; then
echo -e "${RED}[] Copy failed; restoring previous install${NC}" echo -e "${RED}[KO] Copy failed; restoring previous install${NC}"
rm -rf "$LXS_INSTALL_DIR" rm -rf "$LXS_INSTALL_DIR"
[ -d "${LXS_INSTALL_DIR}.old" ] && mv "${LXS_INSTALL_DIR}.old" "$LXS_INSTALL_DIR" [ -d "${LXS_INSTALL_DIR}.old" ] && mv "${LXS_INSTALL_DIR}.old" "$LXS_INSTALL_DIR"
return 1 return 1
@@ -368,8 +346,8 @@ lxs_sync_install() {
local new_version local new_version
new_version=$(head -n1 "${LXS_INSTALL_DIR}/VERSION" 2>/dev/null | tr -d '[:space:]') new_version=$(head -n1 "${LXS_INSTALL_DIR}/VERSION" 2>/dev/null | tr -d '[:space:]')
echo -e "${GREEN}[] lxs ${new_version} installed in ${LXS_INSTALL_DIR}${NC}" echo -e "${GREEN}[OK] lxs ${new_version} installed in ${LXS_INSTALL_DIR}${NC}"
echo -e "${GREEN}[] Symlink: ${LXS_BIN_PATH}${LXS_INSTALL_DIR}/lxs.sh${NC}" echo -e "${GREEN}[OK] Symlink: ${LXS_BIN_PATH}${LXS_INSTALL_DIR}/lxs.sh${NC}"
} }
cmd_update() { cmd_update() {
@@ -381,70 +359,46 @@ cmd_install_self() {
} }
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# Interactive menus # Interactive menu
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
menu_install_apps() {
while true; do
show_header
echo -e "${WHITE}${BOLD}APPLICATIONS${NC}"
echo ""
echo -e " ${GREEN}[1]${NC} Coolify"
echo -e " ${GREEN}[2]${NC} Pterodactyl Panel"
echo -e " ${GREEN}[3]${NC} Uptime Kuma"
echo -e " ${GREEN}[4]${NC} CloudPanel"
echo -e " ${GREEN}[5]${NC} Proxmox VE Tools"
echo -e " ${RED}[0]${NC} Back"
echo ""
echo -e -n "${BOLD}Choice [0-5]: ${NC}"
read -r choice
case $choice in
1) download_and_run "apps/coolify.sh" ;;
2) download_and_run "apps/pterodactyl.sh" ;;
3) download_and_run "apps/uptime-kuma.sh" ;;
4) download_and_run "apps/cloudpanel.sh" ;;
5) download_and_run "apps/proxmox.sh" ;;
0) return ;;
*) echo -e "${RED}[✗] Invalid option. Please select 0-5.${NC}"; sleep 1 ;;
esac
if [ "$choice" != "0" ]; then
echo ""
read -r -p "Press Enter to continue..."
fi
done
}
main_menu() { main_menu() {
check_os check_os
check_remote_version check_remote_version
while true; do while true; do
show_header show_header
echo -e "${WHITE}${BOLD}MAIN MENU${NC}" show_box_mid "SELECT"
echo "" echo ""
echo -e " ${GREEN}[1]${NC} Applications" show_menu_item "01" "APPLICATIONS" "deploy stacks"
echo -e " ${CYAN}[2]${NC} System Tools" show_menu_item "02" "TOOLS" "sysadmin daemons"
echo -e " ${GRAY}[u]${NC} Update lxs" show_menu_item "UU" "FETCH_UPDATE" "sync remote"
echo -e " ${RED}[0]${NC} Exit" show_menu_item "00" "JACK_OUT" "exit shell" exit
echo "" echo ""
echo -e -n "${BOLD}Choice: ${NC}" show_box_bottom
echo ""
show_prompt
read -r choice read -r choice
case $choice in case $choice in
1) menu_install_apps ;; 1) download_and_run "apps/index.sh" ;;
2) download_and_run "tools/system-tools.sh" ;; 2) download_and_run "tools/index.sh" ;;
u|U) cmd_update ;; u|U|uu|UU)
0) if cmd_update && [ -x "${LXS_INSTALL_DIR}/lxs.sh" ]; then
echo ""
read -r -p "Press Enter to reload with the new version..."
exec bash "${LXS_INSTALL_DIR}/lxs.sh"
fi
;;
0|00)
clear clear
show_lxs_logo show_lxs_logo
echo -e "${GREEN}Bye.${NC}\n" echo -e "${CYAN}JACK_OUT${NC} ${GRAY}// session terminated${NC}\n"
exit 0 exit 0
;; ;;
*) echo -e "${RED}[] Invalid option.${NC}"; sleep 1 ;; *) echo -e "${RED}[KO] Invalid protocol.${NC}"; sleep 1 ;;
esac esac
if [[ "$choice" != "0" && "$choice" != "1" ]]; then if [[ "$choice" != "0" && "$choice" != "00" && "$choice" != "1" && "$choice" != "2" ]]; then
echo "" echo ""
read -r -p "Press Enter to continue..." read -r -p "Press Enter to continue..."
fi fi
@@ -464,5 +418,5 @@ case "${1:-}" in
version|-v|--version) cmd_version ;; version|-v|--version) cmd_version ;;
help|-h|--help) cmd_help ;; help|-h|--help) cmd_help ;;
"") main_menu ;; "") main_menu ;;
*) echo -e "${RED}[] Unknown command: $1${NC}"; cmd_help; exit 1 ;; *) echo -e "${RED}[KO] Unknown command: $1${NC}"; cmd_help; exit 1 ;;
esac esac
+17 -62
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# LXS - Server Hardening # LXS - Server Hardening
# Description: Apply baseline security hardening (UFW + fail2ban + SSH key-only + unattended-upgrades) # Description: Apply baseline security hardening (UFW + fail2ban + unattended-upgrades)
# Author: LXS # Author: LXS
# Date: 2025 # Date: 2025
@@ -22,7 +22,6 @@ set -u
DO_UFW=1 DO_UFW=1
DO_FAIL2BAN=1 DO_FAIL2BAN=1
DO_SSH=1
DO_UNATTENDED=1 DO_UNATTENDED=1
ASSUME_YES=0 ASSUME_YES=0
@@ -30,7 +29,6 @@ for arg in "$@"; do
case "$arg" in case "$arg" in
--no-ufw) DO_UFW=0 ;; --no-ufw) DO_UFW=0 ;;
--no-fail2ban) DO_FAIL2BAN=0 ;; --no-fail2ban) DO_FAIL2BAN=0 ;;
--no-ssh) DO_SSH=0 ;;
--no-unattended) DO_UNATTENDED=0 ;; --no-unattended) DO_UNATTENDED=0 ;;
-y|--yes) ASSUME_YES=1 ;; -y|--yes) ASSUME_YES=1 ;;
-h|--help) -h|--help)
@@ -40,7 +38,6 @@ Usage: harden.sh [options]
Options: Options:
--no-ufw Skip UFW firewall setup --no-ufw Skip UFW firewall setup
--no-fail2ban Skip fail2ban setup --no-fail2ban Skip fail2ban setup
--no-ssh Skip SSH key-only enforcement
--no-unattended Skip unattended-upgrades setup --no-unattended Skip unattended-upgrades setup
-y, --yes Skip confirmation prompt -y, --yes Skip confirmation prompt
-h, --help Show this help -h, --help Show this help
@@ -59,18 +56,27 @@ done
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
require_debian_ubuntu || exit 1 require_debian_ubuntu || exit 1
require_disk_space 500 || exit 1
SSH_PORT=$(awk '/^[[:space:]]*Port[[:space:]]+/ {print $2; exit}' /etc/ssh/sshd_config 2>/dev/null) # Read the effective Port from sshd_config + any drop-in under sshd_config.d/
sshd_effective_port() {
local files=(/etc/ssh/sshd_config)
if compgen -G "/etc/ssh/sshd_config.d/*.conf" >/dev/null; then
files+=(/etc/ssh/sshd_config.d/*.conf)
fi
awk '/^[[:space:]]*Port[[:space:]]+/ {print $2}' "${files[@]}" 2>/dev/null | tail -1
}
SSH_PORT=$(sshd_effective_port)
SSH_PORT=${SSH_PORT:-22} SSH_PORT=${SSH_PORT:-22}
echo -e "${WHITE}${BOLD}LXS Server Hardening${NC}" echo -e "${WHITE}${BOLD}LXS Server Hardening${NC}"
echo -e "${GRAY}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" show_separator
echo "The following actions will be applied to this server:" echo "The following actions will be applied to this server:"
[ $DO_UFW -eq 1 ] && echo " • UFW firewall: default deny incoming, allow SSH on port ${SSH_PORT}" [ $DO_UFW -eq 1 ] && echo " • UFW firewall: default deny incoming, allow SSH on port ${SSH_PORT}"
[ $DO_FAIL2BAN -eq 1 ] && echo " • fail2ban: enable sshd jail (bantime 1h, maxretry 5)" [ $DO_FAIL2BAN -eq 1 ] && echo " • fail2ban: enable sshd jail (bantime 1h, maxretry 5)"
[ $DO_SSH -eq 1 ] && echo " • SSH: disable password auth (key-only), prohibit-password root login"
[ $DO_UNATTENDED -eq 1 ] && echo " • unattended-upgrades: enable automatic security updates" [ $DO_UNATTENDED -eq 1 ] && echo " • unattended-upgrades: enable automatic security updates"
echo -e "${GRAY}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" show_separator
if [ $ASSUME_YES -ne 1 ]; then if [ $ASSUME_YES -ne 1 ]; then
read -r -p "Proceed? [y/N] " reply read -r -p "Proceed? [y/N] " reply
@@ -80,7 +86,8 @@ if [ $ASSUME_YES -ne 1 ]; then
esac esac
fi fi
export DEBIAN_FRONTEND=noninteractive apt_noninteractive
wait_for_apt || exit 1
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# UFW # UFW
@@ -121,55 +128,6 @@ EOF
ok "fail2ban enabled (sshd jail active)" ok "fail2ban enabled (sshd jail active)"
} }
# ═══════════════════════════════════════════════════════════════════════════
# SSH key-only
# ═══════════════════════════════════════════════════════════════════════════
has_authorized_key() {
local user_home
for user_home in /root "/home/${SUDO_USER:-}"; do
[ -z "$user_home" ] && continue
[ "$user_home" = "/home/" ] && continue
if [ -s "${user_home}/.ssh/authorized_keys" ]; then
return 0
fi
done
return 1
}
setup_ssh() {
info "Enforcing SSH key-only authentication..."
if ! has_authorized_key; then
err "No authorized_keys found for root or ${SUDO_USER:-current user}."
err "Refusing to disable password auth — you would lock yourself out."
err "Add a public key first, then re-run: harden.sh --no-ufw --no-fail2ban --no-unattended"
return 1
fi
local cfg=/etc/ssh/sshd_config
cp -n "$cfg" "${cfg}.lxs-backup"
sed -i -E \
-e 's/^[#[:space:]]*PasswordAuthentication[[:space:]]+.*/PasswordAuthentication no/' \
-e 's/^[#[:space:]]*ChallengeResponseAuthentication[[:space:]]+.*/ChallengeResponseAuthentication no/' \
-e 's/^[#[:space:]]*KbdInteractiveAuthentication[[:space:]]+.*/KbdInteractiveAuthentication no/' \
-e 's/^[#[:space:]]*PermitRootLogin[[:space:]]+.*/PermitRootLogin prohibit-password/' \
"$cfg"
grep -q '^PasswordAuthentication ' "$cfg" || echo 'PasswordAuthentication no' >> "$cfg"
grep -q '^PermitRootLogin ' "$cfg" || echo 'PermitRootLogin prohibit-password' >> "$cfg"
if sshd -t 2>/tmp/lxs_sshd_test; then
systemctl reload ssh 2>/dev/null || systemctl reload sshd
ok "SSH configured for key-only auth (backup: ${cfg}.lxs-backup)"
else
err "sshd config test failed; restoring backup."
cp "${cfg}.lxs-backup" "$cfg"
cat /tmp/lxs_sshd_test >&2
return 1
fi
}
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# unattended-upgrades # unattended-upgrades
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
@@ -192,7 +150,6 @@ EOF
[ $DO_UFW -eq 1 ] && setup_ufw [ $DO_UFW -eq 1 ] && setup_ufw
[ $DO_FAIL2BAN -eq 1 ] && setup_fail2ban [ $DO_FAIL2BAN -eq 1 ] && setup_fail2ban
[ $DO_SSH -eq 1 ] && setup_ssh || true
[ $DO_UNATTENDED -eq 1 ] && setup_unattended [ $DO_UNATTENDED -eq 1 ] && setup_unattended
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
@@ -201,11 +158,9 @@ EOF
echo "" echo ""
echo -e "${WHITE}${BOLD}Summary${NC}" echo -e "${WHITE}${BOLD}Summary${NC}"
echo -e "${GRAY}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" show_separator
command -v ufw >/dev/null && echo "UFW: $(ufw status | head -1)" command -v ufw >/dev/null && echo "UFW: $(ufw status | head -1)"
systemctl is-active fail2ban >/dev/null 2>&1 && echo "fail2ban: active" || echo "fail2ban: inactive" systemctl is-active fail2ban >/dev/null 2>&1 && echo "fail2ban: active" || echo "fail2ban: inactive"
echo "SSH password auth: $(grep -E '^PasswordAuthentication' /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}' || echo unknown)"
echo "SSH root login: $(grep -E '^PermitRootLogin' /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}' || echo unknown)"
systemctl is-active unattended-upgrades >/dev/null 2>&1 && echo "unattended-upgrades: active" || echo "unattended-upgrades: inactive" systemctl is-active unattended-upgrades >/dev/null 2>&1 && echo "unattended-upgrades: active" || echo "unattended-upgrades: inactive"
echo "" echo ""
ok "Hardening complete." ok "Hardening complete."
Executable
+94
View File
@@ -0,0 +1,94 @@
#!/bin/bash
# LXS - Tools index
# Description: Interactive menu listing the scripts in tools/
# Author: LXS
# Date: 2025
# Load LXS common library (colors, separator, run_spinner, loggers)
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
eval "$_lib"
unset _lib
# Run a sibling tool script. Prefers a file next to this script (installed
# layout); falls back to downloading from LXS_RAW_BASE.
run_sibling() {
local script_path=$1
shift
local script_name self_dir resolved src="${BASH_SOURCE[0]}"
script_name=$(basename "$script_path")
if [ -n "$src" ]; then
resolved=$(readlink -f "$src" 2>/dev/null) \
|| resolved=$(realpath "$src" 2>/dev/null) \
|| resolved="$src"
self_dir=$(dirname "$resolved")
fi
# self_dir points at tools/; the install layout puts siblings here too.
if [ -n "$self_dir" ] && [ -f "${self_dir}/${script_name}" ]; then
chmod +x "${self_dir}/${script_name}" 2>/dev/null || true
"${self_dir}/${script_name}" "$@"
return $?
fi
local temp_file exit_code
temp_file=$(mktemp "/tmp/lxs.${script_name%.*}.XXXXXX.sh")
echo -e "${CYAN}[..] Fetching ${BOLD}${script_name}${NC}${CYAN}...${NC}"
if curl -fsSL -H "Cache-Control: no-cache" -o "${temp_file}" "${LXS_RAW_BASE}/${script_path}"; then
echo -e "${GREEN}[OK] Payload acquired${NC}"
chmod +x "${temp_file}"
"${temp_file}" "$@"
exit_code=$?
rm -f "${temp_file}"
return $exit_code
else
echo -e "${RED}[KO] Failed to download ${script_path}${NC}"
rm -f "${temp_file}"
return 1
fi
}
menu_tools() {
while true; do
clear
show_box_top "TOOLS" "SYS_DAEMONS"
echo ""
show_menu_item "01" "System Infos"
show_menu_item "02" "Server Benchmark"
show_menu_item "03" "Harden Server"
show_menu_item "04" "Change Root Password"
show_menu_item "05" "Update Server"
show_menu_item "06" "Root SSH Password Login"
show_menu_item "07" "Welcome Message (MOTD)"
show_menu_item "00" "BACK" "" exit
echo ""
show_box_bottom
echo ""
show_prompt
read -r choice
child_rc=0
case $choice in
1|01) run_sibling "tools/system-infos.sh"; child_rc=$? ;;
2|02) run_sibling "tools/server-benchmark.sh"; child_rc=$? ;;
3|03) run_sibling "tools/harden.sh"; child_rc=$? ;;
4|04) run_sibling "tools/root-password.sh"; child_rc=$? ;;
5|05) run_sibling "tools/update-server.sh"; child_rc=$? ;;
6|06) run_sibling "tools/root-ssh-login.sh"; child_rc=$? ;;
7|07) run_sibling "tools/welcome-message.sh"; child_rc=$? ;;
0|00) return ;;
*) echo -e "${RED}[KO] Invalid protocol. Select 0-7.${NC}"; sleep 1; continue ;;
esac
# Exit code 75 from a child means it already paused on its own
# (its own "Back" or end-of-run prompt) — skip the redundant prompt.
if [ "$child_rc" -ne 75 ]; then
echo ""
read -r -p "Press Enter to continue..."
fi
done
}
menu_tools
+121
View File
@@ -0,0 +1,121 @@
#!/bin/bash
# LXS - Change root password
# Description: Change the root account password (interactive or generated)
# Author: LXS
# Date: 2025
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
eval "$_lib"
unset _lib
export LXS_LOG_FILE="/tmp/lxs_root_password.log"
require_root "$0" "$@"
set -u
# ═══════════════════════════════════════════════════════════════════════════
# Arguments
# ═══════════════════════════════════════════════════════════════════════════
MODE=""
PASSWORD_LENGTH=24
for arg in "$@"; do
case "$arg" in
-g|--generate) MODE="generate" ;;
-i|--interactive) MODE="interactive" ;;
--length=*) PASSWORD_LENGTH="${arg#*=}" ;;
-h|--help)
cat <<EOF
Usage: root-password.sh [options]
Options:
-i, --interactive Prompt for the new password (passwd root)
-g, --generate Generate a strong random password and apply it
--length=N Length of the generated password (default: 24)
-h, --help Show this help
With no option, an interactive menu is shown.
EOF
exit 0
;;
*)
echo -e "${RED}Unknown option: $arg${NC}" >&2
exit 1
;;
esac
done
# ═══════════════════════════════════════════════════════════════════════════
# Actions
# ═══════════════════════════════════════════════════════════════════════════
change_interactive() {
info "Setting root password interactively..."
show_separator
if passwd root; then
show_separator
ok "Root password updated"
return 0
fi
show_separator
err "Failed to update root password"
return 1
}
change_generated() {
if ! [[ "$PASSWORD_LENGTH" =~ ^[0-9]+$ ]] || [ "$PASSWORD_LENGTH" -lt 12 ]; then
err "--length must be a number ≥ 12 (got: ${PASSWORD_LENGTH})"
return 1
fi
local new_password
new_password=$(generate_password "$PASSWORD_LENGTH")
if [ -z "$new_password" ]; then
err "Failed to generate a password"
return 1
fi
if ! echo "root:${new_password}" | chpasswd; then
err "Failed to apply the generated password"
return 1
fi
show_separator
ok "Root password updated"
echo ""
echo -e "${WHITE}${BOLD}New root password:${NC} ${YELLOW}${new_password}${NC}"
echo ""
warn "Store this password in a secure place. It will not be shown again."
show_separator
return 0
}
# ═══════════════════════════════════════════════════════════════════════════
# Menu (when no mode is given on the CLI)
# ═══════════════════════════════════════════════════════════════════════════
if [ -z "$MODE" ]; then
echo -e "${WHITE}${BOLD}LXS - Change root password${NC}"
show_separator
echo -e " ${CYAN}[1]${NC} Set a new password interactively"
echo -e " ${CYAN}[2]${NC} Generate a strong random password"
echo -e " ${RED}[0]${NC} Cancel"
echo ""
echo -e -n "${BOLD}Choice [0-2]: ${NC}"
read -r choice
case "$choice" in
1) MODE="interactive" ;;
2) MODE="generate" ;;
0|"") info "Cancelled."; exit 0 ;;
*) err "Invalid option."; exit 1 ;;
esac
fi
case "$MODE" in
interactive) change_interactive ;;
generate) change_generated ;;
esac
+189
View File
@@ -0,0 +1,189 @@
#!/bin/bash
# LXS - Root SSH password login
# Description: Enable or disable root login over SSH with a password
# Author: LXS
# Date: 2025
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
eval "$_lib"
unset _lib
export LXS_LOG_FILE="/tmp/lxs_root_ssh_login.log"
require_root "$0" "$@"
set -u
# Drop-in path. Numeric prefix `00-` makes it win over distro defaults — sshd
# applies the first match per option across the included files.
DROPIN_FILE="/etc/ssh/sshd_config.d/00-lxs-root-login.conf"
SSH_SERVICE="ssh"
command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files 2>/dev/null | grep -q '^sshd\.service' && SSH_SERVICE="sshd"
# ═══════════════════════════════════════════════════════════════════════════
# Arguments
# ═══════════════════════════════════════════════════════════════════════════
ACTION=""
for arg in "$@"; do
case "$arg" in
--enable|enable) ACTION="enable" ;;
--disable|disable) ACTION="disable" ;;
--status|status) ACTION="status" ;;
-h|--help)
cat <<EOF
Usage: root-ssh-login.sh [action]
Actions:
--enable Allow root to log in over SSH with a password
--disable Disallow root password login (restore SSH defaults)
--status Show the current effective settings
-h, --help Show this help
With no action, an interactive menu is shown.
Note: the change is written to ${DROPIN_FILE}
and applied by reloading the SSH service after a successful 'sshd -t'.
EOF
exit 0
;;
*)
echo -e "${RED}Unknown option: $arg${NC}" >&2
exit 1
;;
esac
done
# ═══════════════════════════════════════════════════════════════════════════
# Pre-checks
# ═══════════════════════════════════════════════════════════════════════════
if [ ! -d /etc/ssh ] || ! command -v sshd >/dev/null 2>&1; then
err "OpenSSH server is not installed."
exit 1
fi
# ═══════════════════════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════════════════════
current_setting() {
local key=$1
sshd -T 2>/dev/null | awk -v k="${key,,}" 'tolower($1)==k {print $2; exit}'
}
show_status() {
local permit pwauth
permit=$(current_setting PermitRootLogin)
pwauth=$(current_setting PasswordAuthentication)
echo -e "${WHITE}${BOLD}Current SSH login settings${NC}"
show_separator
echo -e " PermitRootLogin: ${BOLD}${permit:-unknown}${NC}"
echo -e " PasswordAuthentication: ${BOLD}${pwauth:-unknown}${NC}"
if [ -f "$DROPIN_FILE" ]; then
echo -e " Drop-in: ${GRAY}${DROPIN_FILE}${NC}"
else
echo -e " Drop-in: ${GRAY}(none — distro defaults)${NC}"
fi
show_separator
if [ "${permit:-}" = "yes" ] && [ "${pwauth:-}" = "yes" ]; then
warn "Root password login is currently ENABLED."
else
ok "Root password login is currently DISABLED."
fi
}
reload_sshd() {
if ! sshd -t 2>>"$LXS_LOG_FILE"; then
err "sshd config test failed — see ${LXS_LOG_FILE}. Reverting."
return 1
fi
if systemctl reload "$SSH_SERVICE" 2>>"$LXS_LOG_FILE"; then
ok "${SSH_SERVICE} reloaded"
return 0
fi
# Fall back to restart (some minimal images ship without reload support)
if systemctl restart "$SSH_SERVICE" 2>>"$LXS_LOG_FILE"; then
ok "${SSH_SERVICE} restarted"
return 0
fi
err "Failed to reload/restart ${SSH_SERVICE} — see ${LXS_LOG_FILE}"
return 1
}
enable_root_login() {
# Warn if root has no password — enabling password auth would be useless.
local pw_status
pw_status=$(passwd -S root 2>/dev/null | awk '{print $2}')
case "$pw_status" in
L|NP)
warn "Root account has no usable password (status: ${pw_status})."
warn "Run 'lxs tool root-password' first, otherwise login will still fail."
;;
esac
cat > "${DROPIN_FILE}.tmp" <<'EOF'
# Managed by LXS (lxs tool root-ssh-login). Remove this file to revert.
PermitRootLogin yes
PasswordAuthentication yes
EOF
chmod 644 "${DROPIN_FILE}.tmp"
mv "${DROPIN_FILE}.tmp" "$DROPIN_FILE"
if ! reload_sshd; then
rm -f "$DROPIN_FILE"
reload_sshd >/dev/null 2>&1 || true
return 1
fi
ok "Root password login over SSH is now ENABLED"
warn "This weakens server security. Disable it again when no longer needed:"
echo -e " ${GRAY}lxs tool root-ssh-login --disable${NC}"
}
disable_root_login() {
if [ ! -f "$DROPIN_FILE" ]; then
info "Drop-in not present — root password login already follows SSH defaults."
else
local backup="${DROPIN_FILE}.bak.$$"
mv "$DROPIN_FILE" "$backup"
if ! reload_sshd; then
mv "$backup" "$DROPIN_FILE"
reload_sshd >/dev/null 2>&1 || true
return 1
fi
rm -f "$backup"
fi
ok "Root password login over SSH is now DISABLED"
echo ""
show_status
}
# ═══════════════════════════════════════════════════════════════════════════
# Menu (when no action is given on the CLI)
# ═══════════════════════════════════════════════════════════════════════════
if [ -z "$ACTION" ]; then
show_status
echo ""
echo -e " ${GREEN}[1]${NC} Enable root SSH password login"
echo -e " ${YELLOW}[2]${NC} Disable root SSH password login"
echo -e " ${RED}[0]${NC} Cancel"
echo ""
echo -e -n "${BOLD}Choice [0-2]: ${NC}"
read -r choice
case "$choice" in
1) ACTION="enable" ;;
2) ACTION="disable" ;;
0|"") info "Cancelled."; exit 0 ;;
*) err "Invalid option."; exit 1 ;;
esac
fi
case "$ACTION" in
enable) enable_root_login ;;
disable) disable_root_login ;;
status) show_status ;;
esac
+76 -24
View File
@@ -12,6 +12,25 @@ eval "$_lib"
unset _lib unset _lib
export LXS_LOG_FILE="/tmp/lxs_benchmark.log" export LXS_LOG_FILE="/tmp/lxs_benchmark.log"
# Always clean up benchmark artifacts on exit, even on error or Ctrl+C. Without
# this, a partial 1-2GB dd file in /tmp can stay behind and fill the disk on
# small servers if the script is re-run.
cleanup_benchmark_artifacts() {
rm -f /tmp/lxs_test_write_* \
/tmp/lxs_test_read_* \
/tmp/lxs_write_result_*.tmp \
/tmp/lxs_read_result_*.tmp \
/tmp/lxs_cpu_result.tmp \
/tmp/lxs_ram_result.tmp \
/tmp/lxs_ping_result.tmp \
/tmp/lxs_ping_*.tmp 2>/dev/null || true
}
trap cleanup_benchmark_artifacts EXIT INT TERM
# Minimum free space (MB) to keep available during disk tests. If /tmp drops
# below this threshold mid-run we abort the remaining passes.
DISK_SAFETY_MARGIN_MB=200
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# Score Variables # Score Variables
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
@@ -28,13 +47,7 @@ SCORE_NETWORK_LATENCY=0
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
show_header() { show_header() {
echo -e "${WHITE}${BOLD}" show_box_top "SERVER PERFORMANCE BENCHMARK"
echo "╔═══════════════════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ SERVER PERFORMANCE BENCHMARK ║"
echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
} }
install_dependencies() { install_dependencies() {
@@ -45,6 +58,8 @@ install_dependencies() {
echo -e "${YELLOW}[!] sysbench not found, installing...${NC}" echo -e "${YELLOW}[!] sysbench not found, installing...${NC}"
echo "" echo ""
require_disk_space 300 || return 1
if command -v apt-get &> /dev/null; then if command -v apt-get &> /dev/null; then
run_spinner "Updating package list..." "apt-get update -qq" run_spinner "Updating package list..." "apt-get update -qq"
run_spinner "Installing sysbench..." "DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sysbench" run_spinner "Installing sysbench..." "DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sysbench"
@@ -184,17 +199,27 @@ test_disk_write() {
# Check available disk space in /tmp # Check available disk space in /tmp
local available_space=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//') local available_space=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//')
# Test configurations: size_mb, block_size, description # Build test configurations dynamically based on free space. Each test needs
local test_configs=( # room for its file PLUS the safety margin, otherwise dd can fill the disk
"100:1M:100MB Sequential" # on small servers (1-2GB VPS). Tests are added from smallest to largest.
"1024:1M:1GB Sequential" local test_configs=()
) if [ "$available_space" -gt $((100 + DISK_SAFETY_MARGIN_MB)) ]; then
test_configs+=("100:1M:100MB Sequential")
# Add 2GB test only if we have enough space (at least 3GB free) fi
if [ "$available_space" -gt 3072 ]; then if [ "$available_space" -gt $((1024 + DISK_SAFETY_MARGIN_MB)) ]; then
test_configs+=("1024:1M:1GB Sequential")
fi
if [ "$available_space" -gt $((2048 + DISK_SAFETY_MARGIN_MB)) ]; then
test_configs+=("2048:1M:2GB Sequential") test_configs+=("2048:1M:2GB Sequential")
fi fi
if [ ${#test_configs[@]} -eq 0 ]; then
echo -e "${RED}[✗] Not enough free space on /tmp (${available_space}MB) to run any write test${NC}"
SCORE_DISK_WRITE=0
echo ""
return 0
fi
local all_speeds=() local all_speeds=()
local all_speeds_mb=() local all_speeds_mb=()
local test_count=0 local test_count=0
@@ -216,6 +241,14 @@ test_disk_write() {
local pass_speeds=() local pass_speeds=()
local pass_count=0 local pass_count=0
# Re-check free space before each config; the previous test may have
# left the filesystem tighter than expected (cache, logs, etc.).
local current_free=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//')
if [ "$current_free" -lt $((size_mb + DISK_SAFETY_MARGIN_MB)) ]; then
printf "\r${YELLOW}[!]${NC} ${description}: Skipped (only ${current_free}MB free)\n"
continue
fi
# Run 3 passes for each configuration # Run 3 passes for each configuration
for pass in 1 2 3; do for pass in 1 2 3; do
# Clean up before test # Clean up before test
@@ -350,17 +383,27 @@ test_disk_read() {
# Check available disk space in /tmp # Check available disk space in /tmp
local available_space=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//') local available_space=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//')
# Test configurations: size_mb, block_size, description # Build test configurations dynamically based on free space (see write test
local test_configs=( # for rationale). The read test also writes a source file first, so the
"100:1M:100MB Sequential" # space requirement is the same as the write test.
"1024:1M:1GB Sequential" local test_configs=()
) if [ "$available_space" -gt $((100 + DISK_SAFETY_MARGIN_MB)) ]; then
test_configs+=("100:1M:100MB Sequential")
# Add 2GB test only if we have enough space (at least 3GB free) fi
if [ "$available_space" -gt 3072 ]; then if [ "$available_space" -gt $((1024 + DISK_SAFETY_MARGIN_MB)) ]; then
test_configs+=("1024:1M:1GB Sequential")
fi
if [ "$available_space" -gt $((2048 + DISK_SAFETY_MARGIN_MB)) ]; then
test_configs+=("2048:1M:2GB Sequential") test_configs+=("2048:1M:2GB Sequential")
fi fi
if [ ${#test_configs[@]} -eq 0 ]; then
echo -e "${RED}[✗] Not enough free space on /tmp (${available_space}MB) to run any read test${NC}"
SCORE_DISK_READ=0
echo ""
return 0
fi
local all_speeds_mb=() local all_speeds_mb=()
local test_count=0 local test_count=0
local spinstr='|/-\' local spinstr='|/-\'
@@ -378,10 +421,18 @@ test_disk_read() {
local block_size=$(echo "$config" | cut -d':' -f2) local block_size=$(echo "$config" | cut -d':' -f2)
local description=$(echo "$config" | cut -d':' -f3) local description=$(echo "$config" | cut -d':' -f3)
# Re-check free space before each config (same rationale as write test).
local current_free=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//')
if [ "$current_free" -lt $((size_mb + DISK_SAFETY_MARGIN_MB)) ]; then
printf "\r${YELLOW}[!]${NC} ${description}: Skipped (only ${current_free}MB free)\n"
continue
fi
# Create test file if it doesn't exist # Create test file if it doesn't exist
if [ ! -f /tmp/lxs_test_read_${size_mb} ]; then if [ ! -f /tmp/lxs_test_read_${size_mb} ]; then
dd if=/dev/zero of=/tmp/lxs_test_read_${size_mb} bs=${block_size} count=${size_mb} 2>/dev/null || { dd if=/dev/zero of=/tmp/lxs_test_read_${size_mb} bs=${block_size} count=${size_mb} 2>/dev/null || {
# If file creation fails, skip this test # If file creation fails, skip this test and remove any partial file
rm -f /tmp/lxs_test_read_${size_mb}
continue continue
} }
sync sync
@@ -760,4 +811,5 @@ fi
# Execute main function # Execute main function
main main
exit 75
+21 -69
View File
@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# LXS - System Tools # LXS - System Infos
# Description: Essential system monitoring and diagnostic tools # Description: Essential system monitoring and diagnostic tools
# Author: LXS # Author: LXS
# Date: 2025 # Date: 2025
@@ -10,67 +10,27 @@ LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; } _lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
eval "$_lib" eval "$_lib"
unset _lib unset _lib
export LXS_LOG_FILE="/tmp/lxs_system_tools.log" export LXS_LOG_FILE="/tmp/lxs_system_infos.log"
# Run a sibling tool script. Prefers a file next to this script (installed # Menu: System Infos
# layout); falls back to downloading from LXS_RAW_BASE. menu_system_infos() {
run_sibling() {
local script_path=$1
shift
local script_name self_dir resolved src="${BASH_SOURCE[0]}"
script_name=$(basename "$script_path")
if [ -n "$src" ]; then
resolved=$(readlink -f "$src" 2>/dev/null) \
|| resolved=$(realpath "$src" 2>/dev/null) \
|| resolved="$src"
self_dir=$(dirname "$resolved")
fi
if [ -n "$self_dir" ] && [ -f "${self_dir}/${script_name}" ]; then
chmod +x "${self_dir}/${script_name}" 2>/dev/null || true
"${self_dir}/${script_name}" "$@"
return $?
fi
local temp_file exit_code
temp_file=$(mktemp "/tmp/lxs.${script_name%.*}.XXXXXX.sh")
echo -e "${PURPLE}[*] Downloading ${BOLD}${script_name}${NC}${PURPLE}...${NC}"
if curl -fsSL -H "Cache-Control: no-cache" -o "${temp_file}" "${LXS_RAW_BASE}/${script_path}"; then
echo -e "${GREEN}[✓] Downloaded${NC}"
chmod +x "${temp_file}"
"${temp_file}" "$@"
exit_code=$?
rm -f "${temp_file}"
return $exit_code
else
echo -e "${RED}[✗] Failed to download ${script_path}${NC}"
rm -f "${temp_file}"
return 1
fi
}
# Menu: System Tools
menu_system_tools() {
while true; do while true; do
clear clear
echo -e "${WHITE}╔═══════════════════════════════════════════════════════╗${NC}" show_box_top "SYSTEM INFOS"
echo -e "${WHITE}║ SYSTEM TOOLS ║${NC}"
echo -e "${WHITE}╚═══════════════════════════════════════════════════════╝${NC}"
echo "" echo ""
echo -e " ${CYAN}[1]${NC} View system informations" show_menu_item "1" "View system informations"
echo -e " ${CYAN}[2]${NC} Check disk space" show_menu_item "2" "Check disk space"
echo -e " ${CYAN}[3]${NC} Check memory usage" show_menu_item "3" "Check memory usage"
echo -e " ${CYAN}[4]${NC} Check CPU load" show_menu_item "4" "Check CPU load"
echo -e " ${CYAN}[5]${NC} Check network" show_menu_item "5" "Check network"
echo -e " ${CYAN}[6]${NC} View system logs (last 50 lines)" show_menu_item "6" "View system logs (last 50 lines)"
echo -e " ${CYAN}[7]${NC} Show top resource-consuming processes" show_menu_item "7" "Show top resource-consuming processes"
echo -e " ${CYAN}[8]${NC} Check disk health (SMART)" show_menu_item "8" "Check disk health (SMART)"
echo -e " ${PURPLE}[9]${NC} Server Benchmark" show_menu_item "0" "Exit" "" exit
echo -e " ${YELLOW}[10]${NC} Harden Server"
echo -e " ${RED}[0]${NC} Exit"
echo "" echo ""
echo -e -n "${BOLD}Choice [0-10]: ${NC}" show_box_bottom
echo ""
show_prompt
read -r choice read -r choice
echo "" echo ""
@@ -79,9 +39,7 @@ menu_system_tools() {
1) 1)
# View system informations # View system informations
clear clear
echo -e "${WHITE}╔═══════════════════════════════════════════════════════╗${NC}" show_box_top "SYSTEM INFORMATION"
echo -e "${WHITE}║ SYSTEM INFORMATION ║${NC}"
echo -e "${WHITE}╚═══════════════════════════════════════════════════════╝${NC}"
echo "" echo ""
echo -e "${CYAN}${BOLD}Operating System:${NC}" echo -e "${CYAN}${BOLD}Operating System:${NC}"
@@ -306,17 +264,11 @@ menu_system_tools() {
fi fi
fi fi
;; ;;
9)
run_sibling "tools/server-benchmark.sh"
;;
10)
run_sibling "tools/harden.sh"
;;
0) 0)
exit 0 exit 75
;; ;;
*) *)
echo -e "${RED}[✗] Invalid option. Please select 0-10.${NC}" echo -e "${RED}[✗] Invalid option. Please select 0-8.${NC}"
sleep 2 sleep 2
continue continue
;; ;;
@@ -329,5 +281,5 @@ menu_system_tools() {
done done
} }
menu_system_tools menu_system_infos
+150
View File
@@ -0,0 +1,150 @@
#!/bin/bash
# LXS - Update server
# Description: Refresh package lists and upgrade installed packages
# Author: LXS
# Date: 2025
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
eval "$_lib"
unset _lib
export LXS_LOG_FILE="/tmp/lxs_update_server.log"
require_root "$0" "$@"
set -u
# ═══════════════════════════════════════════════════════════════════════════
# Arguments
# ═══════════════════════════════════════════════════════════════════════════
DO_FULL_UPGRADE=0
DO_AUTOREMOVE=1
DO_AUTOCLEAN=1
ASSUME_YES=0
for arg in "$@"; do
case "$arg" in
--full|--dist-upgrade) DO_FULL_UPGRADE=1 ;;
--no-autoremove) DO_AUTOREMOVE=0 ;;
--no-autoclean) DO_AUTOCLEAN=0 ;;
-y|--yes) ASSUME_YES=1 ;;
-h|--help)
cat <<EOF
Usage: update-server.sh [options]
Options:
--full, --dist-upgrade Use 'apt-get full-upgrade' (may add/remove packages)
--no-autoremove Skip 'apt-get autoremove'
--no-autoclean Skip 'apt-get autoclean'
-y, --yes Skip confirmation prompt
-h, --help Show this help
EOF
exit 0
;;
*)
echo -e "${RED}Unknown option: $arg${NC}" >&2
exit 1
;;
esac
done
# ═══════════════════════════════════════════════════════════════════════════
# Pre-checks
# ═══════════════════════════════════════════════════════════════════════════
require_debian_ubuntu || exit 1
if ! command -v apt-get >/dev/null 2>&1; then
err "apt-get is not available on this system"
exit 1
fi
require_disk_space 1024 || exit 1
UPGRADE_CMD="upgrade"
UPGRADE_LABEL="Upgrade installed packages"
if [ $DO_FULL_UPGRADE -eq 1 ]; then
UPGRADE_CMD="full-upgrade"
UPGRADE_LABEL="Full-upgrade (may add/remove packages)"
fi
echo -e "${WHITE}${BOLD}LXS Server Update${NC}"
show_separator
echo "The following actions will be performed:"
echo " • Refresh package lists (apt-get update)"
echo "${UPGRADE_LABEL}"
[ $DO_AUTOREMOVE -eq 1 ] && echo " • Remove unused packages (apt-get autoremove)"
[ $DO_AUTOCLEAN -eq 1 ] && echo " • Clean old package archives (apt-get autoclean)"
show_separator
if [ $ASSUME_YES -ne 1 ]; then
echo -e -n "${BOLD}Proceed? [y/N]: ${NC}"
read -r reply
case "$reply" in
[yY]|[yY][eE][sS]) ;;
*) info "Cancelled."; exit 0 ;;
esac
fi
# ═══════════════════════════════════════════════════════════════════════════
# Run
# ═══════════════════════════════════════════════════════════════════════════
apt_noninteractive
wait_for_apt || exit 1
APT_OPTS='-y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold"'
run_spinner "Refreshing package lists..." "apt-get update" || {
err "apt-get update failed — see ${LXS_LOG_FILE}"
exit 1
}
run_spinner "${UPGRADE_LABEL}..." "apt-get ${APT_OPTS} ${UPGRADE_CMD}" || {
err "apt-get ${UPGRADE_CMD} failed — see ${LXS_LOG_FILE}"
exit 1
}
if [ $DO_AUTOREMOVE -eq 1 ]; then
run_spinner "Removing unused packages..." "apt-get ${APT_OPTS} autoremove --purge" \
|| warn "autoremove failed — see ${LXS_LOG_FILE}"
fi
if [ $DO_AUTOCLEAN -eq 1 ]; then
run_spinner "Cleaning old archives..." "apt-get ${APT_OPTS} autoclean" \
|| warn "autoclean failed — see ${LXS_LOG_FILE}"
fi
show_separator
ok "Server updated"
# ═══════════════════════════════════════════════════════════════════════════
# Reboot hint
# ═══════════════════════════════════════════════════════════════════════════
if [ -f /var/run/reboot-required ]; then
echo ""
warn "A reboot is required to complete the update."
if [ -f /var/run/reboot-required.pkgs ]; then
echo -e "${GRAY}Packages requiring reboot:${NC}"
sed 's/^/ • /' /var/run/reboot-required.pkgs
fi
if [ -t 0 ]; then
echo ""
echo -e -n "${BOLD}Reboot now? [y/N]: ${NC}"
read -r reboot_reply
case "$reboot_reply" in
[yY]|[yY][eE][sS])
info "Rebooting..."
systemctl reboot
;;
*)
info "Reboot skipped. Remember to reboot later."
;;
esac
fi
fi
+203
View File
@@ -0,0 +1,203 @@
#!/bin/bash
# LXS - Welcome message (MOTD)
# Description: View, edit, or reset the SSH login welcome message (/etc/motd)
# Author: LXS
# Date: 2025
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
eval "$_lib"
unset _lib
export LXS_LOG_FILE="/tmp/lxs_welcome_message.log"
require_root "$0" "$@"
set -u
MOTD_FILE="/etc/motd"
BACKUP_FILE="/etc/motd.lxs.bak"
DYNAMIC_DIR="/etc/update-motd.d"
# ═══════════════════════════════════════════════════════════════════════════
# Arguments
# ═══════════════════════════════════════════════════════════════════════════
ACTION=""
TEXT=""
FROM_FILE=""
while [ $# -gt 0 ]; do
case "$1" in
view|--view|show|--show) ACTION="view" ;;
set|--set|edit|--edit) ACTION="set" ;;
reset|--reset) ACTION="reset" ;;
--text) shift; TEXT=${1:-} ;;
--text=*) TEXT="${1#*=}" ;;
--from-file) shift; FROM_FILE=${1:-} ;;
--from-file=*) FROM_FILE="${1#*=}" ;;
-h|--help)
cat <<EOF
Usage: welcome-message.sh [action] [options]
Actions:
view Show the current welcome message
set Set a new welcome message (see source options below)
reset Clear the welcome message (a backup is kept)
Source options for 'set' (mutually exclusive):
--text "..." Use the given string as the new message
--from-file PATH Read the new message from PATH
(none) Open an interactive editor (\$EDITOR or nano/vi)
-h, --help Show this help
With no action, an interactive menu is shown.
The welcome message lives in ${MOTD_FILE}.
On Ubuntu, dynamic MOTD scripts in ${DYNAMIC_DIR} also contribute to the
banner shown at login — those are not modified by this tool.
EOF
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}" >&2
exit 1
;;
esac
shift
done
if [ -n "$TEXT" ] && [ -n "$FROM_FILE" ]; then
err "--text and --from-file are mutually exclusive"
exit 1
fi
# ═══════════════════════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════════════════════
view_motd() {
echo -e "${WHITE}${BOLD}Current welcome message${NC} ${GRAY}(${MOTD_FILE})${NC}"
show_separator
if [ ! -s "$MOTD_FILE" ]; then
echo -e "${GRAY}(empty)${NC}"
else
cat "$MOTD_FILE"
fi
show_separator
if [ -d "$DYNAMIC_DIR" ] && compgen -G "${DYNAMIC_DIR}/*" >/dev/null; then
echo ""
warn "Dynamic MOTD scripts are present in ${DYNAMIC_DIR}:"
find "$DYNAMIC_DIR" -maxdepth 1 -type f -executable -printf ' • %f\n' | sort
echo -e "${GRAY}These run at login and add to the banner.${NC}"
fi
}
backup_motd() {
[ -f "$MOTD_FILE" ] || return 0
cp -a "$MOTD_FILE" "$BACKUP_FILE"
info "Previous message backed up to ${BACKUP_FILE}"
}
write_motd() {
local src=$1
backup_motd
install -m 644 "$src" "$MOTD_FILE"
ok "Welcome message updated"
echo ""
view_motd
}
set_from_text() {
local tmp
tmp=$(mktemp /tmp/lxs.motd.XXXXXX) || { err "mktemp failed"; return 1; }
# Preserve embedded newlines; ensure a trailing newline.
printf '%s\n' "$1" > "$tmp"
write_motd "$tmp"
rm -f "$tmp"
}
set_from_file() {
local src=$1
if [ ! -r "$src" ]; then
err "Cannot read file: ${src}"
return 1
fi
write_motd "$src"
}
set_interactive() {
local editor=${EDITOR:-}
if [ -z "$editor" ]; then
if command -v nano >/dev/null 2>&1; then editor="nano"
elif command -v vi >/dev/null 2>&1; then editor="vi"
else
err "No editor found (\$EDITOR unset; nano/vi missing). Use --text or --from-file."
return 1
fi
fi
local tmp
tmp=$(mktemp /tmp/lxs.motd.XXXXXX) || { err "mktemp failed"; return 1; }
[ -s "$MOTD_FILE" ] && cat "$MOTD_FILE" > "$tmp"
info "Opening ${editor}... save and exit to apply, leave empty to cancel."
"$editor" "$tmp"
if [ ! -s "$tmp" ]; then
warn "Empty content — change cancelled."
rm -f "$tmp"
return 0
fi
write_motd "$tmp"
rm -f "$tmp"
}
reset_motd() {
if [ ! -s "$MOTD_FILE" ]; then
info "Welcome message is already empty."
return 0
fi
backup_motd
: > "$MOTD_FILE"
chmod 644 "$MOTD_FILE"
ok "Welcome message cleared"
}
# ═══════════════════════════════════════════════════════════════════════════
# Menu (when no action is given on the CLI)
# ═══════════════════════════════════════════════════════════════════════════
if [ -z "$ACTION" ]; then
echo -e "${WHITE}${BOLD}LXS - Welcome message${NC}"
show_separator
echo -e " ${CYAN}[1]${NC} View current message"
echo -e " ${GREEN}[2]${NC} Set a new message"
echo -e " ${YELLOW}[3]${NC} Reset (clear) the message"
echo -e " ${RED}[0]${NC} Cancel"
echo ""
echo -e -n "${BOLD}Choice [0-3]: ${NC}"
read -r choice
case "$choice" in
1) ACTION="view" ;;
2) ACTION="set" ;;
3) ACTION="reset" ;;
0|"") info "Cancelled."; exit 0 ;;
*) err "Invalid option."; exit 1 ;;
esac
fi
case "$ACTION" in
view) view_motd ;;
set)
if [ -n "$TEXT" ]; then set_from_text "$TEXT"
elif [ -n "$FROM_FILE" ]; then set_from_file "$FROM_FILE"
else set_interactive
fi
;;
reset) reset_motd ;;
esac