Proteger MariaDB y MySQL: los pasos tras la instalación
Después de instalar el paquete, una base de datos todavía no está protegida. Esta guía recorre mysql_secure_installation, aclara la pregunta sobre unix_socket y muestra cómo crear un usuario propio para cada aplicación.
Una base de datos recién instalada en un servidor root rara vez es tan insegura como afirman las guías antiguas, y rara vez es tan segura como sugiere el sistema de paquetes. Entre esos dos extremos están justamente los pasos que describe este artículo: qué hace realmente mysql_secure_installation, por qué la pregunta sobre unix_socket se responde hoy de otra forma que en 2015, cómo es un usuario de aplicación con permisos mínimos y cómo mantener las contraseñas fuera de la lista de procesos.
Todos los comandos se ejecutan como root. Si trabajas con un usuario normal, antepón sudo. Las salidas proceden de Debian 13 y Debian 12, así como de Ubuntu 24.04 y Ubuntu 22.04.
Punto de partida: qué paquete corre en qué distribución
El primer tropiezo aparece antes del primer paso de seguridad. Debian ya no distribuye desde hace años ningún paquete propio llamado mysql-server. Allí el nombre solo existe como paquete virtual sin versión propia, visible únicamente a través de las dependencias de mariadb-server:
apt-cache policy mysql-server
mysql-server:
Installed: (none)
Candidate: (none)
Version table:
Esta comparación de versiones, por tanto, solo da un resultado en Ubuntu, allí la 8.0.46 tanto en 24.04 como en 22.04. En Debian se usa en su lugar apt-cache showpkg mysql-server, que deja a la vista el carácter virtual del nombre.
En Debian, la base de datos es entonces siempre MariaDB. En Ubuntu están las dos y hay que elegir. Las versiones de las distribuciones actuales:
| Distribución | mariadb-server | mysql-server |
| Debian 13 | 11.8 | solo nombre virtual, sin versión |
| Debian 12 | 10.11 | solo nombre virtual, sin versión |
| Ubuntu 24.04 | 10.11 | 8.0.46 |
| Ubuntu 22.04 | 10.6 | 8.0.46 |
La instalación es la de siempre:
apt update
apt install -y mariadb-server
Si realmente has obtenido la versión esperada no lo aclara la versión del paquete por sí sola, sino el servidor en marcha:
mariadb -e "SELECT @@version, @@version_comment;"
El segundo tropiezo es el nombre del programa. Desde MariaDB 11.0, los nombres compatibles con MySQL ya no están en el paquete principal, sino en mariadb-client-compat y mariadb-server-compat. Por eso, en Debian 13 un mysql --version puede responder con un aviso o directamente no existir:
mysql: Deprecated program name. It will be removed in a future release, use '/usr/bin/mariadb' instead
bash: mysql: command not found
Si escribes scripts que deben funcionar en los cuatro sistemas, usa en MariaDB siempre mariadb, mariadb-dump y mariadb-secure-installation. Estos nombres existen desde MariaDB 10.5, así que también en Ubuntu 22.04.
mysql_secure_installation paso a paso
La herramienta se llama de forma distinta según el servidor. En MariaDB, el nombre canónico es mariadb-secure-installation; en MySQL 8.0 sigue siendo mysql_secure_installation. En MariaDB, el nombre antiguo sigue existiendo como enlace simbólico en Debian 12 y en Ubuntu 24.04 y 22.04, pero ya no en Debian 13: allí existe únicamente mariadb-secure-installation.
Merece la pena echar un vistazo a las opciones, porque el programa acepta, entre otras, --defaults-file, --socket y --protocol. Ese resumen, eso sí, solo está en el manual:
man mariadb-secure-installation
Y es que un --help no existe. El script no evalúa los argumentos en absoluto: se traga el parámetro desconocido sin decir nada y arranca de inmediato el proceso interactivo. Si falta una consola real, por ejemplo en una tubería o en un container sin terminal, repite la pregunta por la contraseña sin fin y no vuelve nunca por sí solo. Por eso se llama sin argumentos y desde una shell interactiva:
mariadb-secure-installation
En Debian 13, el script antepone al proceso un aviso muy claro:
NOTE: MariaDB is secure by default in Debian. Running this script is useless at best,
and misleading at worst. This script will be removed in a future MariaDB release in Debian.
Debian considera pues innecesario ejecutarlo allí y eliminará el script en una versión futura, tal como se puede leer en /usr/share/doc/mariadb-server/README.Debian.gz. Aun así, los siguientes pasos merecen la pena, porque muestran qué se comprueba y por qué en un sistema actual apenas queda nada que cambiar.
El proceso empieza con la pregunta por la contraseña de root. La redacción depende de la versión del script:
Enter current password for root (enter for none):
Las versiones más recientes preguntan en su lugar:
Enter root user password or leave blank:
En los dos casos se refiere a lo mismo. En una instalación recién hecha no hay ninguna contraseña, así que aquí basta con pulsar Enter.
Después vienen las decisiones de verdad. El orden y la redacción varían entre MariaDB y MySQL, pero en el fondo se trata de los mismos cinco puntos.
La pregunta sobre unix_socket
En MariaDB aparece a continuación:
Switch to unix_socket authentication [Y/n]
Esta pregunta confunde, porque en todos los sistemas tratados aquí ya está respondida. Desde MariaDB 10.4, root@localhost está protegido de fábrica mediante el plugin unix_socket, y eso vale igual para la 10.6 de Ubuntu 22.04 que para la 11.8 de Debian 13. En MySQL 8.0, el equivalente se llama auth_socket, y el asistente lo dice abiertamente:
Skipping password set for root as authentication with auth_socket is used by default.
En la práctica esto significa: quien tenga la sesión iniciada como usuario del sistema root entra en la base de datos con mariadb sin contraseña. Quien no lo sea no entra en absoluto, ni siquiera con la contraseña correcta. Esto no es una carencia, sino la variante más fuerte. No hay ninguna contraseña que pueda escaparse por un backup, un archivo de configuración o una captura de pantalla. La respuesta a la pregunta es, por tanto, mantener el sí, y una contraseña de root adicional sobra en un servidor de aplicaciones individual.
El estado solo se puede comprobar en la tabla real. La consulta más obvia, SELECT user, host, plugin FROM mysql.user, induce aquí a error: en root muestra el valor mysql_native_password, aunque en realidad quien actúa es unix_socket. En MariaDB, a partir de la 10.4, mysql.user ya solo es una vista sobre mysql.global_priv, y esa vista conoce un único método de autenticación por cuenta. Quien se fíe de ella dará por hecho, equivocadamente, que el servidor se autentica por contraseña. La regla completa está en el campo JSON Priv de la tabla real:
mariadb -e "SELECT User, Host, JSON_DETAILED(Priv) FROM mysql.global_priv;"
Para root en localhost, en MariaDB aparece allí algo así:
{"plugin":"mysql_native_password","authentication_string":"invalid","auth_or":[{},{"plugin":"unix_socket"}]}
La primera entrada es el método por contraseña contra el hash inservible de la cadena invalid, que nadie puede acertar. La segunda entrada, bajo auth_or, es la autenticación por socket que realmente actúa. De forma más compacta, las dos se consultan así:
mariadb -e "SELECT User, Host, JSON_VALUE(Priv,'$.plugin') AS plugin, JSON_QUERY(Priv,'$.auth_or') AS auth_or FROM mysql.global_priv;"
En root y localhost se espera un unix_socket, si no en la columna plugin, entonces bajo auth_or. Si allí solo hay un método por contraseña y nada bajo auth_or, la cuenta trabaja exclusivamente con contraseña. En MySQL 8.0 no existe mysql.global_priv: allí mysql.user es una tabla real y la columna plugin tiene que mostrar auth_socket. Por cierto, de la vista de MariaDB se puede seguir leyendo, pero ya no escribir en ella.
Solo se cambia cuando una herramienta necesita obligatoriamente una contraseña, por ejemplo un sistema de monitorización que no corre como root. Aun así, tiene más sentido crear un segundo usuario de administración que reconvertir la cuenta root. En MariaDB, a partir de la 11.6, y por tanto también en la 11.8 de Debian 13, una cuenta se puede vincular además a un usuario del sistema concreto:
CREATE USER 'dbadmin'@'localhost' IDENTIFIED VIA unix_socket AS 'deploy';
Con eso, el usuario del sistema deploy puede iniciar sesión como usuario de base de datos dbadmin, sin que los nombres tengan que coincidir. En Debian 12 y en las versiones de Ubuntu, la cadena que va después de AS todavía se ignora: allí el usuario del sistema y el de la base de datos tienen que llamarse igual.
Las cuatro preguntas restantes
El resto no admite discusión y se responde siempre que sí: eliminar los usuarios anónimos, prohibir el inicio de sesión remoto de root, eliminar la base de datos test junto con sus permisos y recargar las tablas de privilegios. En un Debian o un Ubuntu actual, los usuarios anónimos y la base de datos de pruebas normalmente ni siquiera existen, y el script se limita a informar de que no había nada que hacer.
En MySQL 8.0 se añade una pregunta que MariaDB no tiene:
Would you like to setup VALIDATE PASSWORD component?
Este componente impone requisitos mínimos a todas las contraseñas que se establezcan en el futuro. Resulta útil cuando varias personas crean usuarios. Resulta molesto cuando un script de aprovisionamiento genera contraseñas aleatorias que por descuido no contienen ningún carácter especial. Entonces el script se interrumpe con:
ERROR 1819 (HY000): Your password does not satisfy the current policy requirements
Si lo activas, ajusta antes el generador de contraseñas. El nivel por defecto es MEDIUM y exige al menos ocho caracteres, mayúsculas y minúsculas, un dígito y un carácter especial.
Cuando algo sale mal: volver a entrar en la base de datos
La forma más habitual de quedarse fuera es el cambio bienintencionado de unix_socket a una contraseña que después se pierde. O al revés: un script antiguo vuelve a crear /etc/mysql/debian.cnf y de repente ya no encaja nada. Los mensajes de error que se buscan entonces son estos:
ERROR 1698 (28000): Access denied for user 'root'@'localhost'
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
ERROR 1524 (HY000): Plugin 'unix_socket' is not loaded
El error 1698 significa: la cuenta espera unix_socket, pero tú no eres el usuario del sistema adecuado. Muchas veces basta con anteponer un sudo. El error 1045 significa: se espera una contraseña y la tuya no es correcta.
Si ya no consigues entrar de ninguna manera, arranca el servidor sin comprobación de permisos. En MariaDB eso se hace de forma limpia con la variable de entorno que evalúa la unidad de systemd incluida, sin tocar ningún archivo del paquete:
systemctl stop mariadb
systemctl set-environment MYSQLD_OPTS="--skip-grant-tables --skip-networking"
systemctl start mariadb
Ahora te conectas con mariadb -u root y restableces el estado. Es importante el FLUSH PRIVILEGES previo, porque sin las tablas de privilegios cargadas ALTER USER falla:
FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket;
Después hay que limpiar sin falta, porque si no el servidor arranca sin protección tras cada reinicio:
systemctl stop mariadb
systemctl unset-environment MYSQLD_OPTS
systemctl start mariadb
En MySQL 8.0 sobre Ubuntu esta vía no funciona, porque la unidad no evalúa ninguna variable de ese tipo y porque ALTER USER con --skip-grant-tables tiene trampas añadidas. Allí se usa un archivo de inicio, que tiene que estar en un directorio que AppArmor le permita al proceso del servidor; de lo contrario el arranque falla con Can't open file. /var/lib/mysql-files está permitido, /tmp no:
systemctl stop mysql
echo "ALTER USER 'root'@'localhost' IDENTIFIED WITH auth_socket;" > /var/lib/mysql-files/reset.sql
chown mysql:mysql /var/lib/mysql-files/reset.sql
systemctl edit mysql
En el editor se escribe una sobrescritura del comando de arranque; la primera línea vacía es necesaria para borrar el valor original:
[Service]
ExecStart=
ExecStart=/usr/sbin/mysqld --init-file=/var/lib/mysql-files/reset.sql
Tras un systemctl daemon-reload y un arranque, la cuenta queda restablecida. Después elimina la sobrescritura con systemctl revert mysql y borra el archivo. Si gestionas bases de datos en sistemas separados, encontrarás indicaciones complementarias sobre la protección del acceso en el artículo Proteger un servidor SSH.
Un usuario propio por aplicación en lugar de root
El paso más eficaz no aparece en ningún asistente. Las aplicaciones no deben conectarse como root, y tampoco necesitan un GRANT ALL. Una aplicación web típica lee y escribe filas: no crea bases de datos ni lee archivos del servidor.
CREATE DATABASE shopdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'shopapp'@'localhost' IDENTIFIED BY 'AquiUnaContrasenaLargaAleatoria';
GRANT SELECT, INSERT, UPDATE, DELETE ON shopdb.* TO 'shopapp'@'localhost';
Aquí hay tres cosas importantes. Primero, el punto de shopdb.* en lugar de *.*: los permisos sobre *.* son permisos globales y valen también para mysql e information_schema. Segundo, la indicación @'localhost' en lugar de @'%': así la cuenta solo se puede usar en local, incluso si el puerto acaba abierto algún día. Tercero, falta WITH GRANT OPTION, porque una cuenta que puede repartir permisos es de hecho un administrador.
Los cambios de esquema pasan entonces por una segunda cuenta que solo se usa durante el deployment:
CREATE USER 'shopmigrate'@'localhost' IDENTIFIED BY 'OtraContrasenaLargaAleatoria';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, DROP, INDEX, REFERENCES ON shopdb.* TO 'shopmigrate'@'localhost';
Suena a trabajo de más, y es justo el punto en el que una inyección SQL en la aplicación se queda en una fuga de datos molesta en lugar de convertirse en una pérdida total. Una cuenta sin DROP no puede borrar ninguna tabla.
El control no se hace sobre el texto del GRANT, sino sobre el resultado:
SHOW GRANTS FOR 'shopapp'@'localhost';
Se esperan exactamente dos líneas: un GRANT USAGE ON *.*, que representa únicamente el derecho a iniciar sesión y no supone ningún acceso a datos, y la línea con los cuatro permisos sobre shopdb.*. Si allí aparece ALL PRIVILEGES ON *.*, el punto estaba en el sitio equivocado. La segunda prueba, más reveladora, consiste en iniciar sesión con la cuenta nueva y ejecutar un SHOW DATABASES;. Solo deben verse information_schema y shopdb.
Un error frecuente al crear la cuenta:
ERROR 1396 (HY000): Operation CREATE USER failed for 'shopapp'@'localhost'
Eso casi siempre significa que la cuenta ya existe, a menudo como resto de un intento anterior. DROP USER 'shopapp'@'localhost'; y otra vez desde el principio.
bind-address: dónde escucha realmente la base de datos
En las cuatro distribuciones, tras la instalación del paquete, la base de datos escucha solo en 127.0.0.1. La línea está, en MariaDB, en /etc/mysql/mariadb.conf.d/50-server.cnf y, en MySQL 8.0, en /etc/mysql/mysql.conf.d/mysqld.cnf. Mejor comprobarlo que suponerlo:
grep -R "bind-address" /etc/mysql/
En MySQL 8.0 hay una segunda línea que se suele pasar por alto: mysqlx-bind-address controla el protocolo X en el puerto 33060. Si solo cambias bind-address, puede que abras únicamente medio acceso o que dejes abierto el segundo puerto.
La comprobación más fiable no es el archivo de configuración, sino el kernel:
ss -lntp
LISTEN 0 80 127.0.0.1:3306 0.0.0.0:* users:(("mariadbd",pid=712,fd=22))
Si allí aparece 0.0.0.0:3306 o *:3306, el servicio es accesible desde la red. Como complemento, el servidor ofrece su propia visión:
mariadb -e "SELECT @@bind_address, @@port, @@skip_networking;"
Aquí hay dos trampas muy extendidas. La primera: los archivos de mariadb.conf.d se leen por orden alfabético, y gana el valor leído en último lugar. Por eso, si escribes tu cambio en un archivo propio, llámalo 99-eigene.cnf y no 10-eigene.cnf. En esto fallan justamente la mayoría de los informes en los que MariaDB supuestamente ignora bind-address. La ventaja de un archivo propio: las actualizaciones de paquetes no piden resolver ningún conflicto, porque el archivo incluido queda intacto.
La segunda trampa: si no necesitas para nada el acceso por red, ve un paso más allá de bind-address y usa skip-networking. Entonces ya no existe ningún puerto TCP, solo el socket Unix. Es la configuración correcta para el caso estándar de aplicación web y base de datos en el mismo servidor, pero cuesta nervios cuando una aplicación tiene 127.0.0.1 en su configuración en lugar de localhost: con 127.0.0.1, las bibliotecas cliente fuerzan TCP.
Acceso desde fuera solo cuando es realmente necesario
Un puerto de base de datos en la red abierta se encuentra en cuestión de horas y se prueba de forma permanente. La mejor respuesta a la pregunta por el acceso externo es, por eso, evitarlo. Para el mantenimiento ocasional basta con un túnel SSH que en tu propio equipo asigne un puerto local al socket de base de datos del otro extremo. La herramienta de base de datos se conecta entonces contra 127.0.0.1, sin que el servidor abra nada en la red.
Para conexiones permanentes entre varios servidores, un túnel WireGuard es la solución limpia. La base de datos se vincula entonces únicamente a la dirección del túnel, no a la dirección IP pública.
Si aun así tiene que haber un puerto abierto, encajan cuatro medidas. El servidor se vincula a exactamente una dirección interna. El firewall deja pasar solo la dirección de origen conocida. La cuenta de base de datos está atada a esa misma dirección, es decir 'shopapp'@'10.0.0.5' y nunca 'shopapp'@'%'. Y la conexión se fuerza cifrada:
ALTER USER 'shopapp'@'10.0.0.5' REQUIRE SSL;
Al probar desde fuera te encuentras con dos errores que a menudo se confunden. El primero significa que no responde nadie, así que se trata del firewall o de bind-address:
ERROR 2003 (HY000): Can't connect to MySQL server on '203.0.113.10:3306' (110)
El segundo significa que el servidor responde y rechaza la conexión a propósito, así que falta la cuenta adecuada para ese origen:
ERROR 1130 (HY000): Host '203.0.113.55' is not allowed to connect to this MariaDB server
En local, en cambio, el clásico es que el servicio sencillamente no esté en marcha:
ERROR 2002 (HY000): Can't connect to local server through socket '/run/mysqld/mysqld.sock' (2)
Contraseñas fuera de la línea de comandos
La llamada mariadb -u shopapp -pGeheim123 funciona y aun así es un error. El propio servidor lo dice:
Warning: Using a password on the command line interface can be insecure.
Detrás hay dos razones. Primera, la línea acaba en el historial de la shell. Segunda, en un sistema estándar la línea de comandos de un proceso es visible para cualquier usuario con sesión iniciada a través de ps. En un servidor con varios clientes o varios servicios, eso es un robo de contraseñas sin ningún esfuerzo.
La vía correcta para las personas es -p sin nada detrás. Entonces se pregunta de forma interactiva y no queda nada en el historial:
mariadb -u shopapp -p shopdb
La vía correcta para scripts y cronjobs es un archivo de opciones con permisos restringidos. Se crea con el modo correcto en un solo paso:
install -m 600 /dev/null /root/.my.cnf
Contenido:
[client]
user=backup
password=AquiUnaContrasenaLargaAleatoria
A partir de ahí, cada cliente encuentra las credenciales por sí mismo. Para tareas separadas se crean varios archivos y se apunta a ellos de forma explícita. Aquí rige una regla con la que mucha gente tropieza: --defaults-extra-file y --defaults-file tienen que ser la primera opción de la llamada, si no se ignoran sin decir nada.
mariadb-dump --defaults-extra-file=/root/.my-backup.cnf --single-transaction shopdb
La variable de entorno MYSQL_PWD no es una solución. Está en /proc y por tanto es igual de visible que la línea de comandos. En MySQL 8.0 existe además mysql_config_editor, que escribe un archivo ~/.mylogin.cnf. El contenido está ofuscado, pero no cifrado, y MariaDB no conoce esa herramienta. En entornos mixtos, el sencillo archivo de opciones con modo 600 es la opción más fiable.
Un último punto tiene que ver con los backups. Un dump contiene todo lo que ve la aplicación, y una cuenta de backup no necesita permisos de escritura para eso. Para mariadb-dump con --single-transaction basta por regla general con:
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES ON shopdb.* TO 'backup'@'localhost';
GRANT PROCESS ON *.* TO 'backup'@'localhost';
Verificación: cómo saber que ha quedado bien
Un comando que termina sin errores no demuestra nada. Estas seis pruebas sí demuestran algo:
- La cuenta root usa autenticación por socket:
mariadb -e "SELECT User, Host, JSON_VALUE(Priv,'$.plugin') AS plugin, JSON_QUERY(Priv,'$.auth_or') AS auth_or FROM mysql.global_priv;"muestra enrootununix_socket, ya sea en la columnaplugino bajoauth_or. La vistamysql.userno sirve para esto, porque solo devuelve el primer método. En MySQL 8.0, al revés, lo determinante es la columnaplugindemysql.user, y allí tiene que aparecerauth_socket. - No hay ninguna cuenta anónima:
mariadb -e "SELECT user, host FROM mysql.user WHERE user = '';"devuelve un conjunto de resultados vacío. - La base de datos de pruebas ya no está:
mariadb -e "SHOW DATABASES LIKE 'test';"no devuelve nada. - El puerto está cerrado:
ss -lntpno muestra nada para el 3306 o muestra únicamente127.0.0.1. - La cuenta de la aplicación está limitada: al iniciar sesión con ella, un
SHOW DATABASES;muestra solo su propia base de datos, y unDROP TABLEfalla. - Ninguna contraseña en texto plano por ahí suelta:
grep -rs "password" /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ /etc/cron.weekly/ /etc/cron.monthly/no encuentra nada, y tampococrontab -lpara root ni un vistazo a/var/spool/cron/crontabs/, y los archivos de opciones tienen el modo 600. El parámetro-sforma parte del comando porque, sin él, un directorio inexistente interrumpe la llamada con el código de salida 2: en Debian 13,/etc/cron.dno existe tras una instalación puramente de base de datos, porque allí todavía no hay ningún paquete cron instalado.
Quien recorra estos seis puntos en un servidor nuevo habrá descartado ya la inmensa mayoría de los ataques contra bases de datos, sin instalar ni un solo software adicional. El resto es disciplina de actualización y un backup que funcione, cuya restauración se haya ensayado al menos una vez.
Preguntas frecuentes
¿Sigo necesitando una contraseña de root en MariaDB?
¿Por qué mi Debian dice que el paquete mysql-server no existe?
¿Cómo vuelvo a entrar en la base de datos si me he quedado fuera?
¿Qué diferencia hay entre ERROR 1698 y ERROR 1045?
¿Basta con bind-address = 127.0.0.1 para proteger la base de datos?
¿Por qué la aplicación no debe acceder a la base de datos como root?
¿Cómo le paso de forma segura una contraseña de base de datos a un cron job?
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.

