Weboberflaeche richtet sich mit ein (rc5)
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:
parent
22763f927f
commit
b6668c600d
33
CHANGELOG.md
33
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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`.
|
||||
|
||||
|
||||
@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
455
scripts/setup.sh
455
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 <host:port> Lauschadresse (Vorgabe: 127.0.0.1:8080)
|
||||
--admin <name> 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}" <<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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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}"
|
||||
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://<adresse>/api/v1/repositories/<id>/enforcement/measure \
|
||||
-H "Authorization: Bearer <token>"
|
||||
|
||||
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}"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user