Sécurité MySQL : pare-feu, TLS, privilèges minimaux et audit

Durcissement MySQL pour PrestaShop : isoler la DB, forcer TLS, séparer comptes runtime/deploy/admin, journaliser utilement et suivre une checklist de production.

Écran avec des icônes de sécurité et de protection des données.

Table des matières :

  1. Exposition réseau : isoler MySQL et poser un pare-feu (L3/L4) qui ne pardonne pas
  2. TLS MySQL : chiffrer le trafic et verrouiller la négociation
  3. Privilèges minimaux : séparer “runtime”, “déploiement” et “administration”
  4. Audit MySQL : journaliser sans se tirer une balle dans le disque
  5. Checklist de durcissement MySQL pour une stack PrestaShop (8.1 / 9.x) sans casser la prod

La sécurité MySQL (et, par extension, MariaDB) n’est pas un « bonus » d’infra : c’est un prérequis dès que vous stockez des comptes clients, paniers, tokens, logs applicatifs ou données de paiement indirectes. Sur une stack PrestaShop, la base est souvent le point unique de compromission : un port 3306 exposé, un compte applicatif trop permissif, ou un trafic non chiffré sur un réseau « interne » suffisent.

Dans un contexte e‑commerce en France/UE, ce durcissement aide aussi à démontrer des « mesures techniques appropriées » (chiffrement en transit, contrôle d’accès, journalisation) au sens de l’article 32 du RGPD (référence : texte du règlement sur EUR‑Lex). L’objectif n’est pas de cocher des cases, mais de réduire concrètement : (1) la probabilité d’intrusion et (2) l’impact si un secret applicatif ou un compte est compromis.

Exposition réseau : isoler MySQL et poser un pare-feu (L3/L4) qui ne pardonne pas

Le premier « pare-feu MySQL » n’est pas un plugin : c’est la topologie réseau + le filtrage L3/L4. Dans la vraie vie, la majorité des incidents MySQL commencent par une exposition : port 3306/33060 ouvert sur Internet (scan massif), accès depuis un réseau mutualisé non cloisonné, ou mauvaise règle de security group. Une base accessible publiquement subit du bruteforce en continu, et votre vrai risque n’est pas la force brute en elle‑même, mais l’exploitation d’un mot de passe réutilisé, d’un compte oublié, ou d’une faille de config (auth plugin faible, comptes anonymes, etc.).

Avant même de parler firewall, posez un principe d’architecture simple (valable on‑prem, cloud, OVHcloud/Scaleway/AWS, etc.) : la base vit sur un segment privé et n’a pas besoin d’IP publique. Un bon « test de réalité » : si vous pouvez vous connecter à MySQL depuis votre laptop sur un Wi‑Fi public sans VPN/bastion, c’est quasiment toujours trop ouvert.

Côté serveur, la règle est simple : ne pas écouter là où ce n’est pas nécessaire. Sur Debian 12 / Ubuntu 24.04 avec MySQL 8.0 ou MariaDB 10.11 LTS, commencez par forcer le bind sur une IP privée (ou localhost si l’app est sur le même hôte) :

# /etc/mysql/mysql.conf.d/mysqld.cnf (MySQL) ou /etc/mysql/mariadb.conf.d/50-server.cnf (MariaDB)
[mysqld]
bind-address = 127.0.0.1
# ou bind-address = 10.0.1.5 (IP privée du serveur DB)
skip-name-resolve = ON

Deux durcissements souvent oubliés, selon votre contexte :

  • Si la DB et l’app sont sur le même hôte, vous pouvez envisager skip-networking (connexions via socket Unix uniquement). C’est radical, mais très efficace pour éliminer toute exposition TCP.
  • Si vous n’utilisez pas MySQL X Protocol, vérifiez l’exposition de 33060. Sur certaines installations MySQL, ce port peut être actif via le plugin X ; inutile de l’ouvrir si votre stack n’en a pas besoin.

skip-name-resolve=ON évite les résolutions DNS lors de l’authentification (latence + surface d’attaque via DNS). Le corollaire : tous vos GRANT doivent être IP-based ('user'@'10.0.1.12' ou CIDR compatible selon moteur), et vous devez arrêter de compter sur des hosts DNS dans les privilèges. Pensez aussi à l’impact ops : si votre infra bouge (autoscaling, changement d’IP), vous devrez gérer proprement la source IP (NAT fixe, bastion, ou réseau privé stable).

Ensuite, filtrez au niveau OS (nftables/iptables/ufw) en allowlist stricte. Exemple minimal en nftables (adapter interfaces et IPs) :

table inet filter {
  chain input {
    type filter hook input priority 0;

    ct state established,related accept
    iif lo accept

    # Autoriser MySQL uniquement depuis les serveurs applicatifs
    tcp dport 3306 ip saddr { 10.0.1.10, 10.0.1.11 } accept

    # Tout le reste est refusé
    tcp dport 3306 drop
  }
}

Checklist réseau pragmatique (rapide à auditer en prod) :

  • Port 3306 non exposé sur Internet (pas d’IP publique, pas de rule 0.0.0.0/0).
  • Allowlist limitée aux IP(s) applicatives et/ou bastion d’admin.
  • Même logique appliquée dans le cloud (security groups / ACL) et au niveau OS.
  • Accès admin DB uniquement via VPN/bastion (pas « depuis n’importe où »).
  • Tests automatisés : un scan depuis une machine hors allowlist doit échouer (ex. nc -vz db 3306).

Si vous êtes sur cloud, appliquez la même logique via security groups / ACL réseau, et ne vous contentez pas du firewall local : le filtrage en amont réduit le bruit (et donc les logs) et limite les scénarios DDoS au niveau TCP. Pour une stack standard, vous pouvez vous caler sur une configuration de référence type « serveur dédié PrestaShop » (Nginx + PHP-FPM + MariaDB) et l’isolant réseau associé : voir la configuration serveur dédiée pour PrestaShop (Nginx, PHP-FPM, MariaDB, Redis).

Enfin, si vous avez besoin d’un pare-feu “SQL” (filtrage de requêtes), ne fantasmez pas un module magique côté MySQL communautaire : regardez plutôt un proxy L7 comme ProxySQL (règles par user, pattern de requêtes, throttling) ou une segmentation par comptes (runtime vs maintenance). C’est plus robuste qu’essayer de « sécuriser par la requête » dans le serveur lui‑même, et ça s’intègre bien en DevOps (canary, rollbacks). Bonus opérationnel : en cas d’incident, un proxy L7 peut aussi servir de point unique de télémétrie (qui se connecte, à quelle fréquence, avec quel user).

TLS MySQL : chiffrer le trafic et verrouiller la négociation

Chiffrer le trafic MySQL n’est pas réservé aux « gros » : dès que votre serveur web et votre serveur DB ne sont pas la même machine (ou que vous traversez un réseau que vous ne contrôlez pas à 100%), TLS devient le minimum. Sur un cluster Kubernetes, un VLAN mutualisé, ou même un simple bridge Docker mal isolé, l’argument “c’est interne” ne tient pas. Et si vous utilisez des réplicas/ETL/outils BI, vous multipliez les chemins réseau donc les points d’écoute.

En pratique, vous forcez TLS, vous coupez les clients legacy, et vous arrêtez d’avoir des connexions “opportunistes” non chiffrées.

Sur MySQL 8.0, vous pouvez générer rapidement une PKI minimale (à évaluer selon votre politique interne) via mysql_ssl_rsa_setup ou OpenSSL. Exemple de configuration serveur (chemins à adapter) :

[mysqld]
require_secure_transport = ON
ssl_ca   = /etc/mysql/ssl/ca.pem
ssl_cert = /etc/mysql/ssl/server-cert.pem
ssl_key  = /etc/mysql/ssl/server-key.pem

# Réduire le scope TLS (selon compatibilité clients)
tls_version = TLSv1.2,TLSv1.3

Deux points concrets qui évitent des incidents « bêtes » :

  • Rotation des certificats : prévoyez une durée de vie raisonnable (ex. 90 jours / 1 an selon votre PKI) et un runbook de rotation. Un certificat expiré sur la DB = panne applicative immédiate si vous avez require_secure_transport=ON.
  • Compatibilité auth : avec MySQL 8, le plugin caching_sha2_password est courant. Sans TLS (ou sans mécanisme RSA adéquat), certains clients/drivers peuvent échouer. Autrement dit : forcer TLS simplifie souvent la compatibilité et la sécurité, au lieu de la compliquer.

Côté client, ne vous contentez pas de “REQUIRED” : préférez VERIFY_CA ou VERIFY_IDENTITY pour éviter un MITM interne. Exemple MySQL CLI :

mysql -h db.internal -u ps_runtime -p \
  --ssl-mode=VERIFY_IDENTITY \
  --ssl-ca=/etc/ssl/certs/db-ca.pem

Pour PHP (PDO), l’activation TLS dépend de votre driver et de la manière dont l’app injecte les options. Exemple PDO générique :

$pdo = new PDO($dsn, $user, $pass, [
  PDO::MYSQL_ATTR_SSL_CA => '/etc/ssl/certs/db-ca.pem',
  PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => true,
]);

Sur PrestaShop 8.1.x / 9.x, le point dur est que le cœur ne vous donne pas un bouton “TLS MySQL” : selon votre stack, vous passez soit par la config Doctrine (partie Symfony), soit par le driver legacy, soit par des variables d’environnement au niveau container. Si vous ne pouvez pas injecter proprement les driverOptions, une approche pragmatique consiste à encapsuler le flux via stunnel (ou un tunnel SSH) entre l’app et la DB — c’est moins élégant, mais c’est parfois la seule solution sans patcher le cœur.

Point opérationnel : activer require_secure_transport=ON est une rupture si vous avez des outils admin, jobs cron ou connecteurs ERP qui n’ont pas TLS. Faites-le d’abord en staging (voir créer un staging PrestaShop Docker fidèle à la production), listez les clients (performance_schema.session_connect_attrs, logs), puis coupez. Et documentez vos choix TLS au même niveau d’exigence que le HTTPS front (utile en audit) : voir audit HTTPS/SEO : certificats, bonnes pratiques et points de contrôle.

Privilèges minimaux : séparer “runtime”, “déploiement” et “administration”

La sécurité MySQL n’est pas seulement “auth + TLS”. Le vrai levier, c’est de réduire le blast radius quand (pas “si”) un secret applicatif fuite. Le principe est connu, mais souvent ignoré dans les boutiques : un seul utilisateur prestashop avec GRANT ALL sur *.* “parce que c’est plus simple”, puis le fichier de config se retrouve exposé via un backup mal protégé, une erreur SFTP, ou un LFI. Vous finissez avec un accès root DB, donc exfiltration + altération.

OWASP rappelle régulièrement (dans ses ressources sur le contrôle d’accès) que les droits doivent être limités au strict nécessaire, et que l’absence de séparation des rôles est un accélérateur d’incident (référence : OWASP Cheat Sheet Series, Access Control Cheat Sheet : OWASP Access Control Cheat Sheet). Appliquez-le littéralement à MySQL : un compte runtime ne doit pas savoir faire des ALTER TABLE, et un compte de migration ne doit pas être utilisé par PHP-FPM en production.

Sur PrestaShop, il faut accepter une réalité : les besoins en privilèges changent selon la phase.

  • Runtime (front + back-office) : principalement SELECT/INSERT/UPDATE/DELETE sur la base de la boutique.
  • Déploiement / upgrade / modules : CREATE/ALTER/DROP/INDEX, parfois TRIGGER si vous utilisez des triggers (voir automatiser la cohérence avec des triggers MySQL en production).
  • Administration : comptes séparés, accès restreint (bastion/VPN), MFA côté SSO si possible (via tooling), pas depuis Internet.

Une façon simple de rendre ça « auditable » est de formaliser vos comptes dans un petit tableau interne (runbook) : qui utilise quoi, depuis où, et pour quelle action. Exemple (à adapter) :

Compte Usage Origine réseau Droits Rotation
ps_runtime Front/BO IP(s) applicatives DML uniquement fréquente (ex. trimestrielle)
ps_deploy CI/CD, upgrades, modules runner/bastion DDL + DML après chaque changement d’équipe/outillage
ps_ro BI/export réseau BI SELECT selon besoin

Exemple concret (MySQL 8.0), avec TLS obligatoire et hôte verrouillé :

-- Runtime : le serveur web uniquement
CREATE USER 'ps_runtime'@'10.0.1.10' IDENTIFIED BY 'motdepasse_robuste'
  REQUIRE SSL;

GRANT SELECT, INSERT, UPDATE, DELETE
ON prestashop.*
TO 'ps_runtime'@'10.0.1.10';

-- Déploiement : votre runner CI/CD ou votre bastion d’admin
CREATE USER 'ps_deploy'@'10.0.1.20' IDENTIFIED BY 'motdepasse_robuste'
  REQUIRE SSL;

GRANT SELECT, INSERT, UPDATE, DELETE,
      CREATE, ALTER, DROP, INDEX,
      CREATE TEMPORARY TABLES, LOCK TABLES,
      TRIGGER
ON prestashop.*
TO 'ps_deploy'@'10.0.1.20';

En complément (utile contre les erreurs applicatives et certains scénarios de bruteforce interne), vous pouvez limiter les ressources par compte (selon votre version) : nombre de connexions simultanées, ou quotas. Cela ne remplace pas le firewall, mais ça évite qu’un bug applicatif ouvre 500 connexions et mette la DB à genoux.

Avec MySQL 8.0, vous pouvez aussi durcir l’authentification et la résistance aux attaques par mot de passe :

ALTER USER 'ps_runtime'@'10.0.1.10'
  FAILED_LOGIN_ATTEMPTS 5
  PASSWORD_LOCK_TIME 1;

Et surtout : nettoyez l’existant. Cherchez les comptes anonymes, les hosts %, les plugins d’auth historiques, les comptes root distants. Check rapide :

SELECT user, host, plugin, account_locked
FROM mysql.user
ORDER BY user, host;

Deux requêtes d’audit qui font gagner du temps en revue de config :

-- Comptes trop ouverts (host wildcard)
SELECT user, host
FROM mysql.user
WHERE host IN ('%', '');

-- Visualiser les droits effectifs d’un compte
SHOW GRANTS FOR 'ps_runtime'@'10.0.1.10';

En complément, gardez une hygiène “secrets” cohérente côté PrestaShop : permissions de fichiers, rotation, et canal d’accès (SFTP/SSH) correctement configuré. Si vous avez déjà eu des soucis de droits/accès, le diagnostic côté serveur est ici : erreurs SFTP : diagnostiquer connexion, authentification et permissions.

Audit MySQL : journaliser sans se tirer une balle dans le disque

L’audit, c’est l’assurance-vie quand vous devez répondre à deux questions après incident : qu’est-ce qui a été fait ? et depuis où ?. Mais sur MySQL, activer “tous les logs” en production est un anti-pattern : vous pouvez saturer l’I/O, exploser le stockage, et surtout loguer de la donnée sensible (PII, tokens) si vous capturez des requêtes complètes.

Traduction opérationnelle : le general log est utile pour du debug court, pas pour tourner 24/7. À l’inverse, le slow query log est une brique plus acceptable (volume moindre) et sert aussi à la performance. Si vous voulez le traiter proprement (pt-query-digest, indexation, corrélation), vous avez déjà un guide orienté PrestaShop : analyser le slow query log MySQL et optimiser les index.

Pour un audit orienté sécurité (connexions, GRANT, DDL, actions admin), vous avez trois options réalistes :

1) Plugin d’audit : MySQL Enterprise Audit (commercial) ou MariaDB Audit Plugin (souvent dispo sur MariaDB, Percona, etc.). Avantage : événements structurés ; limite : overhead + licence/compatibilité. 2) Proxy (ProxySQL/MaxScale) : journalisation et règles L7 sans toucher au moteur ; avantage : agnostique ; limite : complexité opératoire. 3) Binlog (ROW) : utile pour forensics et restauration point-in-time, mais pas un audit “humain” direct (et ça ne capture pas tout, selon config).

Pour éviter de « journaliser n’importe quoi », définissez ce que vous cherchez à prouver (en audit) et à détecter (en sécurité). Typiquement, côté DB e‑commerce, les événements utiles sont :

  • Connexions : succès/échecs, origine, compte, méthode (TLS oui/non).
  • Actions à fort impact : CREATE USER, GRANT, REVOKE, DROP TABLE, ALTER TABLE, changements de paramètres.
  • Éléments d’investigation : erreurs d’authent, pics d’échecs, changements de schéma hors fenêtre de maintenance.

Et ce que vous évitez si possible :

  • Les requêtes complètes contenant PII (emails, adresses) ou tokens (risque RGPD + risque interne).
  • Un niveau de verbosité qui met en péril l’I/O InnoDB.

Une fois les événements collectés, centralisez. Ne laissez pas des logs critiques sur le même disque que vos données InnoDB : c’est la recette du downtime. Un pipeline classique (Filebeat/Vector → OpenSearch) marche très bien, y compris en environnement managé : voir OVHcloud Managed OpenSearch pour la centralisation des logs et la recherche.

Enfin, mettez des alertes sur des signaux simples et actionnables. Avec mysqld_exporter + Prometheus, vous pouvez alerter sur : Aborted_connects, taux de connexions échouées, pics de connexions, augmentation de Questions hors tendance, ou événements de réplication. Pour la brique alerting, la base est ici : configurer Prometheus : alertes, métriques et requêtes PromQL.

Astuce terrain : corrélez vos alertes DB avec les logs Nginx/PHP-FPM. Sur PrestaShop, une hausse de connexions MySQL échouées peut venir d’un déploiement (secret non à jour), d’une montée en charge, ou d’un scan interne. Sans corrélation, vous perdez du temps en diagnostic.

Checklist de durcissement MySQL pour une stack PrestaShop (8.1 / 9.x) sans casser la prod

Prérequis explicites (sinon vous jouez à la roulette russe) : sauvegarde testée (restore validé), fenêtre de maintenance si redémarrage DB requis, accès console (iLO/serial/cloud console), et un environnement de staging fidèle. Sur PrestaShop 9 (Symfony 6.4, PHP 8.3) ou PrestaShop 8.1 (PHP 8.1/8.2 selon contexte), prenez aussi en compte que les modules peuvent introduire des migrations SQL hors cœur. Pour une approche structurée audit → plan d’actions, vous pouvez vous caler sur la méthodologie générale : audit sécurité PrestaShop : méthodologie, livrables et durcissement.

Ordre d’exécution recommandé (réduit les lock-out) :

1) Réseau : bind-address + firewall allowlist + suppression des règles % inutiles. 2) TLS : déployer CA/certs, basculer clients en VERIFY_IDENTITY, puis seulement activer require_secure_transport=ON. 3) Comptes : créer ps_runtime minimal + ps_deploy pour migrations, basculer l’app, puis supprimer l’ancien compte permissif. 4) Audit : activer logs pertinents + exporter métriques + centraliser et alerter.

Quelques contrôles additionnels (souvent rapides, et rentables en audit) :

  • Désactiver ce qui n’est pas utile : si vous n’utilisez pas LOCAL INFILE, désactivez-le côté serveur (local_infile=0) et côté client quand possible.
  • Canal d’admin : pas d’admin DB depuis Internet ; imposez VPN/bastion + allowlist.
  • Séparer lecture seule : pour exports/BI, créez un compte SELECT uniquement (évite qu’un outil d’export soit une porte d’entrée en écriture).
  • Rotation des secrets : planifiée et testée (avec rollback), pas “quand on y pense”.

Le point sensible côté PrestaShop : la séparation runtime/déploiement n’est pas “native”. En pratique, vous faites la bascule au niveau déploiement (CI/CD) en écrivant les paramètres DB au bon moment (variables d’env/secret manager), puis vous redonnez au runtime un compte minimal. Si vous êtes déjà en démarche GitOps/Kubernetes, cette séparation s’aligne bien avec des manifests et secrets versionnés/rotés : migration PrestaShop vers Kubernetes : checklist GitOps et pièges courants.

Exemple terrain (observé régulièrement en e‑commerce) : une boutique avec MariaDB sur VPS exposait 3306 “temporairement”. Les métriques montraient ~20k tentatives de connexion/jour (bots) et des pics CPU sur l’auth. Après passage en allowlist (uniquement IPs applicatives) + désactivation des comptes % + TLS forcé, les tentatives vues côté serveur sont tombées à ~0 (car bloquées au firewall), et les rares connexions échouées restantes étaient des erreurs applicatives internes (mauvais secret). Ce n’est pas “de la sécurité magique” : c’est juste la combinaison pare-feu + privilèges minimaux + journalisation exploitable.

Si vous voulez pousser plus loin, traitez MySQL comme un composant d’un tout : hardening OS/PHP (patch management, CVE), WAF, SBOM modules, et CI/CD sécurisé. Deux ressources internes qui s’imbriquent bien avec ce durcissement DB : sécuriser un serveur PHP/Symfony (hardening + gestion CVE) et sécuriser PrestaShop : APIs et modules avec SBOM, WAF et CI/CD.


À lire aussi