Solucionar el error de MySQL "Access denied for user"
Por qué "Access denied for user" casi nunca tiene que ver con una contraseña incorrecta: unix_socket, localhost frente a 127.0.0.1, permisos y el restablecimiento seguro de la contraseña de root.
"Access denied for user" es el mensaje de error más buscado del mundo MySQL y, en la mayoría de los casos, la contraseña no tiene ninguna culpa. En Debian y Ubuntu, el inicio de sesión falla sobre todo por la autenticación por socket, por confundir localhost con 127.0.0.1 o porque la fila del usuario se creó para el host equivocado. Este artículo recorre las causas en el orden en el que aparecen de verdad, muestra el restablecimiento de la contraseña mediante skip-grant-tables incluido el camino de vuelta, y describe cómo saber que el inicio de sesión está realmente arreglado.
Todos los datos se refieren a Debian 13 (MariaDB 11.8), Debian 12 (MariaDB 10.11), Ubuntu 24.04 (MariaDB 10.11 o MySQL 8.0) y Ubuntu 22.04 (MariaDB 10.6 o MySQL 8.0). Un punto importante antes de empezar: Debian no distribuye ningún paquete mysql-server. Si has instalado "MySQL" en un sistema Debian, lo que corre allí es MariaDB, y eso ya explica una parte de la confusión.
Leer el mensaje de error con atención
La redacción exacta decide qué causa entra en juego. Estas cinco variantes son las que te encuentras en la práctica:
| Mensaje | Significado |
|---|---|
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES) | Se envió una contraseña y no coincidía, o no existe ninguna fila de cuenta que encaje. |
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO) | No se envió ninguna contraseña. Casi siempre falta -p o la aplicación no lee su configuración. |
ERROR 1698 (28000): Access denied for user 'root'@'localhost' | El caso clásico: la cuenta usa unix_socket o bien auth_socket. Aquí una contraseña no significa nada. |
ERROR 1044 (42000): Access denied for user 'app'@'localhost' to database 'shop' | El inicio de sesión funcionó. Solo faltan permisos sobre esta base de datos. |
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/run/mysqld/mysqld.sock' | No es un problema de autenticación. El servicio no está en marcha o la ruta del socket no es correcta. |
Dos detalles se pasan por alto una y otra vez. Primero, el nombre de host que aparece en el mensaje es el host con el que el servidor te vio, no el que tú escribiste. Si allí pone 'app'@'localhost' aunque te hayas conectado con -h 127.0.0.1, entonces el servidor hizo una resolución inversa. Segundo, el 1045 no distingue entre "contraseña incorrecta" y "la cuenta no existe". Los dos casos dan la misma salida, y es a propósito, para que un atacante no pueda ir probando nombres de usuario válidos.
El caso más frecuente: unix_socket en MariaDB
Desde MariaDB 10.4, los paquetes de Debian y Ubuntu crean la cuenta root@localhost de manera que se autentique a través del socket Unix. La fila de la cuenta viene a ser esta:
CREATE USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING 'invalid'
OR unix_socket;
Esto significa: quien tenga la sesión iniciada como usuario del sistema root entra sin contraseña. Quien envíe una contraseña se comprueba contra el hash de la cadena "invalid", y eso no lo acierta nadie. La parte de mysql_native_password solo está ahí porque, si no, SET PASSWORD terminaría con un error. Consecuencia práctica: mysql -u root -p como usuario normal falla sin importar qué contraseña escribas. Lo correcto es:
sudo mariadb
En MySQL 8.0 sobre Ubuntu, el plugin se llama auth_socket en lugar de unix_socket, y el comportamiento es idéntico. Allí la llamada es sudo mysql.
Comprueba qué plugin usa realmente una cuenta, y hazlo con SHOW CREATE USER:
mariadb -e "SHOW CREATE USER 'root'@'localhost';"
La salida muestra la cadena completa de los dos métodos:
CREATE USER `root`@`localhost` IDENTIFIED VIA mysql_native_password USING 'invalid' OR unix_socket
Aquí acecha una trampa que apenas menciona ninguna guía: desde MariaDB 10.4, mysql.user ya solo es una vista, y los datos reales están como JSON en mysql.global_priv. Esa vista conoce un único método de autenticación por cuenta y por eso informa de mysql_native_password para root@localhost, aunque en realidad quien actúa es unix_socket. Quien tome como prueba la consulta tan extendida SELECT User, Host, plugin FROM mysql.user dará por hecho que el servidor se autentica por contraseña y buscará el fallo donde no está. JSON_VALUE(priv,"$.plugin") tiene exactamente el mismo punto ciego, porque también lee solo el primer elemento. El segundo método está en el campo auth_or y hay que consultarlo aparte:
mariadb -e 'SELECT CONCAT(user,"@",host) AS cuenta, JSON_VALUE(priv,"$.plugin") AS plugin, JSON_QUERY(priv,"$.auth_or") AS auth_or FROM mysql.global_priv;'
Para root@localhost, la columna plugin sigue mostrando entonces mysql_native_password, mientras que en auth_or aparece [{},{"plugin":"unix_socket"}]. Solo esta segunda columna revela que el inicio de sesión por socket está activo.
¿Está cargado el plugin? Tras una actualización defectuosa puede faltar, y entonces el servidor informa de ERROR 1524 (HY000): Plugin 'unix_socket' is not loaded:
mariadb -e "SELECT plugin_name, plugin_status FROM information_schema.plugins WHERE plugin_name LIKE '%socket%';"
Si quieres pasar root a una contraseña de forma consciente, por ejemplo porque un script de backup corre bajo otro usuario del sistema, conserva además la variante por socket. De lo contrario dejan de funcionar los scripts de mantenimiento de la distribución y sudo mariadb:
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket
OR mysql_native_password USING PASSWORD('TuNuevaContrasena');
De todas formas, el camino mejor es una cuenta de administración propia en lugar de una contraseña para root. Para las aplicaciones eso vale todavía más: una base de datos, un usuario y solo los permisos necesarios.
localhost no es 127.0.0.1
Esta distinción provoca más mensajes de "Access denied" que cualquier contraseña equivocada. Los clientes de MySQL y MariaDB tratan el nombre de host localhost como un caso especial y se conectan a través del socket Unix. Solo 127.0.0.1 fuerza una conexión TCP. Desde el punto de vista de la gestión de permisos, los dos no son el mismo nombre de máquina, porque en una conexión por socket el servidor anota el host localhost, mientras que en TCP sobre la dirección de loopback anota o bien localhost (tras la resolución inversa) o bien 127.0.0.1, según cómo esté skip_name_resolve.
Una cuenta que existe únicamente como 'app'@'127.0.0.1' no es accesible a través del socket, y al revés lo mismo. Eso es justo lo que pasa cuando una aplicación PHP tiene host=localhost en su configuración: PHP se conecta por el socket, pero los permisos se concedieron para la IP. Dos contrapruebas:
mariadb -u app -p'Contrasena' -e "SELECT USER(), CURRENT_USER();"
mariadb -h 127.0.0.1 -u app -p'Contrasena' -e "SELECT USER(), CURRENT_USER();"
Si solo falla una de las dos, ya tienes la causa. Fíjate además en la resolución de nombres: mientras skip_name_resolve esté desactivado, y así está en las instalaciones por paquete de Debian y Ubuntu, el servidor resuelve la dirección de loopback hacia atrás a localhost. Si ya existe una cuenta 'app'@'localhost', el inicio de sesión con -h 127.0.0.1 también funciona por eso, y CURRENT_USER() informa entonces de app@localhost en lugar de app@127.0.0.1. Una cuenta 'app'@'127.0.0.1' creada de forma adicional se queda sin efecto en ese caso, y solo actúa cuando falta la cuenta de localhost. La solución limpia es crear la cuenta para la vía que la aplicación toma de verdad, y no montar las dos variantes "por si acaso".
Comprueba también si la resolución de nombres está desactivada. Si skip_name_resolve está activo, las filas de cuenta con nombres de host como 'app'@'web01.intern' dejan de funcionar por completo, y a partir de ahí solo cuentan las direcciones IP:
mariadb -e "SHOW VARIABLES LIKE 'skip_name_resolve';"
Otro tropiezo es el orden en el que el servidor elige las filas que encajan. Ordena de lo específico a lo general y se queda con la primera coincidencia, no con la mejor. Si junto a 'app'@'%' existe además una cuenta anónima ''@'localhost', en una conexión local gana la cuenta anónima, y tu inicio de sesión falla con un mensaje que aun así menciona tu nombre de usuario. Los paquetes actuales ya no crean cuentas anónimas, pero en sistemas migrados durante años todavía se encuentran:
mariadb -e "SELECT user, host FROM mysql.global_priv WHERE user = '';"
Contraseña, permisos y mayúsculas
Quedan las causas que sí tienen que ver de verdad con las credenciales.
La shell se come los caracteres especiales
Entre -p y la contraseña no puede haber ningún espacio, porque si no el cliente interpreta la contraseña como nombre de base de datos. Las contraseñas con $, !, & o espacios van entre comillas simples, porque de lo contrario la shell las sustituye o las corta. Lo realmente limpio es no pasar la contraseña por la línea de comandos, porque allí acaba en la lista de procesos y en el archivo de historial. Crea en su lugar un archivo ~/.my.cnf con permisos 0600:
[client]
user=app
password=TuContrasena
Al revés, un ~/.my.cnf olvidado también puede ser la causa del error. Sobrescribe sin decir nada lo que indiques en la línea de comandos, y entonces recibes un "Access denied" para un usuario que nunca escribiste.
Mayúsculas: en el nombre de usuario sí, en el host no
Los nombres de usuario tienen que coincidir carácter por carácter en MySQL y MariaDB, App y app son dos cuentas distintas. Los nombres de host se comparan sin tener en cuenta mayúsculas y minúsculas, pero se guardan exactamente como estaban en el CREATE USER. Quien haya creado sin querer 'app'@'LOCALHOST' verá en las salidas de SHOW GRANTS y en los scripts dos cuentas aparentemente distintas que, sin embargo, encajan con la misma conexión. Duplicados así vuelven la búsqueda del fallo innecesariamente lenta, porque concedes permisos en una fila y el servidor elige la otra. Se localizan así:
mariadb -e "SELECT user, host FROM mysql.global_priv WHERE host <> LOWER(host);"
Faltan permisos, no el inicio de sesión
Si llega ERROR 1044 en vez de 1045, el inicio de sesión funcionó. Entonces faltan permisos sobre una base de datos concreta. Mira qué puede hacer realmente la cuenta:
mariadb -e "SHOW GRANTS FOR 'app'@'localhost';"
FLUSH PRIVILEGES solo hace falta si has cambiado las tablas de permisos directamente con INSERT o UPDATE. Después de GRANT, CREATE USER o ALTER USER sobra, y a veces tapa el hecho de que el comando de verdad no llegó a surtir efecto.
El cliente no entiende el plugin
MySQL 8.0 usa caching_sha2_password de forma predeterminada. Los clientes y las bibliotecas antiguas responden a eso con ERROR 2059 (HY000): Authentication plugin 'caching_sha2_password' cannot be loaded. No es un problema de permisos, sino una incompatibilidad. En MySQL 8.0, mysql_native_password sigue disponible como alternativa; en MySQL 8.4 se ha eliminado. En caso de duda, actualiza mejor el cliente en lugar de dar marcha atrás en el cifrado.
Restablecer la contraseña de root
Cuando ya no hay nada que ayude, el servidor tiene que arrancar una vez sin comprobación de permisos. Hay dos vías que llevan al objetivo. La vía por --init-file es la más segura, porque el servidor corre todo el tiempo con la comprobación de permisos activa.
Variante 1: init-file (recomendada)
El archivo SQL tiene que estar en un sitio que el servicio pueda leer. Bajo /tmp o /root eso falla en Ubuntu con regularidad por AppArmor y por PrivateTmp en el servicio de systemd. Colócalo por eso en el directorio de datos:
sudo systemctl stop mariadb
sudo tee /var/lib/mysql/kh-reset.sql >/dev/null <<'SQL'
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket
OR mysql_native_password USING PASSWORD('NuevaContrasenaRoot');
SQL
sudo chown mysql:mysql /var/lib/mysql/kh-reset.sql
sudo systemctl set-environment MYSQLD_OPTS="--init-file=/var/lib/mysql/kh-reset.sql"
sudo systemctl start mariadb
Después hay que limpiar sin falta, porque si no el servidor vuelve a arrancar con ese archivo en cada inicio:
sudo systemctl unset-environment MYSQLD_OPTS
sudo rm /var/lib/mysql/kh-reset.sql
sudo systemctl restart mariadb
Para MySQL 8.0 sobre Ubuntu, el servicio se llama mysql y la línea SQL es:
ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'NuevaContrasenaRoot';
Variante 2: skip-grant-tables
Esta variante desactiva por completo la comprobación de permisos. Sin --skip-networking, durante ese tiempo cualquiera que alcance el puerto tendría acceso completo a todos los datos. Por eso la opción no es opcional, sino obligatoria.
sudo systemctl stop mariadb
sudo systemctl set-environment MYSQLD_OPTS="--skip-grant-tables --skip-networking"
sudo systemctl start mariadb
sudo mariadb
En la sesión, carga primero las tablas de permisos, porque si no el servidor rechaza ALTER USER con un mensaje de error:
FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket
OR mysql_native_password USING PASSWORD('NuevaContrasenaRoot');
EXIT;
Y luego, otra vez al funcionamiento normal:
sudo systemctl unset-environment MYSQLD_OPTS
sudo systemctl restart mariadb
Si tu distribución no evalúa la variable MYSQLD_OPTS, siempre funciona el archivo drop-in en /etc/systemd/system/mariadb.service.d/reset.conf con el contenido [Service] y Environment="MYSQLD_OPTS=--skip-grant-tables --skip-networking", seguido de sudo systemctl daemon-reload. Borra el archivo después y vuelve a recargar systemd.
Cuando el restablecimiento sale mal
Justo aquí es donde terminan la mayoría de las guías. Los cuatro problemas posteriores más frecuentes:
El servicio ya no arranca. Casi siempre es una errata en el archivo SQL. El servidor aborta entonces durante el arranque. Debian registra MariaDB de forma predeterminada en el journal; Ubuntu con MySQL, además, en un archivo:
sudo journalctl -u mariadb -n 50 --no-pager
sudo tail -n 50 /var/log/mysql/error.log
Dónde escribe el servidor se puede consultar con SHOW VARIABLES LIKE 'log_error';. Pero no esperes ahí una ruta de archivo: en las instalaciones por paquete de Debian y Ubuntu el valor está vacío, porque el servicio corre con --skip-log-error. Todo va entonces a la salida de error estándar y acaba en el journal o bien en /var/log/syslog, y se consulta con journalctl -u mariadb. Para solucionarlo, elimina la variable de entorno con --init-file y reinicia.
El servidor corre de forma permanente sin comprobación de permisos. Eso pasa cuando se ha olvidado el unset-environment, y es la variante más peligrosa, porque desde fuera todo parece normal. Dos controles:
systemctl show-environment
ps -o args= -C mariadbd
Si en alguna de las dos salidas aparece skip-grant-tables, la comprobación de permisos sigue desactivada. systemctl show-environment da por supuesto que systemd corre como PID 1. En un servidor corriente eso se cumple; en un container o en un sistema con SysV init, ese comando no existe. Allí lees el entorno directamente del proceso en marcha:
cat /proc/$(pgrep -n mariadbd)/environ | tr '\0' '\n'
Tampoco deberías fiarte solo de la lista de argumentos: arrancado a través de systemd, ps muestra muchas veces únicamente /usr/sbin/mariadbd sin ninguna opción, mientras que a través de un script SysV init aparece la lista completa. Otro indicio: bajo skip-grant-tables, SHOW GRANTS responde con un mensaje de error en lugar de con permisos.
AppArmor bloquea el archivo de inicio. El síntoma es un servidor que no levanta sin motivo aparente. Un vistazo al log del kernel lo aclara:
sudo dmesg | grep -i denied
Te quedas fuera del todo. Eso ocurre cuando se pasa root a una contraseña pura con ALTER USER ... IDENTIFIED BY, se pierde por el camino la autenticación por socket y después ya no se recuerda la contraseña nueva. La salida es el mismo restablecimiento otra vez, esta vez con la variante doble unix_socket OR mysql_native_password mostrada arriba. Haz una copia de las tablas de permisos antes de cada intervención: son segundos y ahorran horas cuando la cosa se pone seria:
sudo mariadb-dump mysql > /root/mysql-grants.sql
La opción --single-transaction, habitual en otros casos, te la puedes ahorrar aquí. Termina sin errores, pero no surte efecto, porque las tablas de la base de datos mysql están sobre Aria o MyISAM y por tanto no son transaccionales.
En un hosting web gestionado no tienes acceso al sistema y por eso no dispones de ninguna de estas opciones. Allí los usuarios de base de datos y las contraseñas se restablecen desde el panel de administración. En un servidor root KVM o en un servidor dedicado de KernelHost tienes acceso root completo y, si el servidor ya no se alcanza por la conexión de red, llegas al sistema a través de la consola del área de cliente.
Comprobar que está realmente solucionado
Que un comando termine sin errores todavía no significa que el inicio de sesión funcione de forma duradera. Estas cuatro comprobaciones destapan los fallos residuales típicos.
Primero, la diferencia entre USER() y CURRENT_USER(). La primera función muestra por quién te has hecho pasar; la segunda, qué fila de cuenta usa realmente el servidor:
mariadb -u app -p'Contrasena' -e "SELECT USER(), CURRENT_USER();"
Si allí hay dos valores distintos, por ejemplo app@localhost y app@%, entonces tus permisos actúan por una fila diferente de la que pensabas. Eso explica los errores 1044 que aparecen más tarde, antes de que salten en producción.
Segundo, un acceso real a la base de datos de destino en lugar de solo un inicio de sesión, y la evaluación del código de salida:
mariadb -u app -p'Contrasena' mi_db -e "SELECT 1;" ; echo "Codigo de salida: $?"
Tercero, un reinicio del servicio y después el mismo inicio de sesión otra vez. Con eso te aseguras de que el cambio está de verdad en las tablas y no solo en la memoria en marcha, y de que no ha quedado ninguna variable de entorno del restablecimiento:
sudo systemctl restart mariadb
sudo systemctl is-active mariadb
Cuarto, la aplicación misma. Una prueba correcta en la línea de comandos dice poco sobre una aplicación PHP que se conecta bajo el usuario del servidor web y a través del socket. Prueba por eso bajo ese mismo usuario del sistema:
sudo -u www-data mariadb -u app -p'Contrasena' mi_db -e "SELECT CURRENT_USER();"
Diferencias entre las distribuciones
Una guía que afirme lo mismo para los cuatro sistemas se equivoca al menos en un punto. Esta tabla resume las desviaciones relevantes:
| Sistema | Base de datos predeterminada | Plugin de socket | Nota |
|---|---|---|---|
| Debian 13 | MariaDB 11.8 | unix_socket | sin paquete mysql-server disponible; mysql ya solo es un enlace simbólico a mariadb y avisa al llamarlo |
| Debian 12 | MariaDB 10.11 | unix_socket | sin paquete mysql-server disponible |
| Ubuntu 24.04 | MariaDB 10.11 o MySQL 8.0 | unix_socket o auth_socket | los dos paquetes conviven en los repositorios, con riesgo de confusión al seguir guías |
| Ubuntu 22.04 | MariaDB 10.6 o MySQL 8.0 | unix_socket o auth_socket | JSON_VALUE y JSON_QUERY sobre mysql.global_priv funcionan aquí igual que bajo 10.11 y 11.8; también en 10.6 mysql.user es solo una vista, y para la cadena de autenticación completa SHOW CREATE USER sigue siendo el camino más corto |
Más diferencias que cuestan tiempo en la práctica: los archivos de configuración están en MariaDB bajo /etc/mysql/mariadb.conf.d/50-server.cnf y en MySQL bajo /etc/mysql/mysql.conf.d/mysqld.cnf. Los nombres de servicio son mariadb y mysql respectivamente, y MariaDB trae además un alias mysql. En MariaDB, desde la versión 10.4 ya no existe el usuario debian-sys-maint, y el archivo /etc/mysql/debian.cnf apunta allí a root a través del socket. En MySQL sobre Ubuntu, el usuario de mantenimiento sigue existiendo, y es el mejor acceso de emergencia cuando la contraseña de root se ha perdido y no quieres reiniciar el servidor:
sudo mysql --defaults-file=/etc/mysql/debian.cnf
Si respetas este orden, o sea, primero leer el mensaje con atención, después comprobar el plugin y la fila del host, luego la vía de conexión y solo al final restablecer la contraseña, la inmensa mayoría de los casos se resuelven en pocos minutos y sin tiempo de inactividad. El restablecimiento mediante skip-grant-tables es el último recurso, no el primer paso.
Preguntas frecuentes
¿Por qué no funciona mysql -u root -p aunque la contraseña sea correcta?
¿Cuál es la diferencia entre ERROR 1045 y ERROR 1698?
¿localhost es lo mismo que 127.0.0.1?
¿Importan las mayúsculas en el nombre de usuario y en el host?
¿Cómo restablezco la contraseña de root sin skip-grant-tables?
¿Cómo sé si el servidor sigue funcionando sin comprobación de permisos?
¿Existe en Debian un paquete mysql-server?
2026 KernelHost GmbH. Todos los derechos reservados. Esta guía está protegida por derechos de autor. Su publicación en otros sitios web, aunque sea de forma parcial o modificada, no está permitida sin nuestro consentimiento por escrito. Las citas con indicación de la fuente y un enlace son muy bienvenidas.

