security: fix C3, C1 and H1
C3 - config.yml contains credentials, remove from git tracking:
- Add backend/config.yml to .gitignore
- git rm --cached to untrack it
- Add backend/config.yml.example with instructions
C1 - SECRET_KEY must come from .env, no unsafe default:
- Remove hardcoded "change_me_in_production" default from config.py
- App now fails to start if SECRET_KEY is not set (pydantic required field)
- Generate real random key in backend/.env (gitignored)
- Add backend/.env.example for new contributors
H1 - WebSocket /ws/status was unauthenticated:
- Backend: require ?token= query param, validate via decode_token(),
close with code 1008 (Policy Violation) if missing or invalid
- Frontend: append ?token=<jwt> to WebSocket URL
This commit is contained in:
@@ -41,6 +41,9 @@ build/
|
|||||||
htmlcov/
|
htmlcov/
|
||||||
.coverage
|
.coverage
|
||||||
|
|
||||||
|
# App config — contains credentials, never commit
|
||||||
|
backend/config.yml
|
||||||
|
|
||||||
# SQLite
|
# SQLite
|
||||||
*.db
|
*.db
|
||||||
*.db-shm
|
*.db-shm
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
SECRET_KEY=<generate with: python3 -c "import secrets; print(secrets.token_urlsafe(32))">
|
||||||
|
SQLITE_PATH=./data/homelab.db
|
||||||
|
CONFIG_PATH=./config.yml
|
||||||
|
CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"]
|
||||||
@@ -2,6 +2,8 @@ import json
|
|||||||
|
|
||||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
|
from app.core.security import decode_token
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
# Active WebSocket connections
|
# Active WebSocket connections
|
||||||
@@ -9,7 +11,10 @@ _connections: list[WebSocket] = []
|
|||||||
|
|
||||||
|
|
||||||
@router.websocket("/ws/status")
|
@router.websocket("/ws/status")
|
||||||
async def ws_status(websocket: WebSocket) -> None:
|
async def ws_status(websocket: WebSocket, token: str | None = None) -> None:
|
||||||
|
if not token or not decode_token(token):
|
||||||
|
await websocket.close(code=1008) # Policy Violation
|
||||||
|
return
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
_connections.append(websocket)
|
_connections.append(websocket)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
||||||
|
|
||||||
secret_key: str = "change_me_in_production"
|
secret_key: str # Required — set SECRET_KEY in .env
|
||||||
sqlite_path: str = "./data/homelab.db"
|
sqlite_path: str = "./data/homelab.db"
|
||||||
config_path: str = "./config.yml"
|
config_path: str = "./config.yml"
|
||||||
cors_origins: list[str] = ["http://localhost:5173", "http://localhost:3000"]
|
cors_origins: list[str] = ["http://localhost:5173", "http://localhost:3000"]
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
auth:
|
|
||||||
password_hash: $2b$12$o/LWyvmBc978CNpSsHxcveXN0WqjAGW/gBR0.U.HURWbaYD3GCDqS
|
|
||||||
username: admin
|
|
||||||
scanner:
|
|
||||||
interval: null
|
|
||||||
ranges:
|
|
||||||
- 192.168.1.0/24
|
|
||||||
status_checker:
|
|
||||||
interval_seconds: 60
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
auth:
|
||||||
|
username: admin
|
||||||
|
password_hash: "" # Generate with: python -c "from passlib.context import CryptContext; print(CryptContext(schemes=['bcrypt']).hash('yourpassword'))"
|
||||||
|
scanner:
|
||||||
|
interval: null
|
||||||
|
ranges:
|
||||||
|
- 192.168.1.0/24
|
||||||
|
status_checker:
|
||||||
|
interval_seconds: 60
|
||||||
@@ -22,7 +22,7 @@ export function useStatusPolling() {
|
|||||||
|
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||||
const host = window.location.hostname
|
const host = window.location.hostname
|
||||||
const url = `${protocol}://${host}:8000/api/v1/status/ws/status`
|
const url = `${protocol}://${host}:8000/api/v1/status/ws/status?token=${encodeURIComponent(token)}`
|
||||||
|
|
||||||
const ws = new WebSocket(url)
|
const ws = new WebSocket(url)
|
||||||
wsRef.current = ws
|
wsRef.current = ws
|
||||||
|
|||||||
Reference in New Issue
Block a user