#!/usr/bin/env bash # # Richtet Syncova Backups auf einem Linux-Server ein. # # Der Ablauf folgt docs/installation.md. Das Skript nimmt niemandem eine # Entscheidung ab, die er selbst treffen muss — es fragt, erklaert, und legt # nichts an, was es nicht auch wieder zuruecknehmen koennte. # # ./setup.sh fragt nach # ./setup.sh --unbeaufsichtigt liest alles aus der Umgebung # ./setup.sh --hilfe zeigt alle Schalter # # Bei einem Abbruch wird zurueckgebaut, was dieser Lauf angelegt hat. Eine halb # eingerichtete Anlage sieht arbeitsfaehig aus und scheitert beim ersten Lauf. set -euo pipefail # --------------------------------------------------------------------------- # Feste Orte # # Sie stehen gleichlautend in update.sh und uninstall.sh; ein Test haelt die # drei Skripte deckungsgleich. Drei Skripte, die sich ueber den Installationsort # uneinig sind, ergeben eine Anlage, die sich nicht mehr entfernen laesst. # --------------------------------------------------------------------------- # installationRoot ist das Programmverzeichnis. readonly installationRoot="/opt/syncova" # configurationDirectory enthaelt die Umgebungsdatei. readonly configurationDirectory="/etc/syncova" # configurationFile ist die Umgebungsdatei des Dienstes. readonly configurationFile="${configurationDirectory}/syncova.env" # serviceName ist die systemd-Einheit. readonly serviceName="syncova-api" # serviceUnitFile ist deren Datei. readonly serviceUnitFile="/etc/systemd/system/${serviceName}.service" # serviceAccount ist das Dienstkonto. readonly serviceAccount="syncova" # defaultRepositoryPath ist der vorgeschlagene Ort des Repositorys. readonly defaultRepositoryPath="/srv/syncova-repository" # backupDirectory nimmt Sicherungen von Datenbank und Konfiguration auf. readonly backupDirectory="/var/backups/syncova" # --------------------------------------------------------------------------- # Ausgabe # --------------------------------------------------------------------------- if [[ -t 1 ]]; then readonly colorReset=$'\033[0m' readonly colorBold=$'\033[1m' readonly colorRed=$'\033[31m' readonly colorGreen=$'\033[32m' readonly colorYellow=$'\033[33m' else readonly colorReset='' colorBold='' colorRed='' colorGreen='' colorYellow='' fi # writeStep meldet einen Arbeitsschritt. writeStep() { printf '%s==>%s %s\n' "${colorBold}" "${colorReset}" "$1"; } # writeDetail meldet eine Einzelheit. writeDetail() { printf ' %s\n' "$1"; } # writeSuccess meldet einen gelungenen Schritt. writeSuccess() { printf ' %s✓%s %s\n' "${colorGreen}" "${colorReset}" "$1"; } # writeWarning meldet etwas, das Beachtung verlangt. writeWarning() { printf ' %s!%s %s\n' "${colorYellow}" "${colorReset}" "$1" >&2; } # abortWithMessage bricht mit einer Begruendung ab. abortWithMessage() { printf '\n%sAbbruch:%s %s\n' "${colorRed}${colorBold}" "${colorReset}" "$1" >&2 exit 1 } # --------------------------------------------------------------------------- # Rueckbau bei Abbruch # # Jeder Schritt vermerkt, was er angelegt hat. Bricht das Skript ab, wird genau # das wieder entfernt — und nichts sonst. Eine vorgefundene Datenbank oder ein # vorgefundenes Repository bleiben unangetastet, auch wenn der Lauf scheitert. # --------------------------------------------------------------------------- # createdArtifacts sammelt die Dinge, die dieser Lauf angelegt hat. createdArtifacts=() # noteCreated vermerkt ein angelegtes Objekt fuer den Rueckbau. noteCreated() { createdArtifacts+=("$1"); } # rollbackOnFailure baut zurueck, was dieser Lauf angelegt hat. rollbackOnFailure() { local exitCode=$? if [[ ${exitCode} -eq 0 ]] || [[ ${#createdArtifacts[@]} -eq 0 ]]; then exit "${exitCode}" fi printf '\n%sDer Lauf wird zurueckgebaut.%s\n' "${colorYellow}${colorBold}" "${colorReset}" >&2 # Rueckwaerts: zuerst der Dienst, zuletzt die Datenbank. local artifactIndex for (( artifactIndex=${#createdArtifacts[@]}-1; artifactIndex>=0; artifactIndex-- )); do local artifact="${createdArtifacts[artifactIndex]}" local artifactKind="${artifact%%:*}" local artifactValue="${artifact#*:}" case "${artifactKind}" in service) systemctl stop "${serviceName}" 2>/dev/null || true systemctl disable "${serviceName}" 2>/dev/null || true rm -f "${serviceUnitFile}" systemctl daemon-reload 2>/dev/null || true writeDetail "Dienst entfernt" ;; directory) rm -rf "${artifactValue}" writeDetail "Verzeichnis entfernt: ${artifactValue}" ;; file) rm -f "${artifactValue}" writeDetail "Datei entfernt: ${artifactValue}" ;; account) userdel "${artifactValue}" 2>/dev/null || true writeDetail "Dienstkonto entfernt: ${artifactValue}" ;; database) # Nur eine Datenbank, die dieser Lauf angelegt hat. runAsPostgres "DROP DATABASE IF EXISTS ${artifactValue}" || true writeDetail "Datenbank entfernt: ${artifactValue}" ;; dbuser) runAsPostgres "DROP ROLE IF EXISTS ${artifactValue}" || true writeDetail "Datenbankkonto entfernt: ${artifactValue}" ;; esac done printf '\n%sEs wurde nichts angetastet, was vorher schon da war.%s\n' \ "${colorYellow}" "${colorReset}" >&2 exit "${exitCode}" } trap rollbackOnFailure EXIT # --------------------------------------------------------------------------- # Schalter # --------------------------------------------------------------------------- # unattendedMode laeuft ohne Rueckfragen. unattendedMode="nein" # packageDirectory ist das entpackte Auslieferungspaket. packageDirectory="" # repositoryPath ist der Ort des Repositorys. repositoryPath="${SYNCOVA_SETUP_REPOSITORY_PATH:-${defaultRepositoryPath}}" # databaseMode ist "lokal" oder "vorhanden". databaseMode="${SYNCOVA_SETUP_DATABASE_MODE:-}" # databaseHost, -Port, -Name, -User, -Password beschreiben die Datenbank. databaseHost="${SYNCOVA_SETUP_DB_HOST:-127.0.0.1}" databasePort="${SYNCOVA_SETUP_DB_PORT:-5432}" databaseName="${SYNCOVA_SETUP_DB_NAME:-syncova}" databaseUser="${SYNCOVA_SETUP_DB_USER:-syncova}" databasePassword="${SYNCOVA_SETUP_DB_PASSWORD:-}" databaseSSLMode="${SYNCOVA_SETUP_DB_SSLMODE:-}" # listenAddress ist die Adresse des Dienstes. listenAddress="${SYNCOVA_SETUP_LISTEN_ADDRESS:-127.0.0.1:8080}" # administratorName ist der erste Administrator. administratorName="${SYNCOVA_SETUP_ADMIN_USERNAME:-admin}" # administratorPassword ist dessen Passwort. administratorPassword="${SYNCOVA_SETUP_ADMIN_PASSWORD:-}" # createHardenedRepository legt ein gehaertetes Repository an. createHardenedRepository="${SYNCOVA_SETUP_HARDENED:-ja}" # encryptionKeys ist ein vorgegebener Schluessel; leer erzeugt einen neuen. encryptionKeys="${SYNCOVA_SETUP_ENCRYPTION_KEYS:-}" showUsage() { cat <<'USAGE' Verwendung: setup.sh [--paket ] [--unbeaufsichtigt] [weitere Schalter] Schalter: --paket Entpacktes Auslieferungspaket (Vorgabe: Verzeichnis des Skripts) --repository Ort des Repositorys (Vorgabe: /srv/syncova-repository) --adresse Lauschadresse (Vorgabe: 127.0.0.1:8080) --admin Anmeldename des ersten Administrators (Vorgabe: admin) --ohne-haertung Repository ohne Aufbewahrungsschutz anlegen --unbeaufsichtigt Keine Rueckfragen; alle Werte aus der Umgebung --hilfe Diese Uebersicht Umgebung fuer den unbeaufsichtigten Lauf: SYNCOVA_SETUP_DATABASE_MODE lokal | vorhanden SYNCOVA_SETUP_DB_HOST/_PORT/_NAME/_USER/_PASSWORD/_SSLMODE SYNCOVA_SETUP_ADMIN_USERNAME Anmeldename (Vorgabe: admin) SYNCOVA_SETUP_ADMIN_PASSWORD Passwort des ersten Administrators SYNCOVA_SETUP_ENCRYPTION_KEYS Vorhandener Schluessel; leer erzeugt einen neuen SYNCOVA_SETUP_REPOSITORY_PATH Ort des Repositorys SYNCOVA_SETUP_LISTEN_ADDRESS Lauschadresse Ist bereits eine Installation vorhanden, bricht das Skript ab und verweist auf update.sh. Eine bestehende Anlage zu ueberschreiben ist kein Setup. USAGE } while [[ $# -gt 0 ]]; do case "$1" in --paket) packageDirectory="${2:-}"; shift 2 ;; --repository) repositoryPath="${2:-}"; shift 2 ;; --adresse) listenAddress="${2:-}"; shift 2 ;; --admin) administratorName="${2:-}"; shift 2 ;; --ohne-haertung) createHardenedRepository="nein"; shift ;; --unbeaufsichtigt) unattendedMode="ja"; shift ;; --hilfe|-h|--help) showUsage; trap - EXIT; exit 0 ;; *) showUsage >&2; abortWithMessage "unbekannter Schalter: $1" ;; esac done # --------------------------------------------------------------------------- # Hilfsfunktionen # --------------------------------------------------------------------------- # askQuestion stellt eine Frage mit Vorgabewert. askQuestion() { local questionText="$1" defaultValue="${2:-}" answerVariable="$3" enteredValue="" if [[ "${unattendedMode}" == "ja" ]]; then printf -v "${answerVariable}" '%s' "${defaultValue}" return 0 fi if [[ -n "${defaultValue}" ]]; then read -r -p " ${questionText} [${defaultValue}]: " enteredValue else read -r -p " ${questionText}: " enteredValue fi printf -v "${answerVariable}" '%s' "${enteredValue:-${defaultValue}}" } # askPassword liest ein Passwort verdeckt und laesst es wiederholen. askPassword() { local questionText="$1" answerVariable="$2" firstEntry="" secondEntry="" while true; do read -r -s -p " ${questionText}: " firstEntry; printf '\n' read -r -s -p " ${questionText} wiederholen: " secondEntry; printf '\n' if [[ -z "${firstEntry}" ]]; then writeWarning "Ein leeres Passwort wird nicht angenommen." continue fi if [[ "${firstEntry}" != "${secondEntry}" ]]; then writeWarning "Die beiden Eingaben stimmen nicht ueberein." continue fi printf -v "${answerVariable}" '%s' "${firstEntry}" return 0 done } # confirmQuestion stellt eine Ja/Nein-Frage. confirmQuestion() { local questionText="$1" defaultAnswer="${2:-n}" enteredValue="" if [[ "${unattendedMode}" == "ja" ]]; then [[ "${defaultAnswer}" == "j" ]] return $? fi read -r -p " ${questionText} [$( [[ "${defaultAnswer}" == "j" ]] && echo 'J/n' || echo 'j/N' )]: " enteredValue enteredValue="${enteredValue:-${defaultAnswer}}" [[ "${enteredValue}" =~ ^[jJyY] ]] } # generatePassword erzeugt ein zufaelliges Passwort. generatePassword() { # Aus /dev/urandom und nicht aus $RANDOM: Letzteres ist vorhersagbar und # hat 15 Bit Zufall — fuer ein Datenbankpasswort zu wenig. # # Die Reihenfolge der Pipe ist wesentlich: Erst wird eine **begrenzte** # Menge gelesen, am Ende steht "cut". Mit "tr < /dev/urandom | head -c 32" # schliesst head den Pipe nach 32 Zeichen, tr bekommt SIGPIPE, und mit # "set -o pipefail" bricht das Skript ab — mitten in der Einrichtung und # ohne erkennbaren Grund. head -c 512 /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | cut -c1-32 } # runAsPostgres fuehrt SQL als Datenbankverwalter aus. runAsPostgres() { su - postgres -c "psql -v ON_ERROR_STOP=1 -c \"$1\"" >/dev/null 2>&1 } # queryAsPostgres liefert das Ergebnis einer Abfrage als Text. # # Bewusst ohne Pipe zu grep: "psql | grep -q" schliesst den Pipe nach dem ersten # Treffer, psql bekommt SIGPIPE, und mit "set -o pipefail" bricht das ganze # Skript ab — mitten in der Einrichtung, ohne erkennbaren Grund. queryAsPostgres() { su - postgres -c "psql -tAc \"$1\"" 2>/dev/null || true } # detectPackageManager ermittelt das Paketwerkzeug der Distribution. detectPackageManager() { if command -v apt-get >/dev/null 2>&1; then echo "apt"; return; fi if command -v dnf >/dev/null 2>&1; then echo "dnf"; return; fi if command -v yum >/dev/null 2>&1; then echo "yum"; return; fi if command -v zypper >/dev/null 2>&1; then echo "zypper"; return; fi if command -v pacman >/dev/null 2>&1; then echo "pacman"; return; fi echo "unbekannt" } # --------------------------------------------------------------------------- # 1. Voraussetzungen # --------------------------------------------------------------------------- printf '\n%sSyncova Backups — Einrichtung%s\n\n' "${colorBold}" "${colorReset}" writeStep "Voraussetzungen" if [[ "$(id -u)" -ne 0 ]]; then abortWithMessage "Dieses Skript braucht root-Rechte (sudo ./setup.sh)." fi if [[ "$(uname -s)" != "Linux" ]]; then abortWithMessage "Der Syncova-Server wird ausschliesslich fuer Linux ausgeliefert." fi if ! command -v systemctl >/dev/null 2>&1; then abortWithMessage "Es wurde kein systemd gefunden. Der Dienst laesst sich sonst nicht einrichten." fi # Eine bestehende Installation wird nicht ueberschrieben. Dafuer gibt es # update.sh — und der Unterschied ist wesentlich: Ein Update sichert vorher. if [[ -e "${installationRoot}" ]] || [[ -f "${configurationFile}" ]] || [[ -f "${serviceUnitFile}" ]]; then printf '\n' writeWarning "Es ist bereits eine Installation vorhanden:" [[ -e "${installationRoot}" ]] && writeDetail " ${installationRoot}" [[ -f "${configurationFile}" ]] && writeDetail " ${configurationFile}" [[ -f "${serviceUnitFile}" ]] && writeDetail " ${serviceUnitFile}" printf '\n' abortWithMessage "Zum Aktualisieren: ./update.sh — es sichert vorher Datenbank und Konfiguration." fi writeSuccess "root, Linux, systemd" # Das Paket finden. if [[ -z "${packageDirectory}" ]]; then packageDirectory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" fi if [[ ! -x "${packageDirectory}/bin/syncova-api" ]]; then abortWithMessage "Unter ${packageDirectory} liegt kein Auslieferungspaket (bin/syncova-api fehlt). Geben Sie es mit --paket an." fi packageVersion="$("${packageDirectory}/bin/syncova-api" --version 2>/dev/null | awk '{print $2}')" writeSuccess "Paket gefunden: ${packageVersion}" # Pruefsummen kontrollieren, sofern das Paket welche mitbringt. if [[ -f "${packageDirectory}/SHA256SUMS" ]] && command -v sha256sum >/dev/null 2>&1; then if ( cd "${packageDirectory}" && sha256sum --quiet -c SHA256SUMS >/dev/null 2>&1 ); then writeSuccess "Pruefsummen des Pakets stimmen" else writeWarning "Die Pruefsummen des Pakets stimmen nicht. Wurde es vollstaendig uebertragen?" confirmQuestion "Trotzdem fortfahren?" "n" || abortWithMessage "abgebrochen" fi fi # --------------------------------------------------------------------------- # 2. Datenbank # --------------------------------------------------------------------------- printf '\n' writeStep "Datenbank" if [[ -z "${databaseMode}" ]]; then if [[ "${unattendedMode}" == "ja" ]]; then databaseMode="lokal" else writeDetail "PostgreSQL 17 oder neuer wird gebraucht." writeDetail "" writeDetail " 1) auf diesem Server installieren" writeDetail " 2) eine vorhandene Datenbank verwenden" writeDetail "" local_choice="" askQuestion "Auswahl" "1" local_choice [[ "${local_choice}" == "2" ]] && databaseMode="vorhanden" || databaseMode="lokal" fi fi if [[ "${databaseMode}" == "lokal" ]]; then if command -v psql >/dev/null 2>&1 && systemctl is-active --quiet postgresql 2>/dev/null; then writeSuccess "PostgreSQL laeuft bereits" else packageManager="$(detectPackageManager)" writeDetail "PostgreSQL wird installiert (${packageManager})" case "${packageManager}" in apt) DEBIAN_FRONTEND=noninteractive apt-get update -qq DEBIAN_FRONTEND=noninteractive apt-get install -y -qq postgresql postgresql-contrib >/dev/null ;; dnf|yum) "${packageManager}" install -y -q postgresql-server postgresql-contrib >/dev/null # Auf Red-Hat-Systemen muss der Datenbankbereich zuerst angelegt werden. [[ -f /var/lib/pgsql/data/PG_VERSION ]] || postgresql-setup --initdb >/dev/null 2>&1 || true ;; zypper) zypper --non-interactive install -y postgresql-server postgresql-contrib >/dev/null ;; pacman) pacman -Sy --noconfirm postgresql >/dev/null [[ -f /var/lib/postgres/data/PG_VERSION ]] || \ su - postgres -c "initdb -D /var/lib/postgres/data" >/dev/null 2>&1 || true ;; *) abortWithMessage "Die Distribution wurde nicht erkannt. Installieren Sie PostgreSQL von Hand und starten Sie das Skript mit SYNCOVA_SETUP_DATABASE_MODE=vorhanden erneut." ;; esac systemctl enable --now postgresql >/dev/null 2>&1 || \ abortWithMessage "PostgreSQL liess sich nicht starten. Pruefen Sie 'systemctl status postgresql'." writeSuccess "PostgreSQL installiert und gestartet" fi # Welche Fassung hat die Distribution mitgebracht? # # Debian 12 liefert 15, nicht 17. Geprueft ist Syncova gegen 17 (Entwicklung # und CI) und 15 (Einrichtung und Sicherungslauf). Aelter wird abgelehnt: Die # Migrationen setzen unter anderem gen_random_uuid() und DROP DATABASE ... # WITH (FORCE) voraus. postgresVersion="$(queryAsPostgres 'SHOW server_version' | awk '{print $1}' | cut -d. -f1)" if [[ -n "${postgresVersion}" ]]; then if [[ "${postgresVersion}" -lt 15 ]]; then abortWithMessage "PostgreSQL ${postgresVersion} ist zu alt. Gebraucht wird 15 oder neuer." fi writeSuccess "PostgreSQL ${postgresVersion}" fi # Eine lokale Datenbank spricht ueber den Unix-Socket; TLS ist dort ohne # Belang und wuerde den Verbindungsaufbau nur scheitern lassen. databaseHost="127.0.0.1" databaseSSLMode="${databaseSSLMode:-disable}" if [[ -z "${databasePassword}" ]]; then databasePassword="$(generatePassword)" # secretscan:erlaubt: erzeugter Zufallswert, kein hinterlegtes Geheimnis writeDetail "Ein Datenbankpasswort wurde erzeugt." fi # Rolle und Datenbank anlegen — beides nur, wenn es sie nicht schon gibt. existingRole="$(queryAsPostgres "SELECT 1 FROM pg_roles WHERE rolname='${databaseUser}'")" if [[ "${existingRole}" == "1" ]]; then writeDetail "Das Datenbankkonto ${databaseUser} ist bereits vorhanden; das Passwort wird gesetzt." runAsPostgres "ALTER ROLE ${databaseUser} WITH LOGIN PASSWORD '${databasePassword}'" || \ abortWithMessage "Das Datenbankpasswort liess sich nicht setzen." else runAsPostgres "CREATE ROLE ${databaseUser} WITH LOGIN PASSWORD '${databasePassword}'" || \ abortWithMessage "Das Datenbankkonto liess sich nicht anlegen." noteCreated "dbuser:${databaseUser}" writeSuccess "Datenbankkonto ${databaseUser} angelegt" fi existingDatabase="$(queryAsPostgres "SELECT 1 FROM pg_database WHERE datname='${databaseName}'")" if [[ "${existingDatabase}" == "1" ]]; then writeDetail "Die Datenbank ${databaseName} ist bereits vorhanden und wird verwendet." else su - postgres -c "createdb -O ${databaseUser} ${databaseName}" >/dev/null 2>&1 || \ abortWithMessage "Die Datenbank liess sich nicht anlegen." noteCreated "database:${databaseName}" writeSuccess "Datenbank ${databaseName} angelegt" fi else askQuestion "Datenbankserver" "${databaseHost}" databaseHost askQuestion "Port" "${databasePort}" databasePort askQuestion "Datenbankname" "${databaseName}" databaseName askQuestion "Datenbankkonto" "${databaseUser}" databaseUser if [[ -z "${databasePassword}" ]]; then askPassword "Passwort des Datenbankkontos" databasePassword fi # Bei einer entfernten Datenbank ist Verschluesselung der Standard: Ohne sie # ginge das Passwort im Klartext durch das Netz. databaseSSLMode="${databaseSSLMode:-require}" writeDetail "Die Verbindung wird geprueft." if ! PGPASSWORD="${databasePassword}" psql -h "${databaseHost}" -p "${databasePort}" \ -U "${databaseUser}" -d "${databaseName}" -tAc "SELECT 1" >/dev/null 2>&1; then abortWithMessage "Die Datenbank ist nicht erreichbar oder die Anmeldung schlug fehl. Geprueft wurde: ${databaseUser}@${databaseHost}:${databasePort}/${databaseName}" fi writeSuccess "Datenbank erreichbar" fi # --------------------------------------------------------------------------- # 3. Dienstkonto und Verzeichnisse # --------------------------------------------------------------------------- printf '\n' writeStep "Dienstkonto und Verzeichnisse" if id "${serviceAccount}" >/dev/null 2>&1; then writeDetail "Das Dienstkonto ${serviceAccount} ist bereits vorhanden." else useradd --system --no-create-home --shell /usr/sbin/nologin "${serviceAccount}" 2>/dev/null || \ useradd --system --no-create-home --shell /sbin/nologin "${serviceAccount}" || \ abortWithMessage "Das Dienstkonto liess sich nicht anlegen." noteCreated "account:${serviceAccount}" writeSuccess "Dienstkonto ${serviceAccount} angelegt" fi install -d -m 0755 "${installationRoot}" noteCreated "directory:${installationRoot}" install -d -m 0700 -o "${serviceAccount}" -g "${serviceAccount}" "${configurationDirectory}" noteCreated "directory:${configurationDirectory}" install -d -m 0700 -o "${serviceAccount}" -g "${serviceAccount}" "${backupDirectory}" cp -R "${packageDirectory}/bin" "${installationRoot}/" [[ -d "${packageDirectory}/web" ]] && cp -R "${packageDirectory}/web" "${installationRoot}/" [[ -d "${packageDirectory}/migrations" ]] && cp -R "${packageDirectory}/migrations" "${installationRoot}/" [[ -d "${packageDirectory}/docs" ]] && cp -R "${packageDirectory}/docs" "${installationRoot}/" [[ -d "${packageDirectory}/deployment" ]] && cp -R "${packageDirectory}/deployment" "${installationRoot}/" chmod -R 0755 "${installationRoot}/bin" # Das Diagnoseskript kommt neben die Programme. Im Ernstfall sucht niemand das # ausgepackte Paket von vor drei Monaten. [[ -f "${packageDirectory}/diagnose.sh" ]] && \ install -m 0755 "${packageDirectory}/diagnose.sh" "${installationRoot}/diagnose.sh" # Die Version wird festgehalten. update.sh und uninstall.sh lesen sie; ohne sie # muesste man ein Binary aufrufen, um zu wissen, was installiert ist. printf '%s\n' "${packageVersion}" > "${installationRoot}/VERSION" writeSuccess "Programme unter ${installationRoot}" # --------------------------------------------------------------------------- # 4. Verschluesselungsschluessel # --------------------------------------------------------------------------- printf '\n' writeStep "Verschluesselungsschluessel" if [[ -z "${encryptionKeys}" ]]; then # "|| true", damit die eigene Fehlermeldung unten greift: Findet grep # nichts, endet es mit 1, und "set -e" braeche vorher ab — mit einer # Meldung, die niemandem sagt, was fehlt. generatedKeyLine="$("${installationRoot}/bin/syncova-admin" generate-key 2>/dev/null \ | grep '^SYNCOVA_ENCRYPTION_KEYS=' || true)" encryptionKeys="${generatedKeyLine#SYNCOVA_ENCRYPTION_KEYS=}" [[ -n "${encryptionKeys}" ]] || abortWithMessage "Der Schluessel liess sich nicht erzeugen." printf '\n' printf ' %s┌─────────────────────────────────────────────────────────────────┐%s\n' "${colorYellow}${colorBold}" "${colorReset}" printf ' %s│ DIESER SCHLUESSEL EXISTIERT NUR HIER │%s\n' "${colorYellow}${colorBold}" "${colorReset}" printf ' %s└─────────────────────────────────────────────────────────────────┘%s\n' "${colorYellow}${colorBold}" "${colorReset}" printf '\n' printf ' SYNCOVA_ENCRYPTION_KEYS=%s\n' "${encryptionKeys}" printf '\n' writeDetail "Ohne ihn sind MFA-Geheimnisse, die Zugangsdaten der" writeDetail "Virtualisierungsverbuende und die Datenschluessel der Repositories" writeDetail "verloren — und damit die verschluesselten Backups." writeDetail "" writeDetail "Er gehoert an einen Ort ausserhalb dieser Anlage: Passwortspeicher" writeDetail "oder Tresor. In ein Backup, das Syncova erzeugt, gehoert er" writeDetail "ausdruecklich nicht — man braeuchte ihn, um an ihn heranzukommen." printf '\n' if [[ "${unattendedMode}" != "ja" ]]; then acknowledgement="" while [[ "${acknowledgement}" != "gesichert" ]]; do read -r -p " Tippen Sie 'gesichert', wenn der Schluessel anderswo liegt: " acknowledgement done else writeWarning "Unbeaufsichtigter Lauf: Sichern Sie den Schluessel aus ${configurationFile}." fi else writeSuccess "Ein vorgegebener Schluessel wird uebernommen" fi # --------------------------------------------------------------------------- # 5. Konfiguration # --------------------------------------------------------------------------- printf '\n' writeStep "Konfiguration" # Erst mit strengen Rechten anlegen, dann fuellen. Andersherum stuende das # Passwort fuer einen Augenblick weltlesbar auf der Platte. install -m 0600 -o "${serviceAccount}" -g "${serviceAccount}" /dev/null "${configurationFile}" noteCreated "file:${configurationFile}" cat > "${configurationFile}" <&1 | tail -1; then abortWithMessage "Die Migrationen liessen sich nicht anwenden." fi writeSuccess "Schema angelegt" # --------------------------------------------------------------------------- # 7. Erster Administrator # --------------------------------------------------------------------------- printf '\n' writeStep "Erster Administrator" if [[ -z "${administratorPassword}" ]]; then if [[ "${unattendedMode}" == "ja" ]]; then abortWithMessage "Im unbeaufsichtigten Lauf muss SYNCOVA_SETUP_ADMIN_PASSWORD gesetzt sein." fi askQuestion "Anmeldename" "${administratorName}" administratorName askPassword "Passwort fuer ${administratorName}" administratorPassword fi # Das Passwort geht ueber die Standardeingabe, nicht als Aufrufparameter: # Parameter stehen in der Prozessliste und in der Shell-Historie. Das Programm # fragt zweimal, deshalb zwei Zeilen. if ! printf '%s\n%s\n' "${administratorPassword}" "${administratorPassword}" \ | "${installationRoot}/bin/syncova-admin" create-admin --username "${administratorName}" >/dev/null 2>&1; then abortWithMessage "Der Administrator liess sich nicht anlegen. Haeufigste Ursache: Das Passwort erfuellt die Mindestanforderung nicht (12 Zeichen, mindestens 5 verschiedene, Buchstaben plus ein weiteres Zeichen)." fi writeSuccess "Administrator ${administratorName} angelegt" # --------------------------------------------------------------------------- # 8. Repository # --------------------------------------------------------------------------- printf '\n' writeStep "Repository" if [[ "${unattendedMode}" != "ja" ]]; then writeDetail "Das Repository gehoert nicht auf denselben Datentraeger wie die" writeDetail "zu sichernden Daten. Ein Datentraegerausfall naehme sonst Original" writeDetail "und Sicherung gemeinsam mit." printf '\n' askQuestion "Ort des Repositorys" "${repositoryPath}" repositoryPath fi if [[ -e "${repositoryPath}/format/repository.json" ]]; then writeDetail "Unter ${repositoryPath} liegt bereits ein Repository; es wird verwendet." else install -d -m 0700 -o "${serviceAccount}" -g "${serviceAccount}" "${repositoryPath}" hardenedFlag=() [[ "${createHardenedRepository}" == "ja" ]] && hardenedFlag=(--hardened) if ! su -s /bin/bash "${serviceAccount}" -c \ "'${installationRoot}/bin/syncova-repo' create --path '${repositoryPath}' --name 'Hauptziel' ${hardenedFlag[*]:-}" \ >/dev/null 2>&1; then abortWithMessage "Das Repository liess sich nicht anlegen." fi noteCreated "directory:${repositoryPath}" writeSuccess "Repository unter ${repositoryPath}$( [[ "${createHardenedRepository}" == "ja" ]] && echo ' (gehaertet)' )" fi # --------------------------------------------------------------------------- # 9. Dienst # --------------------------------------------------------------------------- printf '\n' writeStep "Dienst" cat > "${serviceUnitFile}" </dev/null 2>&1 systemctl start "${serviceName}" writeDetail "Der Dienst wird geprueft." serviceIsReady="nein" for _attempt in $(seq 1 15); do if curl -fsS "http://${listenAddress}/health/ready" >/dev/null 2>&1; then serviceIsReady="ja" break fi sleep 1 done if [[ "${serviceIsReady}" != "ja" ]]; then printf '\n' writeWarning "Der Dienst meldet sich nicht als betriebsbereit." writeDetail " systemctl status ${serviceName}" writeDetail " journalctl -u ${serviceName} -n 50 --no-pager" abortWithMessage "Die Einrichtung wird zurueckgebaut." fi writeSuccess "Dienst laeuft und ist betriebsbereit" # Ab hier ist die Einrichtung gelungen; der Rueckbau entfaellt. trap - EXIT # --------------------------------------------------------------------------- # Abschluss # --------------------------------------------------------------------------- printf '\n%sDie Einrichtung ist abgeschlossen.%s\n\n' "${colorGreen}${colorBold}" "${colorReset}" printf ' Version %s\n' "${packageVersion}" printf ' Programme %s\n' "${installationRoot}" printf ' Konfiguration %s\n' "${configurationFile}" printf ' Repository %s\n' "${repositoryPath}" printf ' Dienst %s (systemctl status %s)\n' "${serviceName}" "${serviceName}" printf ' Oberflaeche %s/web — braucht einen Webserver mit SPA-Fallback\n' "${installationRoot}" printf '\n' printf '%sWas jetzt zu tun bleibt:%s\n\n' "${colorBold}" "${colorReset}" cat <<'NEXT' 1. Repository in der Control Plane eintragen und die Durchsetzungsstufe MESSEN — sie wird gemessen, nicht behauptet: curl -X POST http:///api/v1/repositories \ -H "Authorization: Bearer " -H 'Content-Type: application/json' \ -d '{"name":"Hauptziel","location":""}' curl -X POST http:///api/v1/repositories//enforcement/measure \ -H "Authorization: Bearer " 2. Oberflaeche ausliefern (nginx, SPA-Fallback try_files $uri /index.html) und TLS einrichten — oder den Dienst an 127.0.0.1 binden. 3. Zweiten Faktor einrichten, mindestens fuer Konten mit Loeschrecht. 4. Benachrichtigungsweg einrichten. Ohne ihn erfaehrt niemand von einem Ausfall. 5. Eine Wiederherstellung durchfuehren, bevor Sie eine brauchen. Erst danach steigt ein Wiederherstellungspunkt auf "recoverable" — alles davor ist ein Indiz, kein Nachweis. NEXT printf '\n Ausfuehrlich: %s/docs/installation.md\n' "${installationRoot}" printf ' Im Ernstfall: %s/docs/recovery-runbook.md\n\n' "${installationRoot}"