Mise en service d’un backend Symfony sur un serveur de recette fourni par le client. La connexion à MariaDB refuse en boucle :
ERROR 1045 (28000): Access denied for user 'app-user'@'localhost' (using password: YES)
On a passé deux jours sur ce 28000. Demande d’un nouveau mot de passe à l’infra, mot de passe régénéré, re-test… toujours KO. On commençait sérieusement à douter du compte applicatif lui-même.
Symptôme
Le mot de passe semblait correct (copié-collé, pas d’espace parasite), le serveur MariaDB tournait, le port 3306 répondait. Et pourtant, depuis le serveur, en testant à la main :
mariadb -u app-user -p'<password>' app_db -e "SELECT 1"
# → ERROR 1045 (28000): Access denied for user 'app-user'@'localhost'
L’infra renvoyait la balle (« le compte est bon de notre côté »), nous renvoyions la nôtre (« il refuse le mot de passe »). Blocage.
Cause
La solution était dans le message d’erreur depuis le début : 'app-user'@'localhost'.
Côté client MySQL/MariaDB, localhost n’est pas un nom d’hôte réseau. C’est un cas spécial qui force la connexion par socket Unix (/run/mysqld/mysqld.sock), pas par TCP. 127.0.0.1, lui, passe par TCP. Ce sont deux chemins de connexion distincts — et côté serveur, MariaDB distingue les grants par host :
'app-user'@'%'→ autorise les connexions TCP (depuis n’importe quelle IP).'app-user'@'localhost'→ autorise les connexions par socket Unix.
Le compte avait bien été créé… mais uniquement avec un grant sur @'%'. Aucun grant sur @'localhost'. Or mariadb -u app-user -p… sans -h tombe par défaut sur le socket → host effectif localhost → aucun grant → Access denied.
Le mot de passe était bon depuis le premier jour. Ce n’était pas un problème d’authentification, c’était un problème de host de connexion. Un SHOW GRANTS l’aurait montré en dix secondes :
SHOW GRANTS FOR 'app-user'@'%';
-- GRANT ALL PRIVILEGES ON `app_db`.* TO `app-user`@`%`
-- (et RIEN pour 'app-user'@'localhost')
Solution
Forcer le TCP, et c’est passé du premier coup :
mariadb -u app-user -p'<password>' -h 127.0.0.1 --protocol=TCP app_db -e "SELECT 1"
# → 1 ✓
Côté application, la même classe de bug se neutralise dans la DATABASE_URL Symfony en utilisant 127.0.0.1 et jamais localhost :
# ❌ force le socket Unix → tombe sur un host sans grant
DATABASE_URL="mysql://app-user:<password>@localhost:3306/app_db?serverVersion=mariadb-10.6"
# ✅ TCP explicite → host '%', grant OK
DATABASE_URL="mysql://app-user:<password>@127.0.0.1:3306/app_db?serverVersion=mariadb-10.6"
Subtilité piégeuse : même avec un port :3306 dans la chaîne, certains clients/connecteurs continuent de privilégier le socket si le host littéral est localhost. Le port n’est pas suffisant pour forcer le TCP — c’est le nom d’hôte qui décide du transport. D’où 127.0.0.1.
Lessons learned
- L’erreur
Access deniedde MySQL/MariaDB mentionne toujours le host effectif —'user'@'<host>'. Ce host est un diagnostic, pas du décor : lisez-le. S’il dit@localhostalors que vous pensiez vous connecter en réseau, vous êtes sur le socket Unix. localhost≠127.0.0.1côté client MySQL/MariaDB. Le premier = socket Unix, le second = TCP. Deux transports, deux grants potentiellement différents côté serveur. C’est un cas spécial historique du connecteur, pas une résolution DNS.- Un grant
@'%'ne couvre pas@'localhost'.%matche les hosts réseau, pas la connexion socket. Si le compte n’a un grant que sur l’un des deux, l’autre chemin échoue avec un message qui ressemble à un mauvais mot de passe. - Dans
DATABASE_URL, écrire127.0.0.1plutôt quelocalhostélimine la classe de bug d’entrée de jeu — sans dépendre de la config exacte des grants côté serveur. - Avant de demander une re-conf à l’infra, demandez un
SHOW GRANTS. Deux jours de ping-pong « régénère le mot de passe » se seraient résumés à une ligne. Quand deux équipes se renvoient la balle, la donnée objective (SHOW GRANTS,--protocol=TCP) tranche plus vite que les hypothèses.
#mariadb #mysql #symfony #devops #debugging