Как исправить ошибку MySQL «Access denied for user»
Почему «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 13 | MariaDB 11.8 | unix_socket | пакета mysql-server нет; mysql теперь лишь символьная ссылка на mariadb и предупреждает при вызове |
| Debian 12 | MariaDB 10.11 | unix_socket | пакета mysql-server нет |
| Ubuntu 24.04 | MariaDB 10.11 или MySQL 8.0 | unix_socket или auth_socket | оба пакета лежат в репозиториях параллельно, руководства легко перепутать |
| Ubuntu 22.04 | MariaDB 10.6 или MySQL 8.0 | unix_socket или auth_socket | JSON_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 не работает, хотя пароль правильный?
В чём разница между ERROR 1045 и ERROR 1698?
localhost и 127.0.0.1 — это одно и то же?
Важен ли регистр букв в имени пользователя и в имени хоста?
Как сбросить пароль root без skip-grant-tables?
По каким признакам видно, что сервер всё ещё работает без проверки прав?
Есть ли в Debian пакет mysql-server?
2026 KernelHost GmbH. Все права защищены. Эта инструкция охраняется авторским правом. Публикация на других сайтах, в том числе частично или в изменённом виде, без нашего письменного согласия не разрешается. Цитирование с указанием источника и активной ссылкой мы приветствуем.

