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": '
Noch kein Zertifikat hinterlegt.
+ {% endif %} + + {% if can_edit %} + + {% 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 %} ++ 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). +
+ +Für das Aktivieren fehlt das Recht „SSL/TLS ändern“.
+ {% endif %} +