Débutant

Variables d'Environnement Python : Sécurité et Configuration des Scripts

Maîtrisez la gestion des variables d'environnement en Python pour sécuriser vos scripts, gérer les configurations et éviter les erreurs de déploiement.

Publié le
20 décembre 2024
Lecture
12 min
Vues
0
Auteur
Florian Courouge
Python
Security
Configuration
Environment
DevOps
Best Practices

Table des matières

📋 Vue d'ensemble rapide des sujets traités dans cet article

Cliquez sur les sections ci-dessous pour naviguer rapidement

Variables d'Environnement Python : Sécurité et Configuration des Scripts

La gestion des variables d'environnement est cruciale pour développer des scripts Python robustes, sécurisés et facilement déployables. Ce guide explore les bonnes pratiques pour utiliser les variables d'environnement dans vos projets Python.

💡Pourquoi Utiliser des Variables d'Environnement ?

1. Sécurité des Données Sensibles

# ❌ MAUVAISE PRATIQUE - Credentials en dur
import requests

API_KEY = "sk-1234567890abcdef"  # Exposé dans le code !
DATABASE_URL = "postgresql://user:password@localhost/db"

def call_api():
    response = requests.get(
        "https://api.example.com/data",
        headers={"Authorization": f"Bearer {API_KEY}"}
    )
    return response.json()
# ✅ BONNE PRATIQUE - Variables d'environnement
import os
import requests
from typing import Optional

def get_env_var(key: str, default: Optional[str] = None, required: bool = True) -> str:
    """Récupère une variable d'environnement de manière sécurisée"""
    value = os.getenv(key, default)
    
    if required and value is None:
        raise ValueError(f"Variable d'environnement '{key}' requise mais non définie")
    
    return value

# Configuration sécurisée
API_KEY = get_env_var("API_KEY")
DATABASE_URL = get_env_var("DATABASE_URL")

def call_api():
    response = requests.get(
        "https://api.example.com/data",
        headers={"Authorization": f"Bearer {API_KEY}"}
    )
    return response.json()

2. Configuration Multi-Environnements

# config/settings.py
import os
from enum import Enum
from dataclasses import dataclass
from typing import Optional

class Environment(Enum):
    DEVELOPMENT = "development"
    STAGING = "staging"
    PRODUCTION = "production"

@dataclass
class DatabaseConfig:
    host: str
    port: int
    name: str
    user: str
    password: str
    ssl_mode: str = "prefer"
    
    @property
    def url(self) -> str:
        return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.name}?sslmode={self.ssl_mode}"

@dataclass
class AppConfig:
    environment: Environment
    debug: bool
    log_level: str
    database: DatabaseConfig
    redis_url: str
    secret_key: str
    
    @classmethod
    def from_env(cls) -> 'AppConfig':
        """Crée la configuration à partir des variables d'environnement"""
        
        env = Environment(os.getenv("ENVIRONMENT", "development"))
        
        # Configuration de base selon l'environnement
        if env == Environment.DEVELOPMENT:
            debug = True
            log_level = "DEBUG"
        elif env == Environment.STAGING:
            debug = False
            log_level = "INFO"
        else:  # PRODUCTION
            debug = False
            log_level = "WARNING"
        
        # Configuration de la base de données
        database = DatabaseConfig(
            host=os.getenv("DB_HOST", "localhost"),
            port=int(os.getenv("DB_PORT", "5432")),
            name=os.getenv("DB_NAME", "myapp"),
            user=os.getenv("DB_USER", "postgres"),
            password=os.getenv("DB_PASSWORD", ""),
            ssl_mode=os.getenv("DB_SSL_MODE", "prefer")
        )
        
        return cls(
            environment=env,
            debug=debug,
            log_level=log_level,
            database=database,
            redis_url=os.getenv("REDIS_URL", "redis://localhost:6379/0"),
            secret_key=os.getenv("SECRET_KEY", "dev-secret-key")
        )
    
    def validate(self) -> None:
        """Valide la configuration"""
        if self.environment == Environment.PRODUCTION:
            if self.secret_key == "dev-secret-key":
                raise ValueError("SECRET_KEY par défaut en production !")
            
            if not self.database.password:
                raise ValueError("Mot de passe DB requis en production")
            
            if self.debug:
                raise ValueError("Debug activé en production !")

# Utilisation
config = AppConfig.from_env()
config.validate()

💡Gestion Avancée avec python-dotenv

Installation et Configuration

pip install python-dotenv
# .env (fichier de développement)
# Base de données
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp_dev
DB_USER=postgres
DB_PASSWORD=dev_password

# Cache
REDIS_URL=redis://localhost:6379/0

# API externes
STRIPE_API_KEY=sk_test_1234567890
SENDGRID_API_KEY=SG.test_key

# Configuration app
SECRET_KEY=dev-secret-key-very-long
DEBUG=true
LOG_LEVEL=DEBUG

# Monitoring
SENTRY_DSN=https://your-sentry-dsn@sentry.io/project
# config/env_loader.py
import os
from pathlib import Path
from dotenv import load_dotenv
from typing import Union, List, Dict, Any
import logging

logger = logging.getLogger(__name__)

class EnvironmentLoader:
    """Gestionnaire avancé des variables d'environnement"""
    
    def __init__(self, env_file: Union[str, Path] = None):
        self.env_file = env_file or ".env"
        self.loaded_vars: Dict[str, str] = {}
        
    def load(self, override: bool = False) -> None:
        """Charge les variables d'environnement"""
        
        # Charger depuis le fichier .env
        if Path(self.env_file).exists():
            load_dotenv(self.env_file, override=override)
            logger.info(f"Variables chargées depuis {self.env_file}")
        else:
            logger.warning(f"Fichier {self.env_file} non trouvé")
        
        # Stocker les variables chargées
        self.loaded_vars = dict(os.environ)
    
    def get(self, key: str, default: Any = None, cast_type: type = str, required: bool = False) -> Any:
        """Récupère une variable avec conversion de type"""
        
        value = os.getenv(key, default)
        
        if required and value is None:
            raise ValueError(f"Variable '{key}' requise mais non définie")
        
        if value is None:
            return default
        
        # Conversion de type
        try:
            if cast_type == bool:
                return value.lower() in ('true', '1', 'yes', 'on')
            elif cast_type == list:
                return [item.strip() for item in value.split(',') if item.strip()]
            else:
                return cast_type(value)
        except (ValueError, TypeError) as e:
            raise ValueError(f"Impossible de convertir '{key}={value}' en {cast_type.__name__}: {e}")
    
    def get_required(self, key: str, cast_type: type = str) -> Any:
        """Raccourci pour les variables requises"""
        return self.get(key, cast_type=cast_type, required=True)
    
    def validate_required(self, required_vars: List[str]) -> None:
        """Valide que toutes les variables requises sont définies"""
        missing = [var for var in required_vars if not os.getenv(var)]
        
        if missing:
            raise ValueError(f"Variables manquantes: {', '.join(missing)}")
    
    def export_template(self, output_file: str = ".env.template") -> None:
        """Génère un template des variables utilisées"""
        
        template_content = []
        template_content.append("# Configuration Template")
        template_content.append("# Copiez ce fichier vers .env et remplissez les valeurs")
        template_content.append("")
        
        # Grouper par préfixe
        groups = {}
        for key in sorted(self.loaded_vars.keys()):
            prefix = key.split('_')[0]
            if prefix not in groups:
                groups[prefix] = []
            groups[prefix].append(key)
        
        for group, keys in groups.items():
            template_content.append(f"# {group.title()} Configuration")
            for key in keys:
                template_content.append(f"{key}=")
            template_content.append("")
        
        Path(output_file).write_text('\n'.join(template_content))
        logger.info(f"Template généré: {output_file}")

# Utilisation globale
env_loader = EnvironmentLoader()
env_loader.load()

💡Patterns de Configuration Robustes

1. Configuration par Classes

# config/database.py
from dataclasses import dataclass
from typing import Optional
from .env_loader import env_loader

@dataclass
class DatabaseConfig:
    """Configuration de base de données"""
    
    host: str
    port: int
    name: str
    user: str
    password: str
    ssl_mode: str = "prefer"
    pool_size: int = 5
    max_overflow: int = 10
    echo: bool = False
    
    @classmethod
    def from_env(cls, prefix: str = "DB") -> 'DatabaseConfig':
        """Crée la config depuis les variables d'environnement"""
        
        return cls(
            host=env_loader.get_required(f"{prefix}_HOST"),
            port=env_loader.get(f"{prefix}_PORT", 5432, int),
            name=env_loader.get_required(f"{prefix}_NAME"),
            user=env_loader.get_required(f"{prefix}_USER"),
            password=env_loader.get_required(f"{prefix}_PASSWORD"),
            ssl_mode=env_loader.get(f"{prefix}_SSL_MODE", "prefer"),
            pool_size=env_loader.get(f"{prefix}_POOL_SIZE", 5, int),
            max_overflow=env_loader.get(f"{prefix}_MAX_OVERFLOW", 10, int),
            echo=env_loader.get(f"{prefix}_ECHO", False, bool)
        )
    
    @property
    def url(self) -> str:
        """URL de connexion"""
        return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.name}"
    
    def validate(self) -> None:
        """Valide la configuration"""
        if self.port < 1 or self.port > 65535:
            raise ValueError(f"Port invalide: {self.port}")
        
        if self.pool_size < 1:
            raise ValueError(f"Pool size invalide: {self.pool_size}")

# config/cache.py
@dataclass
class CacheConfig:
    """Configuration du cache Redis"""
    
    url: str
    ttl: int = 3600
    max_connections: int = 10
    
    @classmethod
    def from_env(cls) -> 'CacheConfig':
        return cls(
            url=env_loader.get("REDIS_URL", "redis://localhost:6379/0"),
            ttl=env_loader.get("CACHE_TTL", 3600, int),
            max_connections=env_loader.get("REDIS_MAX_CONNECTIONS", 10, int)
        )

2. Configuration Hiérarchique

# config/main.py
import os
from pathlib import Path
from typing import Dict, Any
from .database import DatabaseConfig
from .cache import CacheConfig
from .env_loader import env_loader

class AppConfig:
    """Configuration principale de l'application"""
    
    def __init__(self):
        # Charger les variables d'environnement
        env_file = os.getenv("ENV_FILE", ".env")
        env_loader.env_file = env_file
        env_loader.load()
        
        # Configuration par composants
        self.database = DatabaseConfig.from_env()
        self.cache = CacheConfig.from_env()
        
        # Configuration générale
        self.environment = env_loader.get("ENVIRONMENT", "development")
        self.debug = env_loader.get("DEBUG", False, bool)
        self.log_level = env_loader.get("LOG_LEVEL", "INFO")
        self.secret_key = env_loader.get_required("SECRET_KEY")
        
        # Configuration de sécurité
        self.allowed_hosts = env_loader.get("ALLOWED_HOSTS", [], list)
        self.cors_origins = env_loader.get("CORS_ORIGINS", [], list)
        
        # APIs externes
        self.stripe_api_key = env_loader.get("STRIPE_API_KEY")
        self.sendgrid_api_key = env_loader.get("SENDGRID_API_KEY")
        
        # Monitoring
        self.sentry_dsn = env_loader.get("SENTRY_DSN")
        
        # Validation
        self.validate()
    
    def validate(self) -> None:
        """Valide la configuration complète"""
        
        # Validation par composant
        self.database.validate()
        
        # Validation globale
        if self.environment == "production":
            required_prod_vars = [
                "SECRET_KEY", "DB_PASSWORD", "SENTRY_DSN"
            ]
            
            for var in required_prod_vars:
                if not getattr(self, var.lower().replace('_', '_')):
                    raise ValueError(f"{var} requis en production")
            
            if self.debug:
                raise ValueError("DEBUG ne doit pas être activé en production")
    
    def to_dict(self) -> Dict[str, Any]:
        """Exporte la configuration (sans les secrets)"""
        
        return {
            "environment": self.environment,
            "debug": self.debug,
            "log_level": self.log_level,
            "database": {
                "host": self.database.host,
                "port": self.database.port,
                "name": self.database.name,
                "ssl_mode": self.database.ssl_mode
            },
            "cache": {
                "ttl": self.cache.ttl,
                "max_connections": self.cache.max_connections
            }
        }

# Instance globale
config = AppConfig()

💡Scripts de Déploiement et Variables

1. Script de Déploiement avec Validation

#!/usr/bin/env python3
# deploy.py

import os
import sys
import subprocess
from pathlib import Path
from typing import List, Dict
import argparse
import logging

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

class DeploymentManager:
    """Gestionnaire de déploiement avec validation des variables"""
    
    def __init__(self, environment: str):
        self.environment = environment
        self.required_vars = self._get_required_vars()
        
    def _get_required_vars(self) -> Dict[str, List[str]]:
        """Définit les variables requises par environnement"""
        
        base_vars = [
            "DB_HOST", "DB_PORT", "DB_NAME", "DB_USER", "DB_PASSWORD",
            "REDIS_URL", "SECRET_KEY"
        ]
        
        return {
            "development": base_vars,
            "staging": base_vars + [
                "SENTRY_DSN", "STRIPE_API_KEY"
            ],
            "production": base_vars + [
                "SENTRY_DSN", "STRIPE_API_KEY", "SENDGRID_API_KEY",
                "SSL_CERT_PATH", "SSL_KEY_PATH"
            ]
        }
    
    def validate_environment(self) -> None:
        """Valide les variables d'environnement"""
        
        logger.info(f"Validation des variables pour l'environnement: {self.environment}")
        
        required = self.required_vars.get(self.environment, [])
        missing = [var for var in required if not os.getenv(var)]
        
        if missing:
            logger.error(f"Variables manquantes: {', '.join(missing)}")
            sys.exit(1)
        
        logger.info("✓ Toutes les variables requises sont définies")
    
    def load_env_file(self) -> None:
        """Charge le fichier d'environnement approprié"""
        
        env_file = f".env.{self.environment}"
        
        if not Path(env_file).exists():
            logger.error(f"Fichier {env_file} non trouvé")
            sys.exit(1)
        
        # Charger les variables
        with open(env_file) as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith('#'):
                    key, value = line.split('=', 1)
                    os.environ[key] = value
        
        logger.info(f"✓ Variables chargées depuis {env_file}")
    
    def deploy(self) -> None:
        """Exécute le déploiement"""
        
        logger.info(f"Déploiement en cours pour {self.environment}...")
        
        # Étapes de déploiement
        steps = [
            ("Installation des dépendances", "pip install -r requirements.txt"),
            ("Migration de la base", "python manage.py migrate"),
            ("Collecte des fichiers statiques", "python manage.py collectstatic --noinput"),
            ("Redémarrage des services", self._restart_services)
        ]
        
        for step_name, command in steps:
            logger.info(f"Exécution: {step_name}")
            
            if callable(command):
                command()
            else:
                result = subprocess.run(command, shell=True, capture_output=True, text=True)
                
                if result.returncode != 0:
                    logger.error(f"Erreur lors de: {step_name}")
                    logger.error(result.stderr)
                    sys.exit(1)
        
        logger.info("✓ Déploiement terminé avec succès")
    
    def _restart_services(self) -> None:
        """Redémarre les services selon l'environnement"""
        
        if self.environment == "production":
            subprocess.run("sudo systemctl restart myapp", shell=True)
            subprocess.run("sudo systemctl restart nginx", shell=True)
        else:
            subprocess.run("docker-compose restart", shell=True)

def main():
    parser = argparse.ArgumentParser(description="Script de déploiement")
    parser.add_argument("environment", choices=["development", "staging", "production"])
    parser.add_argument("--validate-only", action="store_true", help="Valider seulement")
    
    args = parser.parse_args()
    
    deployer = DeploymentManager(args.environment)
    
    # Charger les variables
    deployer.load_env_file()
    
    # Valider
    deployer.validate_environment()
    
    if args.validate_only:
        logger.info("Validation terminée")
        return
    
    # Déployer
    deployer.deploy()

if __name__ == "__main__":
    main()

2. Générateur de Fichiers d'Environnement

#!/usr/bin/env python3
# generate_env.py

import secrets
import string
from pathlib import Path
from typing import Dict, Any
import argparse

class EnvironmentGenerator:
    """Générateur de fichiers d'environnement"""
    
    def __init__(self):
        self.templates = {
            "development": self._dev_template,
            "staging": self._staging_template,
            "production": self._prod_template
        }
    
    def generate_secret_key(self, length: int = 50) -> str:
        """Génère une clé secrète sécurisée"""
        alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
        return ''.join(secrets.choice(alphabet) for _ in range(length))
    
    def generate_password(self, length: int = 16) -> str:
        """Génère un mot de passe sécurisé"""
        alphabet = string.ascii_letters + string.digits
        return ''.join(secrets.choice(alphabet) for _ in range(length))
    
    def _dev_template(self) -> Dict[str, Any]:
        """Template pour le développement"""
        return {
            "ENVIRONMENT": "development",
            "DEBUG": "true",
            "LOG_LEVEL": "DEBUG",
            "SECRET_KEY": self.generate_secret_key(),
            
            "DB_HOST": "localhost",
            "DB_PORT": "5432",
            "DB_NAME": "myapp_dev",
            "DB_USER": "postgres",
            "DB_PASSWORD": self.generate_password(),
            
            "REDIS_URL": "redis://localhost:6379/0",
            
            "STRIPE_API_KEY": "sk_test_...",
            "SENDGRID_API_KEY": "SG.test_...",
        }
    
    def _staging_template(self) -> Dict[str, Any]:
        """Template pour le staging"""
        return {
            "ENVIRONMENT": "staging",
            "DEBUG": "false",
            "LOG_LEVEL": "INFO",
            "SECRET_KEY": self.generate_secret_key(),
            
            "DB_HOST": "staging-db.example.com",
            "DB_PORT": "5432",
            "DB_NAME": "myapp_staging",
            "DB_USER": "myapp_user",
            "DB_PASSWORD": self.generate_password(32),
            "DB_SSL_MODE": "require",
            
            "REDIS_URL": "redis://staging-redis.example.com:6379/0",
            
            "SENTRY_DSN": "https://your-sentry-dsn@sentry.io/project",
            "STRIPE_API_KEY": "sk_test_...",
            "SENDGRID_API_KEY": "SG.staging_...",
            
            "ALLOWED_HOSTS": "staging.example.com",
            "CORS_ORIGINS": "https://staging-app.example.com"
        }
    
    def _prod_template(self) -> Dict[str, Any]:
        """Template pour la production"""
        return {
            "ENVIRONMENT": "production",
            "DEBUG": "false",
            "LOG_LEVEL": "WARNING",
            "SECRET_KEY": self.generate_secret_key(64),
            
            "DB_HOST": "prod-db.example.com",
            "DB_PORT": "5432",
            "DB_NAME": "myapp_prod",
            "DB_USER": "myapp_user",
            "DB_PASSWORD": self.generate_password(64),
            "DB_SSL_MODE": "require",
            "DB_POOL_SIZE": "20",
            
            "REDIS_URL": "redis://prod-redis.example.com:6379/0",
            
            "SENTRY_DSN": "https://your-prod-sentry-dsn@sentry.io/project",
            "STRIPE_API_KEY": "sk_live_...",
            "SENDGRID_API_KEY": "SG.prod_...",
            
            "SSL_CERT_PATH": "/etc/ssl/certs/myapp.crt",
            "SSL_KEY_PATH": "/etc/ssl/private/myapp.key",
            
            "ALLOWED_HOSTS": "example.com,www.example.com",
            "CORS_ORIGINS": "https://app.example.com"
        }
    
    def generate(self, environment: str, output_file: str = None) -> None:
        """Génère un fichier d'environnement"""
        
        if environment not in self.templates:
            raise ValueError(f"Environnement non supporté: {environment}")
        
        template = self.templates[environment]()
        
        if not output_file:
            output_file = f".env.{environment}"
        
        # Générer le contenu
        content = []
        content.append(f"# Configuration pour l'environnement: {environment}")
        content.append(f"# Généré automatiquement - NE PAS COMMITTER")
        content.append("")
        
        # Grouper par catégorie
        categories = {
            "Application": ["ENVIRONMENT", "DEBUG", "LOG_LEVEL", "SECRET_KEY"],
            "Base de données": [k for k in template.keys() if k.startswith("DB_")],
            "Cache": [k for k in template.keys() if "REDIS" in k],
            "APIs externes": [k for k in template.keys() if k.endswith("_API_KEY")],
            "Sécurité": [k for k in template.keys() if k in ["ALLOWED_HOSTS", "CORS_ORIGINS", "SSL_CERT_PATH", "SSL_KEY_PATH"]],
            "Monitoring": [k for k in template.keys() if "SENTRY" in k]
        }
        
        for category, keys in categories.items():
            if not keys:
                continue
                
            content.append(f"# {category}")
            for key in keys:
                if key in template:
                    content.append(f"{key}={template[key]}")
            content.append("")
        
        # Écrire le fichier
        Path(output_file).write_text('\n'.join(content))
        print(f"✓ Fichier généré: {output_file}")
        
        if environment == "production":
            print("⚠️  ATTENTION: Fichier de production généré avec des valeurs par défaut")
            print("   Modifiez les valeurs avant utilisation !")

def main():
    parser = argparse.ArgumentParser(description="Générateur de fichiers d'environnement")
    parser.add_argument("environment", choices=["development", "staging", "production"])
    parser.add_argument("-o", "--output", help="Fichier de sortie")
    
    args = parser.parse_args()
    
    generator = EnvironmentGenerator()
    generator.generate(args.environment, args.output)

if __name__ == "__main__":
    main()

💡Bonnes Pratiques et Sécurité

1. Checkliste de Sécurité

# security/env_checker.py

import os
import re
from pathlib import Path
from typing import List, Dict, Tuple
import logging

logger = logging.getLogger(__name__)

class SecurityChecker:
    """Vérificateur de sécurité pour les variables d'environnement"""
    
    WEAK_PATTERNS = [
        (r'password.*=.*(password|123|admin)', "Mot de passe faible détecté"),
        (r'key.*=.*(test|dev|example)', "Clé de test en production"),
        (r'secret.*=.{1,10}$', "Secret trop court"),
        (r'token.*=.*(demo|sample)', "Token de démonstration"),
    ]
    
    REQUIRED_PRODUCTION = [
        "SECRET_KEY", "DB_PASSWORD", "SENTRY_DSN"
    ]
    
    def __init__(self, environment: str = "production"):
        self.environment = environment
        self.issues: List[Tuple[str, str]] = []
    
    def check_file(self, env_file: str) -> None:
        """Vérifie un fichier d'environnement"""
        
        if not Path(env_file).exists():
            self.issues.append(("ERROR", f"Fichier {env_file} non trouvé"))
            return
        
        with open(env_file) as f:
            content = f.read()
        
        # Vérifier les patterns dangereux
        for pattern, message in self.WEAK_PATTERNS:
            if re.search(pattern, content, re.IGNORECASE):
                self.issues.append(("WARNING", f"{message} dans {env_file}"))
        
        # Vérifier les variables requises en production
        if self.environment == "production":
            for var in self.REQUIRED_PRODUCTION:
                if not re.search(f'^{var}=.+', content, re.MULTILINE):
                    self.issues.append(("ERROR", f"Variable {var} manquante"))
    
    def check_environment(self) -> None:
        """Vérifie les variables d'environnement actuelles"""
        
        for var in self.REQUIRED_PRODUCTION:
            value = os.getenv(var)
            if not value:
                self.issues.append(("ERROR", f"Variable {var} non définie"))
            elif len(value) < 10:
                self.issues.append(("WARNING", f"Variable {var} trop courte"))
    
    def report(self) -> bool:
        """Génère un rapport de sécurité"""
        
        if not self.issues:
            logger.info("✓ Aucun problème de sécurité détecté")
            return True
        
        errors = [issue for issue in self.issues if issue[0] == "ERROR"]
        warnings = [issue for issue in self.issues if issue[0] == "WARNING"]
        
        if errors:
            logger.error("❌ Erreurs de sécurité critiques:")
            for _, message in errors:
                logger.error(f"  - {message}")
        
        if warnings:
            logger.warning("⚠️  Avertissements de sécurité:")
            for _, message in warnings:
                logger.warning(f"  - {message}")
        
        return len(errors) == 0

# Utilisation
checker = SecurityChecker("production")
checker.check_file(".env.production")
checker.check_environment()

if not checker.report():
    exit(1)

2. Rotation des Secrets

# security/secret_rotation.py

import os
import secrets
import string
from datetime import datetime, timedelta
from typing import Dict, List
import boto3
from pathlib import Path

class SecretRotator:
    """Gestionnaire de rotation des secrets"""
    
    def __init__(self):
        self.aws_client = boto3.client('secretsmanager')
        self.rotation_schedule = {
            "SECRET_KEY": timedelta(days=90),
            "DB_PASSWORD": timedelta(days=30),
            "API_KEYS": timedelta(days=60)
        }
    
    def generate_secret(self, secret_type: str, length: int = 32) -> str:
        """Génère un nouveau secret"""
        
        if secret_type == "password":
            # Mot de passe avec caractères spéciaux
            alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
        elif secret_type == "api_key":
            # Clé API alphanumérique
            alphabet = string.ascii_letters + string.digits
        else:
            # Secret générique
            alphabet = string.ascii_letters + string.digits + "!@#$%^&*-_"
        
        return ''.join(secrets.choice(alphabet) for _ in range(length))
    
    def rotate_secret(self, secret_name: str, secret_type: str = "generic") -> str:
        """Effectue la rotation d'un secret"""
        
        new_secret = self.generate_secret(secret_type)
        
        try:
            # Stocker dans AWS Secrets Manager
            self.aws_client.update_secret(
                SecretId=secret_name,
                SecretString=new_secret
            )
            
            # Mettre à jour localement
            self._update_env_file(secret_name, new_secret)
            
            return new_secret
            
        except Exception as e:
            raise RuntimeError(f"Erreur lors de la rotation de {secret_name}: {e}")
    
    def _update_env_file(self, key: str, value: str) -> None:
        """Met à jour un fichier .env"""
        
        env_file = ".env.production"
        if not Path(env_file).exists():
            return
        
        lines = []
        updated = False
        
        with open(env_file) as f:
            for line in f:
                if line.startswith(f"{key}="):
                    lines.append(f"{key}={value}\n")
                    updated = True
                else:
                    lines.append(line)
        
        if not updated:
            lines.append(f"{key}={value}\n")
        
        with open(env_file, 'w') as f:
            f.writelines(lines)

💡Conclusion

Les variables d'environnement sont essentielles pour :

Points Clés à Retenir

  1. Jamais de secrets dans le code - Utilisez toujours des variables d'environnement
  2. Validation stricte - Vérifiez la présence et le format des variables critiques
  3. Configuration par environnement - Adaptez les valeurs selon dev/staging/prod
  4. Sécurité renforcée - Auditez et faites la rotation des secrets régulièrement
  5. Documentation - Maintenez des templates et de la documentation à jour

En suivant ces pratiques, vos scripts Python seront plus sécurisés, maintenables et déployables dans tous les environnements.

À propos de l'auteur

Florian Courouge - Expert DevOps et Apache Kafka avec plus de 5 ans d'expérience dans l'architecture de systèmes distribués et l'automatisation d'infrastructures.

Cet article vous a été utile ?

Découvrez mes autres articles techniques ou contactez-moi pour discuter de vos projets DevOps et Kafka.