Cómo crear un currículum en inglés efectivo para postularte como Software Engineer (Go, Python, TS y más)
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.
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.
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.
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.
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';
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}")
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.
% 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.
localhost en vez del nombre del servicio Docker. Empieza por el Paso 1 y el Paso 4.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