fix(security): authenticate VNC/terminal WebSockets and harden SSH/config
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 46s
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 46s
Critical security and robustness fixes from a code audit: - WS terminal/VNC now require a JWT (?token=), validate the session, vms.console permission and VM ownership before bridging to Proxmox (previously the LXC terminal gave unauthenticated root shell access). Frontend ConsolePage passes the token in both WS URLs. - Fix SSH fallback for `pvecm delnode`: SSHClient now supports password auth (it previously crashed with TypeError); ProxmoxClient stores the password for SSH fallbacks and the host port suffix is stripped. - Add production startup warnings for default ADMIN_PASSWORD and wildcard CORS_ORIGINS. - dashboard: use tz-aware utcnow() instead of naive datetime.utcnow(). - crypto: narrow decrypt_value except to InvalidToken and log failures. - ssh_client: guard the shared connection cache with a lock; make unknown-host-key rejection opt-in via SSH_REJECT_UNKNOWN_HOSTS. - logging: send loguru console output to stderr instead of stdout. - Sync version fallback to 1.5.1; remove one-off refactor scripts. - Add CODE_REVIEW.md audit report. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
|
||||
from ..db import get_db
|
||||
from ..models import ProxmoxServer, AuditLog, User
|
||||
from ..auth import PermissionChecker
|
||||
from ..config import utcnow
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -39,7 +40,7 @@ def dashboard_stats(
|
||||
cluster_q = cluster_q.filter(ProxmoxServer.id.in_(server_ids))
|
||||
total_clusters = cluster_q.scalar() or 0
|
||||
|
||||
since_24h = datetime.utcnow() - timedelta(hours=24)
|
||||
since_24h = utcnow() - timedelta(hours=24)
|
||||
total_alerts = db.query(AuditLog).filter(
|
||||
AuditLog.level.in_(['error', 'critical']),
|
||||
AuditLog.created_at >= since_24h
|
||||
@@ -88,7 +89,7 @@ def get_dashboard_alerts(
|
||||
current_user: User = Depends(PermissionChecker("logs.view"))
|
||||
):
|
||||
"""Вернуть события error/critical за последние 24 ч для дашборда"""
|
||||
since_24h = datetime.utcnow() - timedelta(hours=24)
|
||||
since_24h = utcnow() - timedelta(hours=24)
|
||||
alerts = db.query(AuditLog).filter(
|
||||
AuditLog.level.in_(['error', 'critical']),
|
||||
AuditLog.created_at >= since_24h
|
||||
|
||||
@@ -13,7 +13,9 @@ from ...db import get_db
|
||||
from ...models import ProxmoxServer, VMInstance, User, IPAMAllocation, IPAMNetwork, VMSnapshotArchive
|
||||
from ...schemas import ProxmoxServerCreate, ProxmoxServerUpdate, ProxmoxServerResponse
|
||||
from ...proxmox import ProxmoxClient, get_proxmox_resources
|
||||
from ...auth import get_current_user, PermissionChecker, require_permission, check_permission
|
||||
from ...auth import (get_current_user, PermissionChecker, require_permission,
|
||||
check_permission, authenticate_ws_token)
|
||||
from ...rbac import PermissionEngine
|
||||
from ...logging_service import LoggingService
|
||||
from ...ipam_service import IPAMService
|
||||
from ._helpers import (check_vm_access, require_vm_access, _get_proxmox_client,
|
||||
@@ -2001,15 +2003,28 @@ async def vnc_websocket_proxy(
|
||||
vncticket: str,
|
||||
vnc_password: str = None, # VNC пароль сгенерированный через generate-password
|
||||
auth_ticket: str = None, # Auth ticket переданный с frontend
|
||||
token: str = Query(None), # JWT панели (?token=) — браузеры не шлют заголовки на WS
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""WebSocket прокси для VNC подключения к Proxmox"""
|
||||
import websockets
|
||||
import urllib.parse
|
||||
|
||||
|
||||
# --- Authenticate the panel user before bridging to Proxmox ------------
|
||||
user = authenticate_ws_token(token, db)
|
||||
if not user:
|
||||
await websocket.close(code=4001, reason="Authentication required")
|
||||
return
|
||||
if not PermissionEngine.has_permission(user, "vms.console"):
|
||||
await websocket.close(code=4003, reason="Permission denied")
|
||||
return
|
||||
if not check_vm_access(db, user, server_id, vmid):
|
||||
await websocket.close(code=4003, reason="Access denied")
|
||||
return
|
||||
|
||||
await websocket.accept()
|
||||
logger.info(f"VNC WebSocket connection accepted for {vmtype}/{vmid}")
|
||||
|
||||
logger.info(f"VNC WebSocket connection accepted for {vmtype}/{vmid} (user={user.username})")
|
||||
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
logger.error(f"Proxmox server {server_id} not found")
|
||||
@@ -2954,6 +2969,7 @@ async def terminal_websocket_fixed(
|
||||
server_id: int,
|
||||
node: str,
|
||||
vmid: int,
|
||||
token: str = Query(None), # JWT панели (?token=) — браузеры не шлют заголовки на WS
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""WebSocket терминал для LXC контейнеров через Proxmox termproxy API"""
|
||||
@@ -2961,8 +2977,20 @@ async def terminal_websocket_fixed(
|
||||
import httpx
|
||||
from urllib.parse import quote
|
||||
|
||||
# --- Authenticate the panel user before opening a root shell -----------
|
||||
user = authenticate_ws_token(token, db)
|
||||
if not user:
|
||||
await websocket.close(code=4001, reason="Authentication required")
|
||||
return
|
||||
if not PermissionEngine.has_permission(user, "vms.console"):
|
||||
await websocket.close(code=4003, reason="Permission denied")
|
||||
return
|
||||
if not check_vm_access(db, user, server_id, vmid):
|
||||
await websocket.close(code=4003, reason="Access denied")
|
||||
return
|
||||
|
||||
await websocket.accept()
|
||||
logger.info(f"Terminal WebSocket accepted for container {vmid} on node {node}")
|
||||
logger.info(f"Terminal WebSocket accepted for container {vmid} on node {node} (user={user.username})")
|
||||
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
|
||||
Reference in New Issue
Block a user