Add security improvements: refresh tokens, passkeys, CSP, audit logging, TOTP encryption
Deploy Dashboard / deploy (push) Successful in 1m26s

- Shorten access token to 30min with 7-day refresh token flow
- Add WebAuthn passkey registration and login with TOTP fallback
- Add Content-Security-Policy header
- Add audit logging middleware to /volume1/docker/nas-dashboard/audit.log
- Block /volume1/docker/ in files endpoint
- Encrypt TOTP secret at rest with Fernet (derived from SECRET_KEY)
- New deps: py_webauthn, cryptography
This commit is contained in:
Gan, Jimmy
2026-02-22 10:54:23 +08:00
parent 8392b49bb7
commit 9683cd3165
10 changed files with 484 additions and 46 deletions
+36 -1
View File
@@ -10,7 +10,10 @@ import auth as auth_module
import asyncio
import httpx
from config import TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, CORS_ORIGINS
import logging
import time
import jwt
from config import TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, CORS_ORIGINS, SECRET_KEY, ALGORITHM, VOLUME_ROOT
limiter = Limiter(key_func=get_remote_address)
app = FastAPI(title="NAS Dashboard")
@@ -35,6 +38,38 @@ async def security_headers(request: Request, call_next):
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' wss://nas.jimmygan.com; frame-ancestors 'none'"
return response
# Audit logger
import os
_audit_log_path = os.path.join(VOLUME_ROOT, "docker/nas-dashboard/audit.log")
os.makedirs(os.path.dirname(_audit_log_path), exist_ok=True)
_audit_logger = logging.getLogger("audit")
_audit_logger.setLevel(logging.INFO)
_audit_handler = logging.FileHandler(_audit_log_path)
_audit_handler.setFormatter(logging.Formatter("%(message)s"))
_audit_logger.addHandler(_audit_handler)
_audit_logger.propagate = False
@app.middleware("http")
async def audit_log(request: Request, call_next):
start = time.time()
response = await call_next(request)
duration = int((time.time() - start) * 1000)
user = "-"
try:
auth_header = request.headers.get("authorization", "")
if auth_header.startswith("Bearer "):
payload = jwt.decode(auth_header[7:], SECRET_KEY, algorithms=[ALGORITHM])
user = payload.get("sub", "-")
except Exception:
pass
_audit_logger.info(
"%s %s %s %s %s %d %dms",
time.strftime("%Y-%m-%dT%H:%M:%S"), request.client.host, user,
request.method, request.url.path, response.status_code, duration,
)
return response
async def monitor_containers():