Weboberflaeche richtet sich mit ein (rc5)
Some checks failed
CI / Backend (Go) (push) Failing after 29s
CI / Frontend (React/TypeScript) (push) Successful in 33s
CI / Sicherheitsprüfungen (push) Successful in 24s

Bisher endete setup.sh mit einer laufenden API auf 127.0.0.1:8080 und der
Aufgabe, einen Webserver von Hand davorzusetzen. Das war der haeufigste Punkt,
an dem eine Einrichtung liegen blieb.

setup.sh richtet jetzt nginx ein und stellt ein selbst signiertes Zertifikat
aus. Es gilt fuer Rechnernamen, vollstaendigen Namen und jede globale
IPv4-Adresse (subjectAltName — moderne Browser lesen den CN nicht mehr), 3650
Tage; der SHA-256-Fingerabdruck wird genannt.

Drei Entscheidungen:

- Die API bleibt an 127.0.0.1:8080 gebunden. Sie auf alle Schnittstellen zu
  legen waere der kuerzere Weg und der falsche: Die Verschluesselung liesse
  sich dann umgehen, indem man Port 8080 direkt anspricht.
- Die Firewall wird gemeldet, nicht geaendert. Eine Einrichtung, die
  selbsttaetig einen Port ins Netz oeffnet, hebelt genau die Entscheidung aus,
  fuer die jemand die Firewall aufgesetzt hat.
- Scheitert die Oberflaeche, scheitert nicht die Einrichtung. Geprueft wird mit
  nginx -t, bevor die Konfiguration uebernommen wird; haelt sie nicht, wird sie
  entfernt und der Nachholweg gezeigt.

Zwei Funde 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.
- setup.sh kopierte nur diagnose.sh neben die Programme. Der eigene Hinweis
  "Spaeter nachholen: /opt/syncova/setup.sh --weboberflaeche" verwies damit auf
  eine Datei, die es nicht gab; schwerer wiegt uninstall.sh — wer das
  ausgepackte Paket aufraeumte, haette die Anlage nie wieder entfernen koennen.
  Jetzt kommen alle vier Skripte mit.

Nachgewiesen im Container gegen Debian 12 mit nginx 1.22: Neuinstallation von
Grund auf, Oberflaeche und /api/ von aussen ueber HTTPS erreichbar (200),
SPA-Fallback traegt, Anmeldung und Repository-Anlage durch nginx hindurch,
Durchsetzungsstufe gemessen, HTTP leitet mit 301 auf HTTPS, Fingerabdruck
stimmt mit dem genannten ueberein, Neuausstellung des Zertifikats geprueft.

Regressionstests fuer beide Funde, beide durch Mutation als fangend bestaetigt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jerrit Fritzsche 2026-08-18 08:53:15 +02:00
parent 22763f927f
commit b6668c600d
6 changed files with 693 additions and 12 deletions

View File

@ -1,5 +1,38 @@
# Änderungen # Ä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 ## V1 — Release Candidate 4, 18. August 2026
Behebt [Issue #2](https://git.jfritzsche.de/jf/syncova-backup/issues/2): Die Behebt [Issue #2](https://git.jfritzsche.de/jf/syncova-backup/issues/2): Die

View File

@ -216,6 +216,21 @@ curl -s http://127.0.0.1:8080/health/ready
## 8. Oberfläche ausliefern ## 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 Die Oberfläche ist ein Satz statischer Dateien unter `web/`. Sie braucht einen
**SPA-Fallback**, sonst ergibt ein Neuladen auf `/recovery-points` einen 404: **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 ## 9. Erstes Repository
Ein Repository entsteht **auf einem Datenträger**, nicht in einer Ein Repository entsteht **auf einem Datenträger**, nicht in einer

View File

@ -285,6 +285,57 @@ offen und in der Liste.
## Oberfläche ## 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 **404 nach dem Neuladen einer Unterseite** — der SPA-Fallback fehlt im
Webserver: `try_files $uri /index.html`. Webserver: `try_files $uri /index.html`.

View File

@ -297,6 +297,55 @@ healthReport+="Abgefragt mit: ${httpToolInUse}"
writeBlock "$(redactSecrets "${healthReport%$'\n\n'}")" 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 # 4. Datenbank
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@ -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)
}
}
}

View File

@ -172,6 +172,10 @@ administratorPassword="${SYNCOVA_SETUP_ADMIN_PASSWORD:-}"
createHardenedRepository="${SYNCOVA_SETUP_HARDENED:-ja}" createHardenedRepository="${SYNCOVA_SETUP_HARDENED:-ja}"
# encryptionKeys ist ein vorgegebener Schluessel; leer erzeugt einen neuen. # encryptionKeys ist ein vorgegebener Schluessel; leer erzeugt einen neuen.
encryptionKeys="${SYNCOVA_SETUP_ENCRYPTION_KEYS:-}" encryptionKeys="${SYNCOVA_SETUP_ENCRYPTION_KEYS:-}"
# setupWebServer richtet nginx samt Zertifikat ein.
setupWebServer="${SYNCOVA_SETUP_WEB:-}"
# webOnlyMode richtet ausschliesslich die Weboberflaeche ein.
webOnlyMode="nein"
showUsage() { showUsage() {
cat <<'USAGE' cat <<'USAGE'
@ -184,6 +188,8 @@ Schalter:
--adresse <host:port> Lauschadresse (Vorgabe: 127.0.0.1:8080) --adresse <host:port> Lauschadresse (Vorgabe: 127.0.0.1:8080)
--admin <name> Anmeldename des ersten Administrators (Vorgabe: admin) --admin <name> Anmeldename des ersten Administrators (Vorgabe: admin)
--ohne-haertung Repository ohne Aufbewahrungsschutz anlegen --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 --unbeaufsichtigt Keine Rueckfragen; alle Werte aus der Umgebung
--hilfe Diese Uebersicht --hilfe Diese Uebersicht
@ -194,7 +200,8 @@ Umgebung fuer den unbeaufsichtigten Lauf:
SYNCOVA_SETUP_ADMIN_PASSWORD Passwort des ersten Administrators SYNCOVA_SETUP_ADMIN_PASSWORD Passwort des ersten Administrators
SYNCOVA_SETUP_ENCRYPTION_KEYS Vorhandener Schluessel; leer erzeugt einen neuen SYNCOVA_SETUP_ENCRYPTION_KEYS Vorhandener Schluessel; leer erzeugt einen neuen
SYNCOVA_SETUP_REPOSITORY_PATH Ort des Repositorys 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 Ist bereits eine Installation vorhanden, bricht das Skript ab und verweist auf
update.sh. Eine bestehende Anlage zu ueberschreiben ist kein Setup. update.sh. Eine bestehende Anlage zu ueberschreiben ist kein Setup.
@ -209,6 +216,8 @@ while [[ $# -gt 0 ]]; do
--admin) administratorName="${2:-}"; shift 2 ;; --admin) administratorName="${2:-}"; shift 2 ;;
--ohne-haertung) createHardenedRepository="nein"; shift ;; --ohne-haertung) createHardenedRepository="nein"; shift ;;
--unbeaufsichtigt) unattendedMode="ja"; shift ;; --unbeaufsichtigt) unattendedMode="ja"; shift ;;
--weboberflaeche) webOnlyMode="ja"; shift ;;
--ohne-weboberflaeche) setupWebServer="nein"; shift ;;
--hilfe|-h|--help) showUsage; trap - EXIT; exit 0 ;; --hilfe|-h|--help) showUsage; trap - EXIT; exit 0 ;;
*) showUsage >&2; abortWithMessage "unbekannter Schalter: $1" ;; *) showUsage >&2; abortWithMessage "unbekannter Schalter: $1" ;;
esac esac
@ -438,12 +447,390 @@ detectPackageManager() {
echo "unbekannt" 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}" <<SITE
# Syncova Backups — Weboberflaeche und API.
#
# Angelegt von setup.sh. Aenderungen bleiben bei einem Update erhalten; die
# Datei wird nicht ueberschrieben, solange sie vorhanden ist.
server {
listen 80;
listen [::]:80;
server_name _;
# Alles auf HTTPS. Ein Anmeldeformular ueber HTTP schickt das Passwort im
# Klartext durch das Netz.
return 301 https://\$host\$request_uri;
}
server {
listen 443 ssl${listenSuffix};
listen [::]:443 ssl${listenSuffix};
${http2Line}
server_name _;
ssl_certificate ${tlsDirectory}/server.crt;
ssl_certificate_key ${tlsDirectory}/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
root ${webServerRootLink};
index index.html;
# Die Oberflaeche kennt ihre Adressen selbst. Ohne diese Zeile ergibt ein
# Neuladen auf /recovery-points einen 404.
location / {
try_files \$uri \$uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
# Ein Integritaetslauf oder eine Wiederherstellung laeuft lange. Die
# Vorgabe von 60 Sekunden braeche sie mit einem 504 ab, waehrend die
# Anlage einwandfrei weiterarbeitet.
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_request_buffering off;
client_max_body_size 64m;
}
location = /health/live { proxy_pass http://127.0.0.1:8080; }
location = /health/ready { proxy_pass http://127.0.0.1:8080; }
}
SITE
}
# setupWebInterface richtet Webserver, Zertifikat und Weiterleitung ein.
setupWebInterface() {
printf '\n'
writeStep "Weboberflaeche"
# openssl und nginx nachinstallieren, falls noetig.
local packageManager
packageManager="$(detectPackageManager)"
local missingPackages=()
command -v openssl >/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 # 1. Voraussetzungen
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
printf '\n%sSyncova Backups — Einrichtung%s\n\n' "${colorBold}" "${colorReset}" 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" writeStep "Voraussetzungen"
if [[ "$(id -u)" -ne 0 ]]; then if [[ "$(id -u)" -ne 0 ]]; then
@ -667,10 +1054,17 @@ cp -R "${packageDirectory}/bin" "${installationRoot}/"
[[ -d "${packageDirectory}/deployment" ]] && cp -R "${packageDirectory}/deployment" "${installationRoot}/" [[ -d "${packageDirectory}/deployment" ]] && cp -R "${packageDirectory}/deployment" "${installationRoot}/"
chmod -R 0755 "${installationRoot}/bin" chmod -R 0755 "${installationRoot}/bin"
# Das Diagnoseskript kommt neben die Programme. Im Ernstfall sucht niemand das # Alle vier Betriebsskripte kommen neben die Programme. Im Ernstfall sucht
# ausgepackte Paket von vor drei Monaten. # niemand das ausgepackte Paket von vor drei Monaten — und wer es geloescht hat,
[[ -f "${packageDirectory}/diagnose.sh" ]] && \ # koennte die Anlage sonst nie wieder deinstallieren oder die Oberflaeche
install -m 0755 "${packageDirectory}/diagnose.sh" "${installationRoot}/diagnose.sh" # 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 # Die Version wird festgehalten. update.sh und uninstall.sh lesen sie; ohne sie
# muesste man ein Binary aufrufen, um zu wissen, was installiert ist. # muesste man ein Binary aufrufen, um zu wissen, was installiert ist.
@ -967,8 +1361,44 @@ fi
writeSuccess "Dienst laeuft und ist betriebsbereit" writeSuccess "Dienst laeuft und ist betriebsbereit"
# Ab hier ist die Einrichtung gelungen; der Rueckbau entfaellt. # 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 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 # Abschluss
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -980,7 +1410,11 @@ printf ' Programme %s\n' "${installationRoot}"
printf ' Konfiguration %s\n' "${configurationFile}" printf ' Konfiguration %s\n' "${configurationFile}"
printf ' Repository %s\n' "${repositoryPath}" printf ' Repository %s\n' "${repositoryPath}"
printf ' Dienst %s (systemctl status %s)\n' "${serviceName}" "${serviceName}" 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 '\n'
printf '%sWas jetzt zu tun bleibt:%s\n\n' "${colorBold}" "${colorReset}" printf '%sWas jetzt zu tun bleibt:%s\n\n' "${colorBold}" "${colorReset}"
@ -995,18 +1429,17 @@ cat <<'NEXT'
curl -X POST http://<adresse>/api/v1/repositories/<id>/enforcement/measure \ curl -X POST http://<adresse>/api/v1/repositories/<id>/enforcement/measure \
-H "Authorization: Bearer <token>" -H "Authorization: Bearer <token>"
2. Oberflaeche ausliefern (nginx, SPA-Fallback try_files $uri /index.html) 2. Zweiten Faktor einrichten, mindestens fuer Konten mit Loeschrecht.
und TLS einrichten — oder den Dienst an 127.0.0.1 binden.
3. Zweiten Faktor einrichten, mindestens fuer Konten mit Loeschrecht. 3. Benachrichtigungsweg einrichten. Ohne ihn erfaehrt niemand von einem
4. Benachrichtigungsweg einrichten. Ohne ihn erfaehrt niemand von einem
Ausfall. 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 steigt ein Wiederherstellungspunkt auf "recoverable" — alles davor ist ein
Indiz, kein Nachweis. Indiz, kein Nachweis.
NEXT NEXT
[[ "${webInterfaceReady}" == "ja" ]] && printWebAccessSummary
printf '\n Ausfuehrlich: %s/docs/installation.md\n' "${installationRoot}" printf '\n Ausfuehrlich: %s/docs/installation.md\n' "${installationRoot}"
printf ' Im Ernstfall: %s/docs/recovery-runbook.md\n\n' "${installationRoot}" printf ' Im Ernstfall: %s/docs/recovery-runbook.md\n\n' "${installationRoot}"