Skip to content

Security: Tounisiano/agentic-dev-template

Security

docs/SECURITY.md

Security — Règles et pratiques

Sécurité by design : ces règles s'appliquent dès la première ligne de code. Vérifiées automatiquement par /security-check et le pipeline CI.


Secrets et variables d'environnement

Règle absolue : aucun secret dans le code

# ❌ JAMAIS
DATABASE_URL = "postgresql://user:password@localhost/db"
API_KEY = "sk-prod-abc123"

# ✅ TOUJOURS
DATABASE_URL = os.environ["DATABASE_URL"]  # fail-fast si absent

Variables requises (fail-fast au démarrage)

Définissez les secrets comme requis sans valeur par défaut. L'application doit refuser de démarrer si un secret est absent plutôt que de tourner avec une valeur de dev en production.

# Python (pydantic-settings)
class Settings(BaseSettings):
    DATABASE_URL: str          # requis — pas de default
    SECRET_KEY: str            # requis — pas de default
    REDIS_URL: str             # requis — pas de default
    DEBUG: bool = False        # optionnel avec défaut sûr

# Node.js
const DATABASE_URL = process.env.DATABASE_URL
if (!DATABASE_URL) throw new Error('DATABASE_URL is required')

Génération des secrets

# Mot de passe fort
openssl rand -base64 32

# Clé JWT (hex)
openssl rand -hex 32

# Clé de chiffrement AES-256
openssl rand -hex 32

Authentification

Mots de passe

  • Hashing : bcrypt (cost factor ≥ 12) ou argon2id
  • Jamais MD5, SHA1, SHA256 seul pour les mots de passe
  • Jamais stocker en clair, jamais logger
# Python — bcrypt direct (pas passlib)
import bcrypt

def hash_password(password: str) -> str:
    return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)).decode()

def verify_password(password: str, hashed: str) -> bool:
    return bcrypt.checkpw(password.encode(), hashed.encode())

JWT

  • Access token : durée courte (15min à 1h)
  • Refresh token : durée longue + révocable (7-30j, stocké en DB pour pouvoir invalider)
  • Algorithme : HS256 minimum, RS256 si multi-services
  • Jamais stocker dans localStorage (XSS) — préférer httpOnly cookie
  • Jamais mettre de données sensibles dans le payload (décodable côté client)

Protection des endpoints

Contrôle d'accès

# Toujours vérifier côté serveur
@router.get("/admin/users")
async def list_users(
    current_user: User = Depends(get_admin_user),  # 403 si pas admin
):
    ...

# Toujours vérifier la propriété de la ressource
@router.get("/documents/{doc_id}")
async def get_document(
    doc_id: str,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    doc = await db.get(Document, doc_id)
    if not doc or doc.user_id != current_user.id:  # ← vérification propriété
        raise HTTPException(404)  # 404, pas 403 — ne pas confirmer l'existence

Rate limiting

Configurer sur les endpoints sensibles :

  • Authentification : 10 req/min par IP
  • Reset de mot de passe : 5 req/min par IP
  • Endpoints publics : 60 req/min par IP

Validation des entrées

Valider à la frontière — jamais faire confiance aux inputs utilisateur :

# Pydantic (Python)
class UserCreate(BaseModel):
    email: EmailStr                    # validation format email
    password: str = Field(min_length=8, max_length=128)
    name: str = Field(min_length=1, max_length=100, pattern=r'^[\w\s-]+$')

# Zod (TypeScript)
const UserCreateSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8).max(128),
  name: z.string().min(1).max(100),
})

SQL et bases de données

# ❌ JAMAIS — injection SQL
query = f"SELECT * FROM users WHERE email = '{email}'"

# ✅ TOUJOURS — requêtes paramétrées
result = await db.execute(select(User).where(User.email == email))

# ✅ SQLAlchemy ORM (paramétré automatiquement)
user = await db.scalar(select(User).where(User.id == user_id))

CORS

# ❌ Jamais en production
allow_origins=["*"]

# ✅ Liste explicite
allow_origins=["https://monapp.fr", "https://www.monapp.fr"]

Headers de sécurité

Configurer via le reverse proxy (Nginx) ou le framework :

add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "DENY";
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header Content-Security-Policy "default-src 'self'; script-src 'self'";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";

Logging et monitoring

# ❌ JAMAIS logger des données sensibles
logger.info(f"Login: email={email}, password={password}")
logger.debug(f"JWT token: {token}")

# ✅ Logger les événements sans les secrets
logger.info(f"Login success: user_id={user.id}")
logger.warning(f"Login failed: email={email[:3]}***")
logger.info(f"Token issued: user_id={user.id}, expires={exp}")

Checklist de sécurité avant mise en production

  • Tous les secrets sont dans des variables d'environnement
  • Aucune valeur par défaut pour les secrets requis
  • CORS configuré avec une liste explicite de domaines
  • Rate limiting actif sur auth et endpoints publics
  • Headers de sécurité configurés
  • HTTPS uniquement (redirection HTTP → HTTPS)
  • Logs sans données PII ni secrets
  • Dépendances auditées (npm audit / pip-audit)
  • Scan SAST dans la CI (Semgrep, Bandit, CodeQL...)

There aren't any published security advisories