feat(gui): security hardening, UI overhaul, light theme
- CSRF protection on all POST forms (session token) - ensure_schema() moved to module-level, removed from before_request - gunicorn now binds to 127.0.0.1 only, runs as unprivileged user wgadmin - nginx reverse proxy with HTTPS (Let's Encrypt, wg.4mont.ru) - HTTP → HTTPS redirect before Basic Auth prompt - Auth moved to nginx level (auth_basic), wg-peerctl called via sudo - ufw firewall: only 22/80/443/51820 open - fail2ban: SSH + nginx (5 attempts → 1h ban) - Add Enable/Disable toggle buttons in peer table - Add .conf file download route - Light theme: white background, blue accent, subtle shadows - Modern sidebar layout, styled badges, responsive forms Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+60
-27
@@ -2,13 +2,14 @@
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import secrets
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import qrcode
|
||||
from flask import Flask, redirect, render_template, request, url_for, flash, Response
|
||||
from flask import Flask, redirect, render_template, request, url_for, flash, Response, session, send_file
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.environ.get("APP_SECRET", "dev-secret")
|
||||
@@ -59,6 +60,10 @@ def ensure_schema():
|
||||
conn.commit()
|
||||
|
||||
|
||||
# Run once per worker process on import
|
||||
ensure_schema()
|
||||
|
||||
|
||||
def run(cmd):
|
||||
return subprocess.check_output(cmd, text=True).strip()
|
||||
|
||||
@@ -203,6 +208,15 @@ def _unauthorized():
|
||||
)
|
||||
|
||||
|
||||
def _get_csrf_token():
|
||||
if "csrf_token" not in session:
|
||||
session["csrf_token"] = secrets.token_hex(32)
|
||||
return session["csrf_token"]
|
||||
|
||||
|
||||
app.jinja_env.globals["csrf_token"] = _get_csrf_token
|
||||
|
||||
|
||||
@app.before_request
|
||||
def _auth():
|
||||
if request.path.startswith("/static/"):
|
||||
@@ -218,8 +232,13 @@ def _auth():
|
||||
|
||||
|
||||
@app.before_request
|
||||
def _schema():
|
||||
ensure_schema()
|
||||
def _csrf_check():
|
||||
if request.method != "POST":
|
||||
return None
|
||||
token = request.form.get("csrf_token")
|
||||
if not token or token != session.get("csrf_token"):
|
||||
return Response("CSRF token invalid", 403)
|
||||
return None
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@@ -253,6 +272,7 @@ def index():
|
||||
"rx": bytes_h(rt.get("rx_bytes", 0)),
|
||||
"tx": bytes_h(rt.get("tx_bytes", 0)),
|
||||
"status": "online" if is_online else "offline",
|
||||
"enabled": row.get("enabled", 1),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -292,6 +312,7 @@ def index():
|
||||
"rx": bytes_h(rt.get("rx_bytes", 0)),
|
||||
"tx": bytes_h(rt.get("tx_bytes", 0)),
|
||||
"status": "online" if is_online else "offline",
|
||||
"enabled": 1,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -321,16 +342,12 @@ def new_peer():
|
||||
client_priv, client_pub, client_psk = gen_keypair_psk()
|
||||
|
||||
cmd = [
|
||||
"/usr/local/sbin/wg-peerctl",
|
||||
"sudo", "/usr/local/sbin/wg-peerctl",
|
||||
"add",
|
||||
"--client-name",
|
||||
name,
|
||||
"--client-public-key",
|
||||
client_pub,
|
||||
"--client-preshared-key",
|
||||
client_psk,
|
||||
"--persistent-keepalive",
|
||||
"25",
|
||||
"--client-name", name,
|
||||
"--client-public-key", client_pub,
|
||||
"--client-preshared-key", client_psk,
|
||||
"--persistent-keepalive", "25",
|
||||
]
|
||||
if routes:
|
||||
cmd += ["--client-routes", routes]
|
||||
@@ -411,6 +428,27 @@ def peer_view(peer_id: int):
|
||||
)
|
||||
|
||||
|
||||
@app.route("/peers/<int:peer_id>/download")
|
||||
def peer_download(peer_id: int):
|
||||
with db_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM peers WHERE id = ?", (peer_id,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
flash("Клиент не найден", "error")
|
||||
return redirect(url_for("index"))
|
||||
item = dict(row)
|
||||
|
||||
conf = item.get("client_conf") or ""
|
||||
if not conf:
|
||||
flash("Для этого клиента не найден сохраненный конфиг", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
name = item.get("name", "peer")
|
||||
buf = io.BytesIO(conf.encode("utf-8"))
|
||||
return send_file(buf, as_attachment=True, download_name=f"{name}.conf", mimetype="text/plain")
|
||||
|
||||
|
||||
@app.post("/peers/<int:peer_id>/disable")
|
||||
def peer_disable(peer_id: int):
|
||||
with db_conn() as conn:
|
||||
@@ -428,7 +466,7 @@ def peer_disable(peer_id: int):
|
||||
return redirect(url_for("index"))
|
||||
|
||||
try:
|
||||
run(["/usr/local/sbin/wg-peerctl", "remove", "--client-public-key", pk])
|
||||
run(["sudo", "/usr/local/sbin/wg-peerctl", "remove", "--client-public-key", pk])
|
||||
except subprocess.CalledProcessError as e:
|
||||
flash(f"Не удалось отключить peer: {e}", "error")
|
||||
return redirect(url_for("index"))
|
||||
@@ -462,18 +500,13 @@ def peer_enable(peer_id: int):
|
||||
return redirect(url_for("index"))
|
||||
|
||||
cmd = [
|
||||
"/usr/local/sbin/wg-peerctl",
|
||||
"sudo", "/usr/local/sbin/wg-peerctl",
|
||||
"add",
|
||||
"--client-name",
|
||||
name,
|
||||
"--client-public-key",
|
||||
pk,
|
||||
"--client-address",
|
||||
addr,
|
||||
"--client-preshared-key",
|
||||
psk,
|
||||
"--persistent-keepalive",
|
||||
"25",
|
||||
"--client-name", name,
|
||||
"--client-public-key", pk,
|
||||
"--client-address", addr,
|
||||
"--client-preshared-key", psk,
|
||||
"--persistent-keepalive", "25",
|
||||
]
|
||||
if routes:
|
||||
cmd += ["--client-routes", routes]
|
||||
@@ -506,7 +539,7 @@ def peer_delete(peer_id: int):
|
||||
pk = item.get("public_key", "")
|
||||
if pk:
|
||||
try:
|
||||
run(["/usr/local/sbin/wg-peerctl", "remove", "--client-public-key", pk])
|
||||
run(["sudo", "/usr/local/sbin/wg-peerctl", "remove", "--client-public-key", pk])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -526,7 +559,7 @@ def peer_delete_by_key():
|
||||
return redirect(url_for("index"))
|
||||
|
||||
try:
|
||||
run(["/usr/local/sbin/wg-peerctl", "remove", "--client-public-key", pk])
|
||||
run(["sudo", "/usr/local/sbin/wg-peerctl", "remove", "--client-public-key", pk])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -547,7 +580,7 @@ def scripts():
|
||||
"apply_wg": "wg syncconf wg0 <(wg-quick strip /etc/wireguard/wg0.conf)",
|
||||
}
|
||||
paths = [
|
||||
"/usr/local/sbin/wg-peerctl",
|
||||
"sudo", "/usr/local/sbin/wg-peerctl",
|
||||
"/etc/wireguard/wg0.conf",
|
||||
"/etc/wireguard/wg-meta.env",
|
||||
"/var/log/wireguard-server-install.log",
|
||||
|
||||
Reference in New Issue
Block a user