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>
1446 lines
58 KiB
Bash
Executable File
1446 lines
58 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Richtet Syncova Backups auf einem Linux-Server ein.
|
|
#
|
|
# Der Ablauf folgt docs/installation.md. Das Skript nimmt niemandem eine
|
|
# Entscheidung ab, die er selbst treffen muss — es fragt, erklaert, und legt
|
|
# nichts an, was es nicht auch wieder zuruecknehmen koennte.
|
|
#
|
|
# ./setup.sh fragt nach
|
|
# ./setup.sh --unbeaufsichtigt liest alles aus der Umgebung
|
|
# ./setup.sh --hilfe zeigt alle Schalter
|
|
#
|
|
# Bei einem Abbruch wird zurueckgebaut, was dieser Lauf angelegt hat. Eine halb
|
|
# eingerichtete Anlage sieht arbeitsfaehig aus und scheitert beim ersten Lauf.
|
|
|
|
set -euo pipefail
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Feste Orte
|
|
#
|
|
# Sie stehen gleichlautend in update.sh und uninstall.sh; ein Test haelt die
|
|
# drei Skripte deckungsgleich. Drei Skripte, die sich ueber den Installationsort
|
|
# uneinig sind, ergeben eine Anlage, die sich nicht mehr entfernen laesst.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# installationRoot ist das Programmverzeichnis.
|
|
readonly installationRoot="/opt/syncova"
|
|
# configurationDirectory enthaelt die Umgebungsdatei.
|
|
readonly configurationDirectory="/etc/syncova"
|
|
# configurationFile ist die Umgebungsdatei des Dienstes.
|
|
readonly configurationFile="${configurationDirectory}/syncova.env"
|
|
# serviceName ist die systemd-Einheit.
|
|
readonly serviceName="syncova-api"
|
|
# serviceUnitFile ist deren Datei.
|
|
readonly serviceUnitFile="/etc/systemd/system/${serviceName}.service"
|
|
# serviceAccount ist das Dienstkonto.
|
|
readonly serviceAccount="syncova"
|
|
# defaultRepositoryPath ist der vorgeschlagene Ort des Repositorys.
|
|
readonly defaultRepositoryPath="/srv/syncova-repository"
|
|
# backupDirectory nimmt Sicherungen von Datenbank und Konfiguration auf.
|
|
readonly backupDirectory="/var/backups/syncova"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ausgabe
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if [[ -t 1 ]]; then
|
|
readonly colorReset=$'\033[0m'
|
|
readonly colorBold=$'\033[1m'
|
|
readonly colorRed=$'\033[31m'
|
|
readonly colorGreen=$'\033[32m'
|
|
readonly colorYellow=$'\033[33m'
|
|
else
|
|
readonly colorReset='' colorBold='' colorRed='' colorGreen='' colorYellow=''
|
|
fi
|
|
|
|
# writeStep meldet einen Arbeitsschritt.
|
|
writeStep() { printf '%s==>%s %s\n' "${colorBold}" "${colorReset}" "$1"; }
|
|
|
|
# writeDetail meldet eine Einzelheit.
|
|
writeDetail() { printf ' %s\n' "$1"; }
|
|
|
|
# writeSuccess meldet einen gelungenen Schritt.
|
|
writeSuccess() { printf ' %s✓%s %s\n' "${colorGreen}" "${colorReset}" "$1"; }
|
|
|
|
# writeWarning meldet etwas, das Beachtung verlangt.
|
|
writeWarning() { printf ' %s!%s %s\n' "${colorYellow}" "${colorReset}" "$1" >&2; }
|
|
|
|
# abortWithMessage bricht mit einer Begruendung ab.
|
|
abortWithMessage() {
|
|
printf '\n%sAbbruch:%s %s\n' "${colorRed}${colorBold}" "${colorReset}" "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Rueckbau bei Abbruch
|
|
#
|
|
# Jeder Schritt vermerkt, was er angelegt hat. Bricht das Skript ab, wird genau
|
|
# das wieder entfernt — und nichts sonst. Eine vorgefundene Datenbank oder ein
|
|
# vorgefundenes Repository bleiben unangetastet, auch wenn der Lauf scheitert.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# createdArtifacts sammelt die Dinge, die dieser Lauf angelegt hat.
|
|
createdArtifacts=()
|
|
|
|
# noteCreated vermerkt ein angelegtes Objekt fuer den Rueckbau.
|
|
noteCreated() { createdArtifacts+=("$1"); }
|
|
|
|
# rollbackOnFailure baut zurueck, was dieser Lauf angelegt hat.
|
|
rollbackOnFailure() {
|
|
local exitCode=$?
|
|
|
|
if [[ ${exitCode} -eq 0 ]] || [[ ${#createdArtifacts[@]} -eq 0 ]]; then
|
|
exit "${exitCode}"
|
|
fi
|
|
|
|
printf '\n%sDer Lauf wird zurueckgebaut.%s\n' "${colorYellow}${colorBold}" "${colorReset}" >&2
|
|
|
|
# Rueckwaerts: zuerst der Dienst, zuletzt die Datenbank.
|
|
local artifactIndex
|
|
for (( artifactIndex=${#createdArtifacts[@]}-1; artifactIndex>=0; artifactIndex-- )); do
|
|
local artifact="${createdArtifacts[artifactIndex]}"
|
|
local artifactKind="${artifact%%:*}"
|
|
local artifactValue="${artifact#*:}"
|
|
|
|
case "${artifactKind}" in
|
|
service)
|
|
systemctl stop "${serviceName}" 2>/dev/null || true
|
|
systemctl disable "${serviceName}" 2>/dev/null || true
|
|
rm -f "${serviceUnitFile}"
|
|
systemctl daemon-reload 2>/dev/null || true
|
|
writeDetail "Dienst entfernt"
|
|
;;
|
|
directory)
|
|
rm -rf "${artifactValue}"
|
|
writeDetail "Verzeichnis entfernt: ${artifactValue}"
|
|
;;
|
|
file)
|
|
rm -f "${artifactValue}"
|
|
writeDetail "Datei entfernt: ${artifactValue}"
|
|
;;
|
|
account)
|
|
userdel "${artifactValue}" 2>/dev/null || true
|
|
writeDetail "Dienstkonto entfernt: ${artifactValue}"
|
|
;;
|
|
database)
|
|
# Nur eine Datenbank, die dieser Lauf angelegt hat.
|
|
runAsPostgres "DROP DATABASE IF EXISTS ${artifactValue}" || true
|
|
writeDetail "Datenbank entfernt: ${artifactValue}"
|
|
;;
|
|
dbuser)
|
|
runAsPostgres "DROP ROLE IF EXISTS ${artifactValue}" || true
|
|
writeDetail "Datenbankkonto entfernt: ${artifactValue}"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
printf '\n%sEs wurde nichts angetastet, was vorher schon da war.%s\n' \
|
|
"${colorYellow}" "${colorReset}" >&2
|
|
|
|
exit "${exitCode}"
|
|
}
|
|
|
|
trap rollbackOnFailure EXIT
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Schalter
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# unattendedMode laeuft ohne Rueckfragen.
|
|
unattendedMode="nein"
|
|
# packageDirectory ist das entpackte Auslieferungspaket.
|
|
packageDirectory=""
|
|
# repositoryPath ist der Ort des Repositorys.
|
|
repositoryPath="${SYNCOVA_SETUP_REPOSITORY_PATH:-${defaultRepositoryPath}}"
|
|
# databaseMode ist "lokal" oder "vorhanden".
|
|
databaseMode="${SYNCOVA_SETUP_DATABASE_MODE:-}"
|
|
# databaseHost, -Port, -Name, -User, -Password beschreiben die Datenbank.
|
|
databaseHost="${SYNCOVA_SETUP_DB_HOST:-127.0.0.1}"
|
|
databasePort="${SYNCOVA_SETUP_DB_PORT:-5432}"
|
|
databaseName="${SYNCOVA_SETUP_DB_NAME:-syncova}"
|
|
databaseUser="${SYNCOVA_SETUP_DB_USER:-syncova}"
|
|
databasePassword="${SYNCOVA_SETUP_DB_PASSWORD:-}"
|
|
databaseSSLMode="${SYNCOVA_SETUP_DB_SSLMODE:-}"
|
|
# listenAddress ist die Adresse des Dienstes.
|
|
listenAddress="${SYNCOVA_SETUP_LISTEN_ADDRESS:-127.0.0.1:8080}"
|
|
# administratorName ist der erste Administrator.
|
|
administratorName="${SYNCOVA_SETUP_ADMIN_USERNAME:-admin}"
|
|
# administratorPassword ist dessen Passwort.
|
|
administratorPassword="${SYNCOVA_SETUP_ADMIN_PASSWORD:-}"
|
|
# createHardenedRepository legt ein gehaertetes Repository an.
|
|
createHardenedRepository="${SYNCOVA_SETUP_HARDENED:-ja}"
|
|
# encryptionKeys ist ein vorgegebener Schluessel; leer erzeugt einen neuen.
|
|
encryptionKeys="${SYNCOVA_SETUP_ENCRYPTION_KEYS:-}"
|
|
# setupWebServer richtet nginx samt Zertifikat ein.
|
|
setupWebServer="${SYNCOVA_SETUP_WEB:-}"
|
|
# webOnlyMode richtet ausschliesslich die Weboberflaeche ein.
|
|
webOnlyMode="nein"
|
|
|
|
showUsage() {
|
|
cat <<'USAGE'
|
|
Verwendung:
|
|
setup.sh [--paket <verzeichnis>] [--unbeaufsichtigt] [weitere Schalter]
|
|
|
|
Schalter:
|
|
--paket <verzeichnis> Entpacktes Auslieferungspaket (Vorgabe: Verzeichnis des Skripts)
|
|
--repository <pfad> Ort des Repositorys (Vorgabe: /srv/syncova-repository)
|
|
--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
|
|
|
|
Umgebung fuer den unbeaufsichtigten Lauf:
|
|
SYNCOVA_SETUP_DATABASE_MODE lokal | vorhanden
|
|
SYNCOVA_SETUP_DB_HOST/_PORT/_NAME/_USER/_PASSWORD/_SSLMODE
|
|
SYNCOVA_SETUP_ADMIN_USERNAME Anmeldename (Vorgabe: admin)
|
|
SYNCOVA_SETUP_ADMIN_PASSWORD Passwort des ersten Administrators
|
|
SYNCOVA_SETUP_ENCRYPTION_KEYS Vorhandener Schluessel; leer erzeugt einen neuen
|
|
SYNCOVA_SETUP_REPOSITORY_PATH Ort des Repositorys
|
|
SYNCOVA_SETUP_LISTEN_ADDRESS Lauschadresse 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.
|
|
USAGE
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--paket) packageDirectory="${2:-}"; shift 2 ;;
|
|
--repository) repositoryPath="${2:-}"; shift 2 ;;
|
|
--adresse) listenAddress="${2:-}"; shift 2 ;;
|
|
--admin) administratorName="${2:-}"; shift 2 ;;
|
|
--ohne-haertung) createHardenedRepository="nein"; shift ;;
|
|
--unbeaufsichtigt) unattendedMode="ja"; shift ;;
|
|
--weboberflaeche) webOnlyMode="ja"; shift ;;
|
|
--ohne-weboberflaeche) setupWebServer="nein"; shift ;;
|
|
--hilfe|-h|--help) showUsage; trap - EXIT; exit 0 ;;
|
|
*) showUsage >&2; abortWithMessage "unbekannter Schalter: $1" ;;
|
|
esac
|
|
done
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Hilfsfunktionen
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# askQuestion stellt eine Frage mit Vorgabewert.
|
|
askQuestion() {
|
|
local questionText="$1" defaultValue="${2:-}" answerVariable="$3" enteredValue=""
|
|
|
|
if [[ "${unattendedMode}" == "ja" ]]; then
|
|
printf -v "${answerVariable}" '%s' "${defaultValue}"
|
|
return 0
|
|
fi
|
|
|
|
if [[ -n "${defaultValue}" ]]; then
|
|
read -r -p " ${questionText} [${defaultValue}]: " enteredValue
|
|
else
|
|
read -r -p " ${questionText}: " enteredValue
|
|
fi
|
|
|
|
printf -v "${answerVariable}" '%s' "${enteredValue:-${defaultValue}}"
|
|
}
|
|
|
|
# askPassword liest ein Passwort verdeckt und laesst es wiederholen.
|
|
askPassword() {
|
|
local questionText="$1" answerVariable="$2" firstEntry="" secondEntry=""
|
|
|
|
while true; do
|
|
read -r -s -p " ${questionText}: " firstEntry; printf '\n'
|
|
read -r -s -p " ${questionText} wiederholen: " secondEntry; printf '\n'
|
|
|
|
if [[ -z "${firstEntry}" ]]; then
|
|
writeWarning "Ein leeres Passwort wird nicht angenommen."
|
|
continue
|
|
fi
|
|
|
|
if [[ "${firstEntry}" != "${secondEntry}" ]]; then
|
|
writeWarning "Die beiden Eingaben stimmen nicht ueberein."
|
|
continue
|
|
fi
|
|
|
|
printf -v "${answerVariable}" '%s' "${firstEntry}"
|
|
return 0
|
|
done
|
|
}
|
|
|
|
# confirmQuestion stellt eine Ja/Nein-Frage.
|
|
confirmQuestion() {
|
|
local questionText="$1" defaultAnswer="${2:-n}" enteredValue=""
|
|
|
|
if [[ "${unattendedMode}" == "ja" ]]; then
|
|
[[ "${defaultAnswer}" == "j" ]]
|
|
return $?
|
|
fi
|
|
|
|
read -r -p " ${questionText} [$( [[ "${defaultAnswer}" == "j" ]] && echo 'J/n' || echo 'j/N' )]: " enteredValue
|
|
enteredValue="${enteredValue:-${defaultAnswer}}"
|
|
|
|
[[ "${enteredValue}" =~ ^[jJyY] ]]
|
|
}
|
|
|
|
# generatePassword erzeugt ein zufaelliges Passwort.
|
|
generatePassword() {
|
|
# Aus /dev/urandom und nicht aus $RANDOM: Letzteres ist vorhersagbar und
|
|
# hat 15 Bit Zufall — fuer ein Datenbankpasswort zu wenig.
|
|
#
|
|
# Die Reihenfolge der Pipe ist wesentlich: Erst wird eine **begrenzte**
|
|
# Menge gelesen, am Ende steht "cut". Mit "tr < /dev/urandom | head -c 32"
|
|
# schliesst head den Pipe nach 32 Zeichen, tr bekommt SIGPIPE, und mit
|
|
# "set -o pipefail" bricht das Skript ab — mitten in der Einrichtung und
|
|
# ohne erkennbaren Grund.
|
|
head -c 512 /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | cut -c1-32
|
|
}
|
|
|
|
# runAsPostgres fuehrt SQL als Datenbankverwalter aus.
|
|
runAsPostgres() {
|
|
su - postgres -c "psql -v ON_ERROR_STOP=1 -c \"$1\"" >/dev/null 2>&1
|
|
}
|
|
|
|
# queryAsPostgres liefert das Ergebnis einer Abfrage als Text.
|
|
#
|
|
# Bewusst ohne Pipe zu grep: "psql | grep -q" schliesst den Pipe nach dem ersten
|
|
# Treffer, psql bekommt SIGPIPE, und mit "set -o pipefail" bricht das ganze
|
|
# Skript ab — mitten in der Einrichtung, ohne erkennbaren Grund.
|
|
queryAsPostgres() {
|
|
su - postgres -c "psql -tAc \"$1\"" 2>/dev/null || true
|
|
}
|
|
|
|
# volatileRepositoryOverride laesst einen fluechtigen Ort ausdruecklich zu.
|
|
readonly volatileRepositoryOverride="${SYNCOVA_SETUP_ALLOW_VOLATILE_REPOSITORY:-nein}"
|
|
|
|
# repositoryIsUnderPrivateTmp meldet einen Ort unterhalb von /tmp.
|
|
#
|
|
# Der Dienst laeuft mit PrivateTmp=yes und bekommt damit ein **eigenes** /tmp.
|
|
# Ein Repository unter /tmp existiert in seiner Sicht nicht; systemd bricht den
|
|
# Start mit 226/NAMESPACE ab, noch bevor das Programm laeuft.
|
|
repositoryIsUnderPrivateTmp() {
|
|
[[ "$1" == /tmp/* || "$1" == "/tmp" ]]
|
|
}
|
|
|
|
# validateRepositoryPath weist einen fluechtigen Ablageort zurueck.
|
|
#
|
|
# **Der wichtigste Punkt dieser ganzen Datei.** Ein Backup-Repository unter /tmp,
|
|
# /var/tmp oder auf einem tmpfs ist nicht unbequem, sondern falsch:
|
|
#
|
|
# - systemd-tmpfiles raeumt /tmp und /var/tmp regelmaessig auf. Die Sicherungen
|
|
# verschwinden dann von selbst, ohne Meldung und ohne dass jemand es merkt —
|
|
# bis er sie braucht.
|
|
# - Ein tmpfs liegt im Arbeitsspeicher. Nach einem Neustart ist es leer.
|
|
#
|
|
# Fuer ein Programm, dessen einziger Zweck die Aufbewahrung von Daten ist, waere
|
|
# das die schlimmste denkbare Voreinstellung: Es meldete jede Nacht erfolgreiche
|
|
# Sicherungen und haette keine.
|
|
validateRepositoryPath() {
|
|
local candidatePath="$1" refusalReason=""
|
|
|
|
case "${candidatePath}" in
|
|
/tmp|/tmp/*) refusalReason="/tmp wird von systemd-tmpfiles regelmaessig aufgeraeumt" ;;
|
|
/var/tmp|/var/tmp/*) refusalReason="/var/tmp wird von systemd-tmpfiles regelmaessig aufgeraeumt" ;;
|
|
/dev/shm|/dev/shm/*) refusalReason="/dev/shm liegt im Arbeitsspeicher" ;;
|
|
/run|/run/*) refusalReason="/run liegt im Arbeitsspeicher" ;;
|
|
esac
|
|
|
|
# Der allgemeine Fall: Was auf einem tmpfs liegt, ueberlebt keinen Neustart —
|
|
# gleich wie das Verzeichnis heisst.
|
|
if [[ -z "${refusalReason}" ]] && [[ -d "${candidatePath}" ]]; then
|
|
local filesystemType
|
|
filesystemType="$(stat -f -c '%T' "${candidatePath}" 2>/dev/null || echo unbekannt)"
|
|
|
|
case "${filesystemType}" in
|
|
tmpfs|ramfs) refusalReason="der Ort liegt auf einem ${filesystemType} (Arbeitsspeicher)" ;;
|
|
esac
|
|
fi
|
|
|
|
[[ -z "${refusalReason}" ]] && return 0
|
|
|
|
if [[ "${volatileRepositoryOverride}" == "ja" ]]; then
|
|
writeWarning "Fluechtiger Ablageort ausdruecklich zugelassen: ${refusalReason}."
|
|
writeDetail "Die Sicherungen dort ueberleben weder Aufraeumlaeufe noch einen Neustart."
|
|
writeDetail "Das ist ausschliesslich fuer Wegwerf-Umgebungen vertretbar."
|
|
|
|
return 0
|
|
fi
|
|
|
|
abortWithMessage "Der Ort ${candidatePath} taugt nicht als Repository:
|
|
${refusalReason}.
|
|
|
|
Die Sicherungen wuerden dort verschwinden, ohne dass es jemandem
|
|
auffaellt — und ein Backupsystem, das jede Nacht Erfolg meldet und
|
|
keine Daten hat, ist schlimmer als gar keines.
|
|
|
|
Nehmen Sie ein dauerhaftes Verzeichnis, etwa /srv/syncova-repository,
|
|
und moeglichst einen anderen Datentraeger als den der Quelldaten.
|
|
|
|
Nur fuer Wegwerf-Umgebungen: SYNCOVA_SETUP_ALLOW_VOLATILE_REPOSITORY=ja"
|
|
}
|
|
|
|
# serviceIsReachable prueft, ob eine Adresse mit HTTP 200 antwortet.
|
|
#
|
|
# Drei Wege, weil keiner ueberall vorhanden ist. Genau daran ist die Einrichtung
|
|
# in Issue #2 gescheitert: Der Dienst lief einwandfrei — Protokoll und
|
|
# Zeitstempel belegen es —, aber auf dem System war kein curl installiert. Die
|
|
# Bereitschaftspruefung hielt das fuer einen gescheiterten Start und baute eine
|
|
# funktionierende Anlage wieder zurueck.
|
|
#
|
|
# Ein Einrichtungsskript darf nicht voraussetzen, was es nicht selbst mitbringt.
|
|
serviceIsReachable() {
|
|
local requestURL="$1"
|
|
|
|
if command -v curl >/dev/null 2>&1; then
|
|
curl -fsS --max-time 5 "${requestURL}" >/dev/null 2>&1
|
|
return $?
|
|
fi
|
|
|
|
# wget endet bei 4xx und 5xx mit einem Fehler — dasselbe Verhalten wie
|
|
# "curl -f".
|
|
if command -v wget >/dev/null 2>&1; then
|
|
wget -q -O /dev/null --timeout=5 "${requestURL}" >/dev/null 2>&1
|
|
return $?
|
|
fi
|
|
|
|
bashHTTPStatusIsOK "${requestURL}"
|
|
}
|
|
|
|
# bashHTTPStatusIsOK spricht HTTP ohne jedes Zusatzwerkzeug.
|
|
#
|
|
# Der letzte Ausweg: /dev/tcp gehoert zur Bash selbst. Damit laesst sich die
|
|
# Bereitschaft auch auf einem System pruefen, auf dem weder curl noch wget
|
|
# liegen — und das ist bei einem schlanken Serverabbild der Normalfall, nicht
|
|
# die Ausnahme.
|
|
bashHTTPStatusIsOK() {
|
|
local strippedURL="${1#http://}"
|
|
local hostAndPort="${strippedURL%%/*}"
|
|
local requestPath="/${strippedURL#*/}"
|
|
|
|
# Ohne Pfadanteil liefert die Ersetzung oben die Adresse zurueck.
|
|
[[ "${requestPath}" == "/${hostAndPort}" ]] && requestPath="/"
|
|
|
|
local targetHost="${hostAndPort%%:*}"
|
|
local targetPort="${hostAndPort##*:}"
|
|
[[ "${targetPort}" == "${targetHost}" ]] && targetPort=80
|
|
|
|
exec 3<>"/dev/tcp/${targetHost}/${targetPort}" 2>/dev/null || return 1
|
|
|
|
printf 'GET %s HTTP/1.0\r\nHost: %s\r\nConnection: close\r\n\r\n' \
|
|
"${requestPath}" "${hostAndPort}" >&3
|
|
|
|
local statusLine=""
|
|
read -r -t 5 statusLine <&3
|
|
|
|
exec 3<&- 3>&- 2>/dev/null
|
|
|
|
[[ "${statusLine}" == *" 200 "* ]]
|
|
}
|
|
|
|
# detectPackageManager ermittelt das Paketwerkzeug der Distribution.
|
|
detectPackageManager() {
|
|
if command -v apt-get >/dev/null 2>&1; then echo "apt"; return; fi
|
|
if command -v dnf >/dev/null 2>&1; then echo "dnf"; return; fi
|
|
if command -v yum >/dev/null 2>&1; then echo "yum"; return; fi
|
|
if command -v zypper >/dev/null 2>&1; then echo "zypper"; return; fi
|
|
if command -v pacman >/dev/null 2>&1; then echo "pacman"; return; fi
|
|
echo "unbekannt"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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
|
|
abortWithMessage "Dieses Skript braucht root-Rechte (sudo ./setup.sh)."
|
|
fi
|
|
|
|
if [[ "$(uname -s)" != "Linux" ]]; then
|
|
abortWithMessage "Der Syncova-Server wird ausschliesslich fuer Linux ausgeliefert."
|
|
fi
|
|
|
|
if ! command -v systemctl >/dev/null 2>&1; then
|
|
abortWithMessage "Es wurde kein systemd gefunden. Der Dienst laesst sich sonst nicht einrichten."
|
|
fi
|
|
|
|
# Eine bestehende Installation wird nicht ueberschrieben. Dafuer gibt es
|
|
# update.sh — und der Unterschied ist wesentlich: Ein Update sichert vorher.
|
|
if [[ -e "${installationRoot}" ]] || [[ -f "${configurationFile}" ]] || [[ -f "${serviceUnitFile}" ]]; then
|
|
printf '\n'
|
|
writeWarning "Es ist bereits eine Installation vorhanden:"
|
|
[[ -e "${installationRoot}" ]] && writeDetail " ${installationRoot}"
|
|
[[ -f "${configurationFile}" ]] && writeDetail " ${configurationFile}"
|
|
[[ -f "${serviceUnitFile}" ]] && writeDetail " ${serviceUnitFile}"
|
|
printf '\n'
|
|
abortWithMessage "Zum Aktualisieren: ./update.sh — es sichert vorher Datenbank und Konfiguration."
|
|
fi
|
|
|
|
writeSuccess "root, Linux, systemd"
|
|
|
|
# Das Paket finden.
|
|
if [[ -z "${packageDirectory}" ]]; then
|
|
packageDirectory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
fi
|
|
|
|
if [[ ! -x "${packageDirectory}/bin/syncova-api" ]]; then
|
|
abortWithMessage "Unter ${packageDirectory} liegt kein Auslieferungspaket (bin/syncova-api fehlt).
|
|
Geben Sie es mit --paket <verzeichnis> an."
|
|
fi
|
|
|
|
packageVersion="$("${packageDirectory}/bin/syncova-api" --version 2>/dev/null | awk '{print $2}')"
|
|
writeSuccess "Paket gefunden: ${packageVersion}"
|
|
|
|
# Den vorgesehenen Ablageort sofort pruefen. Im unbeaufsichtigten Lauf soll das
|
|
# in Sekunden scheitern und nicht erst, nachdem PostgreSQL installiert, das
|
|
# Schema angelegt und ein Administrator eingerichtet wurde.
|
|
validateRepositoryPath "${repositoryPath}"
|
|
|
|
# Pruefsummen kontrollieren, sofern das Paket welche mitbringt.
|
|
if [[ -f "${packageDirectory}/SHA256SUMS" ]] && command -v sha256sum >/dev/null 2>&1; then
|
|
if ( cd "${packageDirectory}" && sha256sum --quiet -c SHA256SUMS >/dev/null 2>&1 ); then
|
|
writeSuccess "Pruefsummen des Pakets stimmen"
|
|
else
|
|
writeWarning "Die Pruefsummen des Pakets stimmen nicht. Wurde es vollstaendig uebertragen?"
|
|
confirmQuestion "Trotzdem fortfahren?" "n" || abortWithMessage "abgebrochen"
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Datenbank
|
|
# ---------------------------------------------------------------------------
|
|
|
|
printf '\n'
|
|
writeStep "Datenbank"
|
|
|
|
if [[ -z "${databaseMode}" ]]; then
|
|
if [[ "${unattendedMode}" == "ja" ]]; then
|
|
databaseMode="lokal"
|
|
else
|
|
writeDetail "PostgreSQL 17 oder neuer wird gebraucht."
|
|
writeDetail ""
|
|
writeDetail " 1) auf diesem Server installieren"
|
|
writeDetail " 2) eine vorhandene Datenbank verwenden"
|
|
writeDetail ""
|
|
|
|
local_choice=""
|
|
askQuestion "Auswahl" "1" local_choice
|
|
[[ "${local_choice}" == "2" ]] && databaseMode="vorhanden" || databaseMode="lokal"
|
|
fi
|
|
fi
|
|
|
|
if [[ "${databaseMode}" == "lokal" ]]; then
|
|
if command -v psql >/dev/null 2>&1 && systemctl is-active --quiet postgresql 2>/dev/null; then
|
|
writeSuccess "PostgreSQL laeuft bereits"
|
|
else
|
|
packageManager="$(detectPackageManager)"
|
|
writeDetail "PostgreSQL wird installiert (${packageManager})"
|
|
|
|
case "${packageManager}" in
|
|
apt)
|
|
DEBIAN_FRONTEND=noninteractive apt-get update -qq
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq postgresql postgresql-contrib >/dev/null
|
|
;;
|
|
dnf|yum)
|
|
"${packageManager}" install -y -q postgresql-server postgresql-contrib >/dev/null
|
|
# Auf Red-Hat-Systemen muss der Datenbankbereich zuerst angelegt werden.
|
|
[[ -f /var/lib/pgsql/data/PG_VERSION ]] || postgresql-setup --initdb >/dev/null 2>&1 || true
|
|
;;
|
|
zypper)
|
|
zypper --non-interactive install -y postgresql-server postgresql-contrib >/dev/null
|
|
;;
|
|
pacman)
|
|
pacman -Sy --noconfirm postgresql >/dev/null
|
|
[[ -f /var/lib/postgres/data/PG_VERSION ]] || \
|
|
su - postgres -c "initdb -D /var/lib/postgres/data" >/dev/null 2>&1 || true
|
|
;;
|
|
*)
|
|
abortWithMessage "Die Distribution wurde nicht erkannt. Installieren Sie PostgreSQL von Hand
|
|
und starten Sie das Skript mit SYNCOVA_SETUP_DATABASE_MODE=vorhanden erneut."
|
|
;;
|
|
esac
|
|
|
|
systemctl enable --now postgresql >/dev/null 2>&1 || \
|
|
abortWithMessage "PostgreSQL liess sich nicht starten. Pruefen Sie 'systemctl status postgresql'."
|
|
|
|
writeSuccess "PostgreSQL installiert und gestartet"
|
|
fi
|
|
|
|
# Welche Fassung hat die Distribution mitgebracht?
|
|
#
|
|
# Debian 12 liefert 15, nicht 17. Geprueft ist Syncova gegen 17 (Entwicklung
|
|
# und CI) und 15 (Einrichtung und Sicherungslauf). Aelter wird abgelehnt: Die
|
|
# Migrationen setzen unter anderem gen_random_uuid() und DROP DATABASE ...
|
|
# WITH (FORCE) voraus.
|
|
postgresVersion="$(queryAsPostgres 'SHOW server_version' | awk '{print $1}' | cut -d. -f1)"
|
|
|
|
if [[ -n "${postgresVersion}" ]]; then
|
|
if [[ "${postgresVersion}" -lt 15 ]]; then
|
|
abortWithMessage "PostgreSQL ${postgresVersion} ist zu alt. Gebraucht wird 15 oder neuer."
|
|
fi
|
|
|
|
writeSuccess "PostgreSQL ${postgresVersion}"
|
|
fi
|
|
|
|
# Eine lokale Datenbank spricht ueber den Unix-Socket; TLS ist dort ohne
|
|
# Belang und wuerde den Verbindungsaufbau nur scheitern lassen.
|
|
databaseHost="127.0.0.1"
|
|
databaseSSLMode="${databaseSSLMode:-disable}"
|
|
|
|
if [[ -z "${databasePassword}" ]]; then
|
|
databasePassword="$(generatePassword)" # secretscan:erlaubt: erzeugter Zufallswert, kein hinterlegtes Geheimnis
|
|
writeDetail "Ein Datenbankpasswort wurde erzeugt."
|
|
fi
|
|
|
|
# Rolle und Datenbank anlegen — beides nur, wenn es sie nicht schon gibt.
|
|
existingRole="$(queryAsPostgres "SELECT 1 FROM pg_roles WHERE rolname='${databaseUser}'")"
|
|
|
|
if [[ "${existingRole}" == "1" ]]; then
|
|
writeDetail "Das Datenbankkonto ${databaseUser} ist bereits vorhanden; das Passwort wird gesetzt."
|
|
runAsPostgres "ALTER ROLE ${databaseUser} WITH LOGIN PASSWORD '${databasePassword}'" || \
|
|
abortWithMessage "Das Datenbankpasswort liess sich nicht setzen."
|
|
else
|
|
runAsPostgres "CREATE ROLE ${databaseUser} WITH LOGIN PASSWORD '${databasePassword}'" || \
|
|
abortWithMessage "Das Datenbankkonto liess sich nicht anlegen."
|
|
noteCreated "dbuser:${databaseUser}"
|
|
writeSuccess "Datenbankkonto ${databaseUser} angelegt"
|
|
fi
|
|
|
|
existingDatabase="$(queryAsPostgres "SELECT 1 FROM pg_database WHERE datname='${databaseName}'")"
|
|
|
|
if [[ "${existingDatabase}" == "1" ]]; then
|
|
writeDetail "Die Datenbank ${databaseName} ist bereits vorhanden und wird verwendet."
|
|
else
|
|
su - postgres -c "createdb -O ${databaseUser} ${databaseName}" >/dev/null 2>&1 || \
|
|
abortWithMessage "Die Datenbank liess sich nicht anlegen."
|
|
noteCreated "database:${databaseName}"
|
|
writeSuccess "Datenbank ${databaseName} angelegt"
|
|
fi
|
|
else
|
|
askQuestion "Datenbankserver" "${databaseHost}" databaseHost
|
|
askQuestion "Port" "${databasePort}" databasePort
|
|
askQuestion "Datenbankname" "${databaseName}" databaseName
|
|
askQuestion "Datenbankkonto" "${databaseUser}" databaseUser
|
|
|
|
if [[ -z "${databasePassword}" ]]; then
|
|
askPassword "Passwort des Datenbankkontos" databasePassword
|
|
fi
|
|
|
|
# Bei einer entfernten Datenbank ist Verschluesselung der Standard: Ohne sie
|
|
# ginge das Passwort im Klartext durch das Netz.
|
|
databaseSSLMode="${databaseSSLMode:-require}"
|
|
|
|
writeDetail "Die Verbindung wird geprueft."
|
|
|
|
if ! PGPASSWORD="${databasePassword}" psql -h "${databaseHost}" -p "${databasePort}" \
|
|
-U "${databaseUser}" -d "${databaseName}" -tAc "SELECT 1" >/dev/null 2>&1; then
|
|
abortWithMessage "Die Datenbank ist nicht erreichbar oder die Anmeldung schlug fehl.
|
|
Geprueft wurde: ${databaseUser}@${databaseHost}:${databasePort}/${databaseName}"
|
|
fi
|
|
|
|
writeSuccess "Datenbank erreichbar"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Dienstkonto und Verzeichnisse
|
|
# ---------------------------------------------------------------------------
|
|
|
|
printf '\n'
|
|
writeStep "Dienstkonto und Verzeichnisse"
|
|
|
|
if id "${serviceAccount}" >/dev/null 2>&1; then
|
|
writeDetail "Das Dienstkonto ${serviceAccount} ist bereits vorhanden."
|
|
else
|
|
useradd --system --no-create-home --shell /usr/sbin/nologin "${serviceAccount}" 2>/dev/null || \
|
|
useradd --system --no-create-home --shell /sbin/nologin "${serviceAccount}" || \
|
|
abortWithMessage "Das Dienstkonto liess sich nicht anlegen."
|
|
noteCreated "account:${serviceAccount}"
|
|
writeSuccess "Dienstkonto ${serviceAccount} angelegt"
|
|
fi
|
|
|
|
install -d -m 0755 "${installationRoot}"
|
|
noteCreated "directory:${installationRoot}"
|
|
|
|
install -d -m 0700 -o "${serviceAccount}" -g "${serviceAccount}" "${configurationDirectory}"
|
|
noteCreated "directory:${configurationDirectory}"
|
|
|
|
install -d -m 0700 -o "${serviceAccount}" -g "${serviceAccount}" "${backupDirectory}"
|
|
|
|
cp -R "${packageDirectory}/bin" "${installationRoot}/"
|
|
[[ -d "${packageDirectory}/web" ]] && cp -R "${packageDirectory}/web" "${installationRoot}/"
|
|
[[ -d "${packageDirectory}/migrations" ]] && cp -R "${packageDirectory}/migrations" "${installationRoot}/"
|
|
[[ -d "${packageDirectory}/docs" ]] && cp -R "${packageDirectory}/docs" "${installationRoot}/"
|
|
[[ -d "${packageDirectory}/deployment" ]] && cp -R "${packageDirectory}/deployment" "${installationRoot}/"
|
|
chmod -R 0755 "${installationRoot}/bin"
|
|
|
|
# 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.
|
|
printf '%s\n' "${packageVersion}" > "${installationRoot}/VERSION"
|
|
|
|
writeSuccess "Programme unter ${installationRoot}"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Verschluesselungsschluessel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
printf '\n'
|
|
writeStep "Verschluesselungsschluessel"
|
|
|
|
if [[ -z "${encryptionKeys}" ]]; then
|
|
# "|| true", damit die eigene Fehlermeldung unten greift: Findet grep
|
|
# nichts, endet es mit 1, und "set -e" braeche vorher ab — mit einer
|
|
# Meldung, die niemandem sagt, was fehlt.
|
|
generatedKeyLine="$("${installationRoot}/bin/syncova-admin" generate-key 2>/dev/null \
|
|
| grep '^SYNCOVA_ENCRYPTION_KEYS=' || true)"
|
|
encryptionKeys="${generatedKeyLine#SYNCOVA_ENCRYPTION_KEYS=}"
|
|
|
|
[[ -n "${encryptionKeys}" ]] || abortWithMessage "Der Schluessel liess sich nicht erzeugen."
|
|
|
|
printf '\n'
|
|
printf ' %s┌─────────────────────────────────────────────────────────────────┐%s\n' "${colorYellow}${colorBold}" "${colorReset}"
|
|
printf ' %s│ DIESER SCHLUESSEL EXISTIERT NUR HIER │%s\n' "${colorYellow}${colorBold}" "${colorReset}"
|
|
printf ' %s└─────────────────────────────────────────────────────────────────┘%s\n' "${colorYellow}${colorBold}" "${colorReset}"
|
|
printf '\n'
|
|
printf ' SYNCOVA_ENCRYPTION_KEYS=%s\n' "${encryptionKeys}"
|
|
printf '\n'
|
|
writeDetail "Ohne ihn sind MFA-Geheimnisse, die Zugangsdaten der"
|
|
writeDetail "Virtualisierungsverbuende und die Datenschluessel der Repositories"
|
|
writeDetail "verloren — und damit die verschluesselten Backups."
|
|
writeDetail ""
|
|
writeDetail "Er gehoert an einen Ort ausserhalb dieser Anlage: Passwortspeicher"
|
|
writeDetail "oder Tresor. In ein Backup, das Syncova erzeugt, gehoert er"
|
|
writeDetail "ausdruecklich nicht — man braeuchte ihn, um an ihn heranzukommen."
|
|
printf '\n'
|
|
|
|
if [[ "${unattendedMode}" != "ja" ]]; then
|
|
acknowledgement=""
|
|
while [[ "${acknowledgement}" != "gesichert" ]]; do
|
|
read -r -p " Tippen Sie 'gesichert', wenn der Schluessel anderswo liegt: " acknowledgement
|
|
done
|
|
else
|
|
writeWarning "Unbeaufsichtigter Lauf: Sichern Sie den Schluessel aus ${configurationFile}."
|
|
fi
|
|
else
|
|
writeSuccess "Ein vorgegebener Schluessel wird uebernommen"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Konfiguration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
printf '\n'
|
|
writeStep "Konfiguration"
|
|
|
|
# Erst mit strengen Rechten anlegen, dann fuellen. Andersherum stuende das
|
|
# Passwort fuer einen Augenblick weltlesbar auf der Platte.
|
|
install -m 0600 -o "${serviceAccount}" -g "${serviceAccount}" /dev/null "${configurationFile}"
|
|
noteCreated "file:${configurationFile}"
|
|
|
|
cat > "${configurationFile}" <<CONFIG
|
|
# Konfiguration von Syncova Backups.
|
|
#
|
|
# Angelegt von setup.sh am $(date -u '+%Y-%m-%d %H:%M:%S UTC').
|
|
# Diese Datei enthaelt Geheimnisse: root, Modus 0600.
|
|
|
|
SYNCOVA_ENV=production
|
|
|
|
SYNCOVA_DB_HOST=${databaseHost}
|
|
SYNCOVA_DB_PORT=${databasePort}
|
|
SYNCOVA_DB_NAME=${databaseName}
|
|
SYNCOVA_DB_USER=${databaseUser}
|
|
SYNCOVA_DB_PASSWORD=${databasePassword}
|
|
SYNCOVA_DB_SSLMODE=${databaseSSLMode}
|
|
|
|
# Ohne diesen Schluessel sind die abgelegten Geheimnisse verloren.
|
|
SYNCOVA_ENCRYPTION_KEYS=${encryptionKeys}
|
|
SYNCOVA_ENCRYPTION_CURRENT_KEY=v1
|
|
|
|
SYNCOVA_HTTP_LISTEN_ADDRESS=${listenAddress}
|
|
SYNCOVA_LOG_LEVEL=info
|
|
SYNCOVA_LOG_FORMAT=json
|
|
|
|
# TLS im Dienst selbst. Nur eines von beiden zu setzen verweigert den Start —
|
|
# sonst liefe der Dienst im Klartext, obwohl Verschluesselung eingerichtet
|
|
# aussieht. Hinter einem Reverse Proxy bleiben beide leer und die Lauschadresse
|
|
# steht auf 127.0.0.1.
|
|
# SYNCOVA_HTTP_TLS_CERT_FILE=
|
|
# SYNCOVA_HTTP_TLS_KEY_FILE=
|
|
|
|
# Begrenzt Wiederherstellungsziele. Ohne diese Angabe kann jeder mit dem Recht
|
|
# restores.execute in jedes beschreibbare Verzeichnis schreiben.
|
|
# SYNCOVA_RESTORE_ALLOWED_ROOTS=/srv/restore
|
|
|
|
# Zweiter Faktor fuer Konten mit Benutzer-, Rollen- oder Sicherheitsrechten.
|
|
# Zuerst einrichten, dann einschalten — sonst sperren Sie sich aus.
|
|
# SYNCOVA_AUTH_REQUIRE_MFA_FOR_PRIVILEGED_USERS=true
|
|
CONFIG
|
|
|
|
chmod 0600 "${configurationFile}"
|
|
chown "${serviceAccount}:${serviceAccount}" "${configurationFile}"
|
|
|
|
writeSuccess "${configurationFile} (0600, ${serviceAccount})"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. Schema
|
|
# ---------------------------------------------------------------------------
|
|
|
|
printf '\n'
|
|
writeStep "Datenbankschema"
|
|
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
. "${configurationFile}"
|
|
set +a
|
|
|
|
if ! "${installationRoot}/bin/syncova-migrate" up 2>&1 | tail -1; then
|
|
abortWithMessage "Die Migrationen liessen sich nicht anwenden."
|
|
fi
|
|
|
|
writeSuccess "Schema angelegt"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. Erster Administrator
|
|
# ---------------------------------------------------------------------------
|
|
|
|
printf '\n'
|
|
writeStep "Erster Administrator"
|
|
|
|
if [[ -z "${administratorPassword}" ]]; then
|
|
if [[ "${unattendedMode}" == "ja" ]]; then
|
|
abortWithMessage "Im unbeaufsichtigten Lauf muss SYNCOVA_SETUP_ADMIN_PASSWORD gesetzt sein."
|
|
fi
|
|
|
|
askQuestion "Anmeldename" "${administratorName}" administratorName
|
|
askPassword "Passwort fuer ${administratorName}" administratorPassword
|
|
fi
|
|
|
|
# Das Passwort geht ueber die Standardeingabe, nicht als Aufrufparameter:
|
|
# Parameter stehen in der Prozessliste und in der Shell-Historie. Das Programm
|
|
# fragt zweimal, deshalb zwei Zeilen.
|
|
if ! printf '%s\n%s\n' "${administratorPassword}" "${administratorPassword}" \
|
|
| "${installationRoot}/bin/syncova-admin" create-admin --username "${administratorName}" >/dev/null 2>&1; then
|
|
abortWithMessage "Der Administrator liess sich nicht anlegen.
|
|
Haeufigste Ursache: Das Passwort erfuellt die Mindestanforderung nicht
|
|
(12 Zeichen, mindestens 5 verschiedene, Buchstaben plus ein weiteres Zeichen)."
|
|
fi
|
|
|
|
writeSuccess "Administrator ${administratorName} angelegt"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. Repository
|
|
# ---------------------------------------------------------------------------
|
|
|
|
printf '\n'
|
|
writeStep "Repository"
|
|
|
|
if [[ "${unattendedMode}" != "ja" ]]; then
|
|
writeDetail "Das Repository gehoert nicht auf denselben Datentraeger wie die"
|
|
writeDetail "zu sichernden Daten. Ein Datentraegerausfall naehme sonst Original"
|
|
writeDetail "und Sicherung gemeinsam mit."
|
|
printf '\n'
|
|
askQuestion "Ort des Repositorys" "${repositoryPath}" repositoryPath
|
|
fi
|
|
|
|
validateRepositoryPath "${repositoryPath}"
|
|
|
|
if [[ -e "${repositoryPath}/format/repository.json" ]]; then
|
|
writeDetail "Unter ${repositoryPath} liegt bereits ein Repository; es wird verwendet."
|
|
else
|
|
install -d -m 0700 -o "${serviceAccount}" -g "${serviceAccount}" "${repositoryPath}"
|
|
|
|
hardenedFlag=()
|
|
[[ "${createHardenedRepository}" == "ja" ]] && hardenedFlag=(--hardened)
|
|
|
|
if ! su -s /bin/bash "${serviceAccount}" -c \
|
|
"'${installationRoot}/bin/syncova-repo' create --path '${repositoryPath}' --name 'Hauptziel' ${hardenedFlag[*]:-}" \
|
|
>/dev/null 2>&1; then
|
|
abortWithMessage "Das Repository liess sich nicht anlegen."
|
|
fi
|
|
|
|
noteCreated "directory:${repositoryPath}"
|
|
writeSuccess "Repository unter ${repositoryPath}$( [[ "${createHardenedRepository}" == "ja" ]] && echo ' (gehaertet)' )"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 9. Dienst
|
|
# ---------------------------------------------------------------------------
|
|
|
|
printf '\n'
|
|
writeStep "Dienst"
|
|
|
|
# PrivateTmp und ein Repository unter /tmp schliessen einander aus.
|
|
#
|
|
# Mit PrivateTmp=yes bekommt der Dienst ein eigenes /tmp; der Ablageort
|
|
# existiert in seiner Sicht dann nicht, und systemd bricht den Start mit
|
|
# 226/NAMESPACE ab — bevor das Programm ueberhaupt laeuft. Der Fall tritt nur
|
|
# noch mit ausdruecklicher Freigabe auf; dann muss die Haertung an dieser einen
|
|
# Stelle weichen, sonst startet der Dienst nie.
|
|
privateTmpSetting="yes"
|
|
|
|
if repositoryIsUnderPrivateTmp "${repositoryPath}"; then
|
|
privateTmpSetting="no"
|
|
writeWarning "PrivateTmp wird abgeschaltet, weil das Repository unter /tmp liegt."
|
|
fi
|
|
|
|
cat > "${serviceUnitFile}" <<UNIT
|
|
[Unit]
|
|
Description=Syncova Control Plane
|
|
Documentation=file://${installationRoot}/docs/installation.md
|
|
After=network-online.target postgresql.service
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=${serviceAccount}
|
|
Group=${serviceAccount}
|
|
EnvironmentFile=${configurationFile}
|
|
ExecStart=${installationRoot}/bin/syncova-api
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
|
|
# Haertung: Der Dienst braucht Netz und sein Repository, sonst nichts.
|
|
NoNewPrivileges=yes
|
|
PrivateTmp=${privateTmpSetting}
|
|
ProtectSystem=strict
|
|
ProtectHome=yes
|
|
ReadWritePaths=${repositoryPath} ${backupDirectory}
|
|
ProtectKernelTunables=yes
|
|
ProtectKernelModules=yes
|
|
ProtectControlGroups=yes
|
|
RestrictSUIDSGID=yes
|
|
# AF_UNIX gehoert in die Liste: Mit systemd-resolved laeuft die Namensaufloesung
|
|
# ueber einen Unix-Socket. Ohne diesen Eintrag findet der Dienst seine Datenbank
|
|
# nicht und meldet einen Netzfehler, waehrend das Netz einwandfrei arbeitet.
|
|
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
|
MemoryDenyWriteExecute=yes
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
UNIT
|
|
|
|
noteCreated "service:${serviceName}"
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable "${serviceName}" >/dev/null 2>&1
|
|
systemctl start "${serviceName}"
|
|
|
|
writeDetail "Der Dienst wird geprueft."
|
|
|
|
serviceIsReady="nein"
|
|
for _attempt in $(seq 1 15); do
|
|
if serviceIsReachable "http://${listenAddress}/health/ready"; then
|
|
serviceIsReady="ja"
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
if [[ "${serviceIsReady}" != "ja" ]]; then
|
|
printf '\n'
|
|
writeWarning "Der Dienst meldet sich nicht als betriebsbereit."
|
|
|
|
# Die Zeilen gleich mitliefern statt auf sie zu verweisen.
|
|
#
|
|
# Ein Abbruch, der nur sagt "sehen Sie ins Journal", verlangt einen zweiten
|
|
# Anlauf, um ueberhaupt zu erfahren, was los war — und beim Rueckbau ist der
|
|
# Dienst dann schon weg.
|
|
if command -v journalctl >/dev/null 2>&1; then
|
|
printf '\n'
|
|
writeDetail "Die letzten Zeilen des Dienstes:"
|
|
printf '\n'
|
|
journalctl -u "${serviceName}" -n 20 --no-pager 2>/dev/null | sed 's/^/ /' >&2
|
|
printf '\n'
|
|
|
|
# 226/NAMESPACE ist der haeufigste Fall und ohne Erklaerung nicht zu
|
|
# deuten: Er bedeutet fast immer einen Pfad in ReadWritePaths, den der
|
|
# Dienst in seiner eigenen Namensraumsicht nicht sieht.
|
|
if journalctl -u "${serviceName}" -n 20 --no-pager 2>/dev/null | grep -q "226/NAMESPACE"; then
|
|
writeDetail "226/NAMESPACE bedeutet: Ein Pfad der Diensteinheit ist aus Sicht"
|
|
writeDetail "des Dienstes nicht vorhanden. Pruefen Sie ReadWritePaths in"
|
|
writeDetail "${serviceUnitFile} — insbesondere den Ort des Repositorys."
|
|
printf '\n'
|
|
fi
|
|
fi
|
|
|
|
abortWithMessage "Die Einrichtung wird zurueckgebaut."
|
|
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
|
|
# ---------------------------------------------------------------------------
|
|
|
|
printf '\n%sDie Einrichtung ist abgeschlossen.%s\n\n' "${colorGreen}${colorBold}" "${colorReset}"
|
|
|
|
printf ' Version %s\n' "${packageVersion}"
|
|
printf ' Programme %s\n' "${installationRoot}"
|
|
printf ' Konfiguration %s\n' "${configurationFile}"
|
|
printf ' Repository %s\n' "${repositoryPath}"
|
|
printf ' Dienst %s (systemctl status %s)\n' "${serviceName}" "${serviceName}"
|
|
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}"
|
|
cat <<'NEXT'
|
|
1. Repository in der Control Plane eintragen und die Durchsetzungsstufe
|
|
MESSEN — sie wird gemessen, nicht behauptet:
|
|
|
|
curl -X POST http://<adresse>/api/v1/repositories \
|
|
-H "Authorization: Bearer <token>" -H 'Content-Type: application/json' \
|
|
-d '{"name":"Hauptziel","location":"<repository>"}'
|
|
|
|
curl -X POST http://<adresse>/api/v1/repositories/<id>/enforcement/measure \
|
|
-H "Authorization: Bearer <token>"
|
|
|
|
2. Zweiten Faktor einrichten, mindestens fuer Konten mit Loeschrecht.
|
|
|
|
3. Benachrichtigungsweg einrichten. Ohne ihn erfaehrt niemand von einem
|
|
Ausfall.
|
|
|
|
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}"
|