Sicherheitsstatus

Dieser Commit ist enthalten in:
2025-06-07 21:01:45 +02:00
Ursprung 25b8a9a33d
Commit df5e0e0365
7 geänderte Dateien mit 771 neuen und 11 gelöschten Zeilen

Datei anzeigen

@@ -40,7 +40,8 @@
"Bash(grep:*)",
"Bash(docker exec:*)",
"Bash(rm:*)",
"Bash(mv:*)"
"Bash(mv:*)",
"Bash(docker-compose restart:*)"
],
"deny": []
}

Datei anzeigen

@@ -568,3 +568,123 @@ Die Session-Daten werden erst gefüllt, wenn der License Server API implementier
- ✅ Saubere SSL-Konfiguration ohne Mixed Content
- ✅ Verbesserte Sicherheits-Header implementiert
- ✅ Admin Panel zeigt jetzt grünes Schloss-Symbol
### 2025-06-07 - Sicherheitslücke geschlossen: License Server Port
- Direkter Zugriff auf License Server Port 8443 entfernt
- Sicherheitsanalyse der exponierten Ports durchgeführt
**Identifiziertes Problem:**
- License Server war direkt auf Port 8443 von außen erreichbar
- Umging damit die Nginx-Sicherheitsschicht und Security Headers
- Besonders kritisch, da nur Platzhalter ohne echte Sicherheit
**Durchgeführte Änderung:**
- Port-Mapping für License Server in docker-compose.yaml entfernt
- Service ist jetzt nur noch über Nginx Reverse Proxy erreichbar
- Gleiche Sicherheitskonfiguration wie Admin Panel
**Aktuelle Port-Exposition:**
- ✅ Nginx: Port 80/443 (benötigt für externen Zugriff)
- ✅ PostgreSQL: Keine Ports exponiert (gut)
- ✅ Admin Panel: Nur über Nginx erreichbar
- ✅ License Server: Nur über Nginx erreichbar (vorher direkt auf 8443)
**Weitere identifizierte Sicherheitsthemen:**
1. Credentials im Klartext in .env Datei
2. SSL-Zertifikate im Repository gespeichert
3. License Server noch nicht implementiert
**Empfehlung:** Docker-Container neu starten für Änderungsübernahme
### 2025-06-07 - License Server Port 8443 wieder aktiviert
- Port 8443 für direkten Zugriff auf License Server wieder geöffnet
- Notwendig für Client-Software Lizenzprüfung
**Begründung:**
- Client-Software benötigt direkten Zugriff für Lizenzprüfung
- Umgehung von möglichen Firewall-Blockaden auf Port 443
- Weniger Latenz ohne Nginx-Proxy
- Flexibilität für verschiedene Client-Implementierungen
**Konfiguration:**
- License Server erreichbar über:
- Direkt: Port 8443 (für Client-Software)
- Via Nginx: https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com (für Browser/Tests)
**Sicherheitshinweis:**
- Port 8443 ist wieder direkt exponiert
- License Server muss vor Produktivbetrieb implementiert werden mit:
- Eigener SSL-Konfiguration
- API-Key Authentifizierung
- Rate Limiting
- Input-Validierung
**Status:**
- Port-Mapping in docker-compose.yaml wiederhergestellt
- Änderung erfordert Docker-Neustart
### 2025-06-07 - Rate-Limiting und Brute-Force-Schutz implementiert
- Umfassender Schutz vor Login-Angriffen mit IP-Sperre
- Dashboard-Integration für Sicherheitsüberwachung
**Implementierte Features:**
1. **Rate-Limiting System:**
- 5 Login-Versuche erlaubt, danach 24h IP-Sperre
- Progressive Fehlermeldungen (zufällig aus 5 lustigen Varianten)
- CAPTCHA nach 2 Fehlversuchen (Google reCAPTCHA v2 vorbereitet)
- E-Mail-Benachrichtigung bei Sperrung (vorbereitet, deaktiviert für PoC)
2. **Timing-Attack Schutz:**
- Mindestens 1 Sekunde Antwortzeit bei allen Login-Versuchen
- Gleiche Antwortzeit bei richtigem/falschem Username
- Verhindert Username-Enumeration
3. **Lustige Fehlermeldungen (zufällig):**
- "NOPE!"
- "ACCESS DENIED, TRY HARDER"
- "WRONG! 🚫"
- "COMPUTER SAYS NO"
- "YOU FAILED"
4. **Dashboard-Sicherheitswidget:**
- Sicherheitslevel-Anzeige (NORMAL/ERHÖHT/KRITISCH)
- Anzahl gesperrter IPs
- Fehlversuche heute
- Letzte 5 Sicherheitsereignisse mit Details
5. **IP-Verwaltung:**
- Übersicht aller gesperrten IPs
- Manuelles Entsperren möglich
- Login-Versuche zurücksetzen
- Detaillierte Informationen pro IP
6. **Audit-Log Erweiterungen:**
- LOGIN_SUCCESS - Erfolgreiche Anmeldung
- LOGIN_FAILED - Fehlgeschlagener Versuch
- LOGIN_BLOCKED - IP wurde gesperrt
- UNBLOCK_IP - IP manuell entsperrt
- CLEAR_ATTEMPTS - Versuche zurückgesetzt
**Neue/Geänderte Dateien:**
- v2_adminpanel/init.sql (login_attempts Tabelle)
- v2_adminpanel/app.py (Rate-Limiting Logik, neue Routen)
- v2_adminpanel/templates/login.html (Fehlermeldungs-Styling, CAPTCHA)
- v2_adminpanel/templates/dashboard.html (Sicherheitswidget)
- v2_adminpanel/templates/blocked_ips.html (neu - IP-Verwaltung)
**Technische Details:**
- IP-Ermittlung berücksichtigt Proxy-Header (X-Forwarded-For)
- Fehlermeldungen mit Animation (shake-effect)
- Farbcodierung: Rot für Fehler, Lila für Sperre, Orange für CAPTCHA
- Automatische Bereinigung alter Einträge möglich
**Sicherheitsverbesserungen:**
- Schutz vor Brute-Force-Angriffen
- Timing-Attack-Schutz implementiert
- IP-basierte Sperrung für 24 Stunden
- Audit-Trail für alle Sicherheitsereignisse
**Hinweis für Produktion:**
- CAPTCHA-Keys müssen in .env konfiguriert werden
- E-Mail-Server für Benachrichtigungen einrichten
- Rate-Limits können über Konstanten angepasst werden

Datei anzeigen

@@ -6,7 +6,7 @@ from flask_session import Session
from functools import wraps
from dotenv import load_dotenv
import pandas as pd
from datetime import datetime
from datetime import datetime, timedelta
import io
import subprocess
import gzip
@@ -15,6 +15,8 @@ from pathlib import Path
import time
from apscheduler.schedulers.background import BackgroundScheduler
import logging
import random
import hashlib
load_dotenv()
@@ -29,6 +31,19 @@ Session(app)
BACKUP_DIR = Path("/app/backups")
BACKUP_DIR.mkdir(exist_ok=True)
# Rate-Limiting Konfiguration
FAIL_MESSAGES = [
"NOPE!",
"ACCESS DENIED, TRY HARDER",
"WRONG! 🚫",
"COMPUTER SAYS NO",
"YOU FAILED"
]
MAX_LOGIN_ATTEMPTS = 5
BLOCK_DURATION_HOURS = 24
CAPTCHA_AFTER_ATTEMPTS = 2
# Scheduler für automatische Backups
scheduler = BackgroundScheduler()
scheduler.start()
@@ -321,11 +336,183 @@ scheduler.add_job(
replace_existing=True
)
# Rate-Limiting Funktionen
def get_client_ip():
"""Ermittelt die echte IP-Adresse des Clients"""
if request.environ.get('HTTP_X_FORWARDED_FOR'):
return request.environ['HTTP_X_FORWARDED_FOR'].split(',')[0]
elif request.environ.get('HTTP_X_REAL_IP'):
return request.environ.get('HTTP_X_REAL_IP')
else:
return request.environ.get('REMOTE_ADDR')
def check_ip_blocked(ip_address):
"""Prüft ob eine IP-Adresse gesperrt ist"""
conn = get_connection()
cur = conn.cursor()
cur.execute("""
SELECT blocked_until FROM login_attempts
WHERE ip_address = %s AND blocked_until IS NOT NULL
""", (ip_address,))
result = cur.fetchone()
cur.close()
conn.close()
if result and result[0]:
if result[0] > datetime.now():
return True, result[0]
return False, None
def record_failed_attempt(ip_address, username):
"""Zeichnet einen fehlgeschlagenen Login-Versuch auf"""
conn = get_connection()
cur = conn.cursor()
# Random Fehlermeldung
error_message = random.choice(FAIL_MESSAGES)
try:
# Prüfen ob IP bereits existiert
cur.execute("""
SELECT attempt_count FROM login_attempts
WHERE ip_address = %s
""", (ip_address,))
result = cur.fetchone()
if result:
# Update bestehenden Eintrag
new_count = result[0] + 1
blocked_until = None
if new_count >= MAX_LOGIN_ATTEMPTS:
blocked_until = datetime.now() + timedelta(hours=BLOCK_DURATION_HOURS)
# E-Mail-Benachrichtigung (wenn aktiviert)
if os.getenv("EMAIL_ENABLED", "false").lower() == "true":
send_security_alert_email(ip_address, username, new_count)
cur.execute("""
UPDATE login_attempts
SET attempt_count = %s,
last_attempt = CURRENT_TIMESTAMP,
blocked_until = %s,
last_username_tried = %s,
last_error_message = %s
WHERE ip_address = %s
""", (new_count, blocked_until, username, error_message, ip_address))
else:
# Neuen Eintrag erstellen
cur.execute("""
INSERT INTO login_attempts
(ip_address, attempt_count, last_username_tried, last_error_message)
VALUES (%s, 1, %s, %s)
""", (ip_address, username, error_message))
conn.commit()
# Audit-Log
log_audit('LOGIN_FAILED', 'user',
additional_info=f"IP: {ip_address}, User: {username}, Message: {error_message}")
except Exception as e:
print(f"Rate limiting error: {e}")
conn.rollback()
finally:
cur.close()
conn.close()
return error_message
def reset_login_attempts(ip_address):
"""Setzt die Login-Versuche für eine IP zurück"""
conn = get_connection()
cur = conn.cursor()
try:
cur.execute("""
DELETE FROM login_attempts
WHERE ip_address = %s
""", (ip_address,))
conn.commit()
except Exception as e:
print(f"Reset attempts error: {e}")
conn.rollback()
finally:
cur.close()
conn.close()
def get_login_attempts(ip_address):
"""Gibt die Anzahl der Login-Versuche für eine IP zurück"""
conn = get_connection()
cur = conn.cursor()
cur.execute("""
SELECT attempt_count FROM login_attempts
WHERE ip_address = %s
""", (ip_address,))
result = cur.fetchone()
cur.close()
conn.close()
return result[0] if result else 0
def send_security_alert_email(ip_address, username, attempt_count):
"""Sendet eine Sicherheitswarnung per E-Mail"""
subject = f"⚠️ SICHERHEITSWARNUNG: {attempt_count} fehlgeschlagene Login-Versuche"
body = f"""
WARNUNG: Mehrere fehlgeschlagene Login-Versuche erkannt!
IP-Adresse: {ip_address}
Versuchter Benutzername: {username}
Anzahl Versuche: {attempt_count}
Zeit: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
Die IP-Adresse wurde für 24 Stunden gesperrt.
Dies ist eine automatische Nachricht vom v2-Docker Admin Panel.
"""
# TODO: E-Mail-Versand implementieren wenn SMTP konfiguriert
logging.warning(f"Sicherheitswarnung: {attempt_count} fehlgeschlagene Versuche von IP {ip_address}")
print(f"E-Mail würde gesendet: {subject}")
@app.route("/login", methods=["GET", "POST"])
def login():
# Timing-Attack Schutz - Start Zeit merken
start_time = time.time()
# IP-Adresse ermitteln
ip_address = get_client_ip()
# Prüfen ob IP gesperrt ist
is_blocked, blocked_until = check_ip_blocked(ip_address)
if is_blocked:
time_remaining = (blocked_until - datetime.now()).total_seconds() / 3600
error_msg = f"IP GESPERRT! Noch {time_remaining:.1f} Stunden warten."
return render_template("login.html", error=error_msg, error_type="blocked")
# Anzahl bisheriger Versuche
attempt_count = get_login_attempts(ip_address)
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
captcha_response = request.form.get("g-recaptcha-response")
# CAPTCHA-Prüfung wenn nötig
if attempt_count >= CAPTCHA_AFTER_ATTEMPTS:
if not captcha_response:
# Timing-Attack Schutz
elapsed = time.time() - start_time
if elapsed < 1.0:
time.sleep(1.0 - elapsed)
return render_template("login.html",
error="CAPTCHA ERFORDERLICH!",
show_captcha=True,
error_type="captcha")
# Check gegen beide Admin-Accounts aus .env
admin1_user = os.getenv("ADMIN1_USERNAME")
@@ -333,16 +520,46 @@ def login():
admin2_user = os.getenv("ADMIN2_USERNAME")
admin2_pass = os.getenv("ADMIN2_PASSWORD")
# Login-Prüfung
login_success = False
if ((username == admin1_user and password == admin1_pass) or
(username == admin2_user and password == admin2_pass)):
login_success = True
# Timing-Attack Schutz - Mindestens 1 Sekunde warten
elapsed = time.time() - start_time
if elapsed < 1.0:
time.sleep(1.0 - elapsed)
if login_success:
# Erfolgreicher Login
session['logged_in'] = True
session['username'] = username
log_audit('LOGIN', 'user', additional_info=f"Erfolgreiche Anmeldung")
reset_login_attempts(ip_address)
log_audit('LOGIN_SUCCESS', 'user',
additional_info=f"Erfolgreiche Anmeldung von IP: {ip_address}")
return redirect(url_for('dashboard'))
else:
return render_template("login.html", error="Ungültige Anmeldedaten")
# Fehlgeschlagener Login
error_message = record_failed_attempt(ip_address, username)
new_attempt_count = get_login_attempts(ip_address)
return render_template("login.html")
# Prüfen ob jetzt gesperrt
is_now_blocked, _ = check_ip_blocked(ip_address)
if is_now_blocked:
log_audit('LOGIN_BLOCKED', 'security',
additional_info=f"IP {ip_address} wurde nach {MAX_LOGIN_ATTEMPTS} Versuchen gesperrt")
return render_template("login.html",
error=error_message,
show_captcha=(new_attempt_count >= CAPTCHA_AFTER_ATTEMPTS),
error_type="failed",
attempts_left=max(0, MAX_LOGIN_ATTEMPTS - new_attempt_count))
# GET Request
return render_template("login.html",
show_captcha=(attempt_count >= CAPTCHA_AFTER_ATTEMPTS),
attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count))
@app.route("/logout")
def logout():
@@ -440,6 +657,56 @@ def dashboard():
""")
last_backup_info = cur.fetchone()
# Sicherheitsstatistiken
# Gesperrte IPs
cur.execute("""
SELECT COUNT(*) FROM login_attempts
WHERE blocked_until IS NOT NULL AND blocked_until > CURRENT_TIMESTAMP
""")
blocked_ips_count = cur.fetchone()[0]
# Fehlversuche heute
cur.execute("""
SELECT COALESCE(SUM(attempt_count), 0) FROM login_attempts
WHERE last_attempt::date = CURRENT_DATE
""")
failed_attempts_today = cur.fetchone()[0]
# Letzte 5 Sicherheitsereignisse
cur.execute("""
SELECT
la.ip_address,
la.attempt_count,
la.last_attempt,
la.blocked_until,
la.last_username_tried,
la.last_error_message
FROM login_attempts la
ORDER BY la.last_attempt DESC
LIMIT 5
""")
recent_security_events = []
for event in cur.fetchall():
recent_security_events.append({
'ip_address': event[0],
'attempt_count': event[1],
'last_attempt': event[2].strftime('%d.%m %H:%M'),
'blocked_until': event[3].strftime('%d.%m %H:%M') if event[3] and event[3] > datetime.now() else None,
'username_tried': event[4],
'error_message': event[5]
})
# Sicherheitslevel berechnen
if blocked_ips_count > 5 or failed_attempts_today > 50:
security_level = 'danger'
security_level_text = 'KRITISCH'
elif blocked_ips_count > 2 or failed_attempts_today > 20:
security_level = 'warning'
security_level_text = 'ERHÖHT'
else:
security_level = 'success'
security_level_text = 'NORMAL'
cur.close()
conn.close()
@@ -454,7 +721,13 @@ def dashboard():
'recent_licenses': recent_licenses,
'expiring_licenses': expiring_licenses,
'active_sessions': active_sessions_count,
'last_backup': last_backup_info
'last_backup': last_backup_info,
# Sicherheitsstatistiken
'blocked_ips_count': blocked_ips_count,
'failed_attempts_today': failed_attempts_today,
'recent_security_events': recent_security_events,
'security_level': security_level,
'security_level_text': security_level_text
}
return render_template("dashboard.html", stats=stats, username=session.get('username'))
@@ -1274,5 +1547,87 @@ def download_backup(backup_id):
return send_file(filepath, as_attachment=True, download_name=filename)
@app.route("/security/blocked-ips")
@login_required
def blocked_ips():
"""Zeigt alle gesperrten IPs an"""
conn = get_connection()
cur = conn.cursor()
cur.execute("""
SELECT
ip_address,
attempt_count,
first_attempt,
last_attempt,
blocked_until,
last_username_tried,
last_error_message
FROM login_attempts
WHERE blocked_until IS NOT NULL
ORDER BY blocked_until DESC
""")
blocked_ips_list = []
for ip in cur.fetchall():
blocked_ips_list.append({
'ip_address': ip[0],
'attempt_count': ip[1],
'first_attempt': ip[2].strftime('%d.%m.%Y %H:%M'),
'last_attempt': ip[3].strftime('%d.%m.%Y %H:%M'),
'blocked_until': ip[4].strftime('%d.%m.%Y %H:%M'),
'is_active': ip[4] > datetime.now(),
'last_username': ip[5],
'last_error': ip[6]
})
cur.close()
conn.close()
return render_template("blocked_ips.html",
blocked_ips=blocked_ips_list,
username=session.get('username'))
@app.route("/security/unblock-ip", methods=["POST"])
@login_required
def unblock_ip():
"""Entsperrt eine IP-Adresse"""
ip_address = request.form.get('ip_address')
if ip_address:
conn = get_connection()
cur = conn.cursor()
cur.execute("""
UPDATE login_attempts
SET blocked_until = NULL
WHERE ip_address = %s
""", (ip_address,))
conn.commit()
cur.close()
conn.close()
# Audit-Log
log_audit('UNBLOCK_IP', 'security',
additional_info=f"IP {ip_address} manuell entsperrt")
return redirect(url_for('blocked_ips'))
@app.route("/security/clear-attempts", methods=["POST"])
@login_required
def clear_attempts():
"""Löscht alle Login-Versuche für eine IP"""
ip_address = request.form.get('ip_address')
if ip_address:
reset_login_attempts(ip_address)
# Audit-Log
log_audit('CLEAR_ATTEMPTS', 'security',
additional_info=f"Login-Versuche für IP {ip_address} zurückgesetzt")
return redirect(url_for('blocked_ips'))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)

Datei anzeigen

@@ -70,3 +70,18 @@ CREATE TABLE IF NOT EXISTS backup_history (
-- Index für bessere Performance
CREATE INDEX idx_backup_history_created_at ON backup_history(created_at DESC);
CREATE INDEX idx_backup_history_status ON backup_history(status);
-- Login-Attempts-Tabelle für Rate-Limiting
CREATE TABLE IF NOT EXISTS login_attempts (
ip_address VARCHAR(45) PRIMARY KEY,
attempt_count INTEGER DEFAULT 0,
first_attempt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_attempt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
blocked_until TIMESTAMP NULL,
last_username_tried TEXT,
last_error_message TEXT
);
-- Index für schnelle Abfragen
CREATE INDEX idx_login_attempts_blocked_until ON login_attempts(blocked_until);
CREATE INDEX idx_login_attempts_last_attempt ON login_attempts(last_attempt DESC);

Datei anzeigen

@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Gesperrte IPs - Admin Panel</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<nav class="navbar navbar-dark bg-dark">
<div class="container">
<span class="navbar-brand">🎛️ Lizenzverwaltung</span>
<div class="d-flex align-items-center">
<span class="text-white me-3">Angemeldet als: {{ username }}</span>
<a href="/logout" class="btn btn-outline-light btn-sm">Abmelden</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>🔒 Gesperrte IPs</h1>
<div>
<a href="/" class="btn btn-secondary">← Dashboard</a>
<a href="/audit" class="btn btn-secondary">📋 Audit-Log</a>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">IP-Sperrverwaltung</h5>
</div>
<div class="card-body">
{% if blocked_ips %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>IP-Adresse</th>
<th>Versuche</th>
<th>Erster Versuch</th>
<th>Letzter Versuch</th>
<th>Gesperrt bis</th>
<th>Letzter User</th>
<th>Letzte Meldung</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for ip in blocked_ips %}
<tr class="{% if ip.is_active %}table-danger{% else %}table-secondary{% endif %}">
<td><code>{{ ip.ip_address }}</code></td>
<td><span class="badge bg-danger">{{ ip.attempt_count }}</span></td>
<td>{{ ip.first_attempt }}</td>
<td>{{ ip.last_attempt }}</td>
<td>{{ ip.blocked_until }}</td>
<td>{{ ip.last_username or '-' }}</td>
<td><strong>{{ ip.last_error or '-' }}</strong></td>
<td>
{% if ip.is_active %}
<span class="badge bg-danger">GESPERRT</span>
{% else %}
<span class="badge bg-secondary">ABGELAUFEN</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
{% if ip.is_active %}
<form method="post" action="/security/unblock-ip" class="d-inline">
<input type="hidden" name="ip_address" value="{{ ip.ip_address }}">
<button type="submit" class="btn btn-success"
onclick="return confirm('IP {{ ip.ip_address }} wirklich entsperren?')">
🔓 Entsperren
</button>
</form>
{% endif %}
<form method="post" action="/security/clear-attempts" class="d-inline ms-1">
<input type="hidden" name="ip_address" value="{{ ip.ip_address }}">
<button type="submit" class="btn btn-warning"
onclick="return confirm('Alle Versuche für IP {{ ip.ip_address }} zurücksetzen?')">
🗑️ Reset
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<strong>Keine gesperrten IPs vorhanden.</strong>
Das System läuft ohne Sicherheitsvorfälle.
</div>
{% endif %}
</div>
</div>
<div class="card mt-4">
<div class="card-body">
<h5 class="card-title"> Informationen</h5>
<ul class="mb-0">
<li>IPs werden nach <strong>{{ 5 }} fehlgeschlagenen Login-Versuchen</strong> für <strong>24 Stunden</strong> gesperrt.</li>
<li>Nach <strong>2 Versuchen</strong> wird ein CAPTCHA angezeigt.</li>
<li>Bei <strong>5 Versuchen</strong> wird eine E-Mail-Benachrichtigung gesendet (wenn aktiviert).</li>
<li>Gesperrte IPs können manuell entsperrt werden.</li>
<li>Die Fehlermeldungen werden zufällig ausgewählt für zusätzliche Verwirrung.</li>
</ul>
</div>
</div>
</div>
</body>
</html>

Datei anzeigen

@@ -118,10 +118,10 @@
</div>
</div>
<!-- Backup-Status -->
<!-- Backup-Status und Sicherheit nebeneinander -->
<div class="row g-3 mb-4">
<div class="col-12">
<div class="card">
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">💾 Backup-Status</h5>
{% if stats.last_backup %}
@@ -152,7 +152,76 @@
</div>
</div>
</div>
<!-- Sicherheitsstatus -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">🔒 Sicherheitsstatus</h5>
<div class="d-flex justify-content-between align-items-center mb-3">
<span>Sicherheitslevel:</span>
<span class="badge bg-{{ stats.security_level }} fs-6">{{ stats.security_level_text }}</span>
</div>
<div class="row text-center">
<div class="col-6">
<h4 class="text-danger mb-0">{{ stats.blocked_ips_count }}</h4>
<small class="text-muted">Gesperrte IPs</small>
</div>
<div class="col-6">
<h4 class="text-warning mb-0">{{ stats.failed_attempts_today }}</h4>
<small class="text-muted">Fehlversuche heute</small>
</div>
</div>
<a href="/security/blocked-ips" class="btn btn-sm btn-outline-danger mt-3">IP-Verwaltung →</a>
</div>
</div>
</div>
</div>
<!-- Sicherheitsereignisse -->
{% if stats.recent_security_events %}
<div class="row g-3 mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-dark text-white">
<h6 class="mb-0">🚨 Letzte Sicherheitsereignisse</h6>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead>
<tr>
<th>Zeit</th>
<th>IP-Adresse</th>
<th>Versuche</th>
<th>Fehlermeldung</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for event in stats.recent_security_events %}
<tr>
<td>{{ event.last_attempt }}</td>
<td><code>{{ event.ip_address }}</code></td>
<td><span class="badge bg-secondary">{{ event.attempt_count }}</span></td>
<td><strong class="text-danger">{{ event.error_message }}</strong></td>
<td>
{% if event.blocked_until %}
<span class="badge bg-danger">Gesperrt bis {{ event.blocked_until }}</span>
{% else %}
<span class="badge bg-warning">Aktiv</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<div class="row g-3">
<!-- Bald ablaufende Lizenzen -->

Datei anzeigen

@@ -5,6 +5,70 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login - Lizenzverwaltung</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.error-failed {
background-color: #dc3545 !important;
color: white !important;
font-weight: bold !important;
font-size: 1.5rem !important;
text-align: center !important;
padding: 1rem !important;
border-radius: 0.5rem !important;
text-transform: uppercase !important;
animation: shake 0.5s;
box-shadow: 0 0 20px rgba(220, 53, 69, 0.5);
}
.error-blocked {
background-color: #6f42c1 !important;
color: white !important;
font-weight: bold !important;
font-size: 1.2rem !important;
text-align: center !important;
padding: 1rem !important;
border-radius: 0.5rem !important;
animation: pulse 2s infinite;
}
.error-captcha {
background-color: #fd7e14 !important;
color: white !important;
font-weight: bold !important;
font-size: 1.2rem !important;
text-align: center !important;
padding: 1rem !important;
border-radius: 0.5rem !important;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); }
20%, 40%, 60%, 80% { transform: translateX(10px); }
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
.attempts-warning {
background-color: #ffc107;
color: #000;
padding: 0.5rem;
border-radius: 0.25rem;
text-align: center;
margin-bottom: 1rem;
font-weight: bold;
}
.security-info {
font-size: 0.875rem;
color: #6c757d;
text-align: center;
margin-top: 1rem;
}
</style>
</head>
<body class="bg-light">
<div class="container">
@@ -15,11 +79,17 @@
<h2 class="text-center mb-4">🔐 Admin Login</h2>
{% if error %}
<div class="alert alert-danger" role="alert">
<div class="error-{{ error_type|default('failed') }}">
{{ error }}
</div>
{% endif %}
{% if attempts_left is defined and attempts_left > 0 and attempts_left < 5 %}
<div class="attempts-warning">
⚠️ Noch {{ attempts_left }} Versuch(e) bis zur IP-Sperre!
</div>
{% endif %}
<form method="post">
<div class="mb-3">
<label for="username" class="form-label">Benutzername</label>
@@ -29,12 +99,27 @@
<label for="password" class="form-label">Passwort</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
{% if show_captcha %}
<div class="mb-3">
<div class="g-recaptcha" data-sitekey="{{ recaptcha_site_key|default('6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI') }}"></div>
</div>
{% endif %}
<button type="submit" class="btn btn-primary w-100">Anmelden</button>
</form>
<div class="security-info">
🛡️ Geschützt durch Rate-Limiting und IP-Sperre
</div>
</div>
</div>
</div>
</div>
</div>
{% if show_captcha %}
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
{% endif %}
</body>
</html>