diff --git a/etc/systemd/system/tesm.service b/etc/systemd/system/tesm.service
index caa078c..64f55ef 100644
--- a/etc/systemd/system/tesm.service
+++ b/etc/systemd/system/tesm.service
@@ -14,8 +14,12 @@ WorkingDirectory=/srv/tesm
# nach 30s auch bei einer offenen, aber gerade inaktiven SSH-Terminal-
# WebSocket-Verbindung und würde den Worker (und damit JEDE Verbindung,
# da nur ein Prozess läuft) killen.
+# "--bind 127.0.0.1:5000" statt 0.0.0.0: Flask/Gunicorn soll ausschließlich
+# über den nginx-Reverse-Proxy (siehe Einstellungen -> NGINX) erreichbar
+# sein, nicht zusätzlich direkt und unverschlüsselt über Port 5000 aus dem
+# Netz -- nginx selbst greift ohnehin nur über 127.0.0.1 zu.
ExecStart=/srv/tesm/venv/bin/gunicorn --workers 1 --worker-class gthread --threads 16 \
- --timeout 0 --bind 0.0.0.0:5000 app:app
+ --timeout 0 --bind 127.0.0.1:5000 app:app
Restart=always
RestartSec=5
diff --git a/install.sh b/install.sh
index 6d86d70..907f152 100644
--- a/install.sh
+++ b/install.sh
@@ -114,7 +114,7 @@ fi
# ---- Pakete ----
step "Installing system packages"
sudo apt-get update >>/var/log/tesm-install.log 2>&1 && print_status "apt update"
-sudo apt-get install -y python3 python3-venv python3-pip nginx sqlite3 expect openssh-client git rsync iputils-ping logrotate >>/var/log/tesm-install.log 2>&1 && print_status "Packages installed"
+sudo apt-get install -y python3 python3-venv python3-pip nginx sqlite3 expect openssh-client git rsync iputils-ping logrotate certbot >>/var/log/tesm-install.log 2>&1 && print_status "Packages installed"
# ---- Log-Verzeichnis ----
# NICHT weltweit beschreibbar (0755 reicht) -- sowohl tesm.service als
diff --git a/srv/tesm/app.py b/srv/tesm/app.py
index 43cfefb..5de3eb4 100644
--- a/srv/tesm/app.py
+++ b/srv/tesm/app.py
@@ -112,6 +112,8 @@ NGINX_SITE_CONFIG_PATH = os.environ.get("TESM_NGINX_CONFIG", "/etc/nginx/sites-a
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")
+TESM_ACME_WEBROOT = os.environ.get("TESM_ACME_WEBROOT", "/var/www/tesm-acme")
+CERTBOT_DEPLOY_HOOK_PATH = "/etc/letsencrypt/renewal-hooks/deploy/tesm-reload-nginx.sh"
try:
# Bewusst AUSSERHALB von /srv/tesm -- das rsync --delete beim Deployen
# (siehe install.sh) würde ein dort abgelegtes Zertifikat sonst bei
@@ -119,9 +121,38 @@ try:
# außerhalb des App-Verzeichnisses, übersteht also jedes Update.
os.makedirs(TESM_SSL_DIR, exist_ok=True)
os.chmod(TESM_SSL_DIR, 0o700)
+ os.makedirs(TESM_ACME_WEBROOT, exist_ok=True)
except OSError:
pass
+
+def _le_cert_path(domain):
+ return f"/etc/letsencrypt/live/{domain}/fullchain.pem"
+
+
+def _le_key_path(domain):
+ return f"/etc/letsencrypt/live/{domain}/privkey.pem"
+
+
+def _write_certbot_deploy_hook():
+ """certbot führt nach JEDER erfolgreichen Erneuerung (egal für welche
+ Domain) automatisch alle ausführbaren Skripte in diesem Verzeichnis aus
+ -- ohne diesen Hook würde nginx nach einer automatischen Verlängerung
+ weiter das alte (bald ablaufende) Zertifikat verwenden, da es die
+ Datei nicht von selbst neu einliest. Läuft wie _write_logrotate_config
+ bei jedem App-Start neu, damit ein frisch installiertes certbot den
+ Hook auch ohne manuellen Aufruf der NGINX-Seite bekommt."""
+ try:
+ os.makedirs(os.path.dirname(CERTBOT_DEPLOY_HOOK_PATH), exist_ok=True)
+ with open(CERTBOT_DEPLOY_HOOK_PATH, "w", encoding="utf-8") as f:
+ f.write("#!/bin/bash\nsystemctl reload nginx\n")
+ os.chmod(CERTBOT_DEPLOY_HOOK_PATH, 0o755)
+ except OSError:
+ pass
+
+
+_write_certbot_deploy_hook()
+
CREDENTIAL_CATEGORIES = [
("switch", "Switch"),
("linux", "Linux-Client"),
@@ -275,9 +306,9 @@ 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"},
+ "settings_nginx": {
+ "label": "NGINX",
+ "rows": {"view": "settings_nginx.view", "edit": "settings_nginx.edit"},
},
"papierkorb": {
"label": "Papierkorb",
@@ -317,8 +348,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)",
+ "settings_nginx.view": "NGINX-Konfiguration und Zertifikatsstatus lesen",
+ "settings_nginx.edit": "NGINX-Konfiguration ändern (Domain/Ports, Zertifikat hochladen oder per Let's Encrypt anfordern, HTTPS/HSTS aktivieren)",
}
ALL_PERMISSION_KEYS = []
@@ -358,7 +389,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": "settings_nginx", "label": "NGINX", "icon": "server", "endpoint": "settings_nginx"},
{"key": "papierkorb", "label": "Papierkorb", "icon": "trash", "endpoint": "papierkorb"},
]},
{"key": "logs_group", "label": "Logs", "icon": "terminal", "children": [
@@ -397,8 +428,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 == "settings_nginx":
+ return user.has_permission("settings_nginx.view")
if key == "logs_live":
return user.can_view_live_log
if key == "logs_history":
@@ -2519,19 +2550,41 @@ def _revert_network_config(token):
# ============================================================================
-# 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 (Systemeinstellungen -> NGINX)
+# Verwaltet den kompletten Reverse-Proxy: Domain/Ports, SSL/HSTS,
+# Zertifikat (Upload ODER Let's Encrypt).
+#
+# Let's Encrypt läuft bewusst über "certbot certonly --webroot", NICHT über
+# das certbot-nginx-Plugin -- das Plugin würde NGINX_SITE_CONFIG_PATH
+# direkt editieren (eigene server-Blöcke/Zeilen einfügen) und diese
+# Änderungen bei der nächsten Änderung über diese Seite (die die Datei
+# komplett neu schreibt, siehe _render_nginx_config) wieder verlieren.
+# Mit der webroot-Methode bleibt _render_nginx_config die EINZIGE Quelle
+# für den Datei-Inhalt; certbot legt nur Dateien unter TESM_ACME_WEBROOT
+# ab und liest/schreibt sein Zertifikat unter /etc/letsencrypt/live/.
# ============================================================================
NGINX_REVERT_SECONDS = 45
_pending_nginx_revert = {}
+_DOMAIN_RE = re.compile(r"^(?=.{1,253}$)([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$")
+
+
+def _valid_server_name(name):
+ """"_" (nginx-Catch-all, kein bestimmter Hostname -- Standard für rein
+ interne Instanzen ohne eigene Domain) oder eine plausibel geformte
+ FQDN."""
+ return name == "_" or bool(_DOMAIN_RE.match(name))
+
+
+def _valid_port(value):
+ try:
+ n = int(value)
+ except (TypeError, ValueError):
+ return False
+ return 1 <= n <= 65535 and n != 5000
+
+
_NGINX_PROXY_LOCATIONS = """ location /ws/ {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
@@ -2560,33 +2613,80 @@ _NGINX_PROXY_LOCATIONS = """ location /ws/ {
"""
-def _render_nginx_config(ssl_enabled, hsts_enabled):
+def _render_nginx_config(server_name, http_port, https_port, ssl_enabled, hsts_enabled, cert_path, key_path):
"""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."""
+ sind bei aktivem SSL identisch, nur hinter einem HTTPS-Block mit
+ Zertifikat/HSTS statt direkt hinter dem HTTP-Port -- der HTTP-Port
+ leitet dann nur noch auf https weiter, damit ein alter Bookmark/Link
+ nicht plötzlich ins Leere läuft. Die ACME-Challenge-Location wird
+ IMMER mitgeschrieben (auch ohne aktives SSL) -- eine Let's-Encrypt-
+ (Erst-)Anforderung braucht sie schon VOR dem ersten SSL-Aktivieren,
+ eine Verlängerung später ebenso. ACME HTTP-01 verbindet sich immer auf
+ Port 80 (protokollbedingt, nicht konfigurierbar) -- weicht der
+ eingestellte HTTP-Port davon ab, wird zusätzlich ein minimaler, nur
+ dafür zuständiger Port-80-Block ergänzt, damit Let's Encrypt trotzdem
+ funktioniert."""
header = 'map $http_upgrade $connection_upgrade {\n default upgrade;\n "" close;\n}\n\n'
+ acme_location = (
+ " location /.well-known/acme-challenge/ {\n"
+ f" root {TESM_ACME_WEBROOT};\n"
+ " try_files $uri =404;\n"
+ " }\n\n"
+ )
+
+ extra_acme_block = ""
+ if str(http_port) != "80":
+ extra_acme_block = (
+ "server {\n listen 80;\n server_name _;\n\n"
+ + acme_location
+ + " location / { return 404; }\n}\n\n"
+ )
+
if not ssl_enabled:
- return header + "server {\n listen 80;\n server_name _;\n\n" + _NGINX_PROXY_LOCATIONS + "}\n"
+ return (
+ header + extra_acme_block
+ + f"server {{\n listen {http_port};\n server_name {server_name};\n\n"
+ + acme_location
+ + _NGINX_PROXY_LOCATIONS
+ + "}\n"
+ )
hsts_line = ""
if hsts_enabled:
hsts_line = ' add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;\n\n'
+ redirect_target = "https://$host$request_uri" if str(https_port) == "443" else f"https://$host:{https_port}$request_uri"
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"
+ header + extra_acme_block
+ + f"server {{\n listen {http_port};\n server_name {server_name};\n\n"
+ + acme_location
+ + f" location / {{\n return 301 {redirect_target};\n }}\n"
+ + "}\n\n"
+ + f"server {{\n listen {https_port} ssl;\n server_name {server_name};\n\n"
+ + f" ssl_certificate {cert_path};\n"
+ + f" ssl_certificate_key {key_path};\n\n"
+ hsts_line
+ _NGINX_PROXY_LOCATIONS
+ "}\n"
)
-def _apply_nginx_config(ssl_enabled, hsts_enabled):
+def _current_cert_paths():
+ """(cert_path, key_path) je nach aktiver Zertifikatsquelle (Setting
+ ssl_source: "upload" oder "letsencrypt"), oder (None, None) wenn
+ keins vorhanden ist."""
+ if get_setting("ssl_source", "upload") == "letsencrypt":
+ domain = get_setting("nginx_server_name", "_")
+ cert, key = _le_cert_path(domain), _le_key_path(domain)
+ if os.path.isfile(cert) and os.path.isfile(key):
+ return cert, key
+ return None, None
+ if os.path.isfile(TESM_SSL_CERT_PATH) and os.path.isfile(TESM_SSL_KEY_PATH):
+ return TESM_SSL_CERT_PATH, TESM_SSL_KEY_PATH
+ return None, None
+
+
+def _apply_nginx_config(server_name, http_port, https_port, 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
@@ -2600,7 +2700,13 @@ def _apply_nginx_config(ssl_enabled, hsts_enabled):
except OSError:
previous = None
- new_content = _render_nginx_config(ssl_enabled, hsts_enabled)
+ cert_path = key_path = None
+ if ssl_enabled:
+ cert_path, key_path = _current_cert_paths()
+ if not cert_path:
+ return False, "Kein Zertifikat verfügbar.", previous
+
+ new_content = _render_nginx_config(server_name, http_port, https_port, ssl_enabled, hsts_enabled, cert_path, key_path)
try:
with open(NGINX_SITE_CONFIG_PATH, "w", encoding="utf-8") as f:
f.write(new_content)
@@ -2626,8 +2732,8 @@ def _apply_nginx_config(ssl_enabled, hsts_enabled):
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."""
+ binnen NGINX_REVERT_SECONDS -> vorherige nginx-Konfiguration UND alle
+ zugehörigen Einstellungen (Domain, Ports, SSL, HSTS) wiederherstellen."""
entry = _pending_nginx_revert.get(token)
if not entry:
return
@@ -2637,9 +2743,9 @@ def _revert_nginx_config(token):
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")
+ for key, value in entry["prev_settings"].items():
+ set_setting(key, value)
+ log_action_system("settings.nginx_revert", "nginx", "automatisch nach Timeout zurückgerollt")
except Exception as e:
app.logger.error("nginx auto-revert failed: %s", e)
finally:
@@ -2693,30 +2799,67 @@ def _validate_cert_key_pair(cert_bytes, key_bytes):
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)):
+ """Für die NGINX-Seite: Status des aktuell aktiven Zertifikats (Upload
+ oder Let's Encrypt, je nach ssl_source) inkl. verbleibender
+ Gültigkeitsdauer."""
+ cert_path, _key_path = _current_cert_paths()
+ if not cert_path:
return None
try:
- with open(TESM_SSL_CERT_PATH, "rb") as f:
+ with open(cert_path, "rb") as f:
_, info = _parse_cert(f.read())
info["days_left"] = (info["not_after"] - datetime.utcnow()).days
+ info["source"] = get_setting("ssl_source", "upload")
return info
except (OSError, ValueError):
return None
-@app.route("/settings/ssl", methods=["GET", "POST"])
+def _request_letsencrypt_cert(domain, email=None):
+ """Fordert per certbot (webroot-Methode, siehe TESM_ACME_WEBROOT) ein
+ Zertifikat für die angegebene Domain an. Setzt voraus, dass die Domain
+ öffentlich auf diesen Host auflöst UND Port 80 aus dem Internet
+ erreichbar ist (ACME HTTP-01) -- beides kann diese App nicht prüfen,
+ ein Fehlschlag hier liegt i.d.R. an der Netzwerk-/DNS-Situation, nicht
+ an TESM selbst. Automatische Verlängerung übernimmt certbots eigener
+ systemd-Timer/Cron (Standard-Paketverhalten unter Debian/Ubuntu), der
+ Deploy-Hook (_write_certbot_deploy_hook) lädt nginx danach neu."""
+ args = [
+ "certbot", "certonly", "--webroot", "-w", TESM_ACME_WEBROOT,
+ "-d", domain, "--non-interactive", "--agree-tos", "--cert-name", domain,
+ ]
+ args += ["--email", email] if email else ["--register-unsafely-without-email"]
+ return _dhcp_run_privileged(args, timeout=120)
+
+
+@app.route("/settings/nginx/raw")
@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")
+def settings_nginx_raw():
+ """Komplette, unbearbeitete nginx-Konfiguration für das RAW-Popup --
+ gleiches Muster wie beim Live-Log (window.openRawLogModal)."""
+ if not current_user.has_permission("settings_nginx.view"):
+ return "Keine Berechtigung.", 403
+ try:
+ with open(NGINX_SITE_CONFIG_PATH, "r", encoding="utf-8", errors="replace") as f:
+ content = f.read()
+ except OSError as e:
+ content = f"Fehler beim Lesen der Konfiguration: {e}"
+ response = app.response_class(content, mimetype="text/plain")
+ response.headers["X-Log-Name"] = os.path.basename(NGINX_SITE_CONFIG_PATH)
+ return response
+
+
+@app.route("/settings/nginx", methods=["GET", "POST"])
+@login_required
+def settings_nginx():
+ if request.method == "GET" and not current_user.has_permission("settings_nginx.view"):
+ flash("Keine Berechtigung, die NGINX-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 not current_user.has_permission("settings_nginx.edit"):
+ flash("Keine Berechtigung, die NGINX-Einstellungen zu ändern.", "danger")
+ return redirect(url_for("settings_nginx"))
if "upload_cert" in request.form:
cert_file = request.files.get("cert_file")
@@ -2738,7 +2881,8 @@ def settings_ssl():
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}")
+ set_setting("ssl_source", "upload")
+ log_action("settings.nginx_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.",
@@ -2747,72 +2891,122 @@ def settings_ssl():
except OSError as e:
flash(f"Zertifikat konnte nicht gespeichert werden: {e}", "danger")
- elif "apply_ssl" in request.form:
+ elif "request_letsencrypt" in request.form:
+ domain = request.form.get("le_domain", "").strip()
+ email = request.form.get("le_email", "").strip() or None
+ if not domain or not _DOMAIN_RE.match(domain):
+ flash("Bitte eine gültige, öffentlich auflösbare Domain angeben.", "danger")
+ else:
+ ok, out = _request_letsencrypt_cert(domain, email)
+ if not ok:
+ flash(f"Let's-Encrypt-Anforderung fehlgeschlagen: {out.strip()[-800:]}", "danger")
+ else:
+ set_setting("ssl_source", "letsencrypt")
+ set_setting("nginx_server_name", domain)
+ log_action("settings.nginx_letsencrypt_request", domain, "Zertifikat erfolgreich ausgestellt")
+ flash(
+ f"Let's-Encrypt-Zertifikat für {domain} ausgestellt (automatische Verlängerung läuft über "
+ f"certbots eigenen Timer). SSL ist damit noch nicht aktiviert -- unten aktivieren.",
+ "success",
+ )
+
+ elif "apply_nginx" in request.form:
+ new_server_name = request.form.get("server_name", "_").strip() or "_"
+ new_http_port = request.form.get("http_port", "80").strip()
+ new_https_port = request.form.get("https_port", "443").strip()
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")
+
+ if not _valid_server_name(new_server_name):
+ flash("Ungültiger Domain-/Servername.", "danger")
+ elif not _valid_port(new_http_port) or not _valid_port(new_https_port):
+ flash("Port muss eine Zahl zwischen 1 und 65535 sein (5000 ist für die App selbst reserviert).", "danger")
+ elif new_http_port == new_https_port:
+ flash("HTTP- und HTTPS-Port müssen unterschiedlich sein.", "danger")
+ elif new_ssl and not _current_cert_paths()[0]:
+ flash("Kein Zertifikat verfügbar -- bitte zuerst hochladen oder per Let's Encrypt anfordern.", "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)
+ prev_settings = {
+ "nginx_server_name": get_setting("nginx_server_name", "_"),
+ "nginx_http_port": get_setting("nginx_http_port", "80"),
+ "nginx_https_port": get_setting("nginx_https_port", "443"),
+ "ssl_enabled": get_setting("ssl_enabled", "0"),
+ "hsts_enabled": get_setting("hsts_enabled", "0"),
+ }
+ ok, err, previous = _apply_nginx_config(new_server_name, new_http_port, new_https_port, new_ssl, new_hsts)
if not ok:
flash(f"nginx-Konfiguration konnte nicht angewendet werden, unverändert gelassen: {err}", "danger")
else:
+ set_setting("nginx_server_name", new_server_name)
+ set_setting("nginx_http_port", new_http_port)
+ set_setting("nginx_https_port", new_https_port)
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}")
+ log_action(
+ "settings.nginx_apply", new_server_name,
+ f"http={new_http_port} https={new_https_port} 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,
- }
+ _pending_nginx_revert[token] = {"timer": timer, "previous": previous, "prev_settings": prev_settings}
timer.start()
scheme = "https" if new_ssl else "http"
+ port = new_https_port if new_ssl else new_http_port
+ port_suffix = "" if port in ("80", "443") else f":{port}"
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.",
+ f"nginx-Konfiguration angewendet. Falls diese Seite jetzt noch über {scheme}{port_suffix} "
+ f"erreichbar ist, bitte unten bestätigen — sonst wird nach {NGINX_REVERT_SECONDS}s "
+ f"automatisch zurückgerollt.",
"success",
)
- elif "confirm_ssl" in request.form:
- token = request.form.get("confirm_ssl")
+ elif "confirm_nginx" in request.form:
+ token = request.form.get("confirm_nginx")
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")
+ log_action("settings.nginx_confirm", "nginx")
+ flash("nginx-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")
+ elif get_setting("ssl_source", "upload") == "letsencrypt":
+ domain = get_setting("nginx_server_name", "_")
+ ok, out = _dhcp_run_privileged(["certbot", "delete", "--cert-name", domain, "--non-interactive"], timeout=30)
+ if not ok:
+ flash(f"Let's-Encrypt-Zertifikat konnte nicht entfernt werden: {out}", "danger")
+ else:
+ log_action("settings.nginx_cert_remove", domain, "Let's Encrypt")
+ flash("Let's-Encrypt-Zertifikat entfernt.", "success")
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")
+ log_action("settings.nginx_cert_remove", "upload")
+ flash("Hochgeladenes Zertifikat entfernt.", "success")
- return redirect(url_for("settings_ssl"))
+ return redirect(url_for("settings_nginx"))
pending_token = next(iter(_pending_nginx_revert), None)
return render_template(
- "settings_ssl.html",
+ "settings_nginx.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,
+ server_name=get_setting("nginx_server_name", "_"),
+ http_port=get_setting("nginx_http_port", "80"),
+ https_port=get_setting("nginx_https_port", "443"),
+ pending_nginx_token=pending_token,
+ nginx_revert_seconds=NGINX_REVERT_SECONDS,
)
diff --git a/srv/tesm/templates/base.html b/srv/tesm/templates/base.html
index 4052437..01f74c1 100644
--- a/srv/tesm/templates/base.html
+++ b/srv/tesm/templates/base.html
@@ -21,6 +21,7 @@
"history": '
+ Neue nginx-Konfiguration wurde angewendet. Wenn diese Seite gerade noch lädt, funktioniert die Verbindung — + bitte bestätigen, bevor automatisch zurückgerollt wird (nach {{ nginx_revert_seconds }}s ohne Bestätigung). +
+ +Für Änderungen fehlt das Recht „NGINX ändern“.
+ {% endif %} +