Как исправить ошибку MySQL «Access denied for user»

Опубликовано 13 мин. чтения

Почему «Access denied for user» чаще всего не связано с неверным паролем: unix_socket, localhost против 127.0.0.1, права и безопасный сброс пароля root.

«Access denied for user» — самое частое сообщение об ошибке, которое ищут в связке с MySQL, и в большинстве случаев пароль тут вообще ни при чём. На Debian и Ubuntu вход срывается прежде всего из-за аутентификации через сокет, из-за путаницы между localhost и 127.0.0.1 или из-за строки пользователя, заведённой для другого хоста. Эта статья разбирает причины в том порядке, в котором они действительно встречаются, показывает сброс пароля через skip-grant-tables вместе с обратным путём и описывает, по каким признакам видно, что вход починен по-настоящему.

Все сведения относятся к Debian 13 (MariaDB 11.8), Debian 12 (MariaDB 10.11), Ubuntu 24.04 (MariaDB 10.11 или MySQL 8.0) и Ubuntu 22.04 (MariaDB 10.6 или MySQL 8.0). Один важный момент сразу: в Debian вообще нет пакета mysql-server. Если вы «установили MySQL» на системе Debian, там работает MariaDB, и это уже объясняет часть путаницы.

Внимательно читаем сообщение об ошибке

Именно формулировка определяет, какая причина вообще возможна. На практике вам встретятся эти пять вариантов:

СообщениеЧто оно означает
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)Пароль был отправлен и не подошёл, либо подходящей строки учётной записи не существует.
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)Пароль не был отправлен вообще. Чаще всего не хватает -p или приложение не читает свою конфигурацию.
ERROR 1698 (28000): Access denied for user 'root'@'localhost'Классика: учётная запись использует unix_socket или, соответственно, auth_socket. Пароль здесь не значит ничего.
ERROR 1044 (42000): Access denied for user 'app'@'localhost' to database 'shop'Вход прошёл успешно. Не хватает только прав на эту базу данных.
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/run/mysqld/mysqld.sock'Это не проблема аутентификации. Служба не запущена или путь к сокету указан неверно.

Две детали регулярно пропускают. Во-первых, имя хоста в сообщении — это тот хост, каким вас увидел сервер, а не тот, который вы набрали. Если там стоит 'app'@'localhost', хотя подключались вы с -h 127.0.0.1, значит сервер выполнил обратное разрешение имени. Во-вторых, 1045 не различает «пароль неверный» и «учётной записи не существует». В обоих случаях вывод одинаковый, и сделано это намеренно, чтобы атакующий не мог перебором нащупать действующие имена пользователей.

Самый частый случай: unix_socket в MariaDB

Начиная с MariaDB 10.4 пакеты Debian и Ubuntu заводят учётную запись root@localhost так, что она аутентифицируется через Unix-сокет. По смыслу строка учётной записи выглядит так:

CREATE USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING 'invalid'
  OR unix_socket;

Это значит: тот, кто вошёл в систему как системный пользователь root, попадает внутрь без пароля. Тот, кто присылает пароль, проверяется по хешу строки «invalid», а в него не попадёт никто. Часть с mysql_native_password стоит там только потому, что иначе SET PASSWORD прерывался бы с ошибкой. Практическое следствие: mysql -u root -p от имени обычного пользователя не сработает, какой бы пароль вы ни ввели. Правильно так:

sudo mariadb

В MySQL 8.0 на Ubuntu плагин называется auth_socket вместо unix_socket, поведение идентичное. Команда там: sudo mysql.

Проверьте, какой плагин учётная запись использует на самом деле, а именно через SHOW CREATE USER:

mariadb -e "SHOW CREATE USER 'root'@'localhost';"

Вывод показывает полную цепочку обоих методов:

CREATE USER `root`@`localhost` IDENTIFIED VIA mysql_native_password USING 'invalid' OR unix_socket

Здесь притаилась ловушка, о которой почти не пишут в руководствах: начиная с MariaDB 10.4 mysql.user — это лишь представление (view), а настоящие данные лежат в виде JSON в mysql.global_priv. Это представление знает для каждой учётной записи только один метод аутентификации и поэтому сообщает для root@localhost плагин mysql_native_password, хотя в действительности работает unix_socket. Кто берёт распространённый запрос SELECT User, Host, plugin FROM mysql.user как доказательство, считает сервер парольным и ищет ошибку не в том месте. Такое же слепое пятно и у JSON_VALUE(priv,"$.plugin"), потому что он тоже читает только первый элемент. Второй метод лежит в поле auth_or и вычитывать его нужно отдельно:

mariadb -e 'SELECT CONCAT(user,"@",host) AS account, JSON_VALUE(priv,"$.plugin") AS plugin, JSON_QUERY(priv,"$.auth_or") AS auth_or FROM mysql.global_priv;'

Для root@localhost в столбце plugin тогда по-прежнему стоит mysql_native_password, а в auth_or[{},{"plugin":"unix_socket"}]. Только этот второй столбец показывает, что вход через сокет активен.

Загружен ли плагин вообще? При сломанном обновлении он может отсутствовать, и тогда сервер сообщает ERROR 1524 (HY000): Plugin 'unix_socket' is not loaded:

mariadb -e "SELECT plugin_name, plugin_status FROM information_schema.plugins WHERE plugin_name LIKE '%socket%';"

Если вы осознанно хотите перевести root на пароль, например потому что скрипт резервного копирования работает от другого системного пользователя, то сохраните вариант с сокетом дополнительно. Иначе перестанут работать служебные скрипты дистрибутива и sudo mariadb:

ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket
  OR mysql_native_password USING PASSWORD('ВашНовыйПароль');

Лучший путь в любом случае — отдельная административная учётная запись вместо пароля для root. Для приложений это верно тем более: одна база данных, один пользователь, только необходимые права.

localhost — это не 127.0.0.1

Это различие порождает больше сообщений «Access denied», чем любой неверный пароль. Клиенты MySQL и MariaDB считают имя хоста localhost особым случаем и подключаются через Unix-сокет. Только 127.0.0.1 принудительно даёт TCP-соединение. С точки зрения управления правами это не одно и то же имя машины, потому что при соединении через сокет сервер записывает хост localhost, а при TCP через loopback-адрес либо localhost (после обратного разрешения имени), либо 127.0.0.1, в зависимости от настройки skip_name_resolve.

Учётная запись, существующая только как 'app'@'127.0.0.1', через сокет недоступна, и наоборот. Ровно это и происходит, когда в конфигурации PHP-приложения прописано host=localhost: PHP подключается через сокет, а права выданы на IP. Две встречные проверки:

mariadb -u app -p'Пароль' -e "SELECT USER(), CURRENT_USER();"
mariadb -h 127.0.0.1 -u app -p'Пароль' -e "SELECT USER(), CURRENT_USER();"

Если не проходит только одна из двух, причина найдена. Обратите при этом внимание на разрешение имён: пока skip_name_resolve выключен, а в пакетных установках Debian и Ubuntu он выключен, сервер разрешает loopback-адрес обратно в localhost. Если учётная запись 'app'@'localhost' уже существует, вход с -h 127.0.0.1 тоже удастся, и CURRENT_USER() сообщит тогда app@localhost вместо app@127.0.0.1. Дополнительно заведённая запись 'app'@'127.0.0.1' в этом случае останется без действия, она сработает только тогда, когда записи для localhost нет. Чистое решение состоит в том, чтобы заводить учётную запись для того пути, которым приложение действительно ходит, а не создавать оба варианта «на всякий случай».

Проверьте дополнительно, не отключено ли разрешение имён. Если skip_name_resolve активен, строки учётных записей с именами хостов вроде 'app'@'web01.intern' перестают работать совсем, считаются тогда только IP-адреса:

mariadb -e "SHOW VARIABLES LIKE 'skip_name_resolve';"

Ещё один камень преткновения — порядок, в котором сервер выбирает подходящие строки. Он сортирует от частного к общему и берёт первое совпадение, а не лучшее. Если рядом с 'app'@'%' есть ещё анонимная учётная запись ''@'localhost', при локальном соединении выигрывает анонимная, и ваш вход срывается с сообщением, в котором тем не менее названо ваше имя пользователя. Актуальные пакеты анонимных учётных записей больше не заводят, но на системах, мигрировавших годами, их ещё можно найти:

mariadb -e "SELECT user, host FROM mysql.global_priv WHERE user = '';"

Пароль, права и регистр букв

Остаются причины, которые действительно связаны с учётными данными.

Оболочка съедает спецсимволы

Между -p и паролем не должно быть пробела, иначе клиент воспримет пароль как имя базы данных. Пароли с $, !, & или пробелами нужно брать в одинарные кавычки, иначе оболочка их подставит или обрежет. По-настоящему аккуратно вообще не передавать пароль в командной строке, потому что там он попадает в список процессов и в файл истории. Заведите вместо этого файл ~/.my.cnf с правами 0600:

[client]
user=app
password=ВашПароль

И наоборот: забытый ~/.my.cnf тоже может быть причиной ошибки. Он молча перекрывает то, что вы указали в командной строке, и тогда вы получаете «Access denied» для пользователя, которого никогда не набирали.

Регистр: у имени пользователя важен, у хоста нет

В имена пользователей в MySQL и MariaDB нужно попадать посимвольно точно, App и app — две разные учётные записи. Имена хостов при сравнении проверяются без учёта регистра, но сохраняются ровно так, как стояли в CREATE USER. Кто по недосмотру завёл 'app'@'LOCALHOST', видит в выводе SHOW GRANTS и в скриптах две внешне разные учётные записи, которые тем не менее подходят к одному и тому же соединению. Такие дубликаты делают поиск ошибки излишне вязким, потому что права вы выдаёте на одной строке, а сервер выбирает другую. Отыскать их можно так:

mariadb -e "SELECT user, host FROM mysql.global_priv WHERE host <> LOWER(host);"

Не хватает прав, а не входа

Если приходит ERROR 1044 вместо 1045, вход прошёл успешно. Значит, не хватает прав на конкретную базу данных. Посмотрите, что учётной записи разрешено на самом деле:

mariadb -e "SHOW GRANTS FOR 'app'@'localhost';"

FLUSH PRIVILEGES нужен только тогда, когда вы меняли таблицы прав напрямую через INSERT или UPDATE. После GRANT, CREATE USER или ALTER USER он излишен и время от времени лишь маскирует то, что сама команда вообще не сработала.

Клиент не понимает плагин

MySQL 8.0 по умолчанию использует caching_sha2_password. Старые клиенты и библиотеки отвечают на это сообщением ERROR 2059 (HY000): Authentication plugin 'caching_sha2_password' cannot be loaded. Это не проблема прав, а несовместимость. В MySQL 8.0 в качестве запасного варианта ещё доступен mysql_native_password, в MySQL 8.4 он удалён. В сомнительном случае лучше обновите клиент, чем откручивайте шифрование назад.

Сброс пароля root

Если больше ничего не помогает, сервер придётся один раз запустить без проверки прав. К цели ведут два пути. Путь через --init-file безопаснее, потому что сервер при этом всё время работает с активной проверкой прав.

Вариант 1: init-file (рекомендуется)

SQL-файл должен лежать там, где служба имеет право его читать. В /tmp или /root на Ubuntu это регулярно срывается из-за AppArmor и из-за PrivateTmp в systemd-юните. Положите его поэтому в каталог данных:

sudo systemctl stop mariadb
sudo tee /var/lib/mysql/kh-reset.sql >/dev/null <<'SQL'
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket
  OR mysql_native_password USING PASSWORD('НовыйПарольRoot');
SQL
sudo chown mysql:mysql /var/lib/mysql/kh-reset.sql
sudo systemctl set-environment MYSQLD_OPTS="--init-file=/var/lib/mysql/kh-reset.sql"
sudo systemctl start mariadb

После этого обязательно приберитесь, иначе сервер при каждом запуске снова будет отрабатывать этот файл:

sudo systemctl unset-environment MYSQLD_OPTS
sudo rm /var/lib/mysql/kh-reset.sql
sudo systemctl restart mariadb

Для MySQL 8.0 на Ubuntu служба называется mysql, а SQL-строка выглядит так:

ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'НовыйПарольRoot';

Вариант 2: skip-grant-tables

Этот вариант отключает проверку прав полностью. Без --skip-networking всё это время любой, кто дотянется до порта, имел бы полный доступ ко всем данным. Поэтому опция не факультативная, а обязательная.

sudo systemctl stop mariadb
sudo systemctl set-environment MYSQLD_OPTS="--skip-grant-tables --skip-networking"
sudo systemctl start mariadb
sudo mariadb

В сессии сначала загрузите таблицы прав, иначе сервер отклонит ALTER USER с сообщением об ошибке:

FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket
  OR mysql_native_password USING PASSWORD('НовыйПарольRoot');
EXIT;

И затем обратно в обычный режим:

sudo systemctl unset-environment MYSQLD_OPTS
sudo systemctl restart mariadb

Если ваш дистрибутив не учитывает переменную MYSQLD_OPTS, всегда сработает drop-in-файл /etc/systemd/system/mariadb.service.d/reset.conf с содержимым [Service] и Environment="MYSQLD_OPTS=--skip-grant-tables --skip-networking", за которым следует sudo systemctl daemon-reload. Удалите этот файл после работы и перечитайте конфигурацию systemd ещё раз.

Если сброс пошёл не так

Ровно здесь большинство руководств и заканчивается. Четыре самые частые последующие проблемы:

Служба больше не запускается. Чаще всего это опечатка в SQL-файле. Сервер тогда прерывает запуск. Debian по умолчанию пишет журнал MariaDB в journal, Ubuntu с MySQL дополнительно в файл:

sudo journalctl -u mariadb -n 50 --no-pager
sudo tail -n 50 /var/log/mysql/error.log

Куда именно пишет сервер, можно посмотреть через SHOW VARIABLES LIKE 'log_error';. Но пути к файлу там не ждите: в пакетных установках Debian и Ubuntu значение пустое, потому что служба работает с --skip-log-error. Всё уходит тогда в стандартный поток ошибок и попадает в journal или, соответственно, в /var/log/syslog, читать это можно через journalctl -u mariadb. Для устранения уберите переменную окружения с --init-file и перезапустите службу.

Сервер постоянно работает без проверки прав. Так случается, когда забыли про unset-environment, и это самый опасный вариант, потому что снаружи всё выглядит нормально. Две проверки:

systemctl show-environment
ps -o args= -C mariadbd

Если в одном из двух выводов всплывает skip-grant-tables, проверка прав всё ещё выключена. systemctl show-environment при этом предполагает, что systemd работает как PID 1. На обычном сервере так и есть, а в контейнере или на системе с SysV-init такой команды просто нет. Там читайте окружение прямо из работающего процесса:

cat /proc/$(pgrep -n mariadbd)/environ | tr '\0' '\n'

На один только список аргументов полагаться тоже не стоит: при запуске через systemd ps часто показывает лишь /usr/sbin/mariadbd без единой опции, тогда как при запуске через SysV-init-скрипт появляется полный список. Ещё один признак: под skip-grant-tables команда SHOW GRANTS отвечает сообщением об ошибке вместо перечня прав.

AppArmor блокирует init-файл. Симптом такой: сервер без видимой причины не поднимается. Взгляд в журнал ядра всё проясняет:

sudo dmesg | grep -i denied

Вы полностью отрезаете себе доступ. Это происходит, когда root переводят через ALTER USER ... IDENTIFIED BY на чистый пароль, теряют при этом аутентификацию через сокет и потом не помнят новый пароль. Выход — тот же сброс ещё раз, на этот раз с показанным выше двойным вариантом unix_socket OR mysql_native_password. Делайте перед каждым вмешательством копию таблиц прав, это занимает секунды и экономит часы в серьёзном случае:

sudo mariadb-dump mysql > /root/mysql-grants.sql

Обычную в других случаях опцию --single-transaction здесь можно не указывать. Отработает она без ошибок, но не подействует, потому что таблицы базы mysql лежат на Aria или, соответственно, MyISAM и потому не транзакционны.

На управляемом веб-хостинге системного доступа у вас нет, а значит нет и ни одной из этих возможностей. Там пользователи базы данных и пароли сбрасываются через панель управления. На KVM root-сервере или выделенном сервере в KernelHost у вас есть полный root-доступ, и если сервер перестал отвечать по сети, вы попадёте в систему через консоль в личном кабинете.

Проверяем, действительно ли всё исправлено

То, что команда отработала без ошибок, ещё не значит, что вход будет работать постоянно. Эти четыре проверки вскрывают типичные остаточные ошибки.

Во-первых, разница между USER() и CURRENT_USER(). Первая функция показывает, кем вы себя назвали, вторая — какую строку учётной записи сервер использовал в действительности:

mariadb -u app -p'Пароль' -e "SELECT USER(), CURRENT_USER();"

Если там стоят два разных значения, например app@localhost и app@%, значит ваши права работают через другую строку, чем вы предполагали. Это объясняет ошибки 1044, которые всплывут позже, ещё до того, как они проявятся в продуктивной работе.

Во-вторых, реальное обращение к целевой базе данных, а не только вход, и разбор кода возврата:

mariadb -u app -p'Пароль' mydb -e "SELECT 1;" ; echo "Код возврата: $?"

В-третьих, перезапуск службы и следом тот же самый вход ещё раз. Так вы убедитесь, что изменение действительно записано в таблицы, а не только в оперативную память, и что от сброса не осталось ни одной переменной окружения:

sudo systemctl restart mariadb
sudo systemctl is-active mariadb

В-четвёртых, само приложение. Успешный тест в командной строке мало что говорит о PHP-приложении, которое подключается от имени пользователя веб-сервера и через сокет. Проверяйте поэтому от того же системного пользователя:

sudo -u www-data mariadb -u app -p'Пароль' mydb -e "SELECT CURRENT_USER();"

Различия между дистрибутивами

Руководство, которое утверждает одно и то же для всех четырёх систем, как минимум в одном месте ошибается. Эта таблица собирает существенные расхождения:

СистемаБаза данных по умолчаниюПлагин сокетаПримечание
Debian 13MariaDB 11.8unix_socketпакета mysql-server нет; mysql теперь лишь символьная ссылка на mariadb и предупреждает при вызове
Debian 12MariaDB 10.11unix_socketпакета mysql-server нет
Ubuntu 24.04MariaDB 10.11 или MySQL 8.0unix_socket или auth_socketоба пакета лежат в репозиториях параллельно, руководства легко перепутать
Ubuntu 22.04MariaDB 10.6 или MySQL 8.0unix_socket или auth_socketJSON_VALUE и JSON_QUERY на mysql.global_priv работают здесь так же, как в 10.11 и 11.8; и в 10.6 mysql.user тоже лишь представление, а для полной цепочки аутентификации SHOW CREATE USER остаётся самым коротким путём

Другие различия, которые на практике стоят времени: файлы конфигурации у MariaDB лежат в /etc/mysql/mariadb.conf.d/50-server.cnf, у MySQL в /etc/mysql/mysql.conf.d/mysqld.cnf. Службы называются mariadb и, соответственно, mysql, причём MariaDB дополнительно приносит с собой алиас mysql. В MariaDB начиная с версии 10.4 больше нет пользователя debian-sys-maint, файл /etc/mysql/debian.cnf ссылается там на root через сокет. В MySQL на Ubuntu служебный пользователь по-прежнему существует, и это лучший аварийный вход, когда пароль root потерян, а перезапускать сервер вы не хотите:

sudo mysql --defaults-file=/etc/mysql/debian.cnf

Если соблюдать этот порядок, то есть сначала внимательно прочитать сообщение, затем проверить плагин и строку хоста, затем путь подключения, и только в самом конце сбрасывать пароль, подавляющее большинство случаев решается за считаные минуты и без простоя. Сброс через skip-grant-tables — последнее средство, а не первый шаг.

Частые вопросы

Почему mysql -u root -p не работает, хотя пароль правильный?
Потому что учётная запись root@localhost на Debian и Ubuntu аутентифицируется через Unix-сокет. Пакет заводит её как IDENTIFIED VIA mysql_native_password USING 'invalid' OR unix_socket. Хеш пароля относится к строке invalid, и попасть в него не может никто. Полную цепочку обоих методов показывает только SHOW CREATE USER для root@localhost, потому что представление mysql.user выдаёт лишь первый из них и сообщает там mysql_native_password. Входите вместо этого через sudo mariadb, а в MySQL через sudo mysql.
В чём разница между ERROR 1045 и ERROR 1698?
ERROR 1045 содержит дополнение (using password: YES или NO) и означает, что подходящей строки учётной записи не нашлось либо пароль неверен. ERROR 1698 без этого дополнения указывает на аутентификацию через сокет: учётная запись существует, но пароль не принимает, а проверяет системного пользователя.
localhost и 127.0.0.1 — это одно и то же?
Нет. Клиенты считают localhost особым случаем и подключаются через Unix-сокет, тогда как 127.0.0.1 принудительно даёт TCP-соединение. Для управления правами это разные указания хоста. Учётная запись, заведённая только как 'app'@'127.0.0.1', через сокет недоступна.
Важен ли регистр букв в имени пользователя и в имени хоста?
Имя пользователя сравнивается посимвольно точно, App и app — две разные учётные записи. Имя хоста при сравнении проверяется без учёта регистра, но сохраняется ровно так, как стояло в CREATE USER. Из-за этого возникают кажущиеся дубликаты вроде 'app'@'LOCALHOST', которые усложняют поиск ошибки.
Как сбросить пароль root без skip-grant-tables?
Через опцию --init-file. Вы кладёте SQL-файл с командой ALTER USER в каталог данных /var/lib/mysql, подключаете его при запуске через MYSQLD_OPTS и после этого убираете и файл, и переменную. Преимущество в том, что проверка прав всё время остаётся активной и окна с открытым полным доступом не возникает.
По каким признакам видно, что сервер всё ещё работает без проверки прав?
Проверьте вывод systemctl show-environment и ps -o args= -C mariadbd на запись skip-grant-tables. У обоих путей есть пределы: systemctl show-environment требует systemd в роли PID 1, иначе окружение читают прямо из процесса через cat /proc/$(pgrep -n mariadbd)/environ, а ps при запуске через systemd часто показывает только имя программы без аргументов. Ещё один признак: в этом режиме SHOW GRANTS отвечает сообщением об ошибке вместо перечня прав. Самая частая причина такого состояния — забытый unset-environment.
Есть ли в Debian пакет mysql-server?
Нет. Debian 13 и Debian 12 поставляют исключительно MariaDB, в версиях 11.8 и 10.11 соответственно. Только Ubuntu 24.04 и 22.04 предлагают mysql-server 8.0 дополнительно к MariaDB. Руководства, которые предлагают установить mysql-server на Debian, ошибочны.

MySQL MariaDB Debian Ubuntu База данных Устранение неполадок Администрирование Linux