commit 5f45325386857399da2f9ad8dafdb59efd71d201 Author: Christoph Date: Sat Jun 13 18:53:52 2026 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca17ee8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ + +# - common +*.log +*.swp +*.tar.gz +log* diff --git a/README.install b/README.install new file mode 100644 index 0000000..54b4282 --- /dev/null +++ b/README.install @@ -0,0 +1,542 @@ +# -------------------- +# Install psono password manager +# -------------------- + +# see also: +# https://doc.psono.com/admin/overview/summary.html + + +# System Requirements +# +# A production grade setup of Psono contains +# +# 1 VM (or server) for the database +# 1 VM (or server) for the server and client module and admin portal +# 1 VM (or server) for the fileserver module (if you plan to use it) +# +# Install guide for the fileserver: +# https://doc.psono.com/admin/installation-optional/install-fileserver.html + + +# 0. Installation Preparation +# =========================== + +# Software Requirements: +# +# Docker +# Postgres 14 (but preferable latest) +# + +# PostgreSQL +# +apt install postgresql postgresql-client + + +# Docker +# +cd /usr/local/src/ +git clone https://git.oopen.de/install/docker +/usr/local/src/docker/install-docker.sh + + +# In case of migration from an existing system, you need +# - 'settings.yaml' from the # existing psono-combo Container. +# - Database dump from the existing system + + +# ********** +# Phase 1 . PostgreSQL vorbereiten +# ********** + +# Schritt 1.1 - Als postgres-Benutzer anmelden +# +sudo -i -u postgres + + +# Schritt 1.2 - PostgreSQL-Konsole öffnen +# +psql + + +# Schritt 1.3 — Datenbank, Benutzer und Rechte anlegen +# +# database name......: psono +# database user......: psono +# database password..: 9Sec-H2.PEPmo.vi +# +# Befehle nacheinander in der psql-Konsole ausführen: +# +CREATE DATABASE psono; +CREATE USER psono WITH PASSWORD '9Sec-H2.PEPmo.vi'; +GRANT ALL PRIVILEGES ON DATABASE psono TO psono; + +# in die neie datenbank wechseln +\c psono +GRANT ALL ON SCHEMA public TO psono; + + +# Schritt 1.4 - Erweiterungen installieren +# +# noch immer in der psql-Konsole +# +# Befehle nacheinander in der psql-Konsole ausführen: +# +CREATE EXTENSION IF NOT EXISTS ltree; +CREATE EXTENSION IF NOT EXISTS pgcrypto; + + +# Schritt 1.5 - psql beenden und zurück zu root +# + +# in der psql Konsole: +\q + +# zurück zu root +exit + + +# ********** +# Phase 2 . Datenbank prüfen +# ********** + +# Wir müssen die pg_hba.conf anpassen. Zuerst den Pfad herausfinden: +# +sudo -u postgres psql -c "SHOW hba_file;" + +# Ausgabe war: +# +# hba_file +# ------------------------------------- +# /etc/postgresql/17/main/pg_hba.conf +# (1 row) + +# Aktuelle Konfiguration ansehen +# +cat /etc/postgresql/17/main/pg_hba.conf | grep -v "^#" | grep -v "^$" + +# Ausgabe war: +# +# local all postgres peer +# local all all peer +# host all all 127.0.0.1/32 scram-sha-256 +# host all all ::1/128 scram-sha-256 +# local replication all peer +# host replication all 127.0.0.1/32 scram-sha-256 +# host replication all ::1/128 scram-sha-256 + + +# Wir müssen eine Zeile hinzufügen, die dem psono-Benutzer Passwort-Authentifizierung über den +# lokalen Socket erlaubt. Das geht mit diesem Befehl: +# +sudo sed -i '/^local all all/i local all psono md5' /etc/postgresql/17/main/pg_hba.conf + +# Konfiguration nochmal ansehen? +# +cat /etc/postgresql/17/main/pg_hba.conf | grep -v "^#" | grep -v "^$" + +# Ausgabe war: +# local all postgres peer +# local all psono md5 +# local all all peer +# host all all 127.0.0.1/32 scram-sha-256 +# host all all ::1/128 scram-sha-256 +# local replication all peer +# host replication all 127.0.0.1/32 scram-sha-256 +# host replication all ::1/128 scram-sha-256 +# +# -> das sieht jetzt gut aus + + +# PostgreSQL neu laden +# +sudo systemctl reload postgresql + +# Jetzt die zuvor erstellte Datenbbank prpfen. Derr Schalter '-W' erzwingt die Passwortabfrage: +# +sudo -u postgres psql -U psono -d psono -W -c '\dx' + +# Ausgabe war: +# +# Password: +# List of installed extensions +# Name | Version | Schema | Description +# ----------+---------+------------+------------------------------------------------- +# ltree | 1.3 | public | data type for hierarchical tree-like structures +# pgcrypto | 1.3 | public | cryptographic functions +# plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language +# (3 rows) + + +# PostgreSQL lauscht standardmäßig nur auf localhost und nicht auf der Docker-Bridge-IP 172.17.0.1. +# Wir müssen PostgreSQL anweisen, auch auf dieser Schnittstelle zu lauschen. +# +# Wir setzen 'listen_addresses' auf ''localhost,172.17.0.1,172.18.0.1, damit PostgreSQL auch auf der +# Docker-Bridge-IP antwortet: +# +# - localhost: für lokale Verbindungen (z.B. psql direkt auf dem Host) +# - 172.17.0.1: Docker default bridge +# - 172.18.0.1: Docker Compose Netzwerk (wo der Psono-Container läuft) +# +# + +sudo -u postgres psql -c "ALTER SYSTEM SET listen_addresses = 'localhost,172.17.0.1,172.18.0.1';" +systemctl restart postgresql +sudo -u postgres psql -c "SHOW listen_addresses;" + +# Ausgabe sollte jetzt sein: +# +# listen_addresses +# ------------------ +# * +# (1 row) + + +# Im Falle einer Migration jetzt den datenbank dump der alten installation einspielen: +# + + +# Schritt 2.2 - Backup entpacken +# ============================== + +mkdir -p /root/psono-backup/dump +tar -xf /tmp/2026-06-13_00-00.tar -C /root/psono-backup/dump + +# das verzeichnis dunp sollte in etwa so aussehen: +# +# ls /root/psono-backup/dump +# +# 3845.dat 3855.dat 3864.dat 3870.dat 3875.dat 3880.dat 3885.dat 3890.dat 3895.dat 3900.dat restore.sql +# 3847.dat 3857.dat 3866.dat 3871.dat 3876.dat 3881.dat 3886.dat 3891.dat 3896.dat 3901.dat toc.dat +# 3849.dat 3859.dat 3867.dat 3872.dat 3877.dat 3882.dat 3887.dat 3892.dat 3897.dat 3902.dat +# 3851.dat 3861.dat 3868.dat 3873.dat 3878.dat 3883.dat 3888.dat 3893.dat 3898.dat 3903.dat +# 3852.dat 3863.dat 3869.dat 3874.dat 3879.dat 3884.dat 3889.dat 3894.dat 3899.dat 3904.dat +# +# toc.dat, alle .dat-Dateien und sogar eine restore.sql + + +# Schritt 2.3 - Backup einspielen +# =============================== + +# Passwort wird abgefragt: +pg_restore \ + -U psono \ + -d psono \ + -Fd \ + --no-owner \ + --no-privileges \ + /root/psono-backup/dump + +# Ausgabe: +# +# Password: +# pg_restore: error: could not execute query: ERROR: must be owner of extension ltree +# Command was: COMMENT ON EXTENSION ltree IS 'data type for hierarchical tree-like structures'; +# +# +# pg_restore: error: could not execute query: ERROR: must be owner of extension pgcrypto +# Command was: COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions'; +# +# +# pg_restore: warning: errors ignored on restore: 2 +# +# WICHTIG: +# Das sind keine echten Fehler - das sind nur Warnungen! Der psono-Benutzer darf keine Kommentare +# auf Extensions schreiben, die als Superuser installiert wurden. Das ist vollkommen harmlos und +# hat keinen Einfluss auf die Daten. +# +# Entscheidend ist die letzte Zeile: +# +# errors ignored on restore: 2 +# +# genau diese 2 Warnungen haben wir gesehen, nichts weiter. + + +# Jetzt prüfen wir ob die Daten wirklich da sind: +# +psql -U psono -d psono -W -c 'SELECT count(*) FROM restapi_user;' + +# Ausgabe etwa: +# +# Password: +# count +# ------- +# 16 +# (1 row) + + +# ********** +# Phase 3 - Psono-Konfiguration wiederherstellen +# ********** + +# Schritt 3.1 - Verzeichnisse anlegen +# =================================== + +mkdir -p /opt/docker/psono +mkdir -p /opt/docker/psono-client + + +# Schritt 3.2 - settings.yaml auf den Host übertragen +# =================================================== +# +scp settings.yaml root@psono-ndm.oopen.de:/opt/docker/psono/settings.yaml + +# die relevanten teile der datei ansehen: +# +grep -E "ALLOWED_HOSTS|WEB_CLIENT_URL|HOST|PORT|NAME|USER|PASSWORD" /opt/docker/psono/settings.yaml | grep -v "^#" + +# mit der Komanndoausgabe: +# +# WEB_CLIENT_URL: 'https://psono.neuemedienmacher.de' +# ALLOWED_HOSTS: ['*'] +# HOST_URL: 'https://psono.neuemedienmacher.de/server' +# EMAIL_HOST: 'smtp.gmail.com' +# EMAIL_HOST_USER: 'sysadmin@neuemedienmacher.de' +# EMAIL_HOST_PASSWORD: 'RQ5ZDdcNVQkD' +# EMAIL_PORT: 587 +# ALLOW_USER_SEARCH_BY_USERNAME_PARTIAL: True +# ALLOW_USER_SEARCH_BY_EMAIL: True +# 'NAME': 'psono' +# 'USER': 'psono' +# 'PASSWORD': 'password' +# #'HOST': '172.21.0.6' +# 'HOST': 'psono-database' +# 'PORT': '5432' +# ALLOW_LOST_PASSWORD: True + +# Die URL-Einstellungen passen bereits für den neuen Host ('psono.neuemedienmacher.de'). +# +# Aber die Datenbankverbindung muss geändert werden - aktuell zeigt HOST auf psono-database (den alten +# Docker-Container-Namen auf der NAS). Auf dem neuen Host läuft PostgreSQL direkt, nicht in Docker. +# +# wir müssen insgesamt 2 Zeilen ändern_ HOST und PASSWORD der Datenbankverbindung: +# +sed -i "s/'HOST': 'psono-database'/'HOST': 'host.docker.internal'/" /opt/docker/psono/settings.yaml +sed -i "s/'PASSWORD': 'password'/'PASSWORD': '9Sec-H2.PEPmo.vi'/" /opt/docker/psono/settings.yaml + +# zur Kontrolle: +# +grep -E "HOST|PASSWORD" /opt/docker/psono/settings.yaml | grep -v "^#" | grep -v EMAIL + +# mit der Ausgabe: +# +# ALLOWED_HOSTS: ['*'] +# HOST_URL: 'https://psono.neuemedienmacher.de/server' +# 'PASSWORD': '9Sec-H2.PEPmo.vi' +# #'HOST': '172.21.0.6' +# 'HOST': 'host.docker.internal' +# ALLOW_LOST_PASSWORD: True +# +# So soll es sein: +# +# - HOST zeigt jetzt auf host.docker.internal +# - Passwort ist korrekt gesetzt. + + +# Disable redis caching (psono-valkey) falls das im alten System aktiviert war +# +sed -i "s/^CACHE_ENABLE: TRUE/CACHE_ENABLE: FALSE/" /opt/docker/psono/settings.yaml +sed -i "s/^CACHE_REDIS: TRUE/CACHE_REDIS: FALSE/" /opt/docker/psono/settings.yaml +sed -i "s/^CACHE_REDIS_LOCATION:/#CACHE_REDIS_LOCATION:/" /opt/docker/psono/settings.yaml + +# Kontrolle: +# +grep -i "cache_enable\|cache_redis" /opt/docker/psono/settings.yaml + +# Ausgabe könnte etwa so aussehen: +# +# CACHE_ENABLE: FALSE +# CACHE_REDIS: FALSE +# #CACHE_REDIS_LOCATION: 'redis://@psono-valkey:6379/13' + + +# Schritt 3.3 - config.json für den Web-Client anlegen +# ==================================================== + +cat > /opt/docker/psono-client/config.json << 'EOF' +{ + "backend_servers": [{ + "title": "Psono Server", + "url": "https://psono.neuemedienmacher.de/server" + }], + "base_url": "https://psono.neuemedienmacher.de/", + "allow_custom_server": true, + "allow_registration": true, + "allow_lost_password": true, + "disable_download_bar": false, + "remember_me_default": false, + "trust_device_default": false, + "authentication_methods": ["AUTHKEY"] +} +EOF + + +# Prüfen +# +cat /opt/docker/psono-client/config.json + +# Ausgabe sollte so aussehen +# +# { +# "backend_servers": [{ +# "title": "Psono Server", +# "url": "https://psono.neuemedienmacher.de/server" +# }], +# "base_url": "https://psono.neuemedienmacher.de/", +# "allow_custom_server": true, +# "allow_registration": true, +# "allow_lost_password": true, +# "disable_download_bar": false, +# "remember_me_default": false, +# "trust_device_default": false, +# "authentication_methods": ["AUTHKEY"] +# } + + +# ********** +# Phase 4 - Psono-Container starten +# ********** + +# Schritt 4.1 - Docker Compose Datei erstellen +# ============================================ + +cat > /opt/docker/psono/docker-compose.yml << 'EOF' +services: + psono-combo: + image: psono/psono-combo:latest + restart: unless-stopped + ports: + - "127.0.0.1:10200:80" + volumes: + - /opt/docker/psono/settings.yaml:/root/.psono_server/settings.yaml:ro + - /opt/docker/psono-client/config.json:/usr/share/nginx/html/config.json:ro + extra_hosts: + - "host.docker.internal:host-gateway" +EOF + + +# Schritt 4.2 - PostgreSQL für Docker-Verbindung öffnen +# ===================================================== + + +# Der Psono-Container wird sich über host.docker.internal mit PostgreSQL verbinden. +# Dafür muss pg_hba.conf noch eine Zeile für de Docker-Netzwerke bekommen: +# +if ! grep -qE "host\s+psono\s+psono\s+172.17.0.0/16" /etc/postgresql/17/main/pg_hba.conf ; then + echo "host psono psono 172.17.0.0/16 md5" >> /etc/postgresql/17/main/pg_hba.conf +fi +if ! grep -qE "host\s+psono\s+psono\s+172.18.0.0/16" /etc/postgresql/17/main/pg_hba.conf ; then + echo "host psono psono 172.18.0.0/16 md5" >> /etc/postgresql/17/main/pg_hba.conf +fi + +# PostgreSQL neu laden: +# +systemctl reload postgresql + + +# Schritt 4.3 - Container starten +# =============================== + +cd /opt/docker/psono +docker compose up -d + +# Ausgabe: +# +# [+] up 16/16 +# ✔ Image psono/psono-combo:latest Pulled 11.0s +# ✔ Network psono_default Created 0.0s +# ✔ Container psono-psono-combo-1 Started + + + +# ********** +# Phase 5 - Nginx Reverse Proxy einrichten +# ********** + + +# Schritt 5.1 - Nginx installieren +# +cd /usr/local/src/nginx +/usr/local/src/nginx/install_nginx.sh + +# TLS Zertifikate installieren - via dehydrated script +# +# a) dehydrated installieren +# +/usr/local/src/dehydrated-cron/install_dehydrated.sh + +# domain.txt erstellen +if ! grep -q 'psono.neuemedienmacher.de' /var/lib/dehydrated/domains.txt ; then + cat >> /var/lib/dehydrated/domains.txt << 'EOF' +psono.neuemedienmacher.de +EOF +fi + +# Zerifikate erstellen +# +/var/lib/dehydrated/cron/dehydrated_cron.sh + + +# nginx Konfiguration erstellen: +# +cat > /etc/nginx/sites-available/psono.neuemedienmacher.de.conf << 'EOF' +server { + listen 80 ; + listen [::]:80 ; + + server_name psono.neuemedienmacher.de; + + # Prevent nginx HTTP Server Detection + server_tokens off; + + # Enforce HTTPS + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + + server_name psono.neuemedienmacher.de; + + include snippets/letsencrypt-acme-challenge.conf; + + ssl_certificate /var/lib/dehydrated/certs/psono.neuemedienmacher.de/fullchain.pem; + ssl_certificate_key /var/lib/dehydrated/certs/psono.neuemedienmacher.de/privkey.pem; + ssl_dhparam /etc/nginx/ssl/dhparam.pem; + + ssl_session_cache shared:MozSSL:50m; + ssl_session_timeout 1d; + ssl_session_tickets off; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + + server_tokens off; + + # HSTS - ohne preload, da interner Dienst + add_header Strict-Transport-Security "max-age=15768000; includeSubDomains;" always; + + # Kein Clickjacking + add_header X-Frame-Options "SAMEORIGIN" always; + + # Kein MIME-Type Sniffing + add_header X-Content-Type-Options "nosniff" always; + + # Referrer nur innerhalb gleicher Domain + add_header Referrer-Policy "same-origin" always; + + # Unnötige Browser-Features deaktivieren + add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always; + + client_max_body_size 256m; + + location / { + proxy_pass http://127.0.0.1:10200; + 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; + } +} +EOF + diff --git a/psono-migration-anleitung-v4.docx b/psono-migration-anleitung-v4.docx new file mode 100644 index 0000000..134f379 Binary files /dev/null and b/psono-migration-anleitung-v4.docx differ diff --git a/psono-migration-anleitung-v4.odt b/psono-migration-anleitung-v4.odt new file mode 100644 index 0000000..643b4f8 Binary files /dev/null and b/psono-migration-anleitung-v4.odt differ diff --git a/psono-migration-anleitung-v4.pdf b/psono-migration-anleitung-v4.pdf new file mode 100644 index 0000000..ec2f184 Binary files /dev/null and b/psono-migration-anleitung-v4.pdf differ diff --git a/psono-migration.sh b/psono-migration.sh new file mode 100755 index 0000000..ee91753 --- /dev/null +++ b/psono-migration.sh @@ -0,0 +1,583 @@ +#!/usr/bin/env bash +# ============================================================ +# Psono Passwortmanager — Migrations-Script +# Von: altem Host (z.B. Synology NAS) → neuer Debian 12/13 +# ============================================================ +set -euo pipefail + +# ── Farben & Symbole ───────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +DIM='\033[2m' +RESET='\033[0m' + +OK=" ${GREEN}[ OK ]${RESET}" +ERR=" ${RED}[ ERROR ]${RESET}" +WARN="${YELLOW}[ WARNING ]${RESET}" +INFO="${CYAN}[ INFO ]${RESET}" + +# ── Hilfsfunktionen ────────────────────────────────────────── +print_header() { + echo "" + echo -e "${BOLD}${BLUE}╔══════════════════════════════════════════════════════════╗${RESET}" + echo -e "${BOLD}${BLUE}║ $1${RESET}" + echo -e "${BOLD}${BLUE}╚══════════════════════════════════════════════════════════╝${RESET}" + echo "" +} + +print_phase() { + echo "" + echo -e "${BOLD}${CYAN}┌──────────────────────────────────────────────────────────┐${RESET}" + echo -e "${BOLD}${CYAN}│ Phase $1: $2${RESET}" + echo -e "${BOLD}${CYAN}└──────────────────────────────────────────────────────────┘${RESET}" + echo "" +} + +print_step() { + echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" + echo -e "${BOLD} Schritt $1${RESET}" + echo "" +} + +# Befehl ausführen mit OK/ERROR-Ausgabe +run() { + local desc="$1"; shift + echo -ne " ${DIM}→${RESET} ${desc} ... " + if output=$("$@" 2>&1); then + echo -e "$OK" + return 0 + else + echo -e "$ERR" + echo -e " ${RED}Ausgabe: ${output}${RESET}" + return 1 + fi +} + +# Befehl ausführen, Fehler nur als Warning +run_warn() { + local desc="$1"; shift + echo -ne " ${DIM}→${RESET} ${desc} ... " + if output=$("$@" 2>&1); then + echo -e "$OK" + else + echo -e "$WARN" + echo -e " ${YELLOW}Ausgabe: ${output}${RESET}" + fi +} + +# Befehl mit sichtbarer Ausgabe (z.B. für pg_restore) +run_visible() { + local desc="$1"; shift + echo -e " ${DIM}→${RESET} ${desc}" + echo -e "${DIM} ────────────────────────────────────────${RESET}" + if "$@" 2>&1 | sed 's/^/ /'; then + echo -e "${DIM} ────────────────────────────────────────${RESET}" + echo -e "$OK ${desc}" + return 0 + else + echo -e "${DIM} ────────────────────────────────────────${RESET}" + echo -e "$ERR ${desc}" + return 1 + fi +} + +# Fehlerbehandlung: fragen ob weiter oder abbrechen +handle_error() { + local msg="${1:-Ein Fehler ist aufgetreten.}" + echo "" + echo -e "$ERR ${RED}${BOLD}${msg}${RESET}" + echo "" + echo -e "${YELLOW} Wie möchtest du fortfahren?${RESET}" + echo -e " ${BOLD}[C]${RESET} Fortsetzen (auf eigene Gefahr)" + echo -e " ${BOLD}[Q]${RESET} Script abbrechen" + echo "" + read -rp " Eingabe [C/Q]: " choice + case "${choice^^}" in + C) echo -e "$WARN Fortgesetzt trotz Fehler." ;; + *) echo -e "$ERR Script abgebrochen."; exit 1 ;; + esac +} + +# Nach jeder Phase pausieren +pause_phase() { + echo "" + echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" + echo -e "${GREEN} Phase $1 abgeschlossen.${RESET}" + echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" + echo "" + echo -e " ${BOLD}[ENTER]${RESET} Weiter mit Phase $2" + echo -e " ${BOLD}[Ctrl+C]${RESET} Script abbrechen" + echo "" + read -rp " → " +} + +# Passwort einlesen (ohne Echo) +read_password() { + local prompt="$1" + local var_name="$2" + local pw1 pw2 + while true; do + read -rsp " ${prompt}: " pw1; echo "" + read -rsp " Wiederholen: " pw2; echo "" + if [[ "$pw1" == "$pw2" ]]; then + if [[ ${#pw1} -lt 12 ]]; then + echo -e "$WARN Passwort zu kurz (min. 12 Zeichen). Bitte nochmal." + else + eval "$var_name='$pw1'" + break + fi + else + echo -e "$ERR Passwörter stimmen nicht überein. Bitte nochmal." + fi + done +} + +# ── Trap für Ctrl+C ────────────────────────────────────────── +trap 'echo -e "\n$ERR Script durch Benutzer abgebrochen (Ctrl+C)."; exit 130' INT + +# ════════════════════════════════════════════════════════════ +# START +# ════════════════════════════════════════════════════════════ +clear +print_header "Psono Passwortmanager — Migration" +echo -e " Dieses Script migriert Psono von einem alten Host" +echo -e " auf diesen Debian 12/13 Server." +echo "" +echo -e " ${YELLOW}Voraussetzungen:${RESET}" +echo -e " • Docker installiert" +echo -e " • PostgreSQL 17 installiert" +echo -e " • Nginx installiert" +echo -e " • Dehydrated mit gültigem Zertifikat für deine Domain" +echo -e " • Datenbank-Backup (.tar) lokal vorhanden" +echo -e " • settings.yaml vom alten psono-combo Container gesichert" +echo "" +echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" + +# ── Parameter einlesen ─────────────────────────────────────── +print_header "Parameter eingeben" + +echo -e "${BOLD} 1/6 Domain${RESET}" +read -rp " Psono-Domain (z.B. psono.example.com): " PSONO_DOMAIN +echo "" + +echo -e "${BOLD} 2/6 Server-IP${RESET}" +read -rp " Öffentliche IP dieses Servers: " SERVER_IP +echo "" + +echo -e "${BOLD} 3/6 Datenbank-Passwort${RESET}" +echo -e "${DIM} Passwort für den PostgreSQL-Benutzer 'psono'${RESET}" +read_password "Passwort" DB_PASSWORD +echo "" + +echo -e "${BOLD} 4/6 Pfad zum Datenbank-Backup${RESET}" +read -rp " Vollständiger Pfad zur .tar-Datei: " BACKUP_PATH +while [[ ! -f "$BACKUP_PATH" ]]; do + echo -e "$ERR Datei nicht gefunden: $BACKUP_PATH" + read -rp " Pfad zur .tar-Datei: " BACKUP_PATH +done +echo "" + +echo -e "${BOLD} 5/6 Pfad zur settings.yaml${RESET}" +read -rp " Vollständiger Pfad zur settings.yaml: " SETTINGS_PATH +while [[ ! -f "$SETTINGS_PATH" ]]; do + echo -e "$ERR Datei nicht gefunden: $SETTINGS_PATH" + read -rp " Pfad zur settings.yaml: " SETTINGS_PATH +done +echo "" + +echo -e "${BOLD} 6/6 E-Mail-Adresse${RESET}" +read -rp " E-Mail für Psono-Benachrichtigungen: " PSONO_EMAIL +echo "" + +# ── Zusammenfassung ────────────────────────────────────────── +print_header "Zusammenfassung" +echo -e " ${BOLD}Domain:${RESET} ${CYAN}${PSONO_DOMAIN}${RESET}" +echo -e " ${BOLD}Server-IP:${RESET} ${CYAN}${SERVER_IP}${RESET}" +echo -e " ${BOLD}DB-Passwort:${RESET} ${CYAN}(gesetzt, ${#DB_PASSWORD} Zeichen)${RESET}" +echo -e " ${BOLD}Backup-Datei:${RESET} ${CYAN}${BACKUP_PATH}${RESET}" +echo -e " ${BOLD}settings.yaml:${RESET} ${CYAN}${SETTINGS_PATH}${RESET}" +echo -e " ${BOLD}E-Mail:${RESET} ${CYAN}${PSONO_EMAIL}${RESET}" +echo "" +echo -e " ${BOLD}Zertifikat erwartet unter:${RESET}" +echo -e " ${DIM}/var/lib/dehydrated/certs/${PSONO_DOMAIN}/fullchain.pem${RESET}" +echo "" +echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" +echo "" +echo -e " ${YELLOW}${BOLD}Bitte prüfe alle Angaben sorgfältig.${RESET}" +echo -e " Gib ${BOLD}YES${RESET} (Großbuchstaben) ein um das Script zu starten." +echo -e " Alles andere bricht ab." +echo "" +read -rp " Bestätigung: " CONFIRM +if [[ "$CONFIRM" != "YES" ]]; then + echo -e "$ERR Abgebrochen. Keine Änderungen vorgenommen." + exit 0 +fi + +# ════════════════════════════════════════════════════════════ +# PHASE 1 — PostgreSQL vorbereiten +# ════════════════════════════════════════════════════════════ +print_phase "1" "PostgreSQL vorbereiten" + +print_step "1.1 — Datenbank anlegen" +if run "Datenbank 'psono' anlegen" sudo -u postgres psql -c "CREATE DATABASE psono;"; then + : # ok +else + handle_error "Datenbank konnte nicht angelegt werden." +fi + +print_step "1.2 — Benutzer anlegen" +if run "Benutzer 'psono' anlegen" sudo -u postgres psql -c "CREATE USER psono WITH PASSWORD '${DB_PASSWORD}';"; then + : # ok +else + handle_error "Benutzer konnte nicht angelegt werden." +fi + +print_step "1.3 — Rechte vergeben" +run "GRANT auf Datenbank" sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE psono TO psono;" \ + || handle_error "GRANT fehlgeschlagen." +run "GRANT auf Schema" sudo -u postgres psql -d psono -c "GRANT ALL ON SCHEMA public TO psono;" \ + || handle_error "GRANT auf Schema fehlgeschlagen." + +print_step "1.4 — Erweiterungen installieren" +run "Extension ltree" sudo -u postgres psql -d psono -c "CREATE EXTENSION IF NOT EXISTS ltree;" \ + || handle_error "Extension ltree konnte nicht installiert werden." +run "Extension pgcrypto" sudo -u postgres psql -d psono -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;" \ + || handle_error "Extension pgcrypto konnte nicht installiert werden." + +print_step "1.5 — pg_hba.conf anpassen" +HBA_FILE=$(sudo -u postgres psql -tAc "SHOW hba_file;") +echo -e " ${INFO} pg_hba.conf: ${HBA_FILE}" + +run "Eintrag für lokalen psono-Benutzer" sudo sed -i \ + '/^local all all/i local all psono md5' \ + "$HBA_FILE" || handle_error "pg_hba.conf konnte nicht angepasst werden." + +echo "host psono psono 172.17.0.0/16 md5" | sudo tee -a "$HBA_FILE" > /dev/null \ + && echo -e "$OK Docker-Netzwerk 172.17.0.0/16 eingetragen" \ + || handle_error "pg_hba.conf Eintrag für 172.17.0.0/16 fehlgeschlagen." + +echo "host psono psono 172.18.0.0/16 md5" | sudo tee -a "$HBA_FILE" > /dev/null \ + && echo -e "$OK Docker-Netzwerk 172.18.0.0/16 eingetragen" \ + || handle_error "pg_hba.conf Eintrag für 172.18.0.0/16 fehlgeschlagen." + +print_step "1.6 — listen_addresses anpassen" +run "listen_addresses setzen" sudo -u postgres psql -c \ + "ALTER SYSTEM SET listen_addresses = 'localhost,172.17.0.1,172.18.0.1';" \ + || handle_error "listen_addresses konnte nicht gesetzt werden." + +run "PostgreSQL neu starten" sudo systemctl restart postgresql \ + || handle_error "PostgreSQL-Neustart fehlgeschlagen." + +print_step "1.7 — Prüfung" +if run "Verbindung als psono-Benutzer testen" sudo -u postgres psql -U psono -d psono \ + -c "SELECT count(*) FROM pg_extension WHERE extname IN ('ltree','pgcrypto');" 2>/dev/null; then + echo -e " ${INFO} Erweiterungen vorhanden" +else + handle_error "Verbindungstest fehlgeschlagen — Erweiterungen oder Benutzer nicht korrekt." +fi + +LISTEN=$(sudo -u postgres psql -tAc "SHOW listen_addresses;" 2>/dev/null | tr -d ' ') +echo -e " ${INFO} listen_addresses = ${CYAN}${LISTEN}${RESET}" + +pause_phase "1" "2" + +# ════════════════════════════════════════════════════════════ +# PHASE 2 — Backup einspielen +# ════════════════════════════════════════════════════════════ +print_phase "2" "Backup einspielen" + +print_step "2.1 — Backup entpacken" +DUMP_DIR="/root/psono-backup/dump" +run "Verzeichnis anlegen" mkdir -p "$DUMP_DIR" \ + || handle_error "Verzeichnis $DUMP_DIR konnte nicht angelegt werden." +run "Backup entpacken" tar -xf "$BACKUP_PATH" -C "$DUMP_DIR" \ + || handle_error "Backup konnte nicht entpackt werden." + +echo -e " ${INFO} Inhalt des Dumps:" +ls "$DUMP_DIR" | head -5 | sed 's/^/ /' +echo -e " ${DIM}... ($(ls "$DUMP_DIR" | wc -l) Dateien gesamt)${RESET}" + +print_step "2.2 — Backup einspielen" +echo -e " ${INFO} Das kann einige Sekunden dauern ..." +echo "" + +PGPASSWORD="$DB_PASSWORD" pg_restore \ + -U psono \ + -d psono \ + -Fd \ + --no-owner \ + --no-privileges \ + "$DUMP_DIR" 2>&1 | while IFS= read -r line; do + if echo "$line" | grep -qi "error:"; then + echo -e " $ERR ${line}" + elif echo "$line" | grep -qi "warning:"; then + echo -e " $WARN ${DIM}${line}${RESET}" + else + echo -e " ${DIM} ${line}${RESET}" + fi + done + +PG_EXIT=${PIPESTATUS[0]} +if [[ $PG_EXIT -eq 0 ]]; then + echo -e "$OK pg_restore abgeschlossen" +else + echo -e "$WARN pg_restore mit Warnungen/Fehlern beendet (Exit: $PG_EXIT)" + handle_error "pg_restore meldete Fehler. Prüfe die Ausgabe oben." +fi + +print_step "2.3 — Prüfung" +USER_COUNT=$(PGPASSWORD="$DB_PASSWORD" psql -U psono -d psono -tAc \ + "SELECT count(*) FROM restapi_user;" 2>/dev/null || echo "0") +echo -e " ${INFO} Importierte Psono-Benutzer: ${CYAN}${USER_COUNT}${RESET}" +if [[ "$USER_COUNT" -gt 0 ]]; then + echo -e "$OK Daten erfolgreich importiert" +else + handle_error "Keine Benutzer gefunden — Import möglicherweise fehlgeschlagen." +fi + +pause_phase "2" "3" + +# ════════════════════════════════════════════════════════════ +# PHASE 3 — Psono-Konfiguration wiederherstellen +# ════════════════════════════════════════════════════════════ +print_phase "3" "Psono-Konfiguration wiederherstellen" + +print_step "3.1 — Verzeichnisse anlegen" +run "Verzeichnis /opt/docker/psono" mkdir -p /opt/docker/psono \ + || handle_error "Verzeichnis konnte nicht angelegt werden." +run "Verzeichnis /opt/docker/psono-client" mkdir -p /opt/docker/psono-client \ + || handle_error "Verzeichnis konnte nicht angelegt werden." + +print_step "3.2 — settings.yaml kopieren" +run "settings.yaml kopieren" cp "$SETTINGS_PATH" /opt/docker/psono/settings.yaml \ + || handle_error "settings.yaml konnte nicht kopiert werden." + +print_step "3.3 — settings.yaml anpassen" +run "HOST auf host.docker.internal setzen" sed -i \ + "s/'HOST': 'psono-database'/'HOST': 'host.docker.internal'/" \ + /opt/docker/psono/settings.yaml || handle_error "HOST konnte nicht ersetzt werden." + +run "DB-Passwort setzen" sed -i \ + "s/'PASSWORD': '.*'/'PASSWORD': '${DB_PASSWORD}'/" \ + /opt/docker/psono/settings.yaml || handle_error "Passwort konnte nicht gesetzt werden." + +# Cache deaktivieren (nur wenn vorhanden) +if grep -q "^CACHE_ENABLE: TRUE" /opt/docker/psono/settings.yaml 2>/dev/null; then + run "CACHE_ENABLE deaktivieren" sed -i \ + "s/^CACHE_ENABLE: TRUE/CACHE_ENABLE: FALSE/" /opt/docker/psono/settings.yaml \ + || handle_error "CACHE_ENABLE konnte nicht deaktiviert werden." + run "CACHE_REDIS deaktivieren" sed -i \ + "s/^CACHE_REDIS: TRUE/CACHE_REDIS: FALSE/" /opt/docker/psono/settings.yaml \ + || true + run "CACHE_REDIS_LOCATION auskommentieren" sed -i \ + "s/^CACHE_REDIS_LOCATION:/#CACHE_REDIS_LOCATION:/" /opt/docker/psono/settings.yaml \ + || true +else + echo -e " ${INFO} Cache war bereits deaktiviert — keine Änderung nötig" +fi + +print_step "3.4 — config.json erstellen" +cat > /opt/docker/psono-client/config.json << EOF +{ + "backend_servers": [{ + "title": "Psono Server", + "url": "https://${PSONO_DOMAIN}/server" + }], + "base_url": "https://${PSONO_DOMAIN}/", + "allow_custom_server": true, + "allow_registration": true, + "allow_lost_password": true, + "disable_download_bar": false, + "remember_me_default": false, + "trust_device_default": false, + "authentication_methods": ["AUTHKEY"] +} +EOF +echo -e "$OK config.json erstellt" + +print_step "3.5 — Prüfung" +echo -e " ${INFO} Aktuelle Datenbankverbindung in settings.yaml:" +grep -E "'HOST'|'PASSWORD'|'NAME'" /opt/docker/psono/settings.yaml 2>/dev/null | sed 's/^/ /' +CACHE_STATE=$(grep "^CACHE_ENABLE" /opt/docker/psono/settings.yaml 2>/dev/null || echo "nicht gefunden") +echo -e " ${INFO} Cache: ${CYAN}${CACHE_STATE}${RESET}" + +pause_phase "3" "4" + +# ════════════════════════════════════════════════════════════ +# PHASE 4 — Docker Container starten +# ════════════════════════════════════════════════════════════ +print_phase "4" "Psono-Container starten" + +print_step "4.1 — docker-compose.yml erstellen" +cat > /opt/docker/psono/docker-compose.yml << 'EOF' +services: + psono-combo: + image: psono/psono-combo:latest + restart: unless-stopped + ports: + - "127.0.0.1:10200:80" + volumes: + - /opt/docker/psono/settings.yaml:/root/.psono_server/settings.yaml:ro + - /opt/docker/psono-client/config.json:/usr/share/nginx/html/config.json:ro + extra_hosts: + - "host.docker.internal:host-gateway" +EOF +echo -e "$OK docker-compose.yml erstellt" + +print_step "4.2 — Container starten" +run "Docker Image pullen" docker compose -f /opt/docker/psono/docker-compose.yml pull \ + || handle_error "Docker Image konnte nicht geladen werden." +run "Container starten" docker compose -f /opt/docker/psono/docker-compose.yml up -d \ + || handle_error "Container konnte nicht gestartet werden." + +print_step "4.3 — Warten und Logs prüfen" +echo -e " ${INFO} Warte 10 Sekunden auf Container-Start ..." +sleep 10 + +echo -e " ${INFO} Letzte Log-Einträge:" +docker compose -f /opt/docker/psono/docker-compose.yml logs psono-combo 2>&1 | tail -8 | sed 's/^/ /' + +print_step "4.4 — Prüfung" +if run "HTTP-Test auf Port 10200" curl -sf http://localhost:10200/server/info/ > /dev/null; then + PSONO_VERSION=$(curl -s http://localhost:10200/server/info/ | grep -o '"version": "[^"]*"' | head -1) + echo -e " ${INFO} ${CYAN}${PSONO_VERSION}${RESET}" +else + handle_error "Psono antwortet nicht auf Port 10200. Logs prüfen." +fi + +pause_phase "4" "5" + +# ════════════════════════════════════════════════════════════ +# PHASE 5 — Nginx einrichten +# ════════════════════════════════════════════════════════════ +print_phase "5" "Nginx Reverse Proxy einrichten" + +print_step "5.1 — Zertifikat prüfen" +CERT_PATH="/var/lib/dehydrated/certs/${PSONO_DOMAIN}" +if [[ -f "${CERT_PATH}/fullchain.pem" && -f "${CERT_PATH}/privkey.pem" ]]; then + echo -e "$OK Zertifikat gefunden: ${CERT_PATH}" + EXPIRY=$(openssl x509 -enddate -noout -in "${CERT_PATH}/fullchain.pem" 2>/dev/null | cut -d= -f2) + echo -e " ${INFO} Gültig bis: ${CYAN}${EXPIRY}${RESET}" +else + handle_error "Zertifikat nicht gefunden unter ${CERT_PATH}. Dehydrated zuerst ausführen." +fi + +print_step "5.2 — Nginx-Konfiguration erstellen" +NGINX_CONF="/etc/nginx/sites-available/${PSONO_DOMAIN}.conf" +cat > "$NGINX_CONF" << EOF +server { + listen 80; + listen [::]:80; + server_name ${PSONO_DOMAIN}; + server_tokens off; + return 301 https://\$host\$request_uri; +} + +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + server_name ${PSONO_DOMAIN}; + + include snippets/letsencrypt-acme-challenge.conf; + + ssl_certificate /var/lib/dehydrated/certs/${PSONO_DOMAIN}/fullchain.pem; + ssl_certificate_key /var/lib/dehydrated/certs/${PSONO_DOMAIN}/privkey.pem; + ssl_dhparam /etc/nginx/ssl/dhparam.pem; + + ssl_session_cache shared:MozSSL:50m; + ssl_session_timeout 1d; + ssl_session_tickets off; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + + server_tokens off; + + add_header Strict-Transport-Security "max-age=15768000; includeSubDomains;" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "same-origin" always; + add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always; + + client_max_body_size 256m; + + location / { + proxy_pass http://127.0.0.1:10200; + 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; + } +} +EOF +echo -e "$OK Nginx-Konfiguration erstellt: $NGINX_CONF" + +print_step "5.3 — Konfiguration aktivieren" +if [[ ! -L "/etc/nginx/sites-enabled/${PSONO_DOMAIN}.conf" ]]; then + run "Symlink erstellen" ln -s "$NGINX_CONF" "/etc/nginx/sites-enabled/${PSONO_DOMAIN}.conf" \ + || handle_error "Symlink konnte nicht erstellt werden." +else + echo -e " ${INFO} Symlink bereits vorhanden" +fi + +run "Nginx-Konfiguration testen" nginx -t \ + || handle_error "Nginx-Konfiguration fehlerhaft — bitte manuell prüfen." + +run "Nginx neu laden" systemctl reload nginx \ + || handle_error "Nginx konnte nicht neu geladen werden." + +print_step "5.4 — Prüfung" +if run "HTTPS-Test" curl -sf "https://${PSONO_DOMAIN}/server/info/" > /dev/null; then + echo -e " ${INFO} ${GREEN}Psono ist über HTTPS erreichbar!${RESET}" +else + echo -e "$WARN HTTPS-Test fehlgeschlagen — DNS noch nicht umgestellt?" + echo -e " ${INFO} Test mit direkter IP-Auflösung:" + if curl -sf --resolve "${PSONO_DOMAIN}:443:127.0.0.1" \ + "https://${PSONO_DOMAIN}/server/info/" > /dev/null 2>&1; then + echo -e "$OK Lokal erreichbar — DNS-Umstellung ausstehend" + else + handle_error "Psono nicht erreichbar. Nginx-Logs prüfen." + fi +fi + +pause_phase "5" "6" + +# ════════════════════════════════════════════════════════════ +# PHASE 6 — Abschluss & Backups +# ════════════════════════════════════════════════════════════ +print_phase "6" "Abschluss & Backups einrichten" + +print_step "6.1 — Backup-Verzeichnis anlegen" +run "Verzeichnis /opt/backups/psono anlegen" mkdir -p /opt/backups/psono \ + || handle_error "Backup-Verzeichnis konnte nicht angelegt werden." + +print_step "6.2 — Cronjobs einrichten" +CRON_BACKUP="0 2 * * * PGPASSWORD='${DB_PASSWORD}' pg_dump -U psono -Ft psono > /opt/backups/psono/psono-\$(date +\\%Y-\\%m-\\%d).tar" +CRON_CLEANUP="0 3 * * * find /opt/backups/psono -name '*.tar' -mtime +30 -delete" + +(crontab -l 2>/dev/null | grep -v "psono"; echo "$CRON_BACKUP"; echo "$CRON_CLEANUP") | crontab - \ + && echo -e "$OK Cronjobs eingerichtet" \ + || echo -e "$WARN Cronjobs konnten nicht eingerichtet werden — bitte manuell eintragen" + +echo -e " ${INFO} Backup täglich um 02:00, Aufbewahrung 30 Tage" + +# ── Abschlussbericht ───────────────────────────────────────── +echo "" +echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════╗${RESET}" +echo -e "${BOLD}${GREEN}║ Migration erfolgreich abgeschlossen! ║${RESET}" +echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════╝${RESET}" +echo "" +echo -e " ${BOLD}Psono läuft unter:${RESET} ${CYAN}https://${PSONO_DOMAIN}${RESET}" +echo -e " ${BOLD}Admin-Panel:${RESET} ${CYAN}https://${PSONO_DOMAIN}/portal/login${RESET}" +echo -e " ${BOLD}Backups:${RESET} ${CYAN}/opt/backups/psono/${RESET}" +echo "" +echo -e " ${YELLOW}Nächste Schritte:${RESET}" +echo -e " • Login mit bekanntem Benutzer testen" +echo -e " • Passwörter in Tresoren prüfen" +echo -e " • Alten psono-combo Container auf der NAS stoppen" +echo -e " • Nach 1–2 Wochen stabilem Betrieb: alte Container löschen" +echo "" diff --git a/psono-neuinstallation-v2.docx b/psono-neuinstallation-v2.docx new file mode 100644 index 0000000..30e18ac Binary files /dev/null and b/psono-neuinstallation-v2.docx differ diff --git a/psono-neuinstallation-v2.odt b/psono-neuinstallation-v2.odt new file mode 100644 index 0000000..261cff2 Binary files /dev/null and b/psono-neuinstallation-v2.odt differ diff --git a/psono-neuinstallation-v2.pdf b/psono-neuinstallation-v2.pdf new file mode 100644 index 0000000..9180547 Binary files /dev/null and b/psono-neuinstallation-v2.pdf differ diff --git a/psono-neuinstallation.sh b/psono-neuinstallation.sh new file mode 100755 index 0000000..f31e146 --- /dev/null +++ b/psono-neuinstallation.sh @@ -0,0 +1,542 @@ +#!/usr/bin/env bash +# ============================================================ +# Psono Passwortmanager — Neuinstallations-Script +# Debian 12/13 · PostgreSQL 17 · Docker · Nginx +# ============================================================ +set -euo pipefail + +# ── Farben & Symbole ───────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +DIM='\033[2m' +RESET='\033[0m' + +OK=" ${GREEN}[ OK ]${RESET}" +ERR=" ${RED}[ ERROR ]${RESET}" +WARN="${YELLOW}[ WARNING ]${RESET}" +INFO="${CYAN}[ INFO ]${RESET}" + +# ── Hilfsfunktionen ────────────────────────────────────────── +print_header() { + echo "" + echo -e "${BOLD}${BLUE}╔══════════════════════════════════════════════════════════╗${RESET}" + echo -e "${BOLD}${BLUE}║ $1${RESET}" + echo -e "${BOLD}${BLUE}╚══════════════════════════════════════════════════════════╝${RESET}" + echo "" +} + +print_phase() { + echo "" + echo -e "${BOLD}${CYAN}┌──────────────────────────────────────────────────────────┐${RESET}" + echo -e "${BOLD}${CYAN}│ Phase $1: $2${RESET}" + echo -e "${BOLD}${CYAN}└──────────────────────────────────────────────────────────┘${RESET}" + echo "" +} + +print_step() { + echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" + echo -e "${BOLD} Schritt $1${RESET}" + echo "" +} + +run() { + local desc="$1"; shift + echo -ne " ${DIM}→${RESET} ${desc} ... " + if output=$("$@" 2>&1); then + echo -e "$OK" + return 0 + else + echo -e "$ERR" + echo -e " ${RED}Ausgabe: ${output}${RESET}" + return 1 + fi +} + +run_warn() { + local desc="$1"; shift + echo -ne " ${DIM}→${RESET} ${desc} ... " + if output=$("$@" 2>&1); then + echo -e "$OK" + else + echo -e "$WARN" + echo -e " ${YELLOW}Ausgabe: ${output}${RESET}" + fi +} + +handle_error() { + local msg="${1:-Ein Fehler ist aufgetreten.}" + echo "" + echo -e "$ERR ${RED}${BOLD}${msg}${RESET}" + echo "" + echo -e "${YELLOW} Wie möchtest du fortfahren?${RESET}" + echo -e " ${BOLD}[C]${RESET} Fortsetzen (auf eigene Gefahr)" + echo -e " ${BOLD}[Q]${RESET} Script abbrechen" + echo "" + read -rp " Eingabe [C/Q]: " choice + case "${choice^^}" in + C) echo -e "$WARN Fortgesetzt trotz Fehler." ;; + *) echo -e "$ERR Script abgebrochen."; exit 1 ;; + esac +} + +pause_phase() { + echo "" + echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" + echo -e "${GREEN} Phase $1 abgeschlossen.${RESET}" + echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" + echo "" + echo -e " ${BOLD}[ENTER]${RESET} Weiter mit Phase $2" + echo -e " ${BOLD}[Ctrl+C]${RESET} Script abbrechen" + echo "" + read -rp " → " +} + +read_password() { + local prompt="$1" + local var_name="$2" + local pw1 pw2 + while true; do + read -rsp " ${prompt}: " pw1; echo "" + read -rsp " Wiederholen: " pw2; echo "" + if [[ "$pw1" == "$pw2" ]]; then + if [[ ${#pw1} -lt 12 ]]; then + echo -e "$WARN Passwort zu kurz (min. 12 Zeichen). Bitte nochmal." + else + eval "$var_name='$pw1'" + break + fi + else + echo -e "$ERR Passwörter stimmen nicht überein. Bitte nochmal." + fi + done +} + +trap 'echo -e "\n$ERR Script durch Benutzer abgebrochen (Ctrl+C)."; exit 130' INT + +# ════════════════════════════════════════════════════════════ +# START +# ════════════════════════════════════════════════════════════ +clear +print_header "Psono Passwortmanager — Neuinstallation" +echo -e " Dieses Script installiert Psono Community Edition" +echo -e " auf diesem Debian 12/13 Server." +echo "" +echo -e " ${YELLOW}Voraussetzungen:${RESET}" +echo -e " • Docker installiert (download.docker.com)" +echo -e " • PostgreSQL 17 installiert" +echo -e " • Nginx installiert" +echo -e " • Dehydrated mit gültigem Zertifikat für deine Domain" +echo -e " • DNS-Eintrag für deine Domain zeigt auf diesen Server" +echo -e " • SMTP-Zugangsdaten für den E-Mail-Versand" +echo "" +echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" + +# ── Parameter einlesen ─────────────────────────────────────── +print_header "Parameter eingeben" + +echo -e "${BOLD} 1/7 Domain${RESET}" +read -rp " Psono-Domain (z.B. psono.example.com): " PSONO_DOMAIN +echo "" + +echo -e "${BOLD} 2/7 Server-IP${RESET}" +read -rp " Öffentliche IP dieses Servers: " SERVER_IP +echo "" + +echo -e "${BOLD} 3/7 Datenbank-Passwort${RESET}" +echo -e "${DIM} Passwort für den PostgreSQL-Benutzer 'psono'${RESET}" +read_password "Passwort" DB_PASSWORD +echo "" + +echo -e "${BOLD} 4/7 E-Mail-Absender${RESET}" +read -rp " E-Mail-Adresse für Psono (z.B. psono@example.com): " PSONO_EMAIL +echo "" + +echo -e "${BOLD} 5/7 SMTP-Server${RESET}" +read -rp " SMTP-Host (z.B. smtp.example.com): " SMTP_HOST +read -rp " SMTP-Port [587]: " SMTP_PORT +SMTP_PORT="${SMTP_PORT:-587}" +echo "" + +echo -e "${BOLD} 6/7 SMTP-Zugangsdaten${RESET}" +read -rp " SMTP-Benutzername: " SMTP_USER +read_password "SMTP-Passwort" SMTP_PASSWORD +echo "" + +echo -e "${BOLD} 7/7 Registrierung${RESET}" +echo -e "${DIM} Sollen sich neue Benutzer selbst registrieren können?${RESET}" +read -rp " Registrierung erlauben? [j/N]: " REG_CHOICE +case "${REG_CHOICE,,}" in + j|ja|y|yes) ALLOW_REGISTRATION="True" ;; + *) ALLOW_REGISTRATION="False" ;; +esac +echo "" + +# ── Zusammenfassung ────────────────────────────────────────── +print_header "Zusammenfassung" +echo -e " ${BOLD}Domain:${RESET} ${CYAN}${PSONO_DOMAIN}${RESET}" +echo -e " ${BOLD}Server-IP:${RESET} ${CYAN}${SERVER_IP}${RESET}" +echo -e " ${BOLD}DB-Passwort:${RESET} ${CYAN}(gesetzt, ${#DB_PASSWORD} Zeichen)${RESET}" +echo -e " ${BOLD}E-Mail:${RESET} ${CYAN}${PSONO_EMAIL}${RESET}" +echo -e " ${BOLD}SMTP-Host:${RESET} ${CYAN}${SMTP_HOST}:${SMTP_PORT}${RESET}" +echo -e " ${BOLD}SMTP-Benutzer:${RESET} ${CYAN}${SMTP_USER}${RESET}" +echo -e " ${BOLD}SMTP-Passwort:${RESET} ${CYAN}(gesetzt, ${#SMTP_PASSWORD} Zeichen)${RESET}" +echo -e " ${BOLD}Registrierung:${RESET} ${CYAN}${ALLOW_REGISTRATION}${RESET}" +echo "" +echo -e " ${BOLD}Zertifikat erwartet unter:${RESET}" +echo -e " ${DIM}/var/lib/dehydrated/certs/${PSONO_DOMAIN}/fullchain.pem${RESET}" +echo "" +echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" +echo "" +echo -e " ${YELLOW}${BOLD}Bitte prüfe alle Angaben sorgfältig.${RESET}" +echo -e " Gib ${BOLD}YES${RESET} (Großbuchstaben) ein um das Script zu starten." +echo -e " Alles andere bricht ab." +echo "" +read -rp " Bestätigung: " CONFIRM +if [[ "$CONFIRM" != "YES" ]]; then + echo -e "$ERR Abgebrochen. Keine Änderungen vorgenommen." + exit 0 +fi + +# ════════════════════════════════════════════════════════════ +# PHASE 1 — PostgreSQL vorbereiten +# ════════════════════════════════════════════════════════════ +print_phase "1" "PostgreSQL vorbereiten" + +print_step "1.1 — Datenbank anlegen" +run "Datenbank 'psono' anlegen" sudo -u postgres psql -c "CREATE DATABASE psono;" \ + || handle_error "Datenbank konnte nicht angelegt werden." + +print_step "1.2 — Benutzer anlegen" +run "Benutzer 'psono' anlegen" sudo -u postgres psql \ + -c "CREATE USER psono WITH PASSWORD '${DB_PASSWORD}';" \ + || handle_error "Benutzer konnte nicht angelegt werden." + +print_step "1.3 — Rechte vergeben" +run "GRANT auf Datenbank" sudo -u postgres psql \ + -c "GRANT ALL PRIVILEGES ON DATABASE psono TO psono;" \ + || handle_error "GRANT fehlgeschlagen." +run "GRANT auf Schema" sudo -u postgres psql -d psono \ + -c "GRANT ALL ON SCHEMA public TO psono;" \ + || handle_error "GRANT auf Schema fehlgeschlagen." + +print_step "1.4 — Erweiterungen installieren" +run "Extension ltree" sudo -u postgres psql -d psono \ + -c "CREATE EXTENSION IF NOT EXISTS ltree;" \ + || handle_error "Extension ltree konnte nicht installiert werden." +run "Extension pgcrypto" sudo -u postgres psql -d psono \ + -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;" \ + || handle_error "Extension pgcrypto konnte nicht installiert werden." + +print_step "1.5 — pg_hba.conf anpassen" +HBA_FILE=$(sudo -u postgres psql -tAc "SHOW hba_file;") +echo -e " ${INFO} pg_hba.conf: ${HBA_FILE}" + +run "Eintrag für lokalen psono-Benutzer" sudo sed -i \ + '/^local all all/i local all psono md5' \ + "$HBA_FILE" || handle_error "pg_hba.conf konnte nicht angepasst werden." + +echo "host psono psono 172.17.0.0/16 md5" | sudo tee -a "$HBA_FILE" > /dev/null \ + && echo -e "$OK Docker-Netzwerk 172.17.0.0/16 eingetragen" \ + || handle_error "pg_hba.conf Eintrag für 172.17.0.0/16 fehlgeschlagen." + +echo "host psono psono 172.18.0.0/16 md5" | sudo tee -a "$HBA_FILE" > /dev/null \ + && echo -e "$OK Docker-Netzwerk 172.18.0.0/16 eingetragen" \ + || handle_error "pg_hba.conf Eintrag für 172.18.0.0/16 fehlgeschlagen." + +print_step "1.6 — listen_addresses anpassen" +run "listen_addresses setzen" sudo -u postgres psql -c \ + "ALTER SYSTEM SET listen_addresses = 'localhost,172.17.0.1,172.18.0.1';" \ + || handle_error "listen_addresses konnte nicht gesetzt werden." + +run "PostgreSQL neu starten" sudo systemctl restart postgresql \ + || handle_error "PostgreSQL-Neustart fehlgeschlagen." + +print_step "1.7 — Prüfung" +if run "Verbindung als psono-Benutzer testen" \ + bash -c "PGPASSWORD='${DB_PASSWORD}' psql -U psono -d psono -c 'SELECT 1;' > /dev/null"; then + echo -e " ${INFO} Verbindung erfolgreich" +else + handle_error "Verbindungstest fehlgeschlagen." +fi + +LISTEN=$(sudo -u postgres psql -tAc "SHOW listen_addresses;" 2>/dev/null | tr -d ' ') +echo -e " ${INFO} listen_addresses = ${CYAN}${LISTEN}${RESET}" + +pause_phase "1" "2" + +# ════════════════════════════════════════════════════════════ +# PHASE 2 — Psono-Konfiguration erstellen +# ════════════════════════════════════════════════════════════ +print_phase "2" "Psono-Konfiguration erstellen" + +print_step "2.1 — Verzeichnisse anlegen" +run "Verzeichnis /opt/docker/psono" mkdir -p /opt/docker/psono \ + || handle_error "Verzeichnis konnte nicht angelegt werden." +run "Verzeichnis /opt/docker/psono-client" mkdir -p /opt/docker/psono-client \ + || handle_error "Verzeichnis konnte nicht angelegt werden." + +print_step "2.2 — Kryptographische Schlüssel generieren" +echo -e " ${INFO} Generiere Schlüssel (Docker-Image wird ggf. heruntergeladen) ..." +echo "" + +KEYS_RAW=$(docker run --rm psono/psono-combo:latest \ + python3 ./psono/manage.py generateserverkeys 2>/dev/null) + +if [[ -z "$KEYS_RAW" ]]; then + handle_error "Schlüssel konnten nicht generiert werden." +fi + +SECRET_KEY=$(echo "$KEYS_RAW" | grep "^SECRET_KEY:" | head -1 | sed "s/SECRET_KEY: '//;s/'$//") +ACT_SECRET=$(echo "$KEYS_RAW" | grep "^ACTIVATION_LINK_SECRET:" | head -1 | sed "s/ACTIVATION_LINK_SECRET: '//;s/'$//") +DB_SECRET=$(echo "$KEYS_RAW" | grep "^DB_SECRET:" | head -1 | sed "s/DB_SECRET: '//;s/'$//") +EMAIL_SALT=$(echo "$KEYS_RAW" | grep "^EMAIL_SECRET_SALT:" | head -1 | sed "s/EMAIL_SECRET_SALT: '//;s/'$//") +PRIVATE_KEY=$(echo "$KEYS_RAW" | grep "^PRIVATE_KEY:" | head -1 | sed "s/PRIVATE_KEY: '//;s/'$//") +PUBLIC_KEY=$(echo "$KEYS_RAW" | grep "^PUBLIC_KEY:" | head -1 | sed "s/PUBLIC_KEY: '//;s/'$//") + +echo -e "$OK Schlüssel generiert" +echo "" +echo -e " ${YELLOW}${BOLD}WICHTIG: Diese Schlüssel jetzt sichern!${RESET}" +echo -e " ${DIM}Ohne diese Schlüssel sind alle Passwörter verloren.${RESET}" +echo "" +echo -e " ${BOLD}PUBLIC_KEY:${RESET} ${CYAN}${PUBLIC_KEY}${RESET}" +echo -e " ${BOLD}PRIVATE_KEY:${RESET} ${CYAN}${PRIVATE_KEY:0:20}...${RESET} ${DIM}(vollständig in settings.yaml)${RESET}" +echo "" + +print_step "2.3 — settings.yaml erstellen" +cat > /opt/docker/psono/settings.yaml << EOF +SECRET_KEY: '${SECRET_KEY}' +ACTIVATION_LINK_SECRET: '${ACT_SECRET}' +DB_SECRET: '${DB_SECRET}' +EMAIL_SECRET_SALT: '${EMAIL_SALT}' +PRIVATE_KEY: '${PRIVATE_KEY}' +PUBLIC_KEY: '${PUBLIC_KEY}' + +DEBUG: False + +ALLOWED_HOSTS: ['${PSONO_DOMAIN}'] +HOST_URL: 'https://${PSONO_DOMAIN}/server' +WEB_CLIENT_URL: 'https://${PSONO_DOMAIN}' + +DATABASES: + default: + ENGINE: 'django.db.backends.postgresql_psycopg2' + NAME: 'psono' + USER: 'psono' + PASSWORD: '${DB_PASSWORD}' + HOST: 'host.docker.internal' + PORT: '5432' + +CACHE_ENABLE: FALSE + +EMAIL_FROM: 'Psono <${PSONO_EMAIL}>' +EMAIL_HOST: '${SMTP_HOST}' +EMAIL_HOST_USER: '${SMTP_USER}' +EMAIL_HOST_PASSWORD: '${SMTP_PASSWORD}' +EMAIL_PORT: ${SMTP_PORT} +EMAIL_USE_TLS: True + +ALLOW_REGISTRATION: ${ALLOW_REGISTRATION} +ALLOW_LOST_PASSWORD: True +EOF +echo -e "$OK settings.yaml erstellt: /opt/docker/psono/settings.yaml" + +print_step "2.4 — config.json erstellen" +cat > /opt/docker/psono-client/config.json << EOF +{ + "backend_servers": [{ + "title": "Psono Server", + "url": "https://${PSONO_DOMAIN}/server" + }], + "base_url": "https://${PSONO_DOMAIN}/", + "allow_custom_server": true, + "allow_registration": ${ALLOW_REGISTRATION,,}, + "allow_lost_password": true, + "disable_download_bar": false, + "remember_me_default": false, + "trust_device_default": false, + "authentication_methods": ["AUTHKEY"] +} +EOF +echo -e "$OK config.json erstellt: /opt/docker/psono-client/config.json" + +pause_phase "2" "3" + +# ════════════════════════════════════════════════════════════ +# PHASE 3 — Docker Container starten +# ════════════════════════════════════════════════════════════ +print_phase "3" "Psono-Container starten" + +print_step "3.1 — docker-compose.yml erstellen" +cat > /opt/docker/psono/docker-compose.yml << 'EOF' +services: + psono-combo: + image: psono/psono-combo:latest + restart: unless-stopped + ports: + - "127.0.0.1:10200:80" + volumes: + - /opt/docker/psono/settings.yaml:/root/.psono_server/settings.yaml:ro + - /opt/docker/psono-client/config.json:/usr/share/nginx/html/config.json:ro + extra_hosts: + - "host.docker.internal:host-gateway" +EOF +echo -e "$OK docker-compose.yml erstellt" + +print_step "3.2 — Container starten" +run "Docker Image pullen" docker compose -f /opt/docker/psono/docker-compose.yml pull \ + || handle_error "Docker Image konnte nicht geladen werden." +run "Container starten" docker compose -f /opt/docker/psono/docker-compose.yml up -d \ + || handle_error "Container konnte nicht gestartet werden." + +print_step "3.3 — Warten und Logs prüfen" +echo -e " ${INFO} Warte 10 Sekunden auf Container-Start ..." +sleep 10 +echo -e " ${INFO} Letzte Log-Einträge:" +docker compose -f /opt/docker/psono/docker-compose.yml logs psono-combo 2>&1 | tail -8 | sed 's/^/ /' + +print_step "3.4 — Prüfung" +if run "HTTP-Test auf Port 10200" curl -sf http://localhost:10200/server/info/ > /dev/null; then + PSONO_VERSION=$(curl -s http://localhost:10200/server/info/ | grep -o '"version": "[^"]*"' | head -1) + echo -e " ${INFO} ${CYAN}${PSONO_VERSION}${RESET}" +else + handle_error "Psono antwortet nicht auf Port 10200. Logs prüfen." +fi + +pause_phase "3" "4" + +# ════════════════════════════════════════════════════════════ +# PHASE 4 — Nginx einrichten +# ════════════════════════════════════════════════════════════ +print_phase "4" "Nginx Reverse Proxy einrichten" + +print_step "4.1 — Zertifikat prüfen" +CERT_PATH="/var/lib/dehydrated/certs/${PSONO_DOMAIN}" +if [[ -f "${CERT_PATH}/fullchain.pem" && -f "${CERT_PATH}/privkey.pem" ]]; then + echo -e "$OK Zertifikat gefunden: ${CERT_PATH}" + EXPIRY=$(openssl x509 -enddate -noout -in "${CERT_PATH}/fullchain.pem" 2>/dev/null | cut -d= -f2) + echo -e " ${INFO} Gültig bis: ${CYAN}${EXPIRY}${RESET}" +else + handle_error "Zertifikat nicht gefunden unter ${CERT_PATH}. Dehydrated zuerst ausführen." +fi + +print_step "4.2 — Nginx-Konfiguration erstellen" +NGINX_CONF="/etc/nginx/sites-available/${PSONO_DOMAIN}.conf" +cat > "$NGINX_CONF" << EOF +server { + listen 80; + listen [::]:80; + server_name ${PSONO_DOMAIN}; + server_tokens off; + return 301 https://\$host\$request_uri; +} + +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + server_name ${PSONO_DOMAIN}; + + include snippets/letsencrypt-acme-challenge.conf; + + ssl_certificate /var/lib/dehydrated/certs/${PSONO_DOMAIN}/fullchain.pem; + ssl_certificate_key /var/lib/dehydrated/certs/${PSONO_DOMAIN}/privkey.pem; + ssl_dhparam /etc/nginx/ssl/dhparam.pem; + + ssl_session_cache shared:MozSSL:50m; + ssl_session_timeout 1d; + ssl_session_tickets off; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + + server_tokens off; + + add_header Strict-Transport-Security "max-age=15768000; includeSubDomains;" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "same-origin" always; + add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always; + + client_max_body_size 256m; + + location / { + proxy_pass http://127.0.0.1:10200; + 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; + } +} +EOF +echo -e "$OK Nginx-Konfiguration erstellt: $NGINX_CONF" + +print_step "4.3 — Konfiguration aktivieren" +if [[ ! -L "/etc/nginx/sites-enabled/${PSONO_DOMAIN}.conf" ]]; then + run "Symlink erstellen" ln -s "$NGINX_CONF" "/etc/nginx/sites-enabled/${PSONO_DOMAIN}.conf" \ + || handle_error "Symlink konnte nicht erstellt werden." +else + echo -e " ${INFO} Symlink bereits vorhanden" +fi + +run "Nginx-Konfiguration testen" nginx -t \ + || handle_error "Nginx-Konfiguration fehlerhaft — bitte manuell prüfen." +run "Nginx neu laden" systemctl reload nginx \ + || handle_error "Nginx konnte nicht neu geladen werden." + +print_step "4.4 — Prüfung" +if run "HTTPS-Test" curl -sf "https://${PSONO_DOMAIN}/server/info/" > /dev/null; then + echo -e " ${INFO} ${GREEN}Psono ist über HTTPS erreichbar!${RESET}" +else + echo -e "$WARN HTTPS-Test fehlgeschlagen — DNS noch nicht propagiert?" + echo -e " ${INFO} Test mit direkter IP-Auflösung:" + if curl -sf --resolve "${PSONO_DOMAIN}:443:127.0.0.1" \ + "https://${PSONO_DOMAIN}/server/info/" > /dev/null 2>&1; then + echo -e "$OK Lokal erreichbar — DNS-Propagierung abwarten" + else + handle_error "Psono nicht erreichbar. Nginx-Logs prüfen." + fi +fi + +pause_phase "4" "5" + +# ════════════════════════════════════════════════════════════ +# PHASE 5 — Ersteinrichtung & Backups +# ════════════════════════════════════════════════════════════ +print_phase "5" "Ersteinrichtung & Backups einrichten" + +print_step "5.1 — Backup-Verzeichnis anlegen" +run "Verzeichnis /opt/backups/psono anlegen" mkdir -p /opt/backups/psono \ + || handle_error "Backup-Verzeichnis konnte nicht angelegt werden." + +print_step "5.2 — Cronjobs einrichten" +CRON_BACKUP="0 2 * * * PGPASSWORD='${DB_PASSWORD}' pg_dump -U psono -Ft psono > /opt/backups/psono/psono-\$(date +\\%Y-\\%m-\\%d).tar" +CRON_CLEANUP="0 3 * * * find /opt/backups/psono -name '*.tar' -mtime +30 -delete" + +(crontab -l 2>/dev/null | grep -v "psono"; echo "$CRON_BACKUP"; echo "$CRON_CLEANUP") | crontab - \ + && echo -e "$OK Cronjobs eingerichtet" \ + || echo -e "$WARN Cronjobs konnten nicht eingerichtet werden — bitte manuell eintragen" + +echo -e " ${INFO} Backup täglich um 02:00, Aufbewahrung 30 Tage" + +# ── Abschlussbericht ───────────────────────────────────────── +echo "" +echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════╗${RESET}" +echo -e "${BOLD}${GREEN}║ Installation erfolgreich abgeschlossen! ║${RESET}" +echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════╝${RESET}" +echo "" +echo -e " ${BOLD}Psono läuft unter:${RESET} ${CYAN}https://${PSONO_DOMAIN}${RESET}" +echo -e " ${BOLD}Admin-Panel:${RESET} ${CYAN}https://${PSONO_DOMAIN}/portal/login${RESET}" +echo -e " ${BOLD}Backups:${RESET} ${CYAN}/opt/backups/psono/${RESET}" +echo -e " ${BOLD}settings.yaml:${RESET} ${CYAN}/opt/docker/psono/settings.yaml${RESET}" +echo "" +echo -e " ${YELLOW}Nächste Schritte:${RESET}" +echo -e " 1. https://${PSONO_DOMAIN} öffnen und ersten Benutzer registrieren" +echo -e " 2. Admin-Rechte vergeben:" +echo -e " ${DIM}docker exec -ti psono-psono-combo-1 \\" +echo -e " python3 ./psono/manage.py promoteuser BENUTZER@${PSONO_DOMAIN} superuser${RESET}" +echo -e " 3. settings.yaml zusätzlich sichern:" +echo -e " ${DIM}cp /opt/docker/psono/settings.yaml /opt/backups/psono/settings.yaml.bak${RESET}" +echo ""