security: remove hardcoded secrets, add health endpoint, add security headers
This commit is contained in:
@@ -6,3 +6,4 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
.env
|
.env
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
dashboard/nas-dashboard.tar
|
||||||
|
|||||||
@@ -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", "")
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user