diff --git a/CHANGELOG.md b/CHANGELOG.md index 9701a12..02002d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Änderungen +## V1 — Release Candidate 5, 18. August 2026 + +Die Oberfläche richtet sich jetzt mit ein. Bisher endete `setup.sh` mit einer +laufenden API auf `127.0.0.1:8080` und der Aufgabe, einen Webserver von Hand +davorzusetzen — der häufigste Punkt, an dem eine Einrichtung liegen blieb. + +- **`setup.sh` richtet nginx und ein selbst signiertes Zertifikat ein.** Das + Zertifikat gilt für den Rechnernamen, den vollständigen Namen und **jede** + globale IPv4-Adresse des Servers (`subjectAltName` — moderne Browser lesen + den `CN` nicht mehr), 3650 Tage. Der SHA-256-Fingerabdruck wird genannt, damit + er sich beim ersten Aufruf im Browser vergleichen lässt. +- **Die API bleibt an `127.0.0.1:8080` gebunden.** Erreichbar ist sie nur durch + nginx hindurch. Sie stattdessen auf alle Schnittstellen zu legen wäre der + kürzere Weg und der falsche: Die Verschlüsselung ließe sich dann umgehen, + indem man Port 8080 direkt anspricht. +- **Die Firewall wird gemeldet, nicht geändert.** `ufw` und `firewalld` werden + erkannt und ihr Zustand ausgegeben; geöffnet wird nichts. Eine Einrichtung, + die selbsttätig einen Port ins Netz öffnet, hebelt genau die Entscheidung aus, + für die jemand die Firewall aufgesetzt hat. +- **Scheitert die Oberfläche, scheitert nicht die Einrichtung.** Die + Konfiguration wird mit `nginx -t` geprüft, **bevor** sie übernommen wird; hält + sie nicht, wird sie entfernt, der Grund genannt und der Nachholweg gezeigt. + Die Anlage läuft in jedem Fall. +- **Nachträglich einrichten:** `sudo /opt/syncova/setup.sh --weboberflaeche`. + Auslassen: `--ohne-weboberflaeche`. + +Ein Fund beim Erproben: + +- **`http2 on;` gibt es erst ab nginx 1.25.1.** Debian 12 liefert 1.22, wo + HTTP/2 ein Parameter von `listen` ist. Die neue Schreibweise ergibt dort + „unknown directive http2", und nginx startet nicht. Die Fassung wird jetzt + gelesen und die passende Schreibweise erzeugt. + ## V1 — Release Candidate 4, 18. August 2026 Behebt [Issue #2](https://git.jfritzsche.de/jf/syncova-backup/issues/2): Die diff --git a/docs/installation.md b/docs/installation.md index a8a7999..d33a52d 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -216,6 +216,21 @@ curl -s http://127.0.0.1:8080/health/ready ## 8. Oberfläche ausliefern +`setup.sh` erledigt diesen Schritt selbst; er steht hier für den Fall, dass Sie +von Hand einrichten oder die Vorgabe ersetzen wollen. Ist die Anlage bereits +eingerichtet und die Oberfläche fehlt noch: + +```bash +sudo /opt/syncova/setup.sh --weboberflaeche +``` + +Das richtet nginx ein, stellt ein selbst signiertes Zertifikat aus und nennt +dessen Fingerabdruck. Mit `--ohne-weboberflaeche` unterbleibt der Schritt. + +**Die API bleibt dabei an `127.0.0.1:8080` gebunden.** Erreichbar ist sie nur +durch nginx hindurch — ein Angreifer im Netz kommt nicht an ihr vorbei, und die +Verschlüsselung lässt sich nicht umgehen, indem man Port 8080 direkt anspricht. + Die Oberfläche ist ein Satz statischer Dateien unter `web/`. Sie braucht einen **SPA-Fallback**, sonst ergibt ein Neuladen auf `/recovery-points` einen 404: @@ -237,6 +252,39 @@ server { } ``` +Zwei Punkte, die in dieser Kurzfassung fehlen und die `setup.sh` mitsetzt: + +- **`http2` schreibt sich je nach nginx-Fassung anders.** Ab 1.25.1 als eigene + Anweisung `http2 on;`, davor als Parameter `listen 443 ssl http2;`. Die neue + Schreibweise auf einer älteren Fassung ergibt „unknown directive http2" — + nginx startet dann nicht. Debian 12 liefert 1.22. +- **Zeitüberschreitungen hochsetzen.** Eine Vorabprüfung liest jeden Block; die + Vorgabe von 60 Sekunden bricht sie bei großen Beständen ab. + +### Selbst signiertes Zertifikat + +Es schützt gegen Mitlesen, nicht gegen einen Mittelsmann — niemand bestätigt, +dass es zu diesem Server gehört. Deshalb nennt `setup.sh` den Fingerabdruck; +vergleichen Sie ihn beim ersten Aufruf mit dem, den der Browser anzeigt. Danach +ist die Warnung unbedenklich. + +Für den Dauerbetrieb gehört ein Zertifikat einer Zertifizierungsstelle hierher +— eine eigene CA oder Let's Encrypt. Legen Sie es nach +`/etc/syncova/tls/server.crt` und `server.key` und laden Sie nginx neu. + +### Firewall + +`setup.sh` **meldet** den Zustand der Firewall und **öffnet nichts**. Eine +Einrichtung, die selbsttätig einen Port ins Netz öffnet, hebelt genau die +Entscheidung aus, für die jemand die Firewall aufgesetzt hat. Port 443 geben +Sie selbst frei: + +```bash +sudo ufw allow 443/tcp # ufw +sudo firewall-cmd --permanent --add-service=https # firewalld +sudo firewall-cmd --reload +``` + ## 9. Erstes Repository Ein Repository entsteht **auf einem Datenträger**, nicht in einer diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e6c7d2b..2fc8483 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -285,6 +285,57 @@ offen und in der Liste. ## Oberfläche +**Die Oberfläche ist gar nicht erreichbar** — prüfen Sie zuerst, ob sie +überhaupt eingerichtet wurde: + +```bash +sudo /opt/syncova/diagnose.sh | sed -n '/## Weboberflaeche/,/^## /p' +``` + +Steht dort „nginx ist nicht vorhanden", holen Sie es nach: + +```bash +sudo /opt/syncova/setup.sh --weboberflaeche +``` + +**Die Verbindung wird abgewiesen, aber nginx läuft** — die Firewall. `setup.sh` +meldet sie nur und öffnet nichts: + +```bash +sudo ufw allow 443/tcp # ufw +sudo firewall-cmd --permanent --add-service=https && sudo firewall-cmd --reload +``` + +**`nginx: unknown directive "http2"`** — die Konfiguration nutzt `http2 on;`, +das es erst ab nginx 1.25.1 gibt. Debian 12 liefert 1.22, wo HTTP/2 ein +Parameter von `listen` ist. Ab rc5 wird die Fassung gelesen und die passende +Schreibweise erzeugt; bei einer von Hand geschriebenen Konfiguration: + +```nginx +listen 443 ssl http2; # nginx < 1.25.1 +``` + +**Der Browser warnt vor dem Zertifikat** — es ist selbst ausgestellt, die +Warnung ist berechtigt. Vergleichen Sie den Fingerabdruck einmal: + +```bash +sudo openssl x509 -in /etc/syncova/tls/server.crt -noout -fingerprint -sha256 +``` + +Stimmt er mit dem im Browser überein, ist die Warnung unbedenklich. Stimmt er +**nicht**, brechen Sie ab — dann sitzt jemand dazwischen. + +**„Ihre Verbindung ist nicht privat" trotz richtigem Fingerabdruck, und der +Browser lässt Sie nicht weiter** — meist fehlt die verwendete Adresse im +Zertifikat. Es gilt für die IP-Adressen, die der Server beim Einrichten hatte; +eine später hinzugekommene ist nicht dabei: + +```bash +sudo openssl x509 -in /etc/syncova/tls/server.crt -noout -ext subjectAltName +``` + +Neu ausstellen: `sudo rm /etc/syncova/tls/server.crt && sudo /opt/syncova/setup.sh --weboberflaeche` + **404 nach dem Neuladen einer Unterseite** — der SPA-Fallback fehlt im Webserver: `try_files $uri /index.html`. diff --git a/scripts/diagnose.sh b/scripts/diagnose.sh index fa090a7..887fae2 100755 --- a/scripts/diagnose.sh +++ b/scripts/diagnose.sh @@ -297,6 +297,55 @@ healthReport+="Abgefragt mit: ${httpToolInUse}" writeBlock "$(redactSecrets "${healthReport%$'\n\n'}")" +# --------------------------------------------------------------------------- +# 3b. Weboberflaeche +# --------------------------------------------------------------------------- +# +# Seit rc5 richtet setup.sh nginx mit ein. Damit kann eine Stoerung auch dort +# liegen — und "die Oberflaeche laedt nicht" ist ohne diese Angaben nicht von +# "der Dienst laeuft nicht" zu unterscheiden. + +writeHeading "Weboberflaeche" + +webReport="" + +if [[ -f "${configurationDirectory}/tls/server.crt" ]]; then + # Gueltigkeit und Geltungsbereich. Ein abgelaufenes Zertifikat oder eine + # fehlende IP im subjectAltName sind zwei der haeufigsten Ursachen dafuer, + # dass ein Browser den Zugang verweigert — und beide sind von aussen nicht + # zu erkennen. + webReport+="Zertifikat: ${configurationDirectory}/tls/server.crt"$'\n' + webReport+="$(openssl x509 -in "${configurationDirectory}/tls/server.crt" \ + -noout -enddate -ext subjectAltName 2>/dev/null || echo "nicht lesbar")"$'\n\n' +else + webReport+="Kein Zertifikat unter ${configurationDirectory}/tls/"$'\n\n' +fi + +if command -v nginx >/dev/null 2>&1; then + webReport+="$(nginx -v 2>&1)"$'\n' + + # Die Fassung entscheidet ueber die HTTP/2-Schreibweise; steht sie hier, + # laesst sich eine abgelehnte Konfiguration sofort zuordnen. + if nginx -t >/dev/null 2>&1; then + webReport+="Konfiguration: angenommen"$'\n' + else + webReport+="Konfiguration: ABGELEHNT"$'\n' + webReport+="$(nginx -t 2>&1)"$'\n' + fi + + # Nicht ueber "|| echo unbekannt": systemctl kann mit Status 0 und leerer + # Ausgabe antworten, und ein leeres Feld im Fehlerbericht ist wertlos — + # es liest sich wie eine Angabe, ist aber keine. + nginxServiceState="$(systemctl is-active nginx 2>/dev/null)" + webReport+="Dienst: ${nginxServiceState:-unbekannt}"$'\n' + webReport+="Site vorhanden: $([[ -f /etc/nginx/sites-enabled/syncova ]] && echo ja || echo nein)" +else + webReport+="nginx ist nicht vorhanden — die Oberflaeche wurde nicht eingerichtet."$'\n' + webReport+="Nachholen: ${installationRoot}/setup.sh --weboberflaeche" +fi + +writeBlock "$(redactSecrets "${webReport}")" + # --------------------------------------------------------------------------- # 4. Datenbank # --------------------------------------------------------------------------- diff --git a/scripts/release_test.go b/scripts/release_test.go index d5757a7..07918f3 100644 --- a/scripts/release_test.go +++ b/scripts/release_test.go @@ -331,3 +331,70 @@ func TestScriptsDoNotDependOnCurl(testInstance *testing.T) { } } } + +// TestSetupEmitsVersionAwareHTTP2Directive haelt fest, dass die +// HTTP/2-Schreibweise von der vorgefundenen nginx-Fassung abhaengt. +// +// Die Anweisung "http2 on;" gibt es erst ab 1.25.1. Davor war HTTP/2 ein +// Parameter von "listen". Wer die neue Schreibweise fest verdrahtet, erzeugt +// auf Debian 12 (nginx 1.22) eine Konfiguration, die nginx ablehnt — die +// Oberflaeche fehlt dann vollstaendig, waehrend die Anlage selbst laeuft. +func TestSetupEmitsVersionAwareHTTP2Directive(testInstance *testing.T) { + setupScript, readError := os.ReadFile(filepath.Join(repositoryRoot, "scripts/setup.sh")) + if readError != nil { + testInstance.Fatalf("setup.sh ließ sich nicht lesen: %v", readError) + } + + scriptContent := string(setupScript) + + // Die Fallunterscheidung muss ueberhaupt stattfinden. + if !strings.Contains(scriptContent, "http2DirectiveFor") { + testInstance.Fatal("setup.sh unterscheidet die nginx-Fassungen nicht; " + + "eine fest verdrahtete HTTP/2-Schreibweise laesst nginx auf " + + "Debian 12 nicht starten") + } + + // Und sie muss beide Schreibweisen kennen. + for _, expectedForm := range []string{"listen-parameter", "eigene-anweisung"} { + if !strings.Contains(scriptContent, expectedForm) { + testInstance.Errorf("setup.sh kennt die Schreibweise %q nicht", expectedForm) + } + } + + // Die neue Anweisung darf nicht ungeschuetzt in der Vorlage stehen. Sie + // gehoert in die Zuweisung an http2Line, nicht in das Here-Document. + if strings.Contains(scriptContent, "\n http2 on;\n") { + testInstance.Error("setup.sh schreibt 'http2 on;' unbedingt in die " + + "nginx-Vorlage; auf nginx < 1.25.1 ergibt das 'unknown directive'") + } +} + +// TestSetupInstallsEveryOperationScript haelt fest, dass alle vier +// Betriebsskripte neben den Programmen landen. +// +// Beim Erproben verwies setup.sh nach einer misslungenen Einrichtung der +// Oberflaeche auf "/opt/syncova/setup.sh --weboberflaeche" — eine Datei, die es +// dort nicht gab. Kopiert wurde nur diagnose.sh. +// +// Der schwerere Fall ist uninstall.sh: Wer das ausgepackte Paket aufraeumt, +// haette die Anlage nie wieder entfernen koennen. +func TestSetupInstallsEveryOperationScript(testInstance *testing.T) { + setupScript, readError := os.ReadFile(filepath.Join(repositoryRoot, "scripts/setup.sh")) + if readError != nil { + testInstance.Fatalf("setup.sh ließ sich nicht lesen: %v", readError) + } + + scriptContent := string(setupScript) + + for _, operationScript := range []string{ + "setup.sh", "update.sh", "uninstall.sh", "diagnose.sh", + } { + // Das Skript muss in der Kopierschleife genannt sein. + if !strings.Contains(scriptContent, "for operationScript in") || + !strings.Contains(scriptContent, operationScript) { + testInstance.Errorf("setup.sh legt %s nicht neben die Programme; "+ + "ein Hinweis auf /opt/syncova/%s liefe dann ins Leere", + operationScript, operationScript) + } + } +} diff --git a/scripts/setup.sh b/scripts/setup.sh index 489e14b..dec54ec 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -172,6 +172,10 @@ administratorPassword="${SYNCOVA_SETUP_ADMIN_PASSWORD:-}" createHardenedRepository="${SYNCOVA_SETUP_HARDENED:-ja}" # encryptionKeys ist ein vorgegebener Schluessel; leer erzeugt einen neuen. encryptionKeys="${SYNCOVA_SETUP_ENCRYPTION_KEYS:-}" +# setupWebServer richtet nginx samt Zertifikat ein. +setupWebServer="${SYNCOVA_SETUP_WEB:-}" +# webOnlyMode richtet ausschliesslich die Weboberflaeche ein. +webOnlyMode="nein" showUsage() { cat <<'USAGE' @@ -184,6 +188,8 @@ Schalter: --adresse Lauschadresse (Vorgabe: 127.0.0.1:8080) --admin Anmeldename des ersten Administrators (Vorgabe: admin) --ohne-haertung Repository ohne Aufbewahrungsschutz anlegen + --weboberflaeche Nur die Weboberflaeche einrichten (bestehende Installation) + --ohne-weboberflaeche Weboberflaeche nicht einrichten --unbeaufsichtigt Keine Rueckfragen; alle Werte aus der Umgebung --hilfe Diese Uebersicht @@ -194,7 +200,8 @@ Umgebung fuer den unbeaufsichtigten Lauf: 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 + SYNCOVA_SETUP_LISTEN_ADDRESS Lauschadresse des API-Dienstes + SYNCOVA_SETUP_WEB ja | nein — nginx samt selbst ausgestelltem Zertifikat Ist bereits eine Installation vorhanden, bricht das Skript ab und verweist auf update.sh. Eine bestehende Anlage zu ueberschreiben ist kein Setup. @@ -209,6 +216,8 @@ while [[ $# -gt 0 ]]; do --admin) administratorName="${2:-}"; shift 2 ;; --ohne-haertung) createHardenedRepository="nein"; shift ;; --unbeaufsichtigt) unattendedMode="ja"; shift ;; + --weboberflaeche) webOnlyMode="ja"; shift ;; + --ohne-weboberflaeche) setupWebServer="nein"; shift ;; --hilfe|-h|--help) showUsage; trap - EXIT; exit 0 ;; *) showUsage >&2; abortWithMessage "unbekannter Schalter: $1" ;; esac @@ -438,12 +447,390 @@ detectPackageManager() { echo "unbekannt" } +# --------------------------------------------------------------------------- +# Weboberflaeche +# --------------------------------------------------------------------------- +# +# Die Oberflaeche ist ein Satz statischer Dateien. Sie braucht einen Webserver, +# und zwar aus zwei Gruenden: +# +# 1. **SPA-Fallback.** Ohne "try_files $uri /index.html" ergibt ein Neuladen +# auf /recovery-points einen 404 — die Oberflaeche kennt ihre Adressen +# selbst, der Webserver kennt sie nicht. +# 2. **Gleiche Herkunft.** Der Client ruft die API relativ unter /api/v1 auf. +# Liefert derselbe Server Oberflaeche und API aus, entfaellt CORS +# vollstaendig — und damit die haeufigste Fehlkonfiguration ueberhaupt. +# +# Der API-Dienst bleibt dabei an 127.0.0.1 gebunden. Erreichbar ist nur der +# Webserver; die API liegt hinter ihm. Das ist ausdruecklich sicherer, als den +# Dienst selbst ins Netz zu haengen. + +# webServerRootLink ist der Ort, an dem nginx die Oberflaeche erwartet. +readonly webServerRootLink="${installationRoot}/web" + +# tlsDirectory nimmt das selbst ausgestellte Zertifikat auf. +readonly tlsDirectory="${configurationDirectory}/tls" + +# certificateValidityDays ist die Gueltigkeit des selbst ausgestellten Zertifikats. +# +# Zehn Jahre. Ein selbst ausgestelltes Zertifikat wird von niemandem erneuert — +# es gibt keine Automatik dafuer. Eine kurze Frist ergaebe deshalb keinen +# Sicherheitsgewinn, sondern eine Oberflaeche, die eines Tages mit einer +# Fehlermeldung stehenbleibt, die niemand einordnen kann. +readonly certificateValidityDays=3650 + +# collectServerAddresses sammelt die Namen und Adressen dieses Servers. +# +# Sie gehoeren als subjectAltName ins Zertifikat. Moderne Browser sehen den +# Common Name gar nicht mehr an; ohne SAN ist das Zertifikat fuer sie +# unbrauchbar, gleich was daraufsteht. +collectServerAddresses() { + local shortName fullName addressList="" + + shortName="$(hostname -s 2>/dev/null || hostname)" + fullName="$(hostname -f 2>/dev/null || echo "${shortName}")" + + addressList="DNS:${shortName}" + [[ "${fullName}" != "${shortName}" ]] && addressList+=",DNS:${fullName}" + addressList+=",DNS:localhost,IP:127.0.0.1" + + # Jede erreichbare Adresse dieses Servers. Ohne sie zeigt der Browser beim + # Aufruf ueber die IP einen Namensfehler an — und genau so ruft man einen + # frisch eingerichteten Server auf, bevor er einen Namen hat. + local singleAddress + while read -r singleAddress; do + [[ -n "${singleAddress}" ]] || continue + [[ "${singleAddress}" == "127.0.0.1" ]] && continue + addressList+=",IP:${singleAddress}" + done < <(ip -4 -o addr show scope global 2>/dev/null | awk '{split($4,a,"/"); print a[1]}') + + printf '%s' "${addressList}" +} + +# createSelfSignedCertificate stellt ein Zertifikat fuer diesen Server aus. +createSelfSignedCertificate() { + local certificateFile="${tlsDirectory}/server.crt" + local keyFile="${tlsDirectory}/server.key" + + if [[ -f "${certificateFile}" && -f "${keyFile}" ]]; then + writeDetail "Ein Zertifikat ist bereits vorhanden und wird verwendet." + + return 0 + fi + + install -d -m 0750 -o root -g "${serviceAccount}" "${tlsDirectory}" + + local subjectAltNames + subjectAltNames="$(collectServerAddresses)" + + local certificateSubject + certificateSubject="/CN=$(hostname -f 2>/dev/null || hostname)/O=Syncova Backups" + + if ! openssl req -x509 -newkey rsa:4096 -sha256 -nodes \ + -days "${certificateValidityDays}" \ + -keyout "${keyFile}" -out "${certificateFile}" \ + -subj "${certificateSubject}" \ + -addext "subjectAltName=${subjectAltNames}" \ + -addext "basicConstraints=critical,CA:FALSE" \ + -addext "keyUsage=critical,digitalSignature,keyEncipherment" \ + -addext "extendedKeyUsage=serverAuth" >/dev/null 2>&1; then + abortWithMessage "Das Zertifikat liess sich nicht ausstellen." + fi + + chmod 0640 "${keyFile}" + chmod 0644 "${certificateFile}" + chown root:"${serviceAccount}" "${keyFile}" "${certificateFile}" + + writeSuccess "Zertifikat ausgestellt (${certificateValidityDays} Tage)" + writeDetail "Gilt fuer: ${subjectAltNames//,/, }" +} + +# http2DirectiveFor liefert die zur nginx-Fassung passende HTTP/2-Schreibweise. +# +# Ab 1.25.1 heisst es "http2 on;" als eigene Anweisung; davor war es ein +# Parameter von "listen". Die neue Schreibweise auf einer aelteren Fassung ergibt +# "unknown directive http2" und damit einen Webserver, der nicht startet — +# Debian 12 liefert 1.22, und daran ist genau das aufgefallen. +http2DirectiveFor() { + local versionText majorVersion minorVersion patchVersion + + versionText="$(nginx -v 2>&1 | sed -n 's|.*nginx/\([0-9.]*\).*|\1|p')" + + IFS='.' read -r majorVersion minorVersion patchVersion <<< "${versionText}" + + majorVersion="${majorVersion:-0}" + minorVersion="${minorVersion:-0}" + patchVersion="${patchVersion:-0}" + + if (( majorVersion > 1 )) || \ + (( majorVersion == 1 && minorVersion > 25 )) || \ + (( majorVersion == 1 && minorVersion == 25 && patchVersion >= 1 )); then + printf 'eigene-anweisung' + else + printf 'listen-parameter' + fi +} + +# writeWebServerSite schreibt die nginx-Konfiguration. +writeWebServerSite() { + local siteFile="$1" + + local http2Style listenSuffix http2Line + http2Style="$(http2DirectiveFor)" + + if [[ "${http2Style}" == "eigene-anweisung" ]]; then + listenSuffix="" + http2Line=" http2 on;" + else + listenSuffix=" http2" + http2Line=" # HTTP/2 steht bei dieser nginx-Fassung als Parameter von listen." + fi + + cat > "${siteFile}" </dev/null 2>&1 || missingPackages+=("openssl") + command -v nginx >/dev/null 2>&1 || missingPackages+=("nginx") + + if [[ ${#missingPackages[@]} -gt 0 ]]; then + writeDetail "Es wird installiert: ${missingPackages[*]}" + + case "${packageManager}" in + apt) + DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null 2>&1 + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${missingPackages[@]}" >/dev/null + ;; + dnf|yum) "${packageManager}" install -y -q "${missingPackages[@]}" >/dev/null ;; + zypper) zypper --non-interactive install -y "${missingPackages[@]}" >/dev/null ;; + pacman) pacman -Sy --noconfirm "${missingPackages[@]}" >/dev/null ;; + *) + writeWarning "Die Distribution wurde nicht erkannt." + writeDetail "Installieren Sie ${missingPackages[*]} von Hand und rufen Sie" + writeDetail "'setup.sh --weboberflaeche' erneut auf." + + return 1 + ;; + esac + fi + + # Belegte Ports melden statt eine fremde Anlage zu ueberschreiben. + local occupyingProcess + occupyingProcess="$(ss -tlnp 2>/dev/null | awk '$4 ~ /:443$/ {print $NF; exit}')" + + if [[ -n "${occupyingProcess}" ]] && ! printf '%s' "${occupyingProcess}" | grep -q nginx; then + writeWarning "Auf Port 443 lauscht bereits etwas anderes: ${occupyingProcess}" + writeDetail "Die Weboberflaeche wird nicht eingerichtet, um nichts zu zerstoeren." + + return 1 + fi + + createSelfSignedCertificate + + # nginx darf die Oberflaeche lesen. + chmod 0755 "${installationRoot}" + [[ -d "${webServerRootLink}" ]] && chmod -R a+rX "${webServerRootLink}" + + # Der Ablageort der Konfiguration unterscheidet sich je Distribution. + local siteFile="" + + if [[ -d /etc/nginx/sites-available ]]; then + siteFile="/etc/nginx/sites-available/syncova" + writeWebServerSite "${siteFile}" + ln -sf "${siteFile}" /etc/nginx/sites-enabled/syncova + + # Die mitgelieferte Standardseite beansprucht Port 80 und 443. + [[ -e /etc/nginx/sites-enabled/default ]] && rm -f /etc/nginx/sites-enabled/default + elif [[ -d /etc/nginx/conf.d ]]; then + siteFile="/etc/nginx/conf.d/syncova.conf" + writeWebServerSite "${siteFile}" + else + writeWarning "Es wurde kein nginx-Konfigurationsverzeichnis gefunden." + + return 1 + fi + + if ! nginx -t >/dev/null 2>&1; then + writeWarning "Die nginx-Konfiguration wird abgelehnt:" + nginx -t 2>&1 | sed 's/^/ /' >&2 + rm -f "${siteFile}" /etc/nginx/sites-enabled/syncova + + return 1 + fi + + systemctl enable nginx >/dev/null 2>&1 || true + systemctl restart nginx >/dev/null 2>&1 || systemctl start nginx >/dev/null 2>&1 + + if ! systemctl is-active --quiet nginx 2>/dev/null; then + writeWarning "nginx laeuft nicht: systemctl status nginx" + + return 1 + fi + + writeSuccess "nginx liefert die Oberflaeche aus" + + reportFirewallState + + return 0 +} + +# reportFirewallState sagt, ob der Zugang tatsaechlich offen ist. +# +# Geoeffnet wird nichts von selbst: Eine Firewallregel ist eine Entscheidung +# ueber die Erreichbarkeit eines Servers und gehoert nicht als Nebenwirkung +# einer Installation gesetzt. Gesagt wird es aber — sonst sucht der Betreiber +# den Fehler in der Anlage, waehrend die Firewall blockt. +reportFirewallState() { + if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "^Status: active"; then + if ufw status 2>/dev/null | grep -qE "^443|Nginx|https"; then + writeSuccess "ufw laesst Port 443 durch" + else + writeWarning "ufw ist aktiv und Port 443 ist nicht freigegeben:" + writeDetail " ufw allow 443/tcp && ufw allow 80/tcp" + fi + + return + fi + + if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then + if firewall-cmd --list-services 2>/dev/null | grep -q https; then + writeSuccess "firewalld laesst https durch" + else + writeWarning "firewalld ist aktiv und https ist nicht freigegeben:" + writeDetail " firewall-cmd --permanent --add-service=https --add-service=http" + writeDetail " firewall-cmd --reload" + fi + + return + fi + + writeDetail "Es wurde keine aktive Firewall erkannt; der Zugang haengt am Netz davor." +} + +# printWebAccessSummary nennt Adresse und Fingerabdruck. +printWebAccessSummary() { + local certificateFile="${tlsDirectory}/server.crt" + + [[ -f "${certificateFile}" ]] || return 0 + + printf '\n%sDie Oberflaeche ist erreichbar unter:%s\n\n' "${colorBold}" "${colorReset}" + + local singleAddress + while read -r singleAddress; do + [[ -n "${singleAddress}" ]] && printf ' https://%s/\n' "${singleAddress}" + done < <(ip -4 -o addr show scope global 2>/dev/null | awk '{split($4,a,"/"); print a[1]}') + + printf ' https://%s/\n' "$(hostname -f 2>/dev/null || hostname)" + + local certificateFingerprint + certificateFingerprint="$(openssl x509 -in "${certificateFile}" -noout -fingerprint -sha256 2>/dev/null | cut -d= -f2)" + + printf '\n%sDas Zertifikat ist selbst ausgestellt.%s Der Browser warnt deshalb — zu Recht:\n' \ + "${colorYellow}${colorBold}" "${colorReset}" + printf 'Niemand bestaetigt, dass es zu diesem Server gehoert. Es schuetzt gegen\n' + printf 'Mitlesen, nicht gegen einen Mittelsmann.\n\n' + printf 'Vergleichen Sie beim ersten Aufruf den Fingerabdruck im Browser mit diesem:\n\n' + printf ' %s\n\n' "${certificateFingerprint}" + printf 'Danach ist die Warnung unbedenklich. Fuer den Dauerbetrieb gehoert ein\n' + printf 'Zertifikat einer Zertifizierungsstelle hierher — eigene CA oder Let'"'"'s Encrypt.\n' +} + # --------------------------------------------------------------------------- # 1. Voraussetzungen # --------------------------------------------------------------------------- printf '\n%sSyncova Backups — Einrichtung%s\n\n' "${colorBold}" "${colorReset}" +# Nur die Oberflaeche, auf einer bereits eingerichteten Anlage. +# +# Sie ist der Teil, den man am ehesten nachtraegt: Erst laeuft die Sicherung, +# dann will jemand sie auch ansehen. Dafuer eine vollstaendige Neuinstallation +# zu verlangen waere unangemessen. +if [[ "${webOnlyMode}" == "ja" ]]; then + trap - EXIT + + if [[ "$(id -u)" -ne 0 ]]; then + abortWithMessage "Dieses Skript braucht root-Rechte (sudo ./setup.sh --weboberflaeche)." + fi + + if [[ ! -d "${installationRoot}/web" ]]; then + abortWithMessage "Unter ${installationRoot}/web liegt keine Oberflaeche. + Richten Sie zuerst die Anlage ein: ./setup.sh" + fi + + if setupWebInterface; then + printWebAccessSummary + printf '\n' + exit 0 + fi + + abortWithMessage "Die Weboberflaeche liess sich nicht einrichten." +fi + writeStep "Voraussetzungen" if [[ "$(id -u)" -ne 0 ]]; then @@ -667,10 +1054,17 @@ cp -R "${packageDirectory}/bin" "${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" +# Alle vier Betriebsskripte kommen neben die Programme. Im Ernstfall sucht +# niemand das ausgepackte Paket von vor drei Monaten — und wer es geloescht hat, +# koennte die Anlage sonst nie wieder deinstallieren oder die Oberflaeche +# nachtraeglich einrichten. Dass hier zuvor nur diagnose.sh stand, ist beim +# Erproben aufgefallen: Der Hinweis "Spaeter nachholen: /opt/syncova/setup.sh" +# verwies auf eine Datei, die es nicht gab. +for operationScript in setup.sh update.sh uninstall.sh diagnose.sh; do + [[ -f "${packageDirectory}/${operationScript}" ]] && \ + install -m 0755 "${packageDirectory}/${operationScript}" \ + "${installationRoot}/${operationScript}" +done # Die Version wird festgehalten. update.sh und uninstall.sh lesen sie; ohne sie # muesste man ein Binary aufrufen, um zu wissen, was installiert ist. @@ -967,8 +1361,44 @@ fi writeSuccess "Dienst laeuft und ist betriebsbereit" # Ab hier ist die Einrichtung gelungen; der Rueckbau entfaellt. +# +# Was danach kommt, ist Zugabe: Scheitert die Weboberflaeche, steht die Anlage +# trotzdem. Sie deshalb wieder abzureissen waere unverhaeltnismaessig — die +# Sicherungen laufen auch ohne Oberflaeche. trap - EXIT +# --------------------------------------------------------------------------- +# 10. Weboberflaeche +# --------------------------------------------------------------------------- + +if [[ -z "${setupWebServer}" ]]; then + if [[ "${unattendedMode}" == "ja" ]]; then + setupWebServer="ja" + else + printf '\n' + writeStep "Weboberflaeche" + writeDetail "Die Oberflaeche braucht einen Webserver. Eingerichtet wird nginx mit" + writeDetail "einem selbst ausgestellten Zertifikat; der API-Dienst bleibt dabei an" + writeDetail "127.0.0.1 gebunden und liegt hinter dem Webserver." + printf '\n' + + confirmQuestion "Weboberflaeche jetzt einrichten?" "j" && setupWebServer="ja" || setupWebServer="nein" + fi +fi + +webInterfaceReady="nein" + +if [[ "${setupWebServer}" == "ja" ]]; then + if setupWebInterface; then + webInterfaceReady="ja" + else + printf '\n' + writeWarning "Die Weboberflaeche wurde nicht eingerichtet." + writeDetail "Die Anlage laeuft trotzdem; die API ist unter ${listenAddress} erreichbar." + writeDetail "Spaeter nachholen: ${installationRoot}/setup.sh --weboberflaeche" + fi +fi + # --------------------------------------------------------------------------- # Abschluss # --------------------------------------------------------------------------- @@ -980,7 +1410,11 @@ 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}" +if [[ "${webInterfaceReady}" == "ja" ]]; then + printf ' Oberflaeche ueber nginx, siehe unten\n' +else + printf ' Oberflaeche %s/web — braucht einen Webserver mit SPA-Fallback\n' "${installationRoot}" +fi printf '\n' printf '%sWas jetzt zu tun bleibt:%s\n\n' "${colorBold}" "${colorReset}" @@ -995,18 +1429,17 @@ cat <<'NEXT' 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. + 2. Zweiten Faktor einrichten, mindestens fuer Konten mit Loeschrecht. - 3. Zweiten Faktor einrichten, mindestens fuer Konten mit Loeschrecht. - - 4. Benachrichtigungsweg einrichten. Ohne ihn erfaehrt niemand von einem + 3. Benachrichtigungsweg einrichten. Ohne ihn erfaehrt niemand von einem Ausfall. - 5. Eine Wiederherstellung durchfuehren, bevor Sie eine brauchen. Erst danach + 4. Eine Wiederherstellung durchfuehren, bevor Sie eine brauchen. Erst danach steigt ein Wiederherstellungspunkt auf "recoverable" — alles davor ist ein Indiz, kein Nachweis. NEXT +[[ "${webInterfaceReady}" == "ja" ]] && printWebAccessSummary + printf '\n Ausfuehrlich: %s/docs/installation.md\n' "${installationRoot}" printf ' Im Ernstfall: %s/docs/recovery-runbook.md\n\n' "${installationRoot}"