Naprawa błędu MySQL „Access denied for user”
Dlaczego „Access denied for user” najczęściej nie ma nic wspólnego z błędnym hasłem: unix_socket, localhost kontra 127.0.0.1, uprawnienia oraz bezpieczny reset hasła root.
„Access denied for user” to najczęściej wyszukiwany komunikat błędu w świecie MySQL, a w większości przypadków hasło nie ma z tym nic wspólnego. Na Debianie i Ubuntu logowanie rozbija się przeważnie o uwierzytelnianie przez socket, o pomylenie localhost z 127.0.0.1 albo o wpis użytkownika założony dla niewłaściwego hosta. Ten artykuł omawia przyczyny w kolejności, w jakiej rzeczywiście występują, pokazuje reset hasła przez skip-grant-tables razem z drogą powrotną i opisuje, po czym poznasz, że logowanie naprawdę jest naprawione.
Wszystkie informacje odnoszą się do Debiana 13 (MariaDB 11.8), Debiana 12 (MariaDB 10.11), Ubuntu 24.04 (MariaDB 10.11 albo MySQL 8.0) oraz Ubuntu 22.04 (MariaDB 10.6 albo MySQL 8.0). Jedna ważna rzecz na wstępie: Debian w ogóle nie dostarcza pakietu mysql-server. Jeśli na systemie Debian zainstalowałeś „MySQL”, działa tam MariaDB, i to już wyjaśnia część zamieszania.
Dokładnie przeczytaj komunikat błędu
Dokładne brzmienie decyduje o tym, która przyczyna wchodzi w grę. W praktyce spotkasz pięć wariantów:
| Komunikat | Znaczenie |
|---|---|
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES) | Hasło zostało wysłane i nie pasowało albo nie ma pasującego wpisu konta. |
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO) | Żadne hasło nie zostało wysłane. Najczęściej brakuje -p albo aplikacja nie czyta konfiguracji. |
ERROR 1698 (28000): Access denied for user 'root'@'localhost' | Klasyk: konto korzysta z unix_socket względnie auth_socket. Hasło nie ma tu żadnego znaczenia. |
ERROR 1044 (42000): Access denied for user 'app'@'localhost' to database 'shop' | Logowanie się powiodło. Brakuje tylko uprawnień do tej bazy danych. |
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/run/mysqld/mysqld.sock' | To nie jest problem z uwierzytelnianiem. Usługa nie działa albo ścieżka socketu się nie zgadza. |
Dwa szczegóły regularnie umykają uwadze. Po pierwsze nazwa hosta w komunikacie to host, jako który zobaczył cię serwer, a nie ten, który wpisałeś. Jeśli widnieje tam 'app'@'localhost', choć łączyłeś się przez -h 127.0.0.1, to serwer wykonał odwrotne rozwiązanie nazwy. Po drugie błąd 1045 nie odróżnia „błędnego hasła” od „konta, którego nie ma”. Oba przypadki dają ten sam komunikat, i to celowo, żeby atakujący nie mógł metodą prób wyłuskać poprawnych nazw użytkowników.
Najczęstszy przypadek: unix_socket w MariaDB
Od MariaDB 10.4 pakiety Debiana i Ubuntu zakładają konto root@localhost tak, żeby było uwierzytelniane przez socket uniksowy. Wpis konta wygląda z grubsza tak:
CREATE USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING 'invalid'
OR unix_socket;
Oznacza to tyle: kto jest zalogowany jako systemowy użytkownik root, wchodzi bez hasła. Kto wysyła hasło, jest sprawdzany względem skrótu łańcucha „invalid”, a takiego skrótu nikt nie trafi. Fragment z mysql_native_password stoi tam wyłącznie dlatego, że inaczej SET PASSWORD przerywałoby pracę błędem. Praktyczny skutek: mysql -u root -p jako zwykły użytkownik zawiedzie niezależnie od tego, jakie hasło wpiszesz. Poprawnie jest tak:
sudo mariadb
W MySQL 8.0 na Ubuntu wtyczka nazywa się auth_socket zamiast unix_socket, zachowanie jest identyczne. Wywołanie brzmi tam sudo mysql.
Sprawdź poleceniem SHOW CREATE USER, jakiej wtyczki konto rzeczywiście używa:
mariadb -e "SHOW CREATE USER 'root'@'localhost';"
Wynik pokazuje pełny łańcuch obu metod:
CREATE USER `root`@`localhost` IDENTIFIED VIA mysql_native_password USING 'invalid' OR unix_socket
Tutaj czai się pułapka, o której wspomina mało który poradnik: od MariaDB 10.4 mysql.user jest już tylko widokiem, a prawdziwe dane leżą w postaci JSON w mysql.global_priv. Ten widok zna na konto tylko jedną metodę uwierzytelniania i dlatego dla root@localhost zgłasza mysql_native_password, choć w rzeczywistości działa unix_socket. Kto bierze popularne zapytanie SELECT User, Host, plugin FROM mysql.user za dowód, uzna serwer za uwierzytelniany hasłem i będzie szukał błędu w złym miejscu. Ten sam martwy punkt ma JSON_VALUE(priv,"$.plugin"), bo to również czyta wyłącznie pierwszy element. Druga metoda stoi w polu auth_or i trzeba ją odczytać osobno:
mariadb -e 'SELECT CONCAT(user,"@",host) AS konto, JSON_VALUE(priv,"$.plugin") AS plugin, JSON_QUERY(priv,"$.auth_or") AS auth_or FROM mysql.global_priv;'
Dla root@localhost w kolumnie plugin nadal stoi wtedy mysql_native_password, a w auth_or za to [{},{"plugin":"unix_socket"}]. Dopiero ta druga kolumna pokazuje, że logowanie przez socket jest aktywne.
Czy wtyczka jest w ogóle wczytana? Po nieudanej aktualizacji może jej zabraknąć, wtedy serwer zgłasza 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%';"
Jeśli świadomie chcesz przestawić konto root na hasło, na przykład dlatego, że skrypt backupu działa jako inny użytkownik systemowy, zachowaj dodatkowo wariant z socketem. Inaczej skrypty serwisowe dystrybucji oraz sudo mariadb przestaną działać:
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket
OR mysql_native_password USING PASSWORD('TwojeNoweHaslo');
Lepszą drogą i tak jest osobne konto administracyjne zamiast hasła dla root. Przy aplikacjach obowiązuje to tym bardziej: jedna baza danych, jeden użytkownik, tylko niezbędne uprawnienia.
localhost to nie jest 127.0.0.1
To rozróżnienie powoduje więcej komunikatów „Access denied” niż jakiekolwiek błędne hasło. Klienty MySQL i MariaDB traktują nazwę hosta localhost jako przypadek szczególny i łączą się przez socket uniksowy. Dopiero 127.0.0.1 wymusza połączenie TCP. Z punktu widzenia zarządzania uprawnieniami to nie są te same nazwy maszyny, bo przy połączeniu przez socket serwer wpisuje host localhost, a przy TCP na adresie pętli zwrotnej albo localhost (po odwrotnym rozwiązaniu nazwy), albo 127.0.0.1, zależnie od ustawienia skip_name_resolve.
Konto, które istnieje wyłącznie jako 'app'@'127.0.0.1', jest nieosiągalne przez socket, i odwrotnie. Dokładnie to się dzieje, gdy aplikacja PHP ma w konfiguracji host=localhost: PHP łączy się przez socket, a uprawnienia nadano dla adresu IP. Dwie próby kontrolne:
mariadb -u app -p'Haslo' -e "SELECT USER(), CURRENT_USER();"
mariadb -h 127.0.0.1 -u app -p'Haslo' -e "SELECT USER(), CURRENT_USER();"
Jeśli zawodzi tylko jedna z nich, przyczyna jest znaleziona. Zwróć przy tym uwagę na rozwiązywanie nazw: dopóki skip_name_resolve jest wyłączone, a w instalacjach pakietowych Debiana i Ubuntu tak właśnie jest, serwer rozwiązuje adres pętli zwrotnej wstecznie na localhost. Jeśli konto 'app'@'localhost' już istnieje, logowanie z -h 127.0.0.1 też się uda, a CURRENT_USER() zgłosi wtedy app@localhost zamiast app@127.0.0.1. Dodatkowo założone konto 'app'@'127.0.0.1' pozostaje w tym przypadku bez efektu, zadziała dopiero wtedy, gdy zabraknie konta dla localhost. Czystym rozwiązaniem jest założenie konta dla tej drogi, którą aplikacja rzeczywiście wybiera, a nie zakładanie obu wariantów „na wszelki wypadek”.
Sprawdź dodatkowo, czy rozwiązywanie nazw nie jest wyłączone. Gdy skip_name_resolve jest aktywne, wpisy kont z nazwami hostów w rodzaju 'app'@'web01.intern' przestają w ogóle działać, liczą się już tylko adresy IP:
mariadb -e "SHOW VARIABLES LIKE 'skip_name_resolve';"
Kolejną przeszkodą jest kolejność, w jakiej serwer wybiera pasujące wpisy. Sortuje je od szczegółowych do ogólnych i bierze pierwsze trafienie, a nie najlepsze. Jeśli obok 'app'@'%' istnieje jeszcze anonimowe konto ''@'localhost', przy połączeniu lokalnym wygrywa konto anonimowe, a twoje logowanie kończy się komunikatem, który mimo wszystko podaje twoją nazwę użytkownika. Aktualne pakiety nie zakładają już anonimowych kont, ale na systemach migrowanych przez lata wciąż da się je spotkać:
mariadb -e "SELECT user, host FROM mysql.global_priv WHERE user = '';"
Hasło, uprawnienia i wielkość liter
Pozostają przyczyny, które faktycznie mają związek z danymi logowania.
Shell zjada znaki specjalne
Między -p a hasłem nie może być spacji, inaczej klient odczyta hasło jako nazwę bazy danych. Hasła zawierające $, !, & albo spację należy ująć w apostrofy, bo w przeciwnym razie shell je podmieni albo utnie. Naprawdę czysto jest w ogóle nie podawać hasła w wierszu poleceń, bo trafia ono wtedy na listę procesów i do pliku historii. Załóż zamiast tego plik ~/.my.cnf z uprawnieniami 0600:
[client]
user=app
password=TwojeHaslo
Odwrotnie, zapomniany plik ~/.my.cnf też bywa przyczyną błędu. Po cichu nadpisuje to, co podajesz w wierszu poleceń, i wtedy dostajesz „Access denied” dla użytkownika, którego nigdy nie wpisałeś.
Wielkość liter: nazwa użytkownika tak, host nie
Nazwy użytkowników trzeba w MySQL i MariaDB trafić znak w znak, App i app to dwa różne konta. Nazwy hostów są porównywane bez względu na wielkość liter, ale zapisywane dokładnie tak, jak stały w CREATE USER. Kto przez pomyłkę założył 'app'@'LOCALHOST', widzi w wynikach SHOW GRANTS i w skryptach dwa pozornie różne konta, które jednak pasują do tego samego połączenia. Takie duplikaty niepotrzebnie wydłużają szukanie błędu, bo uprawnienia nadajesz na jednym wpisie, a serwer wybiera drugi. Wytropisz je tak:
mariadb -e "SELECT user, host FROM mysql.global_priv WHERE host <> LOWER(host);"
Brakuje uprawnień, a nie logowania
Jeśli pojawia się ERROR 1044 zamiast 1045, logowanie się powiodło. Brakuje wtedy uprawnień do konkretnej bazy danych. Sprawdź, co konto naprawdę może:
mariadb -e "SHOW GRANTS FOR 'app'@'localhost';"
Polecenie FLUSH PRIVILEGES jest potrzebne tylko wtedy, gdy zmieniałeś tabele uprawnień bezpośrednio przez INSERT albo UPDATE. Po GRANT, CREATE USER czy ALTER USER jest zbędne, a czasem przykrywa fakt, że właściwe polecenie w ogóle nie zadziałało.
Klient nie rozumie wtyczki
MySQL 8.0 używa domyślnie caching_sha2_password. Starsze klienty i biblioteki kwitują to komunikatem ERROR 2059 (HY000): Authentication plugin 'caching_sha2_password' cannot be loaded. To nie jest problem z uprawnieniami, tylko niezgodność. W MySQL 8.0 mysql_native_password jest jeszcze dostępne jako rozwiązanie zastępcze, w MySQL 8.4 zostało usunięte. W razie wątpliwości lepiej zaktualizuj klienta, zamiast cofać się do słabszego szyfrowania.
Reset hasła root
Gdy nic już nie pomaga, serwer musi raz wystartować bez sprawdzania uprawnień. Do celu prowadzą dwie drogi. Ta przez --init-file jest bezpieczniejsza, bo serwer pracuje przy niej cały czas z aktywnym sprawdzaniem uprawnień.
Wariant 1: init-file (zalecany)
Plik SQL musi leżeć w miejscu, które usługa ma prawo czytać. W /tmp albo /root na Ubuntu regularnie blokuje to AppArmor oraz PrivateTmp w jednostce systemd. Umieść go dlatego w katalogu danych:
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('NoweHasloRoot');
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
Potem koniecznie posprzątaj, inaczej serwer przy każdym starcie znów pociągnie ten plik:
sudo systemctl unset-environment MYSQLD_OPTS
sudo rm /var/lib/mysql/kh-reset.sql
sudo systemctl restart mariadb
Dla MySQL 8.0 na Ubuntu usługa nazywa się mysql, a wiersz SQL brzmi:
ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'NoweHasloRoot';
Wariant 2: skip-grant-tables
Ten wariant całkowicie wyłącza sprawdzanie uprawnień. Bez --skip-networking każdy, kto w tym czasie dosięgnie portu, miałby pełny dostęp do wszystkich danych. Ta opcja nie jest więc opcjonalna, tylko obowiązkowa.
sudo systemctl stop mariadb
sudo systemctl set-environment MYSQLD_OPTS="--skip-grant-tables --skip-networking"
sudo systemctl start mariadb
sudo mariadb
W sesji najpierw wczytaj tabele uprawnień, inaczej serwer odrzuci ALTER USER komunikatem błędu:
FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket
OR mysql_native_password USING PASSWORD('NoweHasloRoot');
EXIT;
A potem z powrotem do normalnej pracy:
sudo systemctl unset-environment MYSQLD_OPTS
sudo systemctl restart mariadb
Jeśli twoja dystrybucja nie uwzględnia zmiennej MYSQLD_OPTS, zawsze zadziała plik drop-in /etc/systemd/system/mariadb.service.d/reset.conf o treści [Service] oraz Environment="MYSQLD_OPTS=--skip-grant-tables --skip-networking", a po nim sudo systemctl daemon-reload. Potem skasuj ten plik i jeszcze raz przeładuj systemd.
Gdy reset pójdzie nie tak
Dokładnie w tym miejscu większość poradników się kończy. Cztery najczęstsze problemy, które pojawiają się później:
Usługa przestaje się uruchamiać. Najczęściej to literówka w pliku SQL. Serwer przerywa wtedy start. Debian domyślnie loguje MariaDB do journala, Ubuntu z MySQL dodatkowo do pliku:
sudo journalctl -u mariadb -n 50 --no-pager
sudo tail -n 50 /var/log/mysql/error.log
Gdzie serwer pisze, sprawdzisz poleceniem SHOW VARIABLES LIKE 'log_error';. Nie spodziewaj się tam jednak ścieżki do pliku: w instalacjach pakietowych Debiana i Ubuntu wartość jest pusta, bo usługa działa z --skip-log-error. Wszystko idzie wtedy na standardowe wyjście błędów i trafia do journala względnie do /var/log/syslog, a odczytasz to poleceniem journalctl -u mariadb. Żeby naprawić sytuację, usuń zmienną środowiskową z --init-file i uruchom usługę na nowo.
Serwer działa trwale bez sprawdzania uprawnień. Dzieje się tak, gdy zapomniałeś o unset-environment, i jest to najgroźniejszy wariant, bo na zewnątrz wszystko wygląda normalnie. Dwie kontrole:
systemctl show-environment
ps -o args= -C mariadbd
Jeśli w którymkolwiek z tych dwóch wyników pojawi się skip-grant-tables, sprawdzanie uprawnień nadal jest wyłączone. systemctl show-environment zakłada przy tym, że systemd działa jako PID 1. Na zwykłym serwerze tak jest, ale w kontenerze albo na systemie z SysV-init tego polecenia nie ma. Tam odczytasz środowisko wprost z działającego procesu:
cat /proc/$(pgrep -n mariadbd)/environ | tr '\0' '\n'
Na samej liście argumentów też nie polegaj: przy starcie przez systemd ps pokazuje często tylko /usr/sbin/mariadbd bez żadnej opcji, a przy starcie przez skrypt SysV-init pojawia się pełna lista. Kolejna poszlaka: przy skip-grant-tables polecenie SHOW GRANTS odpowiada komunikatem błędu zamiast listą uprawnień.
AppArmor blokuje plik init. Objawem jest serwer, który bez widocznej przyczyny nie wstaje. Wyjaśni to zajrzenie do logu jądra:
sudo dmesg | grep -i denied
Odcinasz sobie dostęp całkowicie. Dzieje się tak, gdy przestawisz konto root przez ALTER USER ... IDENTIFIED BY na samo hasło, tracisz przy tym uwierzytelnianie przez socket, a potem nowego hasła już nie pamiętasz. Wyjściem jest ten sam reset jeszcze raz, tym razem z pokazanym wyżej podwójnym wariantem unix_socket OR mysql_native_password. Przed każdą ingerencją zrób sobie kopię tabel uprawnień, to trwa sekundy, a w razie kłopotów oszczędza godziny:
sudo mariadb-dump mysql > /root/mysql-grants.sql
Zwykle stosowaną opcję --single-transaction możesz sobie tutaj darować. Wprawdzie wykona się bez błędu, ale nie zadziała, bo tabele bazy mysql leżą na silniku Aria względnie MyISAM i nie są transakcyjne.
Na zarządzanym hostingu WWW nie masz dostępu do systemu, a więc żadnej z tych opcji. Tam użytkowników bazy i hasła resetuje się przez panel administracyjny hostingu. Na serwerze root KVM albo serwerze dedykowanym w KernelHoście masz pełny dostęp root, a jeśli serwer przestanie być osiągalny przez sieć, dotrzesz do systemu przez konsolę w panelu klienta.
Sprawdź, czy problem naprawdę zniknął
To, że polecenie wykona się bez błędu, nie znaczy jeszcze, że logowanie działa trwale. Te cztery kontrole wyłapują typowe błędy, które jeszcze zostały.
Po pierwsze, różnica między USER() a CURRENT_USER(). Pierwsza funkcja pokazuje, za kogo się podałeś, druga, którego wpisu konta serwer faktycznie używa:
mariadb -u app -p'Haslo' -e "SELECT USER(), CURRENT_USER();"
Jeśli stoją tam dwie różne wartości, na przykład app@localhost i app@%, to twoje uprawnienia działają przez inny wpis, niż myślisz. To wyjaśnia późniejsze błędy 1044, zanim rzucą się w oczy na produkcji.
Po drugie, prawdziwy dostęp do docelowej bazy zamiast samego logowania, plus sprawdzenie kodu wyjścia:
mariadb -u app -p'Haslo' moja_baza -e "SELECT 1;" ; echo "Kod wyjścia: $?"
Po trzecie, restart usługi i po nim to samo logowanie jeszcze raz. Tak upewnisz się, że zmiana naprawdę siedzi w tabelach, a nie tylko w pamięci działającego procesu, oraz że nie została żadna zmienna środowiskowa po resecie:
sudo systemctl restart mariadb
sudo systemctl is-active mariadb
Po czwarte, sama aplikacja. Udany test w wierszu poleceń mówi niewiele o aplikacji PHP, która łączy się jako użytkownik serwera WWW i przez socket. Przetestuj więc jako ten sam użytkownik systemowy:
sudo -u www-data mariadb -u app -p'Haslo' moja_baza -e "SELECT CURRENT_USER();"
Różnice między dystrybucjami
Poradnik, który twierdzi to samo o wszystkich czterech systemach, jest błędny w co najmniej jednym miejscu. Ta tabela zbiera istotne rozbieżności:
| System | Domyślna baza danych | Wtyczka socketu | Uwaga |
|---|---|---|---|
| Debian 13 | MariaDB 11.8 | unix_socket | pakiet mysql-server niedostępny; mysql jest już tylko dowiązaniem do mariadb i ostrzega przy wywołaniu |
| Debian 12 | MariaDB 10.11 | unix_socket | pakiet mysql-server niedostępny |
| Ubuntu 24.04 | MariaDB 10.11 albo MySQL 8.0 | unix_socket względnie auth_socket | oba pakiety równolegle w repozytoriach, w poradnikach łatwo je pomylić |
| Ubuntu 22.04 | MariaDB 10.6 albo MySQL 8.0 | unix_socket względnie auth_socket | JSON_VALUE i JSON_QUERY na mysql.global_priv działają tu tak samo jak w 10.11 i 11.8; również w 10.6 mysql.user jest tylko widokiem, a po pełny łańcuch uwierzytelniania najkrótszą drogą pozostaje SHOW CREATE USER |
Kolejne różnice, które w praktyce kosztują czas: pliki konfiguracyjne leżą przy MariaDB w /etc/mysql/mariadb.conf.d/50-server.cnf, a przy MySQL w /etc/mysql/mysql.conf.d/mysqld.cnf. Nazwy usług to mariadb względnie mysql, przy czym MariaDB dokłada jeszcze alias mysql. W MariaDB od wersji 10.4 nie ma już użytkownika debian-sys-maint, plik /etc/mysql/debian.cnf wskazuje tam na root przez socket. Przy MySQL na Ubuntu użytkownik serwisowy nadal istnieje i to jest najlepsze wejście awaryjne, gdy hasło root przepadło, a serwera nie chcesz restartować:
sudo mysql --defaults-file=/etc/mysql/debian.cnf
Jeśli utrzymasz tę kolejność, czyli najpierw dokładnie przeczytasz komunikat, potem sprawdzisz wtyczkę i wpis hosta, potem drogę połączenia, a hasło zresetujesz dopiero na samym końcu, zdecydowana większość przypadków rozwiąże się w kilka minut i bez przestoju. Reset przez skip-grant-tables to ostateczność, a nie pierwszy krok.
Najczęstsze pytania
Dlaczego mysql -u root -p nie działa, choć hasło jest poprawne?
Czym różni się ERROR 1045 od ERROR 1698?
Czy localhost to to samo co 127.0.0.1?
Czy wielkość liter w nazwie użytkownika i hosta ma znaczenie?
Jak zresetować hasło root bez skip-grant-tables?
Po czym poznam, że serwer nadal działa bez sprawdzania uprawnień?
Czy na Debianie istnieje pakiet mysql-server?
2026 KernelHost GmbH. Wszelkie prawa zastrzeżone. Ten poradnik jest chroniony prawem autorskim. Publikowanie go w innych serwisach, w całości, we fragmentach lub w zmienionej formie, wymaga naszej pisemnej zgody. Cytaty z podaniem źródła i z linkiem są jak najbardziej mile widziane.

