feat(motd): richer host login banner + README git prerequisite

Validated install.sh end-to-end on a fresh Debian 13 VM (Docker apt install, clusev
user, image build, stack up, migrate, admin, MOTD) and a fully idempotent re-run.
Two follow-ups from that test:

- MOTD: replace the bare two-liner with an orange CLUSEV wordmark, the version, a LIVE
  stack status ("X/Y Dienste aktiv", green/red via `docker compose ps` at each login),
  the dashboard URL, a login hint (where the one-time credentials come from — no secret),
  and management commands. install.sh substitutes __CLUSEV_URL__, __CLUSEV_VERSION__ and
  __CLUSEV_COMPOSE__; the status lookup degrades to "unbekannt" and never fails a login.
- README: minimal Debian/Ubuntu images ship without `git`, so the documented `git clone`
  failed before reaching the installer. The install block now installs git first and the
  requirements note it. (curl is missing too but the installer self-installs it.)

Admin password strategy confirmed correct as-is: clusev:install generates a random
20-char password with must_change_password=true (forced rotation on first login) — kept
over a guessable default like "clusev", which would be a default-credential vuln and
would nullify the brute-force hardening.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-19 16:32:25 +02:00
parent 21c087a11f
commit b9b4b333e1
4 changed files with 63 additions and 11 deletions

View File

@ -11,7 +11,17 @@ getaggte Releases (Kanal `stable`, optional `beta`) — niemals Entwicklungs-Bui
## [Unreleased] ## [Unreleased]
_Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `vX.Y.Z` getaggt._ ### Geändert
- **Host-MOTD deutlich aufgewertet.** Statt zwei Zeilen jetzt eine orange CLUSEV-Wortmarke, die
Version, ein **Live-Stack-Status** („X/Y Dienste aktiv", grün/rot — bei jedem Login frisch aus
`docker compose ps`), die Dashboard-URL, eine Login-Hinweiszeile (woher die Zugangsdaten kommen —
ohne Secret) und die Verwaltungs-Befehle. `install.sh` setzt URL, Version und den Compose-Pfad ein;
fällt bei fehlendem Docker robust auf „Status unbekannt" zurück (bricht nie einen Login ab).
### Behoben
- **README-Voraussetzungen:** `git` ist auf minimalen Debian/Ubuntu-Images nicht vorinstalliert, der
`git clone`-Schritt scheiterte dort vor dem Installer. Der Install-Block installiert `git` jetzt
explizit, und „Voraussetzungen" nennt es (ein Full-VM-Installtest hat das aufgedeckt).
## [0.9.4] - 2026-06-17 ## [0.9.4] - 2026-06-17

View File

@ -28,11 +28,12 @@ and phpseclib — all running in Docker. The interface is in German (English ava
- A **Debian or Ubuntu** server (a small VM is enough) with **root** access. - A **Debian or Ubuntu** server (a small VM is enough) with **root** access.
- A public IP. Optionally a domain whose DNS points at the server (for automatic HTTPS). - A public IP. Optionally a domain whose DNS points at the server (for automatic HTTPS).
- Nothing else — the installer sets up Docker for you. - Only **git** to fetch the repo (one line below) — the installer sets up Docker and everything else.
## Install ## Install
```bash ```bash
sudo apt-get update && sudo apt-get install -y git # minimal images often lack git
git clone https://git.bave.dev/boban/clusev.git git clone https://git.bave.dev/boban/clusev.git
cd clusev cd clusev
sudo ./install.sh sudo ./install.sh

View File

@ -1,15 +1,46 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Clusev themed MOTD — printed at host login (Debian/Ubuntu run-parts MOTD). # Clusev themed MOTD — printed at host login (Debian/Ubuntu run-parts MOTD).
# Installed by install.sh; __CLUSEV_URL__ is substituted with the live access URL. # Installed by install.sh, which substitutes __CLUSEV_URL__, __CLUSEV_VERSION__ and
# Signal-orange brand, hairline rule, no emoji. Idempotent: install.sh only ever # __CLUSEV_COMPOSE__ (absolute path to docker-compose.prod.yml). Signal-orange brand,
# overwrites this single 00-clusev file, never other MOTD parts. # live stack status, no emoji (status = colour + dot). Idempotent: install.sh only ever
# overwrites this single 00-clusev file, never other MOTD parts. Never fails a login
# (every dynamic lookup degrades to "unbekannt").
O=$'\033[38;5;208m' # signal-orange (brand) O=$'\033[38;5;208m' # signal-orange (brand)
G=$'\033[38;5;42m' # online green
E=$'\033[38;5;203m' # offline red
D=$'\033[2m' # dim D=$'\033[2m' # dim
B=$'\033[1m' # bold B=$'\033[1m' # bold
R=$'\033[0m' # reset R=$'\033[0m' # reset
URL="__CLUSEV_URL__"
VERSION="__CLUSEV_VERSION__"
COMPOSE="__CLUSEV_COMPOSE__"
# Live stack status — "X/Y Dienste aktiv" (green when all up, red otherwise). Best-effort:
# any failure (no docker, daemon down, file moved) falls back to a dim "Status unbekannt".
stack_status() {
command -v docker >/dev/null 2>&1 || { printf '%s● Status unbekannt%s' "$D" "$R"; return; }
[ -f "$COMPOSE" ] || { printf '%s● Status unbekannt%s' "$D" "$R"; return; }
local total running
total=$(docker compose -f "$COMPOSE" config --services 2>/dev/null | grep -c .)
running=$(docker compose -f "$COMPOSE" ps --status running -q 2>/dev/null | grep -c .)
if [ "${total:-0}" -gt 0 ] && [ "${running:-0}" = "$total" ]; then
printf '%s● %s/%s Dienste aktiv%s' "$G" "$running" "$total" "$R"
elif [ "${total:-0}" -gt 0 ]; then
printf '%s● %s/%s aktiv — Stack pruefen%s' "$E" "$running" "$total" "$R"
else
printf '%s● Status unbekannt%s' "$D" "$R"
fi
}
printf '%s\n' "" printf '%s\n' ""
printf ' %s%sClus%sev%s %s· Fleet Control%s\n' "$B" "$O" "$R$B" "$R" "$D" "$R" printf ' %s%s█▀▀ █ █ █ █▀▀ █▀▀ █ █%s\n' "$B" "$O" "$R"
printf ' %s--------------------------------------%s\n' "$D" "$R" printf ' %s%s█ █ █ █ ▀▀█ █▀▀ █ █%s\n' "$B" "$O" "$R"
printf ' Dashboard: %s%s%s\n' "$O" "__CLUSEV_URL__" "$R" printf ' %s%s▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀ %s %sFleet Control · v%s%s\n' "$B" "$O" "$R" "$D" "$VERSION" "$R"
printf ' %sverwaltet vom Benutzer clusev · docker compose -f docker-compose.prod.yml ps%s\n' "$D" "$R" printf ' %s──────────────────────────────────────────────────────────%s\n' "$D" "$R"
printf ' %sDashboard%s %s%s%s\n' "$D" "$R" "$O" "$URL" "$R"
printf ' %sStack%s %s\n' "$D" "$R" "$(stack_status)"
printf ' %sLogin%s %sAdmin-Zugang stand einmalig im Install-Banner · zuruecksetzen: clusev:reset-admin%s\n' "$D" "$R" "$D" "$R"
printf ' %sVerwalten%s %sdocker compose -f docker-compose.prod.yml ps | logs -f | restart%s\n' "$D" "$R" "$D" "$R"
printf ' %s──────────────────────────────────────────────────────────%s\n' "$D" "$R"
printf '%s\n' "" printf '%s\n' ""

View File

@ -293,12 +293,22 @@ elif [ -n "$DOMAIN" ]; then
else else
if [ "$HTTP_PORT" = "80" ]; then ACCESS_URL="http://${SERVER_IP}"; else ACCESS_URL="http://${SERVER_IP}:${HTTP_PORT}"; fi if [ "$HTTP_PORT" = "80" ]; then ACCESS_URL="http://${SERVER_IP}"; else ACCESS_URL="http://${SERVER_IP}:${HTTP_PORT}"; fi
fi fi
# Version (for the MOTD header) and the absolute compose path (the MOTD shows a LIVE
# stack status via `docker compose ps` and runs from any cwd at login, so it needs the
# full path). cwd is the repo root (cd at the top of this script).
MOTD_VERSION="$(grep -oE "'version' => '[^']+'" config/clusev.php | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)"
MOTD_COMPOSE="$(pwd)/docker-compose.prod.yml"
motd_render() {
sed -e "s|__CLUSEV_URL__|${ACCESS_URL}|" \
-e "s|__CLUSEV_VERSION__|${MOTD_VERSION}|" \
-e "s|__CLUSEV_COMPOSE__|${MOTD_COMPOSE}|" docker/motd/00-clusev
}
if [ -d /etc/update-motd.d ]; then if [ -d /etc/update-motd.d ]; then
sed "s|__CLUSEV_URL__|${ACCESS_URL}|" docker/motd/00-clusev > /etc/update-motd.d/00-clusev motd_render > /etc/update-motd.d/00-clusev
chmod +x /etc/update-motd.d/00-clusev chmod +x /etc/update-motd.d/00-clusev
info "Dynamisches MOTD installiert (/etc/update-motd.d/00-clusev)" info "Dynamisches MOTD installiert (/etc/update-motd.d/00-clusev)"
else else
sed "s|__CLUSEV_URL__|${ACCESS_URL}|" docker/motd/00-clusev | bash > /etc/motd 2>/dev/null || true motd_render | bash > /etc/motd 2>/dev/null || true
info "Statisches MOTD geschrieben (/etc/motd)" info "Statisches MOTD geschrieben (/etc/motd)"
fi fi