feat(hardening): login rate limiting, per-service memory limits, pinned images
- slowapi limiter on POST /auth/login (10/min per IP, Persian error msg); edge-level coarse limits stay in Traefik - mem_limit on every main-compose service (~4.4GB worst case, protects the 8GB shared host during rebuilds) - monitoring images pinned to verified tags (prom v3.4.1, grafana 11.6.3, loki/promtail 3.5.0, node-exporter v1.9.1, postgres-exporter v0.17.1, cadvisor v0.49.1); exporter image name fixed to postgres-exporter
This commit is contained in:
@@ -16,6 +16,8 @@ from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from prometheus_fastapi_instrumentator import Instrumentator
|
||||
from slowapi import _rate_limit_exceeded_handler
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from sqlalchemy import text as sql_text
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
@@ -34,6 +36,7 @@ from app.observability import RequestContextMiddleware, setup_logging
|
||||
from app.routers import ROUTERS
|
||||
from app.services.quote_service import auto_expire_quotes
|
||||
from app.utils.errors import AppError
|
||||
from app.utils.ratelimit import limiter
|
||||
|
||||
setup_logging(settings.LOG_FORMAT)
|
||||
logger = logging.getLogger("procurement")
|
||||
@@ -131,6 +134,10 @@ app.add_middleware(
|
||||
)
|
||||
app.add_middleware(RequestContextMiddleware)
|
||||
|
||||
# Rate limiting — login brute-force brake (edge rate limit lives in Traefik).
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
|
||||
# Prometheus /metrics (requests, latency, errors) — mounted at app level,
|
||||
# outside the authenticated /api/v1 prefix; scrape target for the
|
||||
# monitoring stack.
|
||||
|
||||
@@ -13,7 +13,7 @@ Error mapping (documented in docs/casdoor-auth.md):
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -23,6 +23,7 @@ from app.db.engine import get_db
|
||||
from app.routers.users import _user_projects, user_to_dict
|
||||
from app.services import analytics
|
||||
from app.utils.errors import ServiceUnavailableError, UnauthorizedError
|
||||
from app.utils.ratelimit import limiter
|
||||
|
||||
logger = logging.getLogger("procurement.auth")
|
||||
|
||||
@@ -35,7 +36,8 @@ class LoginRequest(BaseModel):
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(payload: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
@limiter.limit("10/minute", error_message="تلاشهای ورود بیش از حد مجاز — کمی بعد دوباره امتحان کنید")
|
||||
async def login(request: Request, payload: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Exchange username/password for a Casdoor JWT (OAuth password grant)."""
|
||||
url = f"{settings.CASDOOR_ENDPOINT}/api/login/oauth/access_token"
|
||||
form = {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Shared rate limiter (slowapi) — in-memory, single-worker friendly.
|
||||
|
||||
Login brute-force brake: the edge (Traefik) has a coarse per-IP limit for
|
||||
the whole API host; this adds a tight limit on the credential endpoint
|
||||
itself. Kept in its own module so routers can import it without a circular
|
||||
import on app.main.
|
||||
"""
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
@@ -17,3 +17,6 @@ bcrypt>=4.1.0
|
||||
# Observability — metrics + Sentry/GlitchTip exception capture
|
||||
prometheus-fastapi-instrumentator>=7.0.0
|
||||
sentry-sdk[fastapi]>=2.0.0
|
||||
|
||||
# Rate limiting (login brute-force brake; edge limit lives in Traefik)
|
||||
slowapi>=0.1.9
|
||||
|
||||
Reference in New Issue
Block a user