fix(rmi-backend,audit): jwt_secret is required, fail-fast in prod (P1.5)
Some checks failed
CI / build (push) Failing after 2s
Some checks failed
CI / build (push) Failing after 2s
Previously: jwt_secret defaulted to dev-secret-CHANGE-ME so a missing env var in production would silently boot with a known-public dev HMAC key — an instant auth bypass for any service verifying JWTs. Now: - Field(...) with no default → missing env raises ValidationError at boot - min_length=32 enforces minimum entropy (rejects dev placeholder too) - field_validator rejects the literal dev-secret-CHANGE-ME in ENVIRONMENT=prod as belt-and-suspenders in case min_length is loosened later Verified behavior: - no env, no .env -> ValidationError: Field required - ENVIRONMENT=prod, no JWT_SECRET -> ValidationError: Field required - prod, JWT_SECRET=dev-secret-... -> ValidationError: string_too_short - prod, JWT_SECRET=short -> ValidationError: string_too_short - prod, JWT_SECRET=<64 hex> -> OK, len=64 .env.example updated to show the placeholder + generation hint. Refs AUDIT-2026-Q3.md P1.5.
This commit is contained in:
parent
f4d42768a2
commit
9c62549b50
2 changed files with 26 additions and 3 deletions
|
|
@ -12,7 +12,7 @@ DATABASE_URL=postgresql+asyncpg://rmi:rmi@localhost/rmi
|
||||||
REDIS_URL=redis://localhost:6379/0
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
|
||||||
# ── Auth ────────────────────────────────────────────────────────
|
# ── Auth ────────────────────────────────────────────────────────
|
||||||
JWT_SECRET=dev-secret-CHANGE-ME
|
JWT_SECRET= # MANDATORY in production — generate with: openssl rand -hex 32
|
||||||
JWT_ALGORITHM=HS256
|
JWT_ALGORITHM=HS256
|
||||||
JWT_EXPIRE_MINUTES=1440
|
JWT_EXPIRE_MINUTES=1440
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@ Import: `from app.config import settings`
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field, field_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -33,7 +34,14 @@ class Settings(BaseSettings):
|
||||||
redis_url: str = "redis://localhost:6379/0"
|
redis_url: str = "redis://localhost:6379/0"
|
||||||
|
|
||||||
# ── Auth ────────────────────────────────────────────────────────
|
# ── Auth ────────────────────────────────────────────────────────
|
||||||
jwt_secret: str = Field(default="dev-secret-CHANGE-ME")
|
jwt_secret: str = Field( # required, no default, fail-fast in prod
|
||||||
|
min_length=32,
|
||||||
|
description=(
|
||||||
|
"HMAC secret for JWT. Required. Must be >= 32 chars. "
|
||||||
|
"Generate with: openssl rand -hex 32. "
|
||||||
|
"Set via gopass (rmi/prod/jwt_secret) or JWT_SECRET env var."
|
||||||
|
),
|
||||||
|
)
|
||||||
jwt_algorithm: str = "HS256"
|
jwt_algorithm: str = "HS256"
|
||||||
jwt_expire_minutes: int = 60 * 24
|
jwt_expire_minutes: int = 60 * 24
|
||||||
|
|
||||||
|
|
@ -68,6 +76,21 @@ class Settings(BaseSettings):
|
||||||
"phishing_db",
|
"phishing_db",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@field_validator("jwt_secret")
|
||||||
|
@classmethod
|
||||||
|
def _jwt_secret_must_be_set_in_prod(cls, v: str) -> str:
|
||||||
|
"""Reject the dev default in production.
|
||||||
|
|
||||||
|
Pydantic-settings binds v from env (or rejects via min_length above).
|
||||||
|
Here we only enforce the dev placeholder cannot leak into a prod boot.
|
||||||
|
"""
|
||||||
|
if v == "dev-secret-CHANGE-ME" and os.getenv("ENVIRONMENT", "dev") == "prod":
|
||||||
|
raise ValueError(
|
||||||
|
"jwt_secret must be set to a non-default value in production. "
|
||||||
|
"Set via gopass (rmi/prod/jwt_secret) or JWT_SECRET env var."
|
||||||
|
)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def get_settings() -> Settings:
|
def get_settings() -> Settings:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue