Skip to content

Commit 3ef3ab6

Browse files
Clément VALENTINclaude
andcommitted
security: require SECRET_KEY in production
- Remove insecure default value for SECRET_KEY - Add Pydantic validator to enforce secure configuration: - Production (DEBUG=False): Require SECRET_KEY, reject insecure patterns - Development (DEBUG=True): Generate random key with warning - Provide clear error messages with key generation command 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent a3a2a12 commit 3ef3ab6

File tree

1 file changed

+47
-2
lines changed

1 file changed

+47
-2
lines changed

apps/api/src/config/settings.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from typing import Literal
1+
import secrets
2+
from typing import Literal, Self
23

4+
from pydantic import model_validator
35
from pydantic_settings import BaseSettings, SettingsConfigDict
46

57

@@ -33,7 +35,9 @@ def database_type(self) -> str:
3335
RTE_BASE_URL: str = "https://digital.iservices.rte-france.com"
3436

3537
# API Security
36-
SECRET_KEY: str = "dev-secret-key-change-in-production"
38+
# SECRET_KEY is required in production (no default value for security)
39+
# In DEBUG mode, a random key is generated if not provided
40+
SECRET_KEY: str = ""
3741
ALGORITHM: str = "HS256"
3842
ACCESS_TOKEN_EXPIRE_MINUTES: int = 43200
3943

@@ -94,5 +98,46 @@ def enedis_authorize_url(self) -> str:
9498
return "https://mon-compte-particulier.enedis.fr/dataconnect/v1/oauth2/authorize"
9599
return f"{self.enedis_base_url}/dataconnect/v1/oauth2/authorize"
96100

101+
@model_validator(mode="after")
102+
def validate_secret_key(self) -> Self:
103+
"""Validate SECRET_KEY configuration.
104+
105+
- Production (DEBUG=False): SECRET_KEY is required and must be secure
106+
- Development (DEBUG=True): Generate a random key if not provided (with warning)
107+
"""
108+
insecure_patterns = ["dev-", "changeme", "secret", "password", "test", "example"]
109+
110+
if not self.SECRET_KEY:
111+
if self.DEBUG:
112+
# Generate random key for development (will change on restart)
113+
object.__setattr__(self, "SECRET_KEY", secrets.token_urlsafe(32))
114+
import warnings
115+
warnings.warn(
116+
"SECRET_KEY not configured - using random key (sessions will be invalidated on restart). "
117+
"Set SECRET_KEY environment variable for persistent sessions.",
118+
UserWarning,
119+
stacklevel=2,
120+
)
121+
else:
122+
raise ValueError(
123+
"SECRET_KEY environment variable is required in production (DEBUG=False). "
124+
"Generate a secure key with: python -c \"import secrets; print(secrets.token_urlsafe(32))\""
125+
)
126+
elif any(pattern in self.SECRET_KEY.lower() for pattern in insecure_patterns):
127+
if not self.DEBUG:
128+
raise ValueError(
129+
"SECRET_KEY appears to be insecure (contains common patterns). "
130+
"Generate a secure key with: python -c \"import secrets; print(secrets.token_urlsafe(32))\""
131+
)
132+
else:
133+
import warnings
134+
warnings.warn(
135+
"SECRET_KEY appears to be insecure. Use a strong random key in production.",
136+
UserWarning,
137+
stacklevel=2,
138+
)
139+
140+
return self
141+
97142

98143
settings = Settings()

0 commit comments

Comments
 (0)