Una instalación recién hecha de MySQL o MariaDB funciona, pero no está pensada para exponerse a una red: puede tener usuarios anónimos, una base de datos de pruebas y cuentas con más permisos de los necesarios. En este tutorial asegurarás un servidor MySQL 8.0 en Ubuntu 24.04 paso a paso: instalación segura, usuarios con privilegios mínimos, acceso remoto limitado por IP, cifrado TLS obligatorio y una auditoría rápida de cuentas. Donde MariaDB se comporta distinto, se indica.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo.
  • MySQL 8.0 (sudo apt install mysql-server) o MariaDB 10.11 (sudo apt install mariadb-server) instalado desde los repositorios de Ubuntu.
  • UFW activo con el acceso SSH permitido.

Las rutas cambian según el servidor:

MySQL 8.0MariaDB 10.11
Configuración del servidor/etc/mysql/mysql.conf.d/mysqld.cnf/etc/mysql/mariadb.conf.d/50-server.cnf
Servicio systemdmysqlmariadb
Clientemysqlmariadb (también mysql)

Paso 1: Ejecutar la instalación segura

El script mysql_secure_installation elimina los usuarios anónimos, la base de datos test y el acceso remoto de root. En Ubuntu, el usuario root de MySQL se autentica con el plugin auth_socket: solo puede entrar el usuario root del sistema (o con sudo), sin contraseña. Es un buen comportamiento y conviene mantenerlo.

Lanza el script:

sudo mysql_secure_installation

Responde así a las preguntas:

  • VALIDATE PASSWORD component: y, y elige el nivel 1 (MEDIUM) o 2 (STRONG). Rechazará contraseñas débiles al crear usuarios.
  • Remove anonymous users: y.
  • Disallow root login remotely: y.
  • Remove test database and access to it: y.
  • Reload privilege tables now: y.

En MariaDB el script se llama sudo mariadb-secure-installation. Cuando pregunte si quieres cambiar a unix_socket o poner contraseña a root, responde n a ambas: root ya usa autenticación por socket en Ubuntu.

Comprueba que no quedan usuarios anónimos ni root remoto:

sudo mysql -e "SELECT user, host, plugin FROM mysql.user ORDER BY user;"
+------------------+-----------+-----------------------+
| user             | host      | plugin                |
+------------------+-----------+-----------------------+
| debian-sys-maint | localhost | caching_sha2_password |
| mysql.infoschema | localhost | caching_sha2_password |
| mysql.session    | localhost | caching_sha2_password |
| mysql.sys        | localhost | caching_sha2_password |
| root             | localhost | auth_socket           |
+------------------+-----------+-----------------------+

Las cuentas mysql.* y debian-sys-maint son del sistema y están bloqueadas o las usa el paquete; no las borres.

Paso 2: Crear usuarios con privilegios mínimos

Cada aplicación debe tener su propio usuario, con permisos solo sobre su base de datos y limitado al host desde el que se conecta. Nunca uses root ni GRANT ALL ON *.* para una aplicación.

Entra en la consola:

sudo mysql

Crea la base de datos y un usuario que solo pueda conectarse desde el servidor de aplicación (app_server_ip). Si la aplicación corre en el mismo servidor, usa 'localhost' como host:

CREATE DATABASE miapp CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
CREATE USER 'miapp_user'@'app_server_ip' IDENTIFIED BY 'tu_contraseña_segura';
GRANT SELECT, INSERT, UPDATE, DELETE ON miapp.* TO 'miapp_user'@'app_server_ip';

En MariaDB usa la collation utf8mb4_unicode_ci, porque utf8mb4_0900_ai_ci es exclusiva de MySQL 8.

Estos cuatro privilegios bastan para una aplicación en funcionamiento. Si tu framework ejecuta migraciones de esquema, crea un segundo usuario para ellas con CREATE, ALTER, DROP, INDEX, REFERENCES además de los anteriores, y úsalo solo al desplegar.

Revisa los permisos concedidos:

SHOW GRANTS FOR 'miapp_user'@'app_server_ip';
+-----------------------------------------------------------------------------------+
| Grants for miapp_user@app_server_ip                                               |
+-----------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `miapp_user`@`app_server_ip`                                |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `miapp`.* TO `miapp_user`@`app_server_ip` |
+-----------------------------------------------------------------------------------+

USAGE significa "sin privilegios globales", que es justo lo que quieres.

Si necesitas un usuario de administración con contraseña para herramientas externas, restríngelo a localhost y accede a través de un túnel SSH en lugar de abrir el puerto.

Paso 3: Controlar en qué interfaz escucha el servidor

En Ubuntu, MySQL y MariaDB escuchan por defecto solo en 127.0.0.1. Si todas las aplicaciones están en el mismo servidor, déjalo así y salta al paso 5.

Si la aplicación está en otro servidor, haz que MySQL escuche en la IP privada de este servidor (preferible) o en la pública. Edita la configuración:

sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf

En la sección [mysqld], ajusta bind-address y añade las otras dos directivas:

[mysqld]
bind-address        = 127.0.0.1,your_private_ip
mysqlx-bind-address = 127.0.0.1
local_infile        = 0
skip_name_resolve   = ON
  • bind-address admite varias direcciones separadas por comas desde MySQL 8.0.13. En MariaDB 10.11 solo acepta una dirección: usa directamente your_private_ip.
  • mysqlx-bind-address mantiene el protocolo X (puerto 33060) solo en local. No existe en MariaDB; omítelo.
  • local_infile = 0 impide que los clientes lean archivos locales con LOAD DATA LOCAL.
  • skip_name_resolve evita las búsquedas DNS inversas en cada conexión. Con él activo, los usuarios deben definirse por IP, no por nombre de host.

Reinicia y comprueba en qué direcciones escucha:

sudo systemctl restart mysql
sudo ss -tlnp | grep mysqld
LISTEN 0  151  your_private_ip:3306  0.0.0.0:*  users:(("mysqld",pid=6120,fd=24))
LISTEN 0  151       127.0.0.1:3306  0.0.0.0:*  users:(("mysqld",pid=6120,fd=23))
LISTEN 0   70       127.0.0.1:33060 0.0.0.0:*  users:(("mysqld",pid=6120,fd=21))

Paso 4: Filtrar el puerto 3306 con UFW

Abre el puerto solo para los servidores que lo necesitan:

sudo ufw allow from app_server_ip to any port 3306 proto tcp
sudo ufw status numbered
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] OpenSSH                    ALLOW IN    Anywhere
[ 2] 3306/tcp                   ALLOW IN    app_server_ip

Paso 5: Exigir TLS en las conexiones

Si hay conexiones remotas, el tráfico (incluidas las consultas y los datos) debe ir cifrado.

MySQL 8.0 genera en el primer arranque un certificado autofirmado en /var/lib/mysql y activa TLS sin configuración adicional. Compruébalo:

sudo mysql -e "SHOW VARIABLES LIKE 'have_ssl'; SHOW VARIABLES LIKE 'tls_version';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| have_ssl      | YES   |
+---------------+-------+
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tls_version   | TLSv1.2,TLSv1.3 |
+---------------+-----------------+

Para obligar a que todas las conexiones TCP usen TLS, añade a la sección [mysqld] de mysqld.cnf:

require_secure_transport = ON

Las conexiones locales por socket de Unix siguen permitidas, porque no salen del servidor. También puedes exigir TLS solo a un usuario concreto:

ALTER USER 'miapp_user'@'app_server_ip' REQUIRE SSL;

Reinicia el servicio y, desde el servidor de aplicación, conéctate y confirma el cifrado:

sudo systemctl restart mysql
mysql -h your_private_ip -u miapp_user -p --ssl-mode=REQUIRED -e "SHOW SESSION STATUS LIKE 'Ssl_cipher';"
+---------------+------------------------+
| Variable_name | Value                  |
+---------------+------------------------+
| Ssl_cipher    | TLS_AES_128_GCM_SHA256 |
+---------------+------------------------+

Si la columna Value sale vacía, la conexión no está cifrada.

El certificado autofirmado cifra el tráfico, pero el cliente no puede verificar la identidad del servidor. Si necesitas esa verificación (--ssl-mode=VERIFY_IDENTITY), sustituye ssl_ca, ssl_cert y ssl_key por certificados emitidos por tu propia CA.

En MariaDB 10.11 no se genera ningún certificado. Tienes que crear uno o usar el de tu CA, indicar las rutas con ssl_cert, ssl_key y ssl_ca en 50-server.cnf, y después activar require_secure_transport = ON.

Paso 6: Auditar cuentas y privilegios

Repite estas consultas de vez en cuando, y siempre después de dar acceso a alguien. Entra con sudo mysql y ejecuta:

Usuarios que pueden conectarse desde cualquier host:

SELECT user, host FROM mysql.user WHERE host = '%';

Usuarios con privilegios globales peligrosos:

SELECT user, host FROM mysql.user
WHERE Super_priv = 'Y' OR File_priv = 'Y' OR Grant_priv = 'Y' OR Process_priv = 'Y';

Cuentas sin contraseña que no usan autenticación por socket:

SELECT user, host, plugin FROM mysql.user
WHERE authentication_string = '' AND plugin NOT IN ('auth_socket', 'unix_socket');

Lo esperado es que la primera y la tercera consulta no devuelvan filas, y que la segunda solo devuelva root@localhost y cuentas del sistema. Elimina lo que sobre con DROP USER 'usuario'@'host'; o quita permisos con REVOKE.

Paso 7: Revisar el log de errores y los accesos fallidos

MySQL registra en /var/log/mysql/error.log los errores de arranque y, con el nivel de detalle por defecto, los avisos importantes. Para ver los intentos de acceso fallidos, sube el nivel de log a 3 en [mysqld]:

log_error_verbosity = 3

Tras reiniciar, los intentos fallidos aparecen así:

sudo grep 'Access denied' /var/log/mysql/error.log | tail -n 5
2026-09-25T10:14:03.512345Z 12 [Note] [MY-010926] [Server] Access denied for user 'root'@'203.0.113.45' (using password: YES)

Si ves intentos desde IPs que no conoces, revisa las reglas de UFW del paso 4: no deberían poder llegar al puerto.

No actives el general_log en producción para esto: registra todas las consultas, llena el disco rápidamente y puede contener datos sensibles.

Solución de problemas

ERROR 1045 (28000): Access denied for user 'root'@'localhost'. Estás entrando sin sudo. Con auth_socket, root solo funciona como sudo mysql.

ERROR 3159 (HY000): Connections using insecure transport are prohibited while --require_secure_transport=ON. El cliente intenta conectar sin TLS. Añade --ssl-mode=REQUIRED o la opción equivalente de tu driver.

La aplicación no conecta tras activar skip_name_resolve. El usuario está definido con un nombre de host. Créalo de nuevo con la IP de origen.

ERROR 1819 (HY000): Your password does not satisfy the current policy requirements. El componente de validación de contraseñas está actuando. Usa una contraseña más larga con mayúsculas, números y símbolos.

Conclusión

Tu servidor MySQL o MariaDB ya no tiene cuentas anónimas ni base de datos de pruebas, cada aplicación usa un usuario con permisos mínimos, el puerto solo es accesible desde IPs concretas y las conexiones remotas van cifradas. Como siguientes pasos, programa copias de seguridad con mysqldump y prueba la restauración, ajusta el rendimiento en my.cnf según tu carga de trabajo, y aplica las actualizaciones de seguridad de Ubuntu con unattended-upgrades.