#!/usr/bin/env bash # Erstinstallation von checkbot in einer frischen Debian-12/13-VM oder einem LXC (auch Ubuntu ab 22.04). Als root aus dem Git-Checkout starten. set -euo pipefail SELF=$(readlink -f "${BASH_SOURCE[0]}") # shellcheck source=lib/common.sh source "$(dirname "$SELF")/lib/common.sh" # shellcheck source=lib/caddy.sh source "$COMMON_DIR/caddy.sh" # shellcheck source=lib/timezone.sh source "$COMMON_DIR/timezone.sh" # shellcheck source=lib/preflight.sh source "$COMMON_DIR/preflight.sh" PACKAGES=(python3 python3-venv python3-pip ca-certificates curl rsync tzdata) CLI_LINK=/usr/local/sbin/checkbot-cli RELAXED_DROPIN_SRC=$DEPLOY_DIR/systemd/checkbot-lxc-relaxed.conf RELAXED_DROPIN_DST=$SYSTEMD_DIR/checkbot.service.d/lxc-relaxed.conf BACKUP_RELAXED_DROPIN_DST=$SYSTEMD_DIR/checkbot-backup.service.d/lxc-relaxed.conf SECRET_FILES=(master.key session.secret) HOSTNAME_RE='^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$|^[0-9A-Fa-f:]+$' IP_LIST_RE='^[0-9A-Fa-f:./ ,]*$' PATH_RE='^[A-Za-z0-9_./-]+$' FQDN=${CHECKBOT_FQDN:-} MONITOR_IP=${CHECKBOT_MONITOR_IP:-} TLS_MODE=${CHECKBOT_TLS_MODE:-internal} TLS_CERT=${CHECKBOT_TLS_CERT:-} TLS_KEY=${CHECKBOT_TLS_KEY:-} TIMEZONE=${CHECKBOT_TZ:-Europe/Berlin} ASSUME_NO_PROMPT=0 RELAXED=0 SKIP_PREFLIGHT=0 PREFLIGHT_ONLY=0 usage() { cat <&2; die "Unbekannte Option: $1" ;; esac done } validate_args() { (( ! SKIP_PREFLIGHT || ! PREFLIGHT_ONLY )) || die "--skip-preflight und --preflight-only schließen sich aus." if [[ -z $FQDN ]]; then FQDN=$(hostname -f 2>/dev/null || hostname) fi [[ $FQDN =~ $HOSTNAME_RE ]] || die "Ungültiger Hostname für die UI: '$FQDN'" validate_timezone_name "$TIMEZONE" MONITOR_IP=${MONITOR_IP//,/ } [[ $MONITOR_IP =~ $IP_LIST_RE ]] || die "Ungültige Monitoring-IP: '$MONITOR_IP'" case $TLS_MODE in internal) ;; files) [[ -f $TLS_CERT && -f $TLS_KEY ]] || die "TLS-Modus files braucht --tls-cert und --tls-key (lesbare Dateien)." [[ $TLS_CERT =~ $PATH_RE && $TLS_KEY =~ $PATH_RE ]] || die "Zertifikatspfade dürfen nur Buchstaben, Ziffern und ._/- enthalten." ;; *) die "Ungültiger TLS-Modus '$TLS_MODE' (internal oder files)." ;; esac } run_preflight() { (( ! SKIP_PREFLIGHT )) || { pf_detect_env; return 0; } PF_PACKAGES_PENDING=1 preflight_run "$APP_DIR" "$DATA_DIR" || die "Preflight fehlgeschlagen (FEHLER-Zeilen oben). --skip-preflight übergeht die Prüfung." } install_packages() { log "Installiere Pakete." apt-get update -qq apt_install "${PACKAGES[@]}" } ensure_user_and_dirs() { if ! getent passwd "$APP_USER" >/dev/null; then log "Lege Systembenutzer $APP_USER an." useradd --system --user-group --home-dir "$DATA_DIR" --no-create-home --shell /usr/sbin/nologin "$APP_USER" fi install -d -m 0750 -o root -g "$APP_GROUP" "$CONF_DIR" install -d -m 0750 -o "$APP_USER" -g "$APP_GROUP" "$DATA_DIR" install -d -m 0700 -o "$APP_USER" -g "$APP_GROUP" "$BACKUP_DIR" } # Das Konfigurationsverzeichnis gehört root; checkbot kann dort keine Dateien anlegen, daher als root erzeugen und übergeben. ensure_keys() { local name ( umask 077 cd "$APP_DIR" CHECKBOT_CONFIG_DIR=$CONF_DIR PYTHONDONTWRITEBYTECODE=1 "$VENV_DIR/bin/python" -m app.cli gen-keys ) for name in "${SECRET_FILES[@]}"; do [[ -f $CONF_DIR/$name ]] || die "$CONF_DIR/$name fehlt nach gen-keys." chown "$APP_USER:$APP_GROUP" "$CONF_DIR/$name" chmod 0600 "$CONF_DIR/$name" done } ensure_env_file() { if [[ -f $ENV_FILE ]]; then log "$ENV_FILE existiert, bleibt unverändert." else install -m 0640 -o root -g "$APP_GROUP" "$DEPLOY_DIR/checkbot.env.example" "$ENV_FILE" log "$ENV_FILE angelegt." fi chown root:"$APP_GROUP" "$ENV_FILE" chmod 0640 "$ENV_FILE" } install_dropins() { if (( RELAXED )); then if ! pf_is_container; then warn "--lxc-relaxed ohne erkannten Container (Umgebung: $PF_VIRT_KIND); das Drop-In schwächt die Sandbox und wird nur auf ausdrücklichen Wunsch installiert." fi install -D -m 0644 "$RELAXED_DROPIN_SRC" "$RELAXED_DROPIN_DST" install -D -m 0644 "$RELAXED_DROPIN_SRC" "$BACKUP_RELAXED_DROPIN_DST" systemctl daemon-reload log "Gelockertes systemd-Drop-In installiert." elif [[ -e $RELAXED_DROPIN_DST ]]; then warn "$RELAXED_DROPIN_DST existiert noch (frühere Installation mit --lxc-relaxed) und lockert die Sandbox weiter." fi } install_cli_link() { ln -sfn "$APP_DIR/deploy/checkbot-cli" "$CLI_LINK" } configure_caddy() { local port host upstream rendered ensure_caddy port=$(env_value CHECKBOT_BIND_PORT "$DEFAULT_BIND_PORT") host=$(env_value CHECKBOT_BIND_HOST "$DEFAULT_BIND_HOST") case $host in 0.0.0.0|::|"") host=127.0.0.1 ;; esac [[ $host == *:* ]] && host="[$host]" upstream="$host:$port" if [[ $TLS_MODE == files ]]; then install_tls_files "$TLS_CERT" "$TLS_KEY" fi rendered=$(render_caddyfile "$FQDN" "$TLS_MODE" "$MONITOR_IP" "$upstream") apply_caddyfile "$rendered" } start_service() { systemctl enable "$SERVICE_NAME" >/dev/null systemctl enable --now checkbot-backup.timer >/dev/null systemctl restart "$SERVICE_NAME" if ! wait_health; then warn "Dienst antwortet nicht innerhalb von ${HEALTH_WAIT_SECONDS} s." warn "Logs: journalctl -u $SERVICE_NAME -n 100 --no-pager." if pf_is_container; then warn "Bei Exit-Code 218/226/228 im Container: --lxc-relaxed (siehe deploy/README.md)." fi return 1 fi } has_admin() { local db db=$(db_file) [[ -f $db ]] || return 1 runuser -u "$APP_USER" -- python3 -I -c ' import sqlite3, sys try: n = sqlite3.connect(sys.argv[1]).execute("SELECT COUNT(*) FROM users WHERE role = ? AND disabled = 0", ("admin",)).fetchone()[0] except sqlite3.Error: n = 0 sys.exit(0 if n else 1) ' "$db" } offer_admin() { local answer="" if has_admin; then log "Ein Admin existiert bereits." return 0 fi if (( ! ASSUME_NO_PROMPT )) && [[ -t 0 && -t 1 ]]; then read -r -p "Ersten Admin jetzt anlegen? [J/n] " answer || answer=n if [[ ! $answer =~ ^[nN] ]]; then "$APP_DIR/deploy/checkbot-cli" create-admin && return 0 warn "Admin-Anlage fehlgeschlagen. Später: sudo checkbot-cli create-admin" fi fi ADMIN_PENDING=1 } summary() { cat <