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
+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")
# Auth
SECRET_KEY = os.environ.get("SECRET_KEY", "c1a776b228421830e7c7df0887adc75f74bac65aaa1fc364be96861b1f8c8701")
SECRET_KEY = os.environ.get("SECRET_KEY", "")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
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", "$pbkdf2-sha256$29000$25tTKoUQIgQghLC2ds4ZYw$H/fCb3H5wmOkwj6l6KrVlmns0TWfzKPOrG3sgVDR.eg")
TOTP_SECRET = os.environ.get("TOTP_SECRET", "JBSWY3DPEHPK3PXP") # Base32 encoded secretans 2FA disabled by default
ADMIN_PASSWORD_HASH = os.environ.get("ADMIN_PASSWORD_HASH", "")
TOTP_SECRET = os.environ.get("TOTP_SECRET", "")
# SSH terminal
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_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
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")
async def startup_event():
asyncio.create_task(monitor_containers())
@app.get("/api/health")
async def health():
return {"status": "ok"}
# Public auth endpoints
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
+31 -3
View File
@@ -1,5 +1,8 @@
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
import config
import auth
@@ -26,10 +29,33 @@ class Verify2FARequest(BaseModel):
secret: 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)
async def login(creds: LoginRequest):
async def login(creds: LoginRequest, request: Request):
client_ip = request.client.host
# Verify username/password
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(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
@@ -40,13 +66,15 @@ async def login(creds: LoginRequest):
totp_secret = auth.load_totp_secret()
if totp_secret:
if not creds.totp_code:
asyncio.create_task(send_failed_login_alert(client_ip, creds.username, "Missing 2FA Code"))
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="2FA code required",
headers={"WWW-Authenticate": "Bearer"},
)
if not auth.verify_totp(creds.totp_code, totp_secret):
raise HTTPException(
asyncio.create_task(send_failed_login_alert(client_ip, creds.username, "Invalid 2FA Code"))
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid 2FA code",
headers={"WWW-Authenticate": "Bearer"},