MySQL y MariaDB controlan el acceso con cuentas formadas por un usuario y un host ('usuario'@'host') y con privilegios que se conceden a nivel global, de base de datos, de tabla o de columna. Usar root para todo es cómodo pero peligroso: un fallo en la aplicación expone el servidor entero. En este tutorial crearás usuarios con los privilegios mínimos que necesita cada uno, los agruparás con roles, permitirás el acceso desde otra máquina de forma controlada y comprobarás que los permisos funcionan como esperas, en Ubuntu 24.04.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con MySQL 8.0 (sudo apt install mysql-server) o MariaDB 10.11 (sudo apt install mariadb-server).
  • Un usuario no root con privilegios sudo.
  • Una base de datos de ejemplo. En la guía se usa appdb; puedes crearla en el paso 1.

Los comandos son iguales en los dos motores salvo donde se indica.

Paso 1: Conectarse como administrador y revisar las cuentas

En Ubuntu, la cuenta root de MySQL y MariaDB se autentica por socket Unix: el usuario root del sistema entra sin contraseña y nadie más puede usarla. Abre la consola:

sudo mysql

Si todavía no tienes una base de datos de ejemplo, créala con una tabla:

CREATE DATABASE appdb;
CREATE TABLE appdb.clientes (id INT PRIMARY KEY AUTO_INCREMENT, nombre VARCHAR(100), email VARCHAR(255), notas_internas TEXT);

Lista las cuentas existentes:

SELECT user, host, plugin, account_locked FROM mysql.user;
+------------------+-----------+-----------------------+----------------+
| user             | host      | plugin                | account_locked |
+------------------+-----------+-----------------------+----------------+
| debian-sys-maint | localhost | caching_sha2_password | N              |
| mysql.infoschema | localhost | caching_sha2_password | Y              |
| mysql.session    | localhost | caching_sha2_password | Y              |
| mysql.sys        | localhost | caching_sha2_password | Y              |
| root             | localhost | auth_socket           | N              |
+------------------+-----------+-----------------------+----------------+

Las cuentas mysql.* son internas y están bloqueadas; debian-sys-maint la usan los scripts del paquete. No las modifiques. En MariaDB, la columna account_locked no existe en mysql.user; usa SELECT user, host, plugin FROM mysql.user;.

Paso 2: Crear un usuario para la aplicación

Una cuenta se identifica por usuario y host. 'app'@'localhost' solo puede conectarse desde el propio servidor, lo más seguro cuando la aplicación está en la misma máquina. Sustituye your_strong_password por una contraseña larga y aleatoria:

CREATE USER 'app'@'localhost' IDENTIFIED BY 'your_strong_password';

Concede solo lo que la aplicación necesita para trabajar con los datos de appdb:

GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'app'@'localhost';

appdb.* significa todas las tablas de appdb. Este usuario puede leer y modificar filas, pero no crear ni borrar tablas. Si la aplicación ejecuta sus propias migraciones, crea una cuenta aparte para ellas con CREATE, ALTER, INDEX, DROP, REFERENCES además de los anteriores, y úsala solo durante los despliegues.

No hace falta ejecutar FLUSH PRIVILEGES: CREATE USER y GRANT aplican los cambios de inmediato. Ese comando solo es necesario si modificas las tablas de mysql directamente, algo que no deberías hacer.

Revisa los privilegios del usuario:

SHOW GRANTS FOR 'app'@'localhost';
+-----------------------------------------------------------------------------+
| Grants for app@localhost                                                    |
+-----------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `app`@`localhost`                                     |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `appdb`.* TO `app`@`localhost`      |
+-----------------------------------------------------------------------------+

USAGE significa "sin privilegios": solo permite conectarse.

Paso 3: Conceder privilegios a nivel de tabla y columna

Los privilegios pueden ser más finos que una base de datos completa. Un usuario de informes que solo debe leer el nombre y el correo de los clientes, pero no las notas internas, puede recibir permiso sobre columnas concretas:

CREATE USER 'informes'@'localhost' IDENTIFIED BY 'your_strong_password';
GRANT SELECT (id, nombre, email) ON appdb.clientes TO 'informes'@'localhost';

Con este permiso, SELECT nombre FROM appdb.clientes funciona, pero SELECT * o SELECT notas_internas fallan.

Para retirar un privilegio usa REVOKE con la misma forma que el GRANT. Por ejemplo, para que app deje de poder borrar filas:

REVOKE DELETE ON appdb.* FROM 'app'@'localhost';

Vuelve a concederlo si lo necesitas para el resto de la guía:

GRANT DELETE ON appdb.* TO 'app'@'localhost';

Paso 4: Agrupar privilegios con roles

Cuando varias personas o servicios necesitan el mismo acceso, es más fácil definir un rol con los privilegios y asignarlo, que repetir los GRANT en cada cuenta. Crea un rol de solo lectura y otro de lectura y escritura:

CREATE ROLE 'appdb_lectura';
CREATE ROLE 'appdb_escritura';
GRANT SELECT ON appdb.* TO 'appdb_lectura';
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'appdb_escritura';

Crea un usuario para una analista y asígnale el rol de lectura:

CREATE USER 'ana'@'localhost' IDENTIFIED BY 'your_strong_password';
GRANT 'appdb_lectura' TO 'ana'@'localhost';

Un rol asignado no está activo hasta que se activa en la sesión. Haz que se active automáticamente al conectar. En MySQL 8.0:

SET DEFAULT ROLE ALL TO 'ana'@'localhost';

En MariaDB la sintaxis es distinta y solo admite un rol por defecto:

SET DEFAULT ROLE 'appdb_lectura' FOR 'ana'@'localhost';

Comprueba el resultado:

SHOW GRANTS FOR 'ana'@'localhost';
+----------------------------------------------------+
| Grants for ana@localhost                           |
+----------------------------------------------------+
| GRANT USAGE ON *.* TO `ana`@`localhost`            |
| GRANT `appdb_lectura`@`%` TO `ana`@`localhost`     |
+----------------------------------------------------+

Si más adelante cambias los privilegios del rol, todos los usuarios que lo tienen reciben el cambio.

Paso 5: Permitir el acceso desde otro servidor

Si la aplicación se ejecuta en otra máquina, crea la cuenta para la IP concreta de esa máquina (o su red privada) en lugar de usar '%', que acepta conexiones desde cualquier dirección. En este ejemplo, el servidor de aplicación tiene la IP privada 10.0.0.5:

CREATE USER 'app'@'10.0.0.5' IDENTIFIED BY 'your_strong_password' REQUIRE SSL;
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'app'@'10.0.0.5';
EXIT;

'app'@'localhost' y 'app'@'10.0.0.5' son cuentas distintas, con su propia contraseña y privilegios. REQUIRE SSL obliga a cifrar la conexión; MySQL 8.0 genera certificados al instalarse. En MariaDB 10.11 tendrás que configurar ssl_cert y ssl_key antes de exigir SSL, o quitar REQUIRE SSL si la conexión va por una red privada de confianza.

Por defecto, el servidor solo escucha en 127.0.0.1. Cambia bind-address a la IP privada del servidor de base de datos (en el ejemplo, 10.0.0.2). En MySQL el archivo es /etc/mysql/mysql.conf.d/mysqld.cnf; en MariaDB, /etc/mysql/mariadb.conf.d/50-server.cnf:

sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
bind-address = 10.0.0.2

Reinicia el servicio (mysql o mariadb) y abre el puerto solo para el servidor de aplicación:

sudo systemctl restart mysql
sudo ufw allow from 10.0.0.5 to any port 3306 proto tcp

Desde 10.0.0.5, comprueba la conexión:

mysql -h 10.0.0.2 -u app -p appdb -e "SELECT CURRENT_USER();"
+----------------+
| CURRENT_USER() |
+----------------+
| [email protected]   |
+----------------+

Paso 6: Aplicar límites y bloquear cuentas

Puedes limitar las conexiones simultáneas de una cuenta para que una aplicación con fugas de conexiones no agote las del servidor:

ALTER USER 'app'@'localhost' WITH MAX_USER_CONNECTIONS 50;

Para cambiar una contraseña:

ALTER USER 'ana'@'localhost' IDENTIFIED BY 'your_new_strong_password';

Si una persona deja el equipo, bloquea su cuenta sin borrarla (útil si tiene objetos o quieres auditar) o elimínala directamente:

ALTER USER 'ana'@'localhost' ACCOUNT LOCK;
DROP USER 'informes'@'localhost';

Un intento de conexión con una cuenta bloqueada devuelve ERROR 3118 (HY000): Access denied for user 'ana'@'localhost'. Account is locked. en MySQL. Para reactivarla, usa ACCOUNT UNLOCK.

En MySQL 8.0 también puedes hacer que las contraseñas caduquen, lo que obliga al usuario a cambiarla al conectar:

ALTER USER 'ana'@'localhost' PASSWORD EXPIRE INTERVAL 90 DAY;

No lo apliques a cuentas de aplicaciones: una contraseña caducada dejaría la aplicación sin acceso.

Paso 7: Comprobar que los permisos funcionan

No te fíes solo de SHOW GRANTS: conéctate como el usuario y prueba lo que debe y lo que no debe poder hacer. Como 'app'@'localhost':

mysql -u app -p appdb
INSERT INTO clientes (nombre, email) VALUES ('Prueba', '[email protected]');
SELECT id, nombre FROM clientes;
DROP TABLE clientes;
Query OK, 1 row affected (0.01 sec)
...
ERROR 1142 (42000): DROP command denied to user 'app'@'localhost' for table 'clientes'

El INSERT y el SELECT funcionan; el DROP TABLE se rechaza, como debe. Si haces la prueba como ana (rol de solo lectura), el INSERT fallará con INSERT command denied.

Para revisar de un vistazo quién tiene privilegios sobre appdb, consulta como administrador:

SELECT grantee, privilege_type FROM information_schema.schema_privileges WHERE table_schema = 'appdb';

Solución de problemas

ERROR 1045 (28000): Access denied for user 'app'@'localhost' (using password: YES): la contraseña no coincide o la cuenta existe con otro host. Comprueba las cuentas con SELECT user, host FROM mysql.user WHERE user = 'app'; y restablece la contraseña con ALTER USER.

ERROR 1130 (HY000): Host '10.0.0.5' is not allowed to connect to this MySQL server: no existe ninguna cuenta con ese usuario para esa IP. Créala con el host correcto, como en el paso 5.

ERROR 2003 (HY000): Can't connect to MySQL server on '10.0.0.2:3306': el servidor no escucha en esa IP o el cortafuegos bloquea el puerto. Revisa bind-address, ejecuta sudo ss -tlnp | grep 3306 y la regla de UFW.

Un usuario con un rol asignado recibe command denied: el rol no está activo en su sesión. Comprueba con SELECT CURRENT_ROLE(); y configura el rol por defecto como en el paso 4.

Conclusión

Tus aplicaciones y usuarios usan ahora cuentas separadas con los privilegios mínimos, agrupados en roles, con acceso remoto limitado a IP concretas y comprobado con pruebas reales. Como siguientes pasos, puedes activar el componente validate_password de MySQL para exigir contraseñas robustas, revisar periódicamente las cuentas con SHOW GRANTS y retirar las que ya no se usan.