OWASP Top 10 : Vulnérabilités Web et Protection Complète
Introduction : Comprendre les Menaces Web Modernes L'OWASP (Open Web Application Security Project) publie tous…
L’exposition accidentelle de secrets (mots de passe, clés API, tokens) dans le code source est l’une des failles de sécurité les plus courantes et les plus dangereuses. En 2025, GitHub détecte en moyenne 2 millions de secrets exposés par an dans les dépôts publics.
Ce guide complet explore les meilleures pratiques de gestion des secrets : variables d’environnement, fichiers .env sécurisés, solutions de vaults (Vault, AWS Secrets Manager), et rotation automatique des secrets.
PASSWORD', 'SuperSecret123!');
define('APIKEY', 'sklive51Hxyz...');
define('JWTSECRET', 'my-super-secret-key');
// Ce fichier est commité dans Git
// → Visible dans l'historique pour toujours
// → Accessible aux anciens employés
// → Exposé si le dépôt devient public
# Scénario réel (2024)
# 1. Développeur commit accidentellement .env dans Git
git add .
git commit -m "Initial commit"
git push
# 2. Bot de scraping détecte le secret en 5 minutes
# 3. Compte AWS compromis en 1 heure
# 4. 10 000€ de facture EC2 en 24h
# 5. Données clients exfiltrées
# Même après suppression, le secret reste dans l'historique Git
git log --all --full-history -- .env
# → Toujours accessible !
# .env - Configuration de développement local
# ⚠️ IMPORTANT : Ce fichier NE DOIT JAMAIS être commité dans Git
# Base de données
DBHOST=localhost
DBPORT=3306
DBNAME=myappdev
DBUSER=devuser
DBPASSWORD=devpasswordchangeme
# API Externe
STRIPEAPIKEY=sktest51Hxyz...
STRIPEWEBHOOKSECRET=whsec...
SENDGRIDAPIKEY=SG.xyz...
# Application
APPENV=development
APPDEBUG=true
APPURL=http://localhost:8000
# Sécurité
JWTSECRET=generate-with-openssl-rand-base64-32
ENCRYPTIONKEY=exactly-32-characters-long!!!
# Services Tiers
REDISHOST=127.0.0.1
REDISPORT=6379
REDISPASSWORD=
# OAuth
GOOGLECLIENTID=123456789-xxx.apps.googleusercontent.com
GOOGLECLIENTSECRET=GOCSPX-xxx
# .env.example - Template à commiter dans Git
# Les développeurs copient ce fichier en .env et remplissent les valeurs
# Base de données
DBHOST=localhost
DBPORT=3306
DBNAME=
DBUSER=
DBPASSWORD=
# API Externe
STRIPEAPIKEY=
STRIPEWEBHOOKSECRET=
SENDGRIDAPIKEY=
# Application
APPENV=development
APPDEBUG=true
APPURL=http://localhost:8000
# Sécurité (générer avec: openssl rand -base64 32)
JWTSECRET=
ENCRYPTIONKEY=
# Services Tiers
REDISHOST=127.0.0.1
REDISPORT=6379
REDISPASSWORD=
# OAuth
GOOGLECLIENTID=
GOOGLECLIENTSECRET=
# .gitignore - Empêcher le commit de secrets
# Variables d'environnement
.env
.env.local
.env..local
.env.production
# Fichiers de configuration sensibles
config/secrets.yml
config/credentials.yml
secrets.json
private-key.pem
.key
.pem
# Fichiers IDE contenant parfois des secrets
.vscode/settings.json
.idea/workspace.xml
# Logs pouvant contenir des secrets
.log
logs/
# Backups de base de données
.sql
.dump
backups/
Gestionnaire de variables d'environnement sécurisé
/
class EnvironmentManager {
private static bool $loaded = false;
private static array $required = [];
/
Charge le fichier .env
/
public static function load(string $path = DIR . '/../.env'): void {
if (self::$loaded) {
return;
}
if (!fileexists($path)) {
throw new RuntimeException(".env file not found at {$path}");
}
// Vérifier les permissions (doit être 600 ou 400)
$perms = substr(sprintf('%o', fileperms($path)), -3);
if ($perms !== '600' && $perms !== '400') {
triggererror(
".env file has insecure permissions ({$perms}). Should be 600 or 400.",
EUSERWARNING
);
}
$lines = file($path, FILEIGNORENEWLINES | FILESKIPEMPTYLINES);
foreach ($lines as $line) {
// Ignorer les commentaires
if (strpos(trim($line), '#') === 0) {
continue;
}
// Parser KEY=VALUE
if (strpos($line, '=') !== false) {
[$key, $value] = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
// Supprimer les guillemets si présents
$value = trim($value, '"'');
// Ne pas écraser les variables déjà définies (priorité ENV système)
if (!isset($ENV[$key]) && !isset($SERVER[$key])) {
$ENV[$key] = $value;
$SERVER[$key] = $value;
putenv("{$key}={$value}");
}
}
}
self::$loaded = true;
}
/
Récupère une variable d'environnement
/
public static function get(string $key, $default = null) {
return $ENV[$key] ?? $SERVER[$key] ?? getenv($key) ?: $default;
}
/
Récupère une variable requise (lève une exception si absente)
/
public static function require(string $key): string {
$value = self::get($key);
if ($value === null || $value === '') {
throw new RuntimeException(
"Required environment variable '{$key}' is not set"
);
}
return $value;
}
/
Définit les variables requises
/
public static function setRequired(array $keys): void {
self::$required = $keys;
}
/
Valide que toutes les variables requises sont définies
/
public static function validate(): void {
$missing = [];
foreach (self::$required as $key) {
if (self::get($key) === null || self::get($key) === '') {
$missing[] = $key;
}
}
if (!empty($missing)) {
throw new RuntimeException(
"Missing required environment variables: " . implode(', ', $missing)
);
}
}
/
Vérifie si on est en production
/
public static function isProduction(): bool {
return self::get('APPENV') === 'production';
}
/
Vérifie si on est en développement
/
public static function isDevelopment(): bool {
return self::get('APPENV') === 'development';
}
}
// Utilisation dans votre bootstrap
requireonce DIR . '/EnvironmentManager.php';
// Charger les variables
EnvironmentManager::load(DIR . '/../.env');
// Définir les variables obligatoires
EnvironmentManager::setRequired([
'DBHOST',
'DBNAME',
'DBUSER',
'DBPASSWORD',
'JWTSECRET',
'ENCRYPTIONKEY'
]);
// Valider
EnvironmentManager::validate();
// Utiliser les variables
$dbHost = EnvironmentManager::require('DBHOST');
$apiKey = EnvironmentManager::get('STRIPEAPIKEY', 'default-test-key');
if (EnvironmentManager::isProduction()) {
// Configuration production
iniset('displayerrors', '0');
errorreporting(0);
} else {
// Configuration développement
iniset('displayerrors', '1');
errorreporting(EALL);
}
once DIR . '/vendor/autoload.php';
use DotenvDotenv;
// Charger le .env
$dotenv = Dotenv::createImmutable(DIR . '/..');
$dotenv->load();
// Exiger certaines variables
$dotenv->required(['DBHOST', 'DBNAME', 'DBUSER', 'DBPASSWORD']);
// Exiger avec validation
$dotenv->required('APPENV')->allowedValues(['development', 'staging', 'production']);
$dotenv->required('APPDEBUG')->isBoolean();
$dotenv->required('DBPORT')->isInteger();
// Utilisation
$dbHost = $ENV['DBHOST'];
$dbPassword = $ENV['DBPASSWORD'];
// Connexion base de données
$pdo = new PDO(
"mysql:host={$ENV['DBHOST']};dbname={$ENV['DBNAME']};charset=utf8mb4",
$ENV['DBUSER'],
$ENV['DBPASSWORD'],
[
PDO::ATTRERRMODE => PDO::ERRMODEEXCEPTION,
PDO::ATTRDEFAULTFETCHMODE => PDO::FETCHASSOC
]
);
#!/bin/bash
# generate-secrets.sh - Générateur de secrets sécurisés
echo "=== Générateur de Secrets Sécurisés ==="
echo
# 1. Clé de chiffrement 32 octets (256 bits)
echo "Clé de chiffrement (32 octets pour AES-256) :"
openssl rand -base64 32
echo
# 2. JWT Secret
echo "JWT Secret :"
openssl rand -base64 64
echo
# 3. Mot de passe aléatoire
echo "Mot de passe fort (32 caractères) :"
openssl rand -base64 32 | tr -d "=+/" | cut -c1-32
echo
# 4. API Key format custom
echo "API Key :"
echo "sklive$(openssl rand -hex 32)"
echo
# 5. UUID v4
echo "UUID v4 :"
uuidgen
echo
# 6. Hash SHA-256 d'une valeur
echo "Hash SHA-256 de 'secret-value' :"
echo -n "secret-value" | openssl dgst -sha256 | cut -d' ' -f2
echo
#!/bin/bash
# setup-env.sh - Configuration initiale du .env
ENVFILE=".env"
EXAMPLEFILE=".env.example"
if [ -f "$ENVFILE" ]; then
echo "❌ Le fichier .env existe déjà. Supprimez-le d'abord si vous voulez le régénérer."
exit 1
fi
if [ ! -f "$EXAMPLEFILE" ]; then
echo "❌ Le fichier .env.example n'existe pas."
exit 1
fi
echo "🔧 Configuration de l'environnement..."
echo
# Copier le template
cp "$EXAMPLEFILE" "$ENVFILE"
# Générer des secrets
JWTSECRET=$(openssl rand -base64 64 | tr -d 'n')
ENCRYPTIONKEY=$(openssl rand -base64 32 | head -c 32)
APPKEY=$(openssl rand -base64 32 | tr -d 'n')
# Injecter les secrets dans .env
sed -i "s|JWTSECRET=|JWTSECRET=$JWTSECRET|" "$ENVFILE"
sed -i "s|ENCRYPTIONKEY=|ENCRYPTIONKEY=$ENCRYPTIONKEY|" "$ENVFILE"
sed -i "s|APPKEY=|APPKEY=$APPKEY|" "$ENVFILE"
# Sécuriser les permissions
chmod 600 "$ENVFILE"
echo "✅ Fichier .env créé avec succès"
echo "⚠️ N'oubliez pas de compléter les autres variables (DB, API keys, etc.)"
echo
echo "Secrets générés :"
echo "- JWTSECRET: [64 caractères]"
echo "- ENCRYPTIONKEY: [32 caractères]"
echo "- APPKEY: [44 caractères]"
echo
echo "📝 Permissions définies à 600 (lecture/écriture propriétaire uniquement)"
# Installation de Vault
wget https://releases.hashicorp.com/vault/1.15.0/vault1.15.0linuxamd64.zip
unzip vault1.15.0linuxamd64.zip
sudo mv vault /usr/local/bin/
vault --version
# Démarrer Vault en mode développement (UNIQUEMENT pour tests locaux)
vault server -dev
# Dans un autre terminal, exporter l'adresse et le token
export VAULTADDR='http://127.0.0.1:8200'
export VAULTTOKEN='hvs.xyz...' # Affiché au démarrage du serveur
# Créer des secrets
vault kv put secret/myapp/db
host=localhost
port=3306
username=myappuser
password=SuperSecretPassword123
vault kv put secret/myapp/api
stripekey=sklivexyz
sendgridkey=SG.xyz
# Lire des secrets
vault kv get secret/myapp/db
vault kv get -field=password secret/myapp/db
Client HashiCorp Vault
/
class VaultClient {
private string $address;
private string $token;
private string $namespace;
public function construct(
string $address = 'http://127.0.0.1:8200',
?string $token = null,
string $namespace = 'secret/data'
) {
$this->address = rtrim($address, '/');
$this->token = $token ?? getenv('VAULTTOKEN');
$this->namespace = trim($namespace, '/');
if (!$this->token) {
throw new RuntimeException('VAULTTOKEN not set');
}
}
/
Récupère un secret
/
public function getSecret(string $path): array {
$url = "{$this->address}/v1/{$this->namespace}/{$path}";
$ch = curlinit($url);
curlsetoptarray($ch, [
CURLOPTRETURNTRANSFER => true,
CURLOPTHTTPHEADER => [
"X-Vault-Token: {$this->token}",
'Content-Type: application/json'
]
]);
$response = curlexec($ch);
$httpCode = curlgetinfo($ch, CURLINFOHTTPCODE);
curlclose($ch);
if ($httpCode !== 200) {
throw new RuntimeException("Failed to fetch secret from Vault: HTTP {$httpCode}");
}
$data = jsondecode($response, true);
if (!isset($data['data']['data'])) {
throw new RuntimeException('Invalid response from Vault');
}
return $data['data']['data'];
}
/
Écrit un secret
/
public function putSecret(string $path, array $data): void {
$url = "{$this->address}/v1/{$this->namespace}/{$path}";
$payload = jsonencode(['data' => $data]);
$ch = curlinit($url);
curlsetoptarray($ch, [
CURLOPTRETURNTRANSFER => true,
CURLOPTPOST => true,
CURLOPTPOSTFIELDS => $payload,
CURLOPTHTTPHEADER => [
"X-Vault-Token: {$this->token}",
'Content-Type: application/json'
]
]);
$response = curlexec($ch);
$httpCode = curlgetinfo($ch, CURLINFOHTTPCODE);
curlclose($ch);
if ($httpCode !== 200 && $httpCode !== 204) {
throw new RuntimeException("Failed to write secret to Vault: HTTP {$httpCode}");
}
}
/
Supprime un secret
/
public function deleteSecret(string $path): void {
$url = "{$this->address}/v1/{$this->namespace}/{$path}";
$ch = curlinit($url);
curlsetoptarray($ch, [
CURLOPTRETURNTRANSFER => true,
CURLOPTCUSTOMREQUEST => 'DELETE',
CURLOPTHTTPHEADER => [
"X-Vault-Token: {$this->token}"
]
]);
curlexec($ch);
$httpCode = curlgetinfo($ch, CURLINFOHTTPCODE);
curlclose($ch);
if ($httpCode !== 204) {
throw new RuntimeException("Failed to delete secret from Vault: HTTP {$httpCode}");
}
}
}
// Utilisation
$vault = new VaultClient('http://127.0.0.1:8200', getenv('VAULTTOKEN'));
// Récupérer les credentials de base de données
$dbSecrets = $vault->getSecret('myapp/db');
$dbPassword = $dbSecrets['password'];
// Connexion à la base de données
$pdo = new PDO(
"mysql:host={$dbSecrets['host']};dbname=myapp",
$dbSecrets['username'],
$dbSecrets['password']
);
// Stocker un nouveau secret
$vault->putSecret('myapp/stripe', [
'apikey' => 'sklivexyz',
'webhooksecret' => 'whsecxyz'
]);
Client AWS Secrets Manager
/
class AWSSecretsManager {
private SecretsManagerClient $client;
public function construct(string $region = 'eu-west-1') {
$this->client = new SecretsManagerClient([
'version' => 'latest',
'region' => $region,
// Les credentials AWS sont chargées depuis :
// 1. Variables d'env (AWSACCESSKEYID, AWSSECRETACCESSKEY)
// 2. Fichier ~/.aws/credentials
// 3. IAM role (si sur EC2/ECS)
]);
}
/
Récupère un secret
/
public function getSecret(string $secretName): array {
try {
$result = $this->client->getSecretValue([
'SecretId' => $secretName
]);
// Les secrets sont stockés en JSON
if (isset($result['SecretString'])) {
return jsondecode($result['SecretString'], true);
}
// Ou en binaire
if (isset($result['SecretBinary'])) {
return jsondecode(base64decode($result['SecretBinary']), true);
}
throw new RuntimeException('Secret has no value');
} catch (AwsException $e) {
throw new RuntimeException(
"Failed to fetch secret '{$secretName}': " . $e->getMessage()
);
}
}
/
Crée ou met à jour un secret
/
public function putSecret(string $secretName, array $secretValue): void {
$secretString = jsonencode($secretValue);
try {
// Essayer de mettre à jour
$this->client->updateSecret([
'SecretId' => $secretName,
'SecretString' => $secretString
]);
} catch (AwsException $e) {
// Si le secret n'existe pas, le créer
if ($e->getAwsErrorCode() === 'ResourceNotFoundException') {
$this->client->createSecret([
'Name' => $secretName,
'SecretString' => $secretString
]);
} else {
throw $e;
}
}
}
/
Supprime un secret (avec période de récupération)
/
public function deleteSecret(string $secretName, int $recoveryDays = 30): void {
$this->client->deleteSecret([
'SecretId' => $secretName,
'RecoveryWindowInDays' => $recoveryDays
]);
}
}
// Utilisation
$secrets = new AWSSecretsManager('eu-west-1');
// Récupérer les secrets de base de données
$dbCreds = $secrets->getSecret('prod/myapp/database');
$pdo = new PDO(
"mysql:host={$dbCreds['host']};dbname={$dbCreds['database']}",
$dbCreds['username'],
$dbCreds['password']
);
// Stocker un nouveau secret
$secrets->putSecret('prod/myapp/stripe', [
'apikey' => 'sklivexyz',
'webhooksecret' => 'whsecxyz'
]);
Gestionnaire de rotation de secrets
/
class SecretRotationManager {
private PDO $pdo;
private VaultClient $vault;
public function construct(PDO $pdo, VaultClient $vault) {
$this->pdo = $pdo;
$this->vault = $vault;
}
/
Effectue la rotation d'un mot de passe de base de données
/
public function rotateDBPassword(string $username): void {
echo "Démarrage de la rotation pour l'utilisateur {$username}...n";
// 1. Générer un nouveau mot de passe
$newPassword = $this->generateSecurePassword();
// 2. Mettre à jour dans la base de données
$stmt = $this->pdo->prepare("ALTER USER ?@'%' IDENTIFIED BY ?");
$stmt->execute([$username, $newPassword]);
echo "✓ Mot de passe mis à jour dans MySQLn";
// 3. Mettre à jour dans Vault
$currentSecrets = $this->vault->getSecret('myapp/db');
$currentSecrets['password'] = $newPassword;
$currentSecrets['rotatedat'] = date('Y-m-d H:i:s');
$this->vault->putSecret('myapp/db', $currentSecrets);
echo "✓ Mot de passe mis à jour dans Vaultn";
// 4. Tester la nouvelle connexion
$this->testConnection($currentSecrets);
echo "✓ Connexion testée avec succèsn";
// 5. Notifier l'équipe
$this->notifyRotation($username);
echo "✓ Notification envoyéen";
echo "Rotation terminée avec succès.n";
}
/
Génère un mot de passe sécurisé
/
private function generateSecurePassword(int $length = 32): string {
$uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$lowercase = 'abcdefghijklmnopqrstuvwxyz';
$numbers = '0123456789';
$special = '!@#$%^&()-=+[]{}|;:,.<>?';
$allChars = $uppercase . $lowercase . $numbers . $special;
$password = '';
$password .= $uppercase[randomint(0, strlen($uppercase) - 1)];
$password .= $lowercase[randomint(0, strlen($lowercase) - 1)];
$password .= $numbers[randomint(0, strlen($numbers) - 1)];
$password .= $special[randomint(0, strlen($special) - 1)];
for ($i = 4; $i < $length; $i++) {
$password .= $allChars[randomint(0, strlen($allChars) - 1)];
}
return strshuffle($password);
}
/
Teste une connexion avec les nouveaux credentials
/
private function testConnection(array $credentials): void {
try {
$testPdo = new PDO(
"mysql:host={$credentials['host']};dbname=mysql",
$credentials['username'],
$credentials['password']
);
$testPdo->query('SELECT 1');
} catch (PDOException $e) {
throw new RuntimeException('Connection test failed: ' . $e->getMessage());
}
}
/
Notifie l'équipe de la rotation
/
private function notifyRotation(string $username): void {
// Implémenter selon votre système de notification
// Slack, email, etc.
$message = "Secret rotation completed for database user: {$username}";
// Exemple : webhook Slack
$webhook = getenv('SLACKWEBHOOKURL');
if ($webhook) {
$data = jsonencode(['text' => $message]);
$ch = curlinit($webhook);
curlsetopt($ch, CURLOPTPOSTFIELDS, $data);
curlsetopt($ch, CURLOPTHTTPHEADER, ['Content-Type:application/json']);
curlsetopt($ch, CURLOPTRETURNTRANSFER, true);
curlexec($ch);
curlclose($ch);
}
}
}
// Script CLI pour rotation manuelle
if (phpsapiname() === 'cli') {
$vault = new VaultClient();
$dbSecrets = $vault->getSecret('myapp/db');
$pdo = new PDO(
"mysql:host={$dbSecrets['host']};dbname=mysql",
'root', // Utilisateur avec permissions ALTER USER
getenv('MYSQLROOTPASSWORD')
);
$rotationManager = new SecretRotationManager($pdo, $vault);
$rotationManager->rotateDBPassword('myappuser');
}
#!/bin/bash
# /etc/cron.d/rotate-secrets
# Rotation tous les 90 jours à 2h du matin
0 2 /90 /usr/bin/php /var/www/scripts/rotate-db-password.php >> /var/log/secret-rotation.log 2>&1
# Installation de git-secrets
git clone https://github.com/awslabs/git-secrets.git
cd git-secrets
sudo make install
# Configuration dans votre repository
cd /path/to/your/repo
git secrets --install
# Ajouter des patterns à détecter
git secrets --register-aws # Patterns AWS
# Patterns custom
git secrets --add 'passwords=s["'][^"']+["']'
git secrets --add 'api[-]?keys=s["'][^"']+["']'
git secrets --add 'secrets=s["'][^"']+["']'
git secrets --add '[a-zA-Z0-9+/]{40,}' # Tokens base64
# Activer pour tous les repos futurs
git secrets --install ~/.git-templates/git-secrets
git config --global init.templateDir ~/.git-templates/git-secrets
# Scanner l'historique complet
git secrets --scan-history
# Installation
pip install trufflehog
# Scanner un dépôt
trufflehog git https://github.com/votre-org/votre-repo
# Scanner le dépôt local
trufflehog filesystem /path/to/repo
# Scanner avec règles custom
trufflehog --rules custom-rules.json git file:///path/to/repo
# Scanner uniquement depuis un commit
trufflehog --since-commit abc123 git file:///path/to/repo
# .github/workflows/secret-scan.yml
name: Secret Scanning
on:
push:
branches: [ main, develop ]
pullrequest:
branches: [ main, develop ]
jobs:
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Scanner tout l'historique
- name: TruffleHog Scan
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.defaultbranch }}
head: HEAD
- name: GitLeaks Scan
uses: gitleaks/gitleaks-action@v2
env:
GITHUBTOKEN: ${{ secrets.GITHUBTOKEN }}
# Checklist Gestion des Secrets
## Configuration
- [ ] Fichier .env créé avec permissions 600
- [ ] .env ajouté au .gitignore
- [ ] .env.example fourni pour l'équipe
- [ ] Variables requises documentées
- [ ] Secrets générés de manière cryptographiquement sécurisée
## Stockage
- [ ] Aucun secret dans le code source
- [ ] Aucun secret dans les fichiers de configuration commités
- [ ] Secrets stockés dans variables d'environnement ou vault
- [ ] Pas de secrets dans les logs
- [ ] Pas de secrets dans les messages de commit
## Production
- [ ] Variables d'environnement définies au niveau du serveur/conteneur
- [ ] Vault ou Secrets Manager utilisé
- [ ] Rotation des secrets configurée
- [ ] Accès aux secrets audité et loggé
- [ ] Secrets chiffrés au repos
## Détection
- [ ] git-secrets installé et configuré
- [ ] Pre-commit hooks actifs
- [ ] Scan automatique dans CI/CD
- [ ] Historique Git scanné
- [ ] Alertes configurées pour détection de secrets
## Processus
- [ ] Procédure de rotation documentée
- [ ] Plan de réponse en cas d'exposition
- [ ] Accès aux secrets restreint (principe du moindre privilège)
- [ ] Secrets révoqués à la suppression du compte
- [ ] Audit régulier des accès aux secrets
Une startup française gérant 50 000 utilisateurs stockait ses secrets dans des fichiers .env sur les serveurs. Un départ d’employé a créé un risque de sécurité.
Migration progressive vers AWS Secrets Manager
/
class SecretsMigration {
private AWSSecretsManager $aws;
private array $localEnv;
public function construct() {
$this->aws = new AWSSecretsManager('eu-west-1');
$this->loadLocalEnv();
}
/
Charge les variables locales
/
private function loadLocalEnv(): void {
if (fileexists(DIR . '/../.env')) {
$lines = file(DIR . '/../.env', FILEIGNORENEWLINES | FILESKIPEMPTYLINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0 || strpos($line, '=') === false) {
continue;
}
[$key, $value] = explode('=', $line, 2);
$this->localEnv[trim($key)] = trim($value, '"'');
}
}
}
/
Migre tous les secrets vers AWS
/
public function migrateToAWS(): void {
$secretsToMigrate = [
'database' => [
'DBHOST', 'DBPORT', 'DBNAME', 'DBUSER', 'DBPASSWORD'
],
'stripe' => [
'STRIPEAPIKEY', 'STRIPEWEBHOOKSECRET'
],
'jwt' => [
'JWTSECRET', 'JWTISSUER', 'JWT_AUDIENCE'
]
];
foreach ($secretsToMigrate as $secretName => $keys) {
$secretData = [];
foreach ($keys as $key) {
if (isset($this->localEnv[$key])) {
$secretData[strtolower($key)] = $this->localEnv[$key];
}
}
if (!empty($secretData)) {
$this->aws->putSecret("prod/myapp/{$secretName}", $secretData);
echo "✓ Migré : {$secretName}n";
}
}
echo "nMigration terminée. Vérifiez les secrets dans AWS Console.n";
echo "Prochaine étape : Mettre à jour le code pour charger depuis AWS.n";
}
}
// Exécution
$migration = new SecretsMigration();
$migration->migrateToAWS();
La gestion des secrets est une discipline critique de la sécurité applicative. Une exposition accidentelle peut coûter très cher, tant financièrement qu’en termes de réputation.
Commencez simplement avec des fichiers .env sécurisés, puis évoluez vers des solutions de vault quand votre application grandit. L’important est de ne jamais compromettre la sécurité des secrets.
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.