Защита MariaDB и MySQL: что делать сразу после установки
После установки пакета база данных ещё не защищена. Это руководство проходит mysql_secure_installation по шагам, разбирает вопрос про unix_socket и показывает, как завести отдельного пользователя для каждого приложения.
Свежеустановленная база данных на root-сервере редко бывает настолько уязвимой, как утверждают старые руководства, и редко настолько защищённой, как обещает пакетный менеджер. Между этими двумя полюсами лежат ровно те действия, которые описывает эта статья: что на самом деле делает mysql_secure_installation, почему на современных системах вопрос про unix_socket решается иначе, чем в 2015 году, как выглядит учётная запись приложения с минимальными правами и как не дать паролям попасть в список процессов.
Все команды выполняются от имени root. Если вы работаете под обычным пользователем, добавляйте перед ними sudo. Приведённый вывод получен на Debian 13 и Debian 12, а также на Ubuntu 24.04 и Ubuntu 22.04.
Исходная точка: какой пакет работает в каком дистрибутиве
Первая ловушка поджидает ещё до первого шага по безопасности. Debian уже несколько лет не поставляет собственный пакет с именем mysql-server. Это имя существует там только как виртуальный пакет без собственной версии, и увидеть его можно лишь через зависимости mariadb-server:
apt-cache policy mysql-server
mysql-server:
Installed: (none)
Candidate: (none)
Version table:
То есть сравнение версий даёт результат только в Ubuntu, где это 8.0.46 и на 24.04, и на 22.04. В Debian вместо этого используют apt-cache showpkg mysql-server, который наглядно показывает виртуальную природу имени.
В Debian база данных, таким образом, всегда MariaDB. В Ubuntu доступны обе, и выбор придётся сделать самому. Версии в актуальных дистрибутивах:
| Дистрибутив | mariadb-server | mysql-server |
| Debian 13 | 11.8 | только виртуальное имя, версии нет |
| Debian 12 | 10.11 | только виртуальное имя, версии нет |
| Ubuntu 24.04 | 10.11 | 8.0.46 |
| Ubuntu 22.04 | 10.6 | 8.0.46 |
Установка выполняется как обычно:
apt update
apt install -y mariadb-server
Действительно ли установлена ожидаемая версия, показывает не одна лишь версия пакета, а работающий сервер:
mariadb -e "SELECT @@version, @@version_comment;"
Вторая ловушка: имя программы. Начиная с MariaDB 11.0 совместимые с MySQL имена лежат не в основном пакете, а в mariadb-client-compat и mariadb-server-compat. Поэтому на Debian 13 команда mysql --version либо ответит предупреждением, либо не найдётся вовсе:
mysql: Deprecated program name. It will be removed in a future release, use '/usr/bin/mariadb' instead
bash: mysql: command not found
Если скрипт должен работать на всех четырёх системах, в MariaDB стоит везде использовать mariadb, mariadb-dump и mariadb-secure-installation. Эти имена существуют начиная с MariaDB 10.5, то есть и на Ubuntu 22.04.
mysql_secure_installation по шагам
Инструмент называется по-разному в зависимости от сервера. В MariaDB каноническое имя: mariadb-secure-installation, в MySQL 8.0 остаётся mysql_secure_installation. Старое имя в MariaDB на Debian 12, а также на Ubuntu 24.04 и 22.04 по-прежнему есть в виде символьной ссылки, а на Debian 13 его уже нет: там существует только mariadb-secure-installation.
На список параметров стоит взглянуть, потому что программа принимает в том числе --defaults-file, --socket и --protocol. Правда, этот обзор есть только в руководстве:
man mariadb-secure-installation
Дело в том, что --help тут нет. Скрипт вообще не разбирает аргументы: он молча проглатывает неизвестный ключ и сразу запускает интерактивный диалог. Если при этом нет настоящей консоли (например, в конвейере или в контейнере без терминала), он бесконечно повторяет запрос пароля и сам никогда не завершается. Поэтому вызывать его нужно без аргументов и в интерактивной оболочке:
mariadb-secure-installation
На Debian 13 скрипт предваряет диалог недвусмысленным предупреждением:
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 считает такой прогон излишним и удалит скрипт в одной из будущих версий, о чём написано в /usr/share/doc/mariadb-server/README.Debian.gz. Пройти по шагам всё равно стоит: они показывают, что именно проверяется и почему на современной системе менять почти нечего.
Диалог начинается с вопроса о пароле для root. Формулировка зависит от версии скрипта:
Enter current password for root (enter for none):
Более новые версии спрашивают иначе:
Enter root user password or leave blank:
Смысл в обоих случаях один и тот же. На свежей установке пароля нет, поэтому здесь просто нажимают Enter.
Дальше идут собственно решения. Порядок и формулировки у MariaDB и MySQL различаются, но по сути речь об одних и тех же пяти пунктах.
Вопрос про unix_socket
В MariaDB следующим появляется такой вопрос:
Switch to unix_socket authentication [Y/n]
Он сбивает с толку, потому что на всех рассматриваемых здесь системах ответ на него уже дан. Начиная с MariaDB 10.4 учётная запись root@localhost по умолчанию защищена плагином unix_socket, и это верно как для 10.6 на Ubuntu 22.04, так и для 11.8 на Debian 13. В MySQL 8.0 аналог называется auth_socket, и мастер настройки прямо об этом сообщает:
Skipping password set for root as authentication with auth_socket is used by default.
На практике это означает следующее. Тот, кто вошёл в систему как пользователь root, попадает в базу командой mariadb без пароля. Тот, кто вошёл под другим пользователем, не попадёт туда вообще, даже зная правильный пароль. Это не недостаток, а более надёжный вариант. Нет пароля, который мог бы утечь из резервной копии, конфигурационного файла или скриншота. Поэтому ответ на вопрос звучит так: оставить «Да», а дополнительный пароль root на отдельно стоящем сервере приложений просто не нужен.
Проверить состояние можно только по настоящей таблице. Напрашивающийся запрос SELECT user, host, plugin FROM mysql.user здесь как раз вводит в заблуждение: для root он показывает значение mysql_native_password, хотя на самом деле работает unix_socket. В MariaDB начиная с 10.4 mysql.user — это лишь представление над mysql.global_priv, и такое представление знает про каждую учётную запись только один метод аутентификации. Кто на него полагается, ошибочно считает сервер настроенным на вход по паролю. Полное правило лежит в JSON-поле Priv настоящей таблицы:
mariadb -e "SELECT User, Host, JSON_DETAILED(Priv) FROM mysql.global_priv;"
Для root на localhost в MariaDB там записано примерно следующее:
{"plugin":"mysql_native_password","authentication_string":"invalid","auth_or":[{},{"plugin":"unix_socket"}]}
Первая запись: парольный метод, проверяющий пароль против непригодного хеша строки invalid, попасть в который никто не может. Вторая запись, в auth_or: реально работающая аутентификация через сокет. Компактнее оба значения запрашиваются так:
mariadb -e "SELECT User, Host, JSON_VALUE(Priv,'$.plugin') AS plugin, JSON_QUERY(Priv,'$.auth_or') AS auth_or FROM mysql.global_priv;"
Для root и localhost ожидается unix_socket: если не в столбце plugin, то в auth_or. Если там стоит исключительно парольный метод и в auth_or ничего нет, учётная запись работает только по паролю. В MySQL 8.0 таблицы mysql.global_priv нет, там mysql.user — настоящая таблица, и столбец plugin должен показывать auth_socket. Из представления MariaDB, кстати, по-прежнему можно читать, а вот писать в него уже нельзя.
Переключаться стоит только тогда, когда какому-то инструменту пароль нужен обязательно, например мониторингу, который работает не от root. Но и в этом случае разумнее завести вторую административную учётную запись, а не переделывать root. В MariaDB начиная с 11.6, то есть и в 11.8 на Debian 13, учётную запись можно дополнительно привязать к конкретному системному пользователю:
CREATE USER 'dbadmin'@'localhost' IDENTIFIED VIA unix_socket AS 'deploy';
Так системный пользователь deploy может входить в базу как пользователь dbadmin, и совпадать имена при этом не обязаны. На Debian 12 и в версиях Ubuntu строка после AS пока игнорируется, там имя системного пользователя и имя пользователя базы должны совпадать.
Четыре оставшихся вопроса
Остальное сомнений не вызывает, и на все вопросы отвечают «Да»: удалить анонимных пользователей, запретить удалённый вход для root, удалить базу test вместе с правами на неё, перезагрузить таблицы привилегий. На актуальных Debian или Ubuntu анонимных пользователей и тестовой базы обычно и так нет, и скрипт просто сообщает, что делать было нечего.
В MySQL 8.0 добавляется вопрос, которого в MariaDB нет:
Would you like to setup VALIDATE PASSWORD component?
Этот компонент навязывает минимальные требования ко всем паролям, которые будут задаваться дальше. Он полезен, когда пользователей заводят несколько человек. И он мешает, когда скрипт подготовки генерирует случайные пароли, в которых по случайности не оказалось спецсимвола. Тогда скрипт прерывается с ошибкой:
ERROR 1819 (HY000): Your password does not satisfy the current policy requirements
Если компонент включают, генератор паролей стоит заранее настроить под него. Уровень по умолчанию: MEDIUM, он требует минимум восемь символов, буквы в верхнем и нижнем регистре, цифру и спецсимвол.
Если что-то пошло не так: как вернуться в базу
Чаще всего доступ теряют из-за благих намерений: переключились с unix_socket на пароль, а пароль потом потерялся. Или наоборот: старый скрипт заново создаёт /etc/mysql/debian.cnf, и внезапно ничего ни с чем не совпадает. Сообщения об ошибках, которые в такой момент ищут, выглядят так:
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
Ошибка 1698 означает, что учётная запись ожидает unix_socket, а вызывающий не является подходящим системным пользователем. Часто помогает уже одно sudo перед командой. Ошибка 1045 означает, что ожидается пароль и введённый пароль не подходит.
Если попасть внутрь не удаётся вообще, сервер запускают без проверки прав. В MariaDB это делается аккуратно, через переменную окружения, которую учитывает штатный systemd-юнит, и вообще без правки файлов из пакета:
systemctl stop mariadb
systemctl set-environment MYSQLD_OPTS="--skip-grant-tables --skip-networking"
systemctl start mariadb
Теперь подключаются командой mariadb -u root и возвращают состояние в норму. Важно сначала выполнить FLUSH PRIVILEGES, потому что без загруженных таблиц привилегий ALTER USER завершится ошибкой:
FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket;
После этого обязательно уберите за собой, иначе сервер будет запускаться незащищённым после каждой перезагрузки:
systemctl stop mariadb
systemctl unset-environment MYSQLD_OPTS
systemctl start mariadb
В MySQL 8.0 на Ubuntu этот путь не работает: юнит такую переменную не учитывает, а у ALTER USER в режиме --skip-grant-tables есть дополнительные подводные камни. Там берут стартовый файл. Он должен лежать в каталоге, который AppArmor разрешает процессу сервера, иначе запуск закончится сообщением Can't open file. Каталог /var/lib/mysql-files разрешён, /tmp нет:
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
В редакторе задают переопределение команды запуска, причём пустая первая строка нужна для того, чтобы стереть исходное значение:
[Service]
ExecStart=
ExecStart=/usr/sbin/mysqld --init-file=/var/lib/mysql-files/reset.sql
После systemctl daemon-reload и запуска учётная запись сброшена. Затем переопределение убирают командой systemctl revert mysql, а сам файл удаляют. Если базы данных работают на отдельных системах, дополнительные рекомендации по защите доступа есть в статье Защита SSH-сервера.
Отдельный пользователь для каждого приложения вместо root
Самый действенный шаг не встречается ни в одном мастере настройки. Приложения не должны подключаться от имени root, и GRANT ALL им тоже не нужен. Типичное веб-приложение читает и пишет строки, оно не создаёт базы данных и не читает файлы с сервера.
CREATE DATABASE shopdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'shopapp'@'localhost' IDENTIFIED BY 'ЗдесьДлинныйСлучайныйПароль';
GRANT SELECT, INSERT, UPDATE, DELETE ON shopdb.* TO 'shopapp'@'localhost';
Здесь важны три вещи. Первое: точка в shopdb.* вместо *.*. Права на *.* глобальны и распространяются в том числе на mysql и information_schema. Второе: запись @'localhost' вместо @'%'. Так учётная запись пригодна исключительно для локального использования, даже если порт когда-нибудь окажется открытым. Третье: отсутствует WITH GRANT OPTION, ведь учётная запись, способная раздавать права, фактически является администратором.
Изменения схемы выполняются тогда под второй учётной записью, которая используется только при развёртывании:
CREATE USER 'shopmigrate'@'localhost' IDENTIFIED BY 'ДругойДлинныйСлучайныйПароль';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, DROP, INDEX, REFERENCES ON shopdb.* TO 'shopmigrate'@'localhost';
Звучит как лишняя работа, но именно здесь SQL-инъекция в приложении превращается в досадную утечку данных вместо полной потери. Учётная запись без права DROP не может удалить таблицу.
Проверяют не формулировку GRANT, а результат:
SHOW GRANTS FOR 'shopapp'@'localhost';
Ожидаются ровно две строки: GRANT USAGE ON *.*, который означает лишь право на вход и никакого доступа к данным, и строка с четырьмя правами на shopdb.*. Если там появляется ALL PRIVILEGES ON *.*, значит точка стояла не в том месте. Вторая, более показательная проверка: вход под новой учётной записью и SHOW DATABASES;. Видны должны быть только information_schema и shopdb.
Частая ошибка при создании:
ERROR 1396 (HY000): Operation CREATE USER failed for 'shopapp'@'localhost'
Почти всегда это означает, что учётная запись уже существует, часто как остаток от предыдущей попытки. Помогает DROP USER 'shopapp'@'localhost'; и повтор с самого начала.
bind-address: где база данных вообще слушает
Во всех четырёх дистрибутивах после установки пакета база данных слушает только на 127.0.0.1. У MariaDB эта строка находится в /etc/mysql/mariadb.conf.d/50-server.cnf, у MySQL 8.0 в /etc/mysql/mysql.conf.d/mysqld.cnf. Лучше посмотреть, чем предполагать:
grep -R "bind-address" /etc/mysql/
В MySQL 8.0 есть вторая строка, которую охотно упускают из виду: mysqlx-bind-address управляет протоколом X на порту 33060. Изменив только bind-address, можно при некоторых условиях открыть доступ наполовину или оставить открытым второй порт.
Самая надёжная проверка: не конфигурационный файл, а ядро:
ss -lntp
LISTEN 0 80 127.0.0.1:3306 0.0.0.0:* users:(("mariadbd",pid=712,fd=22))
Если там стоит 0.0.0.0:3306 или *:3306, служба доступна из сети. Дополнительно сервер сообщает собственную точку зрения:
mariadb -e "SELECT @@bind_address, @@port, @@skip_networking;"
Здесь распространены две ловушки. Первая: файлы в mariadb.conf.d читаются в алфавитном порядке, и побеждает значение, прочитанное последним. Поэтому своё изменение, вынесенное в отдельный файл, называют 99-eigene.cnf, а не 10-eigene.cnf. Именно на этом спотыкается большинство сообщений о том, что MariaDB якобы игнорирует bind-address. Преимущество отдельного файла: обновления пакетов не спрашивают о разрешении конфликтов, потому что поставляемый файл остаётся нетронутым.
Вторая ловушка: если сетевой доступ не нужен вовсе, стоит пойти дальше bind-address и задать skip-networking. Тогда TCP-порта больше нет, остаётся только Unix-сокет. Для стандартного случая, когда веб-приложение и база данных живут на одном сервере, это правильная настройка, но она стоит нервов, если в конфигурации приложения указано 127.0.0.1 вместо localhost: при 127.0.0.1 клиентские библиотеки принудительно используют TCP.
Доступ извне только тогда, когда он действительно нужен
Порт базы данных в открытой сети находят в течение нескольких часов, после чего его перебирают непрерывно. Поэтому лучший ответ на вопрос о внешнем доступе: обойтись без него. Для нечастого обслуживания достаточно SSH-туннеля, который пробрасывает локальный порт на своей машине к сокету базы данных на удалённой стороне. Инструмент для работы с базой подключается тогда к 127.0.0.1, а сервер при этом ничего не открывает в сети.
Для постоянных соединений между несколькими серверами аккуратное решение: туннель WireGuard. База данных привязывается тогда исключительно к адресу туннеля, а не к публичному IP-адресу.
Если открытый порт всё же необходим, вместе работают четыре меры. Сервер привязывается ровно к одному внутреннему адресу. Файрвол пропускает только известный адрес источника. Учётная запись базы привязана к тому же адресу, то есть 'shopapp'@'10.0.0.5' и никогда 'shopapp'@'%'. И соединение принудительно шифруется:
ALTER USER 'shopapp'@'10.0.0.5' REQUIRE SSL;
При проверке снаружи встречаются две ошибки, которые часто путают. Первая означает, что никто не отвечает, то есть дело в файрволе или в bind-address:
ERROR 2003 (HY000): Can't connect to MySQL server on '203.0.113.10:3306' (110)
Вторая означает, что сервер отвечает и осознанно отклоняет соединение, то есть для этого источника нет подходящей учётной записи:
ERROR 1130 (HY000): Host '203.0.113.55' is not allowed to connect to this MariaDB server
А локально классика жанра в том, что служба попросту не запущена:
ERROR 2002 (HY000): Can't connect to local server through socket '/run/mysqld/mysqld.sock' (2)
Пароли не в командной строке
Вызов mariadb -u shopapp -pGeheim123 работает и всё равно является ошибкой. Сервер говорит об этом сам:
Warning: Using a password on the command line interface can be insecure.
За этим стоят две причины. Во-первых, строка попадает в историю оболочки. Во-вторых, командная строка процесса на обычной системе видна через ps любому вошедшему пользователю. На сервере с несколькими клиентами или несколькими службами это кража пароля без всяких усилий.
Правильный путь для человека: -p без значения следом. Тогда пароль спрашивают интерактивно, и в истории ничего не остаётся:
mariadb -u shopapp -p shopdb
Правильный путь для скриптов и cron-заданий: файл параметров с жёсткими правами. Создать его сразу с нужным режимом можно одной командой:
install -m 600 /dev/null /root/.my.cnf
Содержимое:
[client]
user=backup
password=ЗдесьДлинныйСлучайныйПароль
После этого любой клиент находит учётные данные сам. Для разных задач заводят несколько файлов и ссылаются на нужный явно. При этом действует правило, на котором спотыкаются многие: --defaults-extra-file и --defaults-file должны быть первым параметром вызова, иначе они игнорируются без единого сообщения.
mariadb-dump --defaults-extra-file=/root/.my-backup.cnf --single-transaction shopdb
Переменная окружения MYSQL_PWD решением не является. Она видна в /proc, то есть примерно так же доступна, как и командная строка. В MySQL 8.0 дополнительно есть mysql_config_editor, который пишет файл ~/.mylogin.cnf. Его содержимое замаскировано, но не зашифровано, а MariaDB такого инструмента не знает. В смешанных средах простой файл параметров с режимом 600 остаётся более надёжным выбором.
Последний пункт касается резервного копирования. Дамп содержит всё, что видит приложение, и права на запись учётной записи для резервных копий при этом не нужны. Для mariadb-dump с --single-transaction обычно достаточно такого набора:
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES ON shopdb.* TO 'backup'@'localhost';
GRANT PROCESS ON *.* TO 'backup'@'localhost';
Приёмка: по каким признакам видно, что всё сделано
Команда, отработавшая без ошибок, не доказывает ничего. А вот эти шесть проверок доказывают:
- Учётная запись root использует аутентификацию через сокет:
mariadb -e "SELECT User, Host, JSON_VALUE(Priv,'$.plugin') AS plugin, JSON_QUERY(Priv,'$.auth_or') AS auth_or FROM mysql.global_priv;"показывает дляrootзначениеunix_socket, либо в столбцеplugin, либо вauth_or. Представлениеmysql.userдля этого не годится, оно выводит только первый метод. В MySQL 8.0, наоборот, определяющим является столбецpluginвmysql.user, там должно стоятьauth_socket. - Анонимных учётных записей нет:
mariadb -e "SELECT user, host FROM mysql.user WHERE user = '';"возвращает пустой результат. - Тестовая база удалена:
mariadb -e "SHOW DATABASES LIKE 'test';"не выводит ничего. - Порт закрыт:
ss -lntpдля 3306 показывает либо вообще ничего, либо только127.0.0.1. - Учётная запись приложения ограничена: при входе под ней
SHOW DATABASES;показывает только её собственную базу, аDROP TABLEзавершается ошибкой. - Нигде нет пароля открытым текстом:
grep -rs "password" /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ /etc/cron.weekly/ /etc/cron.monthly/ничего не находит, как иcrontab -lдля root и просмотр каталога/var/spool/cron/crontabs/, а у файлов параметров стоит режим 600. Ключ-sздесь обязателен, потому что при отсутствующем каталоге вызов иначе прерывается с кодом возврата 2: на Debian 13 после установки одной только базы данных каталога/etc/cron.dнет, пакет cron там ещё не установлен.
Кто проходит эти шесть пунктов на новом сервере, тот уже исключает подавляющее большинство атак на базы данных, не устанавливая ни одной дополнительной программы. Остальное: дисциплина обновлений и работающая резервная копия, восстановление которой хотя бы раз проверили на практике.
Частые вопросы
Нужен ли в MariaDB вообще пароль для root?
Почему Debian сообщает, что пакета mysql-server не существует?
Как вернуться в базу данных, если доступ к ней потерян?
Чем ERROR 1698 отличается от ERROR 1045?
Достаточно ли bind-address = 127.0.0.1 для защиты базы данных?
Почему приложению не следует обращаться к базе данных от имени root?
Как безопасно передать cron-заданию пароль от базы данных?
2026 KernelHost GmbH. Все права защищены. Эта инструкция охраняется авторским правом. Публикация на других сайтах, в том числе частично или в изменённом виде, без нашего письменного согласия не разрешается. Цитирование с указанием источника и активной ссылкой мы приветствуем.

