security: remove hardcoded secrets, add health endpoint, add security headers

This commit is contained in:
Gan, Jimmy
2026-02-20 22:04:01 +08:00
parent c6392556d5
commit 18605926ca
5 changed files with 44 additions and 8 deletions
+1
View File
@@ -6,3 +6,4 @@ node_modules/
dist/ dist/
.env .env
*.tar.gz *.tar.gz
dashboard/nas-dashboard.tar
+4 -4
View File
@@ -7,13 +7,12 @@ DOCKER_HOST = os.environ.get("DOCKER_HOST", "tcp://docker-socket-proxy:2375")
VOLUME_ROOT = os.environ.get("VOLUME_ROOT", "/volume1") VOLUME_ROOT = os.environ.get("VOLUME_ROOT", "/volume1")
# Auth # Auth
SECRET_KEY = os.environ.get("SECRET_KEY", "c1a776b228421830e7c7df0887adc75f74bac65aaa1fc364be96861b1f8c8701") SECRET_KEY = os.environ.get("SECRET_KEY", "")
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
ADMIN_USER = os.environ.get("ADMIN_USER", "admin") ADMIN_USER = os.environ.get("ADMIN_USER", "admin")
# Default hash for "admin" using pbkdf2_sha256 scheme to avoid bcrypt compat issues ADMIN_PASSWORD_HASH = os.environ.get("ADMIN_PASSWORD_HASH", "")
ADMIN_PASSWORD_HASH = os.environ.get("ADMIN_PASSWORD_HASH", "$pbkdf2-sha256$29000$25tTKoUQIgQghLC2ds4ZYw$H/fCb3H5wmOkwj6l6KrVlmns0TWfzKPOrG3sgVDR.eg") TOTP_SECRET = os.environ.get("TOTP_SECRET", "")
TOTP_SECRET = os.environ.get("TOTP_SECRET", "JBSWY3DPEHPK3PXP") # Base32 encoded secretans 2FA disabled by default
# SSH terminal # SSH terminal
SSH_HOST = os.environ.get("SSH_HOST", "host.docker.internal") SSH_HOST = os.environ.get("SSH_HOST", "host.docker.internal")
@@ -27,3 +26,4 @@ VPS_SSH_KEY_PATH = os.environ.get("VPS_SSH_KEY_PATH", "/app/ssh/vps_id_ed25519")
# Telegram notifications # Telegram notifications
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "") TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "") TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
TELEGRAM_PROXY = os.environ.get("TELEGRAM_PROXY", "")
+5
View File
@@ -43,6 +43,11 @@ async def monitor_containers():
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
asyncio.create_task(monitor_containers()) asyncio.create_task(monitor_containers())
@app.get("/api/health")
async def health():
return {"status": "ok"}
# Public auth endpoints # Public auth endpoints
app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
+30 -2
View File
@@ -1,5 +1,8 @@
from datetime import timedelta from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status import asyncio
import os
import httpx
from fastapi import APIRouter, Depends, HTTPException, status, Request
from pydantic import BaseModel from pydantic import BaseModel
import config import config
import auth import auth
@@ -26,10 +29,33 @@ class Verify2FARequest(BaseModel):
secret: str secret: str
code: str code: str
async def send_failed_login_alert(ip_address: str, username: str, reason: str):
print(f"Triggering Telegram alert for {username} from {ip_address}: {reason}", flush=True)
if not config.TELEGRAM_BOT_TOKEN or not config.TELEGRAM_CHAT_ID:
print("Skipping Telegram alert: missing config", flush=True)
return
msg = f"🚨 *Dashboard Security Alert* 🚨\nFailed login attempt detected.\n\n👤 User: `{username}`\n🌐 IP: `{ip_address}`\n❌ Reason: {reason}"
try:
if config.TELEGRAM_PROXY:
os.environ["HTTP_PROXY"] = config.TELEGRAM_PROXY
os.environ["HTTPS_PROXY"] = config.TELEGRAM_PROXY
async with httpx.AsyncClient() as client:
res = await client.post(
f"https://api.telegram.org/bot{config.TELEGRAM_BOT_TOKEN}/sendMessage",
data={"chat_id": config.TELEGRAM_CHAT_ID, "text": msg, "parse_mode": "Markdown"},
timeout=10.0
)
print(f"Telegram alert sent, status: {res.status_code}", flush=True)
except Exception as e:
print(f"Failed to send Telegram alert: {e}", flush=True)
@router.post("/login", response_model=Token) @router.post("/login", response_model=Token)
async def login(creds: LoginRequest): async def login(creds: LoginRequest, request: Request):
client_ip = request.client.host
# Verify username/password # Verify username/password
if creds.username != config.ADMIN_USER or not auth.verify_password(creds.password, auth.load_password_hash()): if creds.username != config.ADMIN_USER or not auth.verify_password(creds.password, auth.load_password_hash()):
asyncio.create_task(send_failed_login_alert(client_ip, creds.username, "Incorrect username or password"))
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password", detail="Incorrect username or password",
@@ -40,12 +66,14 @@ async def login(creds: LoginRequest):
totp_secret = auth.load_totp_secret() totp_secret = auth.load_totp_secret()
if totp_secret: if totp_secret:
if not creds.totp_code: if not creds.totp_code:
asyncio.create_task(send_failed_login_alert(client_ip, creds.username, "Missing 2FA Code"))
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="2FA code required", detail="2FA code required",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
if not auth.verify_totp(creds.totp_code, totp_secret): if not auth.verify_totp(creds.totp_code, totp_secret):
asyncio.create_task(send_failed_login_alert(client_ip, creds.username, "Invalid 2FA Code"))
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid 2FA code", detail="Invalid 2FA code",
+3 -1
View File
@@ -34,6 +34,8 @@ services:
- SSH_USER=zjgump - SSH_USER=zjgump
- VPS_SSH_HOST=158.101.140.85 - VPS_SSH_HOST=158.101.140.85
- VPS_SSH_USER=jimmyg - VPS_SSH_USER=jimmyg
- SECRET_KEY=${SECRET_KEY}
- ADMIN_PASSWORD_HASH=${ADMIN_PASSWORD_HASH}
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
- TELEGRAM_PROXY=${TELEGRAM_PROXY:-} - TELEGRAM_PROXY=${TELEGRAM_PROXY:-}
@@ -42,7 +44,7 @@ services:
- /volume1/docker/nas-dashboard/ssh/dashboard_terminal:/app/ssh/id_ed25519:ro - /volume1/docker/nas-dashboard/ssh/dashboard_terminal:/app/ssh/id_ed25519:ro
- /volume1/docker/nas-dashboard/ssh/vps_terminal:/app/ssh/vps_id_ed25519:ro - /volume1/docker/nas-dashboard/ssh/vps_terminal:/app/ssh/vps_id_ed25519:ro
healthcheck: healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:4000/api/system/stats')"] test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:4000/api/health')"]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3