Proteger MariaDB y MySQL: los pasos tras la instalación

Publicado el 18 min de lectura

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ónmariadb-servermysql-server
Debian 1311.8solo nombre virtual, sin versión
Debian 1210.11solo nombre virtual, sin versión
Ubuntu 24.0410.118.0.46
Ubuntu 22.0410.68.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:

  1. 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 en root un unix_socket, ya sea en la columna plugin o bajo auth_or. La vista mysql.user no sirve para esto, porque solo devuelve el primer método. En MySQL 8.0, al revés, lo determinante es la columna plugin de mysql.user, y allí tiene que aparecer auth_socket.
  2. No hay ninguna cuenta anónima: mariadb -e "SELECT user, host FROM mysql.user WHERE user = '';" devuelve un conjunto de resultados vacío.
  3. La base de datos de pruebas ya no está: mariadb -e "SHOW DATABASES LIKE 'test';" no devuelve nada.
  4. El puerto está cerrado: ss -lntp no muestra nada para el 3306 o muestra únicamente 127.0.0.1.
  5. 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 un DROP TABLE falla.
  6. 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 tampoco crontab -l para root ni un vistazo a /var/spool/cron/crontabs/, y los archivos de opciones tienen el modo 600. El parámetro -s forma parte del comando porque, sin él, un directorio inexistente interrumpe la llamada con el código de salida 2: en Debian 13, /etc/cron.d no 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?
En Debian 12 y 13 y en Ubuntu 22.04 y 24.04, no. Allí la cuenta root@localhost usa de fábrica el plugin unix_socket, así que el acceso pasa por la identidad del usuario del sistema. Una contraseña adicional no aumenta la seguridad, solo crea otro secreto más que se puede perder o filtrar. Únicamente si una herramienta exige por fuerza un inicio de sesión con contraseña conviene crear una cuenta propia para eso, en lugar de reconvertir la de root.
¿Por qué mi Debian dice que el paquete mysql-server no existe?
Desde hace varias versiones, Debian ya no distribuye un paquete mysql-server propio. Ni en Debian 12 ni en Debian 13 hay una versión instalable: apt-cache policy mysql-server informa allí de Candidate: (none) y de una Version table vacía, porque el nombre solo existe ya de forma virtual a través de mariadb-server. Quien lo deja a la vista es apt-cache showpkg mysql-server. El comando para la instalación es apt install -y mariadb-server. En Ubuntu 22.04 y 24.04 están disponibles los dos, MySQL en la versión 8.0.46 y MariaDB en la 10.6 o en la 10.11 según la distribución.
¿Cómo vuelvo a entrar en la base de datos si me he quedado fuera?
En MariaDB se detiene el servicio, se pone con systemctl set-environment la variable MYSQLD_OPTS en --skip-grant-tables --skip-networking y se arranca de nuevo. Tras iniciar sesión hace falta primero un FLUSH PRIVILEGES, después la cuenta se restablece con ALTER USER. Al terminar hay que quitar otra vez la variable con systemctl unset-environment. En MySQL 8.0 esta vía no funciona: allí se usa un archivo de inicio a través de --init-file, que por culpa de AppArmor tiene que estar en /var/lib/mysql-files.
¿Qué diferencia hay entre ERROR 1698 y ERROR 1045?
ERROR 1698 (28000) significa que la cuenta espera autenticación por socket y que el usuario del sistema que la invoca no encaja. Aquí suele bastar con un sudo delante del comando. ERROR 1045 (28000) con el añadido (using password: YES) significa, en cambio, que se intentó un inicio de sesión con contraseña y que la contraseña no es correcta. Los dos errores tienen, por tanto, causas y soluciones completamente distintas.
¿Basta con bind-address = 127.0.0.1 para proteger la base de datos?
Es el paso más importante, pero no el único. En MySQL 8.0 existe además mysqlx-bind-address para el puerto 33060, que hay que establecer por separado. Aparte de eso, en /etc/mysql/mariadb.conf.d gana el último archivo leído, así que los cambios propios van en un archivo con un número alto como 99-eigene.cnf. El resultado se comprueba siempre con ss -lntp, nunca en el archivo de configuración.
¿Por qué la aplicación no debe acceder a la base de datos como root?
Porque así cualquier fallo de la aplicación acaba en acceso completo a todas las bases de datos, incluidas las tablas de privilegios. Una cuenta con GRANT SELECT, INSERT, UPDATE, DELETE ON midb.* no puede borrar ninguna tabla, ni leer bases de datos ajenas, ni repartir permisos. Los cambios de esquema pasan por una cuenta separada que solo se usa durante el deployment.
¿Cómo le paso de forma segura una contraseña de base de datos a un cron job?
A través de un archivo de opciones con modo 600, creado por ejemplo con install -m 600 /dev/null /root/.my.cnf y con una sección [client] que contenga user y password. La llamada apunta después a él con --defaults-extra-file, obligatoriamente como primera opción, porque si no el parámetro se ignora. Las contraseñas escritas directamente detrás de -p en la línea de comandos son visibles para cualquier usuario a través de ps, y MYSQL_PWD se puede consultar de forma parecida a través de /proc.

MariaDB MySQL Base de datos Seguridad de servidores Debian Ubuntu Administración de Linux