Cómo solucionar MySQL Error 1045: Access Denied for User (guía definitiva 2026)

Son las 2 de la mañana. Tu deploy funcionaba hace cinco minutos. Cambiaste una variable de entorno, reiniciaste el contenedor, y ahora tu aplicación —o tu terminal, o tu script de Python— te devuelve esto:

ERROR 1045 (28000): Access denied for user 'app_user'@'localhost' (using password: YES)

Si llegaste aquí googleando ese mensaje exacto, no estás solo: es uno de los errores más reportados en foros de MySQL, WordPress, Docker y stacks de Python/Flask. La buena noticia es que, aunque el mensaje suena genérico, las causas reales son solo cuatro o cinco, y casi siempre se resuelven en menos de diez minutos si sabes por dónde empezar.

En este artículo vamos a diagnosticar tu caso específico —no solo "probá cambiar la contraseña"— y a resolverlo con los comandos exactos, incluyendo el conflicto de autenticación que MySQL 8 introdujo y que sigue rompiendo proyectos en 2026.

Qué significa realmente el error 1045

Antes de tocar nada, lee el mensaje completo. Trae más información de la que parece:

ERROR 1045 (28000): Access denied for user 'app_user'@'192.168.1.50' (using password: YES)

Tres datos clave:

  • 'app_user'@'192.168.1.50': es la combinación exacta de usuario y host que MySQL intentó autenticar. MySQL no valida solo el nombre de usuario: valida el par usuario + origen de la conexión. Un usuario creado como 'app_user'@'localhost' no puede conectarse desde 127.0.0.1, desde un contenedor Docker, ni desde una IP remota, aunque la contraseña sea correcta.
  • (using password: YES): te dice si el cliente envió una contraseña. Si ves NO, el problema es que tu aplicación no está enviando ninguna, generalmente por una variable de entorno vacía o mal leída.
  • (28000): es el SQLSTATE estándar para errores de autorización, común a MySQL, MariaDB y otros motores compatibles.

En resumen: <cite index="2-1">MySQL rechazó la conexión porque la combinación de usuario, host y contraseña no coincidió con ninguna entrada autorizada en la tabla mysql.user</cite>. Dicho de otro modo, el error 1045 siempre se reduce a una de estas cinco causas: <cite index="1-1">una contraseña incorrecta, una cuenta de usuario inexistente, un problema de privilegios, un desajuste de host, o un conflicto del plugin de autenticación en MySQL 8.0</cite>.

Vamos a revisar cada una, en el orden en que conviene descartarlas.

Paso 1: Confirma que el usuario existe (y en qué host)

Conéctate como root o con un usuario administrativo y corre:

SELECT User, Host, plugin FROM mysql.user WHERE User = 'app_user';

<cite index="1-1">Si no se devuelve ninguna fila, el usuario no existe</cite> para ese nombre, sin importar cuántas veces hayas escrito bien la contraseña. Si aparece pero con un Host distinto al que estás usando para conectarte, ahí está tu problema: MySQL trata 'app_user'@'localhost', 'app_user'@'127.0.0.1' y 'app_user'@'%' como cuentas completamente distintas.

Si el usuario no existe, créalo:

CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'ContraseñaSegura!';
GRANT ALL PRIVILEGES ON tu_base_de_datos.* TO 'app_user'@'localhost';
FLUSH PRIVILEGES;

Si necesitas acceso desde cualquier host (por ejemplo, porque tu app corre en un contenedor Docker con IP dinámica), usa el comodín %, pero con cuidado en producción:

CREATE USER 'app_user'@'%' IDENTIFIED BY 'ContraseñaSegura!';
GRANT ALL PRIVILEGES ON tu_base_de_datos.* TO 'app_user'@'%';
FLUSH PRIVILEGES;

Truco de diagnóstico rápido: si conectarte con -h 127.0.0.1 funciona pero -h localhost falla (o viceversa), el problema no es la contraseña: es que estás forzando una conexión TCP cuando el usuario solo tiene permiso vía socket Unix, o al revés.

Paso 2: Verifica el conflicto de autenticación de MySQL 8

Esta es, hoy en día, la causa más frecuente en proyectos nuevos —y la que menos gente relaciona con el error 1045 a simple vista. <cite index="1-1">MySQL 8.0 cambió el plugin de autenticación por defecto de mysql_native_password a caching_sha2_password</cite>. El problema: <cite index="3-1">muchos clientes antiguos no soportan el nuevo plugin</cite>, lo que genera un 1045 aunque la contraseña sea correcta, porque el handshake de autenticación falla antes de siquiera comparar credenciales.

Verifica qué plugin usa tu usuario:

SELECT User, Host, plugin FROM mysql.user WHERE User = 'app_user';

Si ves caching_sha2_password y tu cliente es antiguo (PyMySQL desactualizado, un driver PHP viejo, o una librería mysqlclient sin compilar contra OpenSSL reciente), tienes dos rutas:

Opción recomendada: actualiza tu librería cliente. En Python, por ejemplo:

pip install --upgrade pymysql mysqlclient

Opción de compatibilidad temporal: cambia el usuario al plugin legado.

ALTER USER 'app_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'ContraseñaSegura!';
FLUSH PRIVILEGES;

Ojo con este detalle si usas MySQL 8.4 o más reciente: <cite index="4-1">el plugin mysql_native_password está incluido pero deshabilitado por defecto, y ese mismo comando ALTER USER falla con el error 1524 indicando que el plugin no está cargado</cite>. Para habilitarlo hay que reiniciar el servidor con la opción correspondiente activada en el archivo de configuración de mysqld.

Paso 3: Revisa si un usuario anónimo te está robando la conexión

Esta causa es menos conocida pero explica muchos casos de "estoy seguro de que la contraseña es correcta y aun así falla". <cite index="4-1">Cuando dos filas de cuentas podrían coincidir con una conexión, MySQL prioriza primero el valor de Host más específico y luego el de User más específico</cite>. Esto significa que una fila anónima ''@'localhost' puede ganarle a tu cuenta 'app_user'@'%' cuando te conectás desde el host local, porque localhost es más específico que %. MySQL intenta autenticarte contra esa cuenta anónima (normalmente con contraseña vacía), falla, y te devuelve el 1045 sin que tu contraseña real llegue a evaluarse.

Para descartarlo:

SELECT User, Host FROM mysql.user WHERE User = '';

Si aparecen filas, elimínalas (a menos que las uses deliberadamente):

DROP USER ''@'localhost';

Paso 4: Diagnóstico específico para Docker

Si tu error aparece solo al conectar desde un contenedor, el motivo casi siempre es el mismo: <cite index="6-1">si creaste 'myuser'@'localhost' pero te conectas desde un contenedor Docker, en realidad te estás conectando desde la IP del contenedor, no desde localhost</cite>. Docker asigna IPs internas (normalmente en el rango 172.17.0.0/16 por defecto), así que tu usuario necesita permiso para ese host o rango, no para localhost.

Solución rápida en docker-compose.yml + SQL:

CREATE USER 'app_user'@'%' IDENTIFIED BY 'ContraseñaSegura!';
GRANT ALL PRIVILEGES ON tu_base_de_datos.* TO 'app_user'@'%';
FLUSH PRIVILEGES;

Y en tu conexión desde Python (usando el nombre del servicio de MySQL definido en docker-compose.yml, no localhost):

import pymysql
import os

try:
    connection = pymysql.connect(
        host=os.environ.get('DB_HOST', 'mysql'),  # nombre del servicio, no localhost
        port=int(os.environ.get('DB_PORT', 3306)),
        user=os.environ['DB_USER'],
        password=os.environ['DB_PASSWORD'],
        database=os.environ['DB_NAME'],
        connect_timeout=10,
    )
    print("Conexión exitosa")
except pymysql.err.OperationalError as e:
    error_code = e.args[0]
    if error_code == 1045:
        print("Acceso denegado: revisa usuario, contraseña y host")
    elif error_code == 1049:
        print("La base de datos no existe")
    else:
        print(f"Error de conexión: {e}")

Paso 5: Si perdiste el acceso de root

Cuando ni siquiera el usuario root funciona, necesitas el modo de recuperación:

sudo systemctl stop mysql
sudo mysqld_safe --skip-grant-tables &
mysql -u root

Dentro de la sesión:

FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'NuevaContraseñaSegura!';
FLUSH PRIVILEGES;

Y para volver a producción:

sudo kill $(sudo cat /var/run/mysqld/mysqld.pid)
sudo systemctl start mysql

Importante: durante el modo --skip-grant-tables, MySQL no valida ninguna contraseña. No dejes el servidor así ni un segundo más de lo necesario, y nunca lo hagas en un servidor expuesto a internet.

El contraargumento: ¿deberías usar % como host?

Es tentador resolver todo con 'usuario'@'%' para no volver a pensar en el tema. No lo hagas por defecto. <cite index="1-1">La recomendación de buenas prácticas es usar usuarios de base de datos separados por aplicación, nunca conectar aplicaciones web con la cuenta root, y crear un usuario dedicado con los privilegios mínimos necesarios para cada aplicación</cite>. Además, <cite index="1-1">se recomienda restringir el acceso remoto solo a IPs específicas</cite> cuando sea posible, en vez de abrir la puerta a cualquier host.

El comodín % es razonable en desarrollo local o dentro de una red Docker interna que no está expuesta a internet. En producción, si tu arquitectura lo permite, es mejor listar hosts o rangos específicos.

Resumen práctico según tu perfil

  • Si sos vibe coder o estás prototipando rápido: lo más probable es que te falte crear el usuario o que estés usando localhost en vez del nombre del servicio Docker. Empieza por el Paso 1 y el Paso 4.
  • Si mantenés un proyecto en producción con MySQL 8: revisa primero el plugin de autenticación (Paso 2). Es la causa que más sorprende porque "la contraseña es correcta" y aun así falla.
  • Si administrás infraestructura o Debian/Ubuntu en servidores propios: además de los pasos anteriores, corré siempre FLUSH PRIVILEGES después de editar mysql.user manualmente con UPDATE en vez de ALTER USER o GRANT —de lo contrario MySQL no relee los cambios hasta el próximo reinicio o flush.

El error 1045 parece intimidante por lo genérico del mensaje, pero en la práctica es uno de los errores de MySQL más fáciles de diagnosticar con el método correcto: usuario, host, contraseña, plugin, y privilegios, en ese orden. La próxima vez que te aparezca a las 2am, ya sabés por dónde empezar.

¿Te trabó algo que no está en esta lista? Contame en los comentarios tu mensaje de error exacto y lo agregamos a la guía.

Comentarios

Entradas populares de este blog

El vibe coding: ¿el fin de los programadores o el inicio de una nueva era?

El USB-C de la IA: cómo MCP pasó de protocolo de Anthropic a infraestructura de toda la industria en 16 meses

Claude encontró un bug de 29 años que nadie había visto — y ahora los gobiernos quieren controlar la IA que puede hacer esto