diff --git a/install.sh b/install.sh index 60261aa..6d86d70 100644 --- a/install.sh +++ b/install.sh @@ -44,6 +44,21 @@ NC='\033[0m' print_status() { echo -e "${GREEN}✔${NC} ${1} completed."; } step() { echo -e "${RED}→${NC} ${1}..." | tee -a /var/log/tesm-install.log; } +# Kopiert nur, wenn der Inhalt sich tatsächlich unterscheidet (oder das +# Ziel noch fehlt) -- bei einem Update sollen ausschließlich Dateien +# angefasst werden, die sich zwischen der laufenden und der neuen Version +# wirklich geändert haben (unverändert lässt "cp" Zeitstempel/Inode aber +# trotzdem hochzählen, was z.B. Monitoring/Backups unnötig triggert). +# Gibt 0 zurück, wenn tatsächlich kopiert wurde, 1 wenn übersprungen. +install_if_changed() { + local src="$1" dst="$2" mode="${3:-644}" + if [ -f "$dst" ] && cmp -s "$src" "$dst"; then + return 1 + fi + sudo install -m "$mode" "$src" "$dst" + return 0 +} + # "clear" schlägt ohne TERM/Pseudo-Terminal fehl (z.B. bei Automatisierung # per SSH exec_command ohne pty, genau wie update.sh dieses Skript aufruft) # -- mit "set -e" bricht das sonst die GESAMTE Installation sofort und ohne @@ -150,10 +165,13 @@ fi # ---- Custom-Skripte + systemd ---- step "Copying custom scripts and systemd units" sudo mkdir -p /usr/local/bin/custom -sudo cp "$REPO_DIR/usr/local/bin/custom/poe.sh" /usr/local/bin/custom/poe.sh -sudo chmod +x /usr/local/bin/custom/poe.sh -sudo cp "$REPO_DIR"/etc/systemd/system/*.service /etc/systemd/system/ -sudo cp "$REPO_DIR"/etc/systemd/system/*.timer /etc/systemd/system/ +UNITS_CHANGED=0 +install_if_changed "$REPO_DIR/usr/local/bin/custom/poe.sh" /usr/local/bin/custom/poe.sh 755 || true +for f in "$REPO_DIR"/etc/systemd/system/*.service "$REPO_DIR"/etc/systemd/system/*.timer; do + if install_if_changed "$f" "/etc/systemd/system/$(basename "$f")" 644; then + UNITS_CHANGED=1 + fi +done print_status "Scripts and units copied" # ---- SSH-Login-Banner (MOTD) ---- @@ -162,8 +180,7 @@ print_status "Scripts and units copied" # Die Standard-Skripte bleiben erhalten, werden aber deaktiviert (chmod -x, # nicht gelöscht), damit ein Rückbau jederzeit per chmod +x möglich ist. step "Setting up TESM SSH login banner" -sudo cp "$REPO_DIR/etc/update-motd.d/50-tesm" /etc/update-motd.d/50-tesm -sudo chmod 755 /etc/update-motd.d/50-tesm +install_if_changed "$REPO_DIR/etc/update-motd.d/50-tesm" /etc/update-motd.d/50-tesm 755 || true sudo chmod -x /etc/update-motd.d/00-header /etc/update-motd.d/10-help-text \ /etc/update-motd.d/50-motd-news /etc/update-motd.d/60-unminimize \ /etc/update-motd.d/91-release-upgrade /etc/update-motd.d/92-unattended-upgrades \ @@ -171,8 +188,16 @@ sudo chmod -x /etc/update-motd.d/00-header /etc/update-motd.d/10-help-text \ print_status "SSH login banner configured" # ---- nginx ---- +# Nur bei einer Frischinstallation aus dem Repo-Template geschrieben -- +# eine bereits vorhandene Konfiguration (egal ob unverändert oder von der +# App selbst z.B. um SSL/HSTS erweitert, siehe Systemeinstellungen -> SSL) +# wird NIE wieder überschrieben. Symlink/Default-Site-Entfernung und der +# Validierungs-Reload laufen dagegen immer -- rein idempotent, ändern an +# einer bestehenden Konfiguration selbst nichts. step "Configuring nginx reverse proxy" -sudo cp "$REPO_DIR/etc/nginx/sites-available/tesm" /etc/nginx/sites-available/tesm +if [ ! -f /etc/nginx/sites-available/tesm ]; then + sudo cp "$REPO_DIR/etc/nginx/sites-available/tesm" /etc/nginx/sites-available/tesm +fi sudo ln -sf /etc/nginx/sites-available/tesm /etc/nginx/sites-enabled/tesm sudo rm -f /etc/nginx/sites-enabled/default sudo nginx -t >>/var/log/tesm-install.log 2>&1 && sudo systemctl reload nginx @@ -180,7 +205,9 @@ print_status "nginx configured" # ---- Dienste aktivieren ---- step "Enabling services" -sudo systemctl daemon-reload +if [ "$UNITS_CHANGED" -eq 1 ]; then + sudo systemctl daemon-reload +fi sudo systemctl enable --now tesm.service sudo systemctl enable --now tesm-check.service tesm-check-restart.timer print_status "Services enabled and started" diff --git a/srv/tesm/app.py b/srv/tesm/app.py index 3cd869e..43cfefb 100644 --- a/srv/tesm/app.py +++ b/srv/tesm/app.py @@ -16,7 +16,8 @@ from simple_websocket.errors import ConnectionClosed from werkzeug.utils import secure_filename from cryptography.fernet import Fernet from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import hashes, serialization +from cryptography import x509 from datetime import datetime, timedelta import base64, csv, ipaddress, logging, shlex, shutil, socket, sqlite3, glob, json, os, re, secrets, stat, subprocess, threading, time, traceback import paramiko @@ -107,6 +108,20 @@ except OSError: SWITCH_DEFAULT_SSH_PORT = 22 ALLOWED_AVATAR_EXT = {"png", "jpg", "jpeg", "gif", "webp"} +NGINX_SITE_CONFIG_PATH = os.environ.get("TESM_NGINX_CONFIG", "/etc/nginx/sites-available/tesm") +TESM_SSL_DIR = os.environ.get("TESM_SSL_DIR", "/etc/tesm/ssl") +TESM_SSL_CERT_PATH = os.path.join(TESM_SSL_DIR, "cert.pem") +TESM_SSL_KEY_PATH = os.path.join(TESM_SSL_DIR, "key.pem") +try: + # Bewusst AUSSERHALB von /srv/tesm -- das rsync --delete beim Deployen + # (siehe install.sh) würde ein dort abgelegtes Zertifikat sonst bei + # jedem Update wieder löschen. Analog zu /etc/nginx/ selbst: liegt + # außerhalb des App-Verzeichnisses, übersteht also jedes Update. + os.makedirs(TESM_SSL_DIR, exist_ok=True) + os.chmod(TESM_SSL_DIR, 0o700) +except OSError: + pass + CREDENTIAL_CATEGORIES = [ ("switch", "Switch"), ("linux", "Linux-Client"), @@ -260,6 +275,10 @@ PERMISSIONS = { "label": "LDAP", "rows": {"view": "settings_ldap.view", "edit": "settings_ldap.edit"}, }, + "settings_ssl": { + "label": "SSL/TLS", + "rows": {"view": "settings_ssl.view", "edit": "settings_ssl.edit"}, + }, "papierkorb": { "label": "Papierkorb", "rows": {"view": "papierkorb.view", "edit": "papierkorb.edit"}, @@ -298,6 +317,8 @@ PERMISSION_LABELS = { "settings_dhcp.edit": "DHCP-Konfiguration speichern und Reservierungsdatei schreiben", "settings_ldap.view": "LDAP/AD-Konfiguration lesen", "settings_ldap.edit": "LDAP/AD-Konfiguration speichern (Bind-Konto, Gruppenzuordnungen)", + "settings_ssl.view": "SSL/TLS-Konfiguration und Zertifikatsstatus lesen", + "settings_ssl.edit": "SSL/TLS-Konfiguration ändern (Zertifikat hochladen, HTTPS/HSTS aktivieren)", } ALL_PERMISSION_KEYS = [] @@ -337,6 +358,7 @@ NAV_ITEMS = [ {"key": "settings_ldap", "label": "LDAP", "icon": "users", "endpoint": "settings_ldap"}, {"key": "settings_dhcp", "label": "DHCP", "icon": "network", "endpoint": "settings_dhcp"}, {"key": "settings_importexport", "label": "Im-/Export", "icon": "transfer", "endpoint": "settings_import_export"}, + {"key": "settings_ssl", "label": "SSL/TLS", "icon": "shield", "endpoint": "settings_ssl"}, {"key": "papierkorb", "label": "Papierkorb", "icon": "trash", "endpoint": "papierkorb"}, ]}, {"key": "logs_group", "label": "Logs", "icon": "terminal", "children": [ @@ -375,6 +397,8 @@ def _nav_key_visible(key, user): return user.can_view_settings_dhcp if key == "settings_ldap": return user.can_view_settings_ldap + if key == "settings_ssl": + return user.has_permission("settings_ssl.view") if key == "logs_live": return user.can_view_live_log if key == "logs_history": @@ -2494,6 +2518,304 @@ def _revert_network_config(token): _pending_network_revert.pop(token, None) +# ============================================================================ +# SSL/TLS (Systemeinstellungen -> SSL/TLS) +# Let's-Encrypt-Ausstellung ist bewusst (noch) nicht Teil dieser Funktion -- +# alle bekannten TESM-Instanzen laufen aktuell rein im internen Netz ohne +# öffentliche Domain (server_name "_"), HTTP-01 würde dort nicht +# funktionieren und DNS-01 bräuchte Provider-spezifische API-Zugänge. Für +# den Anfang daher nur manueller Zertifikat-Upload; Let's Encrypt kann bei +# Bedarf als spätere Erweiterung ergänzt werden, ohne dieses Fundament +# (Speicherort, Validierung, sicheres Anwenden) ändern zu müssen. +# ============================================================================ + +NGINX_REVERT_SECONDS = 45 +_pending_nginx_revert = {} + +_NGINX_PROXY_LOCATIONS = """ location /ws/ { + proxy_pass http://127.0.0.1:5000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + 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; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + location / { + proxy_pass http://127.0.0.1:5000; + 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; + } + + location /static/ { + alias /srv/tesm/static/; + expires 7d; + } +""" + + +def _render_nginx_config(ssl_enabled, hsts_enabled): + """Erzeugt den kompletten Inhalt von NGINX_SITE_CONFIG_PATH. Die + Reverse-Proxy-Regeln (WebSocket-Terminal, Haupt-App, statische Dateien) + sind bei aktivem SSL identisch, nur hinter einem 443-Block mit + Zertifikat/HSTS statt direkt hinter Port 80 -- Port 80 leitet dann nur + noch auf https weiter, damit ein alter Bookmark/Link nicht plötzlich + ins Leere läuft.""" + header = 'map $http_upgrade $connection_upgrade {\n default upgrade;\n "" close;\n}\n\n' + if not ssl_enabled: + return header + "server {\n listen 80;\n server_name _;\n\n" + _NGINX_PROXY_LOCATIONS + "}\n" + + hsts_line = "" + if hsts_enabled: + hsts_line = ' add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;\n\n' + return ( + header + + "server {\n listen 80;\n server_name _;\n return 301 https://$host$request_uri;\n}\n\n" + + "server {\n listen 443 ssl;\n server_name _;\n\n" + + f" ssl_certificate {TESM_SSL_CERT_PATH};\n" + + f" ssl_certificate_key {TESM_SSL_KEY_PATH};\n\n" + + hsts_line + + _NGINX_PROXY_LOCATIONS + + "}\n" + ) + + +def _apply_nginx_config(ssl_enabled, hsts_enabled): + """Schreibt die neue nginx-Konfiguration, validiert sie (nginx -t) und + lädt nginx erst danach neu -- schlägt Validierung ODER Reload fehl, + wird der vorherige Inhalt sofort wiederhergestellt (Selbstschutz gegen + einen Tippfehler/ein defektes Zertifikat, das sonst den kompletten + Reverse-Proxy lahmlegen würde). Gibt (ok, fehlermeldung, alter_inhalt) + zurück -- den alten Inhalt braucht der Aufrufer zusätzlich für das + zeitgesteuerte Rückroll-Sicherheitsnetz, siehe NGINX_REVERT_SECONDS.""" + try: + with open(NGINX_SITE_CONFIG_PATH, encoding="utf-8") as f: + previous = f.read() + except OSError: + previous = None + + new_content = _render_nginx_config(ssl_enabled, hsts_enabled) + try: + with open(NGINX_SITE_CONFIG_PATH, "w", encoding="utf-8") as f: + f.write(new_content) + except OSError as e: + return False, f"Konfiguration konnte nicht geschrieben werden: {e}", previous + + ok, out = _dhcp_run_privileged(["nginx", "-t"], timeout=10) + if ok: + ok, out = _dhcp_run_privileged(["systemctl", "reload", "nginx"], timeout=10) + + if not ok: + if previous is not None: + try: + with open(NGINX_SITE_CONFIG_PATH, "w", encoding="utf-8") as f: + f.write(previous) + subprocess.run(["systemctl", "reload", "nginx"], capture_output=True, timeout=10) + except OSError: + pass + return False, out, previous + + return True, "", previous + + +def _revert_nginx_config(token): + """Timer-Callback analog zu _revert_network_config: keine Bestätigung + binnen NGINX_REVERT_SECONDS -> vorherige nginx-Konfiguration UND die + zugehörigen ssl_enabled/hsts_enabled-Einstellungen wiederherstellen.""" + entry = _pending_nginx_revert.get(token) + if not entry: + return + try: + if entry["previous"] is not None: + with open(NGINX_SITE_CONFIG_PATH, "w", encoding="utf-8") as f: + f.write(entry["previous"]) + subprocess.run(["nginx", "-t"], capture_output=True, timeout=10) + subprocess.run(["systemctl", "reload", "nginx"], capture_output=True, timeout=10) + set_setting("ssl_enabled", entry["prev_ssl_enabled"]) + set_setting("hsts_enabled", entry["prev_hsts_enabled"]) + log_action_system("settings.ssl_revert", "nginx", "automatisch nach Timeout zurückgerollt") + except Exception as e: + app.logger.error("nginx auto-revert failed: %s", e) + finally: + _pending_nginx_revert.pop(token, None) + + +def _parse_cert(cert_bytes): + """Lädt ein PEM-Zertifikat und liefert sowohl das cryptography-Objekt + (für den Schlüssel-Abgleich) als auch die für die UI relevanten Felder + (Subject, Aussteller, Ablaufdatum).""" + cert = x509.load_pem_x509_certificate(cert_bytes) + try: + not_after = cert.not_valid_after_utc.replace(tzinfo=None) + except AttributeError: + not_after = cert.not_valid_after + return cert, { + "subject": cert.subject.rfc4514_string(), + "issuer": cert.issuer.rfc4514_string(), + "not_after": not_after, + } + + +def _validate_cert_key_pair(cert_bytes, key_bytes): + """Prüft ein hochgeladenes Zertifikat+Key-Paar VOR dem Speichern -- + verhindert, dass ein kaputtes oder nicht zusammenpassendes Paar erst + beim tatsächlichen SSL-Aktivieren auffällt (mit dem entsprechenden + Ausfallrisiko für den Reverse-Proxy). Der Schlüssel-Abgleich + funktioniert algorithmus-unabhängig (RSA/EC/...), da nur die + öffentlichen Schlüssel als DER-Bytes verglichen werden, nicht + algorithmus-spezifische Felder. Gibt (ok, fehlermeldung, info-dict) + zurück.""" + try: + cert, info = _parse_cert(cert_bytes) + except ValueError as e: + return False, f"Datei ist kein gültiges PEM-Zertifikat ({e}).", {} + + if info["not_after"] < datetime.utcnow(): + return False, f"Zertifikat ist bereits abgelaufen (gültig bis {info['not_after']:%d.%m.%Y}).", info + + try: + private_key = serialization.load_pem_private_key(key_bytes, password=None) + except (ValueError, TypeError) as e: + return False, f"Datei ist kein gültiger, unverschlüsselter privater Schlüssel ({e}).", info + + cert_pub = cert.public_key().public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo) + key_pub = private_key.public_key().public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo) + if cert_pub != key_pub: + return False, "Zertifikat und privater Schlüssel passen nicht zusammen.", info + + return True, "", info + + +def _current_cert_info(): + """Für die SSL-Seite: Status des aktuell gespeicherten Zertifikats + (falls vorhanden) inkl. verbleibender Gültigkeitsdauer.""" + if not (os.path.isfile(TESM_SSL_CERT_PATH) and os.path.isfile(TESM_SSL_KEY_PATH)): + return None + try: + with open(TESM_SSL_CERT_PATH, "rb") as f: + _, info = _parse_cert(f.read()) + info["days_left"] = (info["not_after"] - datetime.utcnow()).days + return info + except (OSError, ValueError): + return None + + +@app.route("/settings/ssl", methods=["GET", "POST"]) +@login_required +def settings_ssl(): + if request.method == "GET" and not current_user.has_permission("settings_ssl.view"): + flash("Keine Berechtigung, die SSL/TLS-Einstellungen anzusehen.", "danger") + return redirect(url_for("index")) + + if request.method == "POST": + if not current_user.has_permission("settings_ssl.edit"): + flash("Keine Berechtigung, die SSL/TLS-Einstellungen zu ändern.", "danger") + return redirect(url_for("settings_ssl")) + + if "upload_cert" in request.form: + cert_file = request.files.get("cert_file") + key_file = request.files.get("key_file") + if not cert_file or not cert_file.filename or not key_file or not key_file.filename: + flash("Bitte sowohl Zertifikat (PEM) als auch privaten Schlüssel (PEM) auswählen.", "danger") + else: + cert_bytes = cert_file.read() + key_bytes = key_file.read() + ok, msg, info = _validate_cert_key_pair(cert_bytes, key_bytes) + if not ok: + flash(f"Zertifikat/Schlüssel nicht gespeichert: {msg}", "danger") + else: + try: + os.makedirs(TESM_SSL_DIR, exist_ok=True) + with open(TESM_SSL_CERT_PATH, "wb") as f: + f.write(cert_bytes) + with open(TESM_SSL_KEY_PATH, "wb") as f: + f.write(key_bytes) + os.chmod(TESM_SSL_KEY_PATH, 0o600) + os.chmod(TESM_SSL_CERT_PATH, 0o644) + log_action("settings.ssl_cert_upload", info.get("subject", ""), f"gültig bis {info['not_after']:%d.%m.%Y}") + flash( + f"Zertifikat gespeichert (Subject: {info.get('subject')}, gültig bis " + f"{info['not_after']:%d.%m.%Y}). SSL ist damit noch nicht aktiviert -- unten aktivieren.", + "success", + ) + except OSError as e: + flash(f"Zertifikat konnte nicht gespeichert werden: {e}", "danger") + + elif "apply_ssl" in request.form: + new_ssl = "ssl_enabled" in request.form + new_hsts = "hsts_enabled" in request.form + if new_ssl and not (os.path.isfile(TESM_SSL_CERT_PATH) and os.path.isfile(TESM_SSL_KEY_PATH)): + flash("Kein Zertifikat hinterlegt -- bitte zuerst Zertifikat + Schlüssel hochladen.", "danger") + elif new_hsts and not new_ssl: + flash("HSTS setzt aktives SSL voraus.", "danger") + else: + prev_ssl = get_setting("ssl_enabled", "0") + prev_hsts = get_setting("hsts_enabled", "0") + ok, err, previous = _apply_nginx_config(new_ssl, new_hsts) + if not ok: + flash(f"nginx-Konfiguration konnte nicht angewendet werden, unverändert gelassen: {err}", "danger") + else: + set_setting("ssl_enabled", "1" if new_ssl else "0") + set_setting("hsts_enabled", "1" if new_hsts else "0") + log_action("settings.ssl_apply", "nginx", f"ssl={new_ssl} hsts={new_hsts}") + token = secrets.token_hex(8) + timer = threading.Timer(NGINX_REVERT_SECONDS, _revert_nginx_config, args=(token,)) + timer.daemon = True + _pending_nginx_revert.clear() + _pending_nginx_revert[token] = { + "timer": timer, "previous": previous, + "prev_ssl_enabled": prev_ssl, "prev_hsts_enabled": prev_hsts, + } + timer.start() + scheme = "https" if new_ssl else "http" + flash( + f"nginx-Konfiguration angewendet. Falls diese Seite jetzt noch über {scheme} erreichbar " + f"ist, bitte unten bestätigen — sonst wird nach {NGINX_REVERT_SECONDS}s automatisch " + f"zurückgerollt.", + "success", + ) + + elif "confirm_ssl" in request.form: + token = request.form.get("confirm_ssl") + entry = _pending_nginx_revert.pop(token, None) + if entry: + entry["timer"].cancel() + log_action("settings.ssl_confirm", "nginx") + flash("SSL-Konfiguration bestätigt — kein automatisches Rollback mehr.", "success") + else: + flash("Keine ausstehende Bestätigung gefunden (evtl. bereits abgelaufen).", "danger") + + elif "remove_cert" in request.form: + if get_setting("ssl_enabled", "0") == "1": + flash("Zertifikat kann nicht entfernt werden, solange SSL aktiv ist -- bitte zuerst deaktivieren.", "danger") + else: + for path in (TESM_SSL_CERT_PATH, TESM_SSL_KEY_PATH): + try: + os.remove(path) + except OSError: + pass + log_action("settings.ssl_cert_remove", "nginx") + flash("Zertifikat entfernt.", "success") + + return redirect(url_for("settings_ssl")) + + pending_token = next(iter(_pending_nginx_revert), None) + return render_template( + "settings_ssl.html", + cert_info=_current_cert_info(), + ssl_enabled=get_setting("ssl_enabled", "0") == "1", + hsts_enabled=get_setting("hsts_enabled", "0") == "1", + pending_ssl_token=pending_token, + ssl_revert_seconds=NGINX_REVERT_SECONDS, + ) + + @app.route("/settings", methods=["GET", "POST"]) @login_required def settings(): diff --git a/srv/tesm/templates/base.html b/srv/tesm/templates/base.html index 5c36318..4052437 100644 --- a/srv/tesm/templates/base.html +++ b/srv/tesm/templates/base.html @@ -20,6 +20,7 @@ "terminal": '', "history": '', "clock": '', + "shield": '', "sliders": '', "logout": '', "gear": '', diff --git a/srv/tesm/templates/settings_ssl.html b/srv/tesm/templates/settings_ssl.html new file mode 100644 index 0000000..e6a9ab6 --- /dev/null +++ b/srv/tesm/templates/settings_ssl.html @@ -0,0 +1,135 @@ +{% extends "base.html" %} +{% set active_page = "settings_ssl" %} +{% set can_edit = current_user.has_permission('settings_ssl.edit') %} +{% block page_title %}SSL/TLS{% endblock %} +{% block page_sub %}
HTTPS-Verschlüsselung und HSTS für den nginx-Reverse-Proxy
{% endblock %} + +{% block content %} +
+ +
+
+
+

Zertifikat

+
Zertifikat + privater Schlüssel im PEM-Format. Automatische Ausstellung über Let's Encrypt ist für rein interne Instanzen (noch) nicht verfügbar — Upload eines vorhandenen (z.B. selbst ausgestellten oder von einer internen CA signierten) Zertifikats.
+
+
+ + {% if cert_info %} +
+
Subject{{ cert_info.subject }}
+
Aussteller{{ cert_info.issuer }}
+
+ Gültig bis + + {{ cert_info.not_after.strftime('%d.%m.%Y') }} + {% if cert_info.days_left < 0 %} + abgelaufen + {% elif cert_info.days_left <= 30 %} + noch {{ cert_info.days_left }} Tage + {% else %} + noch {{ cert_info.days_left }} Tage + {% endif %} + +
+
+ {% else %} +

Noch kein Zertifikat hinterlegt.

+ {% endif %} + + {% if can_edit %} +
+
+ + +
+
+ + +
Ein passwortgeschützter Schlüssel wird abgelehnt — nginx könnte ihn beim Start ohnehin nicht ohne manuelle Passworteingabe laden.
+
+ +
+ {% if cert_info and not ssl_enabled %} +
+ +
+ {% elif cert_info %} +

SSL muss zuerst deaktiviert werden, bevor das Zertifikat entfernt werden kann.

+ {% endif %} + {% else %} +

Für Upload/Entfernen fehlt das Recht „SSL/TLS ändern“.

+ {% endif %} +
+ +
+
+
+

HTTPS aktivieren

+
Schaltet nginx auf Port 443 mit dem oben hinterlegten Zertifikat um. Port 80 leitet dann automatisch weiter, damit bestehende Lesezeichen/Links nicht ins Leere laufen.
+
+
+ + {% if pending_ssl_token %} +
+

+ Neue nginx-Konfiguration wurde angewendet. Wenn diese Seite gerade noch lädt, funktioniert die Verbindung — + bitte bestätigen, bevor automatisch zurückgerollt wird (nach {{ ssl_revert_seconds }}s ohne Bestätigung). +

+
+ + +
+
+ {% elif can_edit %} +
+
+ + {% if not cert_info %}
Erst nach Hochladen eines Zertifikats verfügbar.
{% endif %} +
+
+ +
Weist Browser an, diese Instanz künftig NUR noch über HTTPS aufzurufen — auch bei einem späteren Zurückschalten auf HTTP bleibt das im Browser für die eingestellte Dauer (1 Jahr) bestehen. Nur aktivieren, wenn das Zertifikat dauerhaft gepflegt wird.
+
+ +
+ {% else %} +

Für das Aktivieren fehlt das Recht „SSL/TLS ändern“.

+ {% endif %} +
+ +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/update.sh b/update.sh index fde8ecd..eb22e37 100644 --- a/update.sh +++ b/update.sh @@ -9,16 +9,15 @@ # automatischem Backup + Migrations-Sicherheitsnetz samt Rückroll- # Möglichkeit -- siehe install.sh für Details). # -# Dieses Skript selbst ist nur noch für das Ablösen ÄLTERER, TESM -# vorausgehender Installationsstufen auf diesem Host zuständig: -# 1. Das ursprüngliche, reine Bash-Setup aus dem separaten Repo -# https://gitea.int.eertmoed.net/WiS/Aruba-PoE (rpi-check.service + -# statische ips.list, keine Datenbank/Web-UI). -# 2. Die spätere Flask-App "PoE Manager" unter /srv/poe_manager -# (poe_web.service, Vorgänger von TESM in diesem Repo vor der -# Umbenennung). -# Beide werden NICHT gelöscht, sondern nur gestoppt/deaktiviert und -# (falls vorhanden) beiseite verschoben. +# Dieses Skript ist reiner Bootstrapper: Download + Entpacken + Aufruf von +# install.sh. Die frühere Sonderbehandlung zweier TESM vorausgehender +# Installationsstufen (rpi-check.service aus dem ursprünglichen reinen +# Bash-Setup, poe_web.service/poe_manager als Flask-Vorgänger von TESM) +# wurde entfernt -- diese Stufen sind auf allen aktuellen Installationen +# längst migriert, die dazugehörigen Unit-Dateien wurden dort manuell +# entfernt. Ein heute noch unmigriertes System wäre ohnehin so alt, dass +# ein manueller Blick vor dem Update angebracht ist statt automatischer +# stillschweigender Bereinigung. # # Auszuführen als root auf dem Zielsystem: # sudo ./update.sh @@ -38,7 +37,6 @@ GITEA_TOKEN="${TESM_GITEA_TOKEN:-}" PACKAGE_NAME="tesm-${RELEASE_TAG}.tar.gz" DOWNLOAD_URL="${GITEA_BASE}/${REPO_OWNER}/${REPO_NAME}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" -TIMESTAMP="$(date +%Y%m%d-%H%M%S)" WORK_DIR="/tmp/tesm-update-$(date +%s)" PACKAGE_FILE="$WORK_DIR/${PACKAGE_NAME}" @@ -61,10 +59,6 @@ echo "install.sh daraus -- das erkennt selbst, ob es sich um eine frische" echo "Installation oder ein Update einer bestehenden handelt, und sichert" echo "die Datenbank bei einer Schema-Änderung automatisch vorsorglich ab." echo -echo "Ältere, TESM vorausgehende Installationsstufen (PoE Manager unter" -echo "/srv/poe_manager, das ursprüngliche reine Bash-Setup) werden dabei" -echo "erkannt, gestoppt/deaktiviert und beiseite verschoben (nicht gelöscht)." -echo read -r -p "Fortfahren? [y/N] " confirm if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then @@ -86,22 +80,6 @@ mkdir -p "$WORK_DIR/pkg" tar xzf "$PACKAGE_FILE" -C "$WORK_DIR/pkg" --strip-components=1 echo -e "${GREEN}✔${NC} Paket entpackt." -echo -e "${RED}→${NC} Stoppe/deaktiviere ältere Installationsstufen (falls vorhanden)..." -# "|| true" macht nichts kaputt, falls einzelne Units gar nicht vorhanden -# sind. tesm.service selbst wird NICHT hier, sondern von install.sh -# gestoppt -- das entscheidet dort auch gleich, ob ein Backup nötig ist. -systemctl stop poe_web.service 2>/dev/null || true -systemctl disable poe_web.service 2>/dev/null || true -systemctl stop rpi-check.service rpi-check-restart.service rpi-check-restart.timer 2>/dev/null || true -systemctl disable rpi-check.service rpi-check-restart.service rpi-check-restart.timer 2>/dev/null || true -if [ -d /srv/poe_manager ]; then - dest="/srv/poe_manager-backup-${TIMESTAMP}" - echo -e "${RED}→${NC} Sichere /srv/poe_manager nach ${dest}..." - mv /srv/poe_manager "$dest" - echo -e "${GREEN}✔${NC} Gesichert (nicht gelöscht)." -fi -echo -e "${GREEN}✔${NC} Ältere Installationsstufen bereinigt." - echo -e "${RED}→${NC} Starte install.sh aus dem Paket..." bash "$WORK_DIR/pkg/install.sh"