MySQL ile İzin Hataları: Access Denied Sorunlarını Çözme

MySQL yönetiminde en sinir bozucu anlardan biri, uygulamanın tam production’da “Access denied for user” hatasıyla düşmesidir. Geliştirici panikler, müşteri şikayet eder, sen de terminale koşarsın. Bu yazıda MySQL izin hatalarını kökten anlamayı ve hızlıca çözmeyi ele alacağız.

Access Denied Hatası Neden Oluşur?

MySQL’in yetkilendirme sistemi, kullanıcı adı + host kombinasyonuna dayalı çalışır. Bu çok önemli bir nokta çünkü ali@localhost ile ali@% MySQL için tamamen farklı iki kullanıcıdır. Bunu anlamadan izin sorunlarını çözmek, karanlıkta anahtar aramak gibi olur.

Hata genellikle şu mesajlardan biriyle karşınıza çıkar:

ERROR 1045 (28000): Access denied for user 'uygulama'@'localhost' (using password: YES)
ERROR 1044 (42000): Access denied for user 'uygulama'@'%' to database 'production_db'
ERROR 1142 (42000): SELECT command denied to user 'uygulama'@'localhost' for table 'kullanicilar'

Her hata kodu farklı bir katmanda sorun olduğunu gösterir. 1045 bağlantı seviyesinde, 1044 veritabanı seviyesinde, 1142 ise tablo seviyesinde bir izin sorununa işaret eder.

Teşhis: Önce Durumu Anla

Hata aldığında ilk yapman gereken şey panik değil, sistematik teşhistir. Önce mevcut kullanıcıları ve yetkilerini kontrol et.

mysql -u root -p -e "SELECT user, host, authentication_string FROM mysql.user;"

Çıktıda kullanıcının hangi host’lardan bağlanabildiğini göreceksin. Eğer uygulama localhost‘tan bağlanmaya çalışıyorsa ama kayıt sadece % ile varsa, sorunun kaynağını buldun demektir.

Bir kullanıcının tam yetkilerini görmek için:

mysql -u root -p -e "SHOW GRANTS FOR 'uygulama'@'localhost';"

Eğer kullanıcı hiç yoksa bu komut hata verir. Yoksa çıktı şöyle görünür:

+------------------------------------------------------------------+
| Grants for uygulama@localhost                                    |
+------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `uygulama`@`localhost`                    |
| GRANT SELECT, INSERT ON `production_db`.* TO `uygulama`@`localhost` |
+------------------------------------------------------------------+

Burada USAGE yetkisi aslında “hiçbir yetki yok ama kullanıcı var” anlamına gelir. Dikkatli ol.

Senaryo 1: Uygulama Sunucusundan Uzak Bağlantı

Diyelim ki web sunucun 192.168.1.100 IP’sinde, MySQL sunucun ise 192.168.1.200‘de. Web uygulaması veritabanına bağlanamıyor.

Önce MySQL sunucusunda sorunun gerçekten izin mi, yoksa ağ mı olduğunu anlayalım:

# Web sunucusundan test et
mysql -h 192.168.1.200 -u uygulama -p production_db

# Bağlantı timeout alıyorsa ağ/firewall sorunu var demektir
# Access denied alıyorsan izin sorunudur

İzin sorunuysa MySQL sunucusunda root olarak giriş yap ve kullanıcıyı düzelt:

mysql -u root -p

-- Kullanıcı sadece localhost için tanımlıysa
CREATE USER 'uygulama'@'192.168.1.100' IDENTIFIED BY 'güçlü_şifre';
GRANT SELECT, INSERT, UPDATE, DELETE ON production_db.* TO 'uygulama'@'192.168.1.100';
FLUSH PRIVILEGES;

Eğer bütün IP’lerden bağlanmasını istiyorsan % kullanabilirsin ama güvenlik açısından bunu production’da önermiyorum. Spesifik IP veya subnet belirtmek her zaman daha iyidir.

Ayrıca MySQL’in dışarıdan bağlantı kabul edip etmediğini kontrol et:

grep -E "bind-address|skip-networking" /etc/mysql/mysql.conf.d/mysqld.cnf
# veya
grep -E "bind-address|skip-networking" /etc/my.cnf

Eğer bind-address = 127.0.0.1 görüyorsan, MySQL sadece localhost dinliyor demektir. Bu ayarı değiştir:

# /etc/mysql/mysql.conf.d/mysqld.cnf
bind-address = 0.0.0.0  # Tüm arayüzler, ya da spesifik IP
# bind-address = 192.168.1.200

Sonra servisi yeniden başlat:

systemctl restart mysql

Senaryo 2: Şifre Değişikliği Sonrası Bağlanamama

Bu çok klasik bir durum. DBA şifreyi değiştirdi, uygulamayı güncellemeyi unuttu veya tam tersi. Ya da şifre doğru ama hash yöntemi uyumsuz.

MySQL 8.0 ile birlikte varsayılan authentication plugin caching_sha2_password oldu. Eski istemciler bunu desteklemeyebilir ve “Access denied” hatası alırsın, üstelik şifre doğru olsa bile.

# Kullanıcının authentication plugin'ini kontrol et
mysql -u root -p -e "SELECT user, host, plugin FROM mysql.user WHERE user='uygulama';"

Eğer eski bir PHP uygulaması kullanıyorsan ve caching_sha2_password görüyorsan, plugin’i değiştir:

mysql -u root -p

ALTER USER 'uygulama'@'localhost' IDENTIFIED WITH mysql_native_password BY 'yeni_şifre';
FLUSH PRIVILEGES;

Şifreyi sıfırlamak için:

ALTER USER 'uygulama'@'%' IDENTIFIED BY 'yeni_güçlü_şifre';
FLUSH PRIVILEGES;

Senaryo 3: Root Şifresi Unutuldu

Bu gerçek bir kabus senaryosu. Özellikle yıllardır dokunulmamış bir MySQL kurulumunda olur. Panik yapma, çözüm var.

# MySQL servisini durdur
systemctl stop mysql

# Yetki tablolarını bypass ederek başlat
mysqld_safe --skip-grant-tables --skip-networking &

# Root olarak şifresiz bağlan
mysql -u root

# Şifreyi sıfırla
FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED BY 'yeni_root_şifresi';
FLUSH PRIVILEGES;
EXIT;

# mysqld_safe'i öldür ve normal servisi başlat
kill $(cat /var/run/mysqld/mysqld.pid)
systemctl start mysql

MySQL 5.7 ve öncesinde yöntem biraz farklıydı:

mysqld_safe --skip-grant-tables &
mysql -u root

USE mysql;
UPDATE user SET authentication_string=PASSWORD('yeni_şifre') WHERE User='root';
FLUSH PRIVILEGES;

Güvenlik notu: --skip-grant-tables ile çalışırken --skip-networking de eklemek kritik. Aksi takdirde o kısa sürede herkes root olarak bağlanabilir.

FLUSH PRIVILEGES Ne Zaman Gerekli?

Bu soruyu çok alıyorum. MySQL’in grant tabloları bellekte önbelleğe alınır. GRANT, REVOKE, CREATE USER, DROP USER ve ALTER USER komutları kullandığında MySQL bu tabloları otomatik olarak günceller, FLUSH PRIVILEGES gerekmez.

Ama mysql.user tablosunu doğrudan INSERT, UPDATE, DELETE ile değiştirdiysen, o zaman FLUSH PRIVILEGES zorunludur:

# Bu yöntemde FLUSH PRIVILEGES GEREKİR
UPDATE mysql.user SET authentication_string='' WHERE User='root';
FLUSH PRIVILEGES;

# Bu yöntemde FLUSH PRIVILEGES GEREKMEz (ama zarar vermez)
ALTER USER 'root'@'localhost' IDENTIFIED BY 'şifre';

Yetki Sorunlarını Debug Etmek: Adım Adım

Sistematik bir yaklaşım izlemen gerekiyor. İşte gerçek dünyada kullandığım kontrol listesi:

# 1. Kullanıcı var mı?
mysql -u root -p -e "SELECT user, host FROM mysql.user WHERE user='uygulama';"

# 2. Hangi yetkiler var?
mysql -u root -p -e "SHOW GRANTS FOR 'uygulama'@'localhost';"

# 3. Host eşleşiyor mu? Wildcard durumunu kontrol et
mysql -u root -p -e "SELECT user, host FROM mysql.user WHERE user='uygulama' AND host IN ('localhost', '127.0.0.1', '%');"

# 4. Veritabanı var mı?
mysql -u root -p -e "SHOW DATABASES LIKE 'production_db';"

# 5. Tablo bazlı yetki var mı?
mysql -u root -p -e "SELECT * FROM mysql.tables_priv WHERE user='uygulama';"

Bir de MySQL’in kendi hata logunu incele:

tail -f /var/log/mysql/error.log
# veya
journalctl -u mysql -f

Minimum Yetki Prensibi ile Kullanıcı Oluşturma

Pek çok sistem yöneticisi “çalışsın da nasıl olursa olsun” mantığıyla her şeye tam yetki verir. Bu güvenlik felaketi. Doğru yaklaşım minimum yetki prensibidir.

Bir web uygulaması için tipik yetki seti:

mysql -u root -p

-- Kullanıcı oluştur
CREATE USER 'webapp'@'192.168.1.100' IDENTIFIED BY 'güçlü_şifre_buraya';

-- Sadece gerekli veritabanına yetki ver
GRANT SELECT, INSERT, UPDATE, DELETE ON production_db.* TO 'webapp'@'192.168.1.100';

-- Eğer stored procedure kullanıyorsan
GRANT EXECUTE ON production_db.* TO 'webapp'@'192.168.1.100';

-- Backup kullanıcısı için
CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'başka_güçlü_şifre';
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON *.* TO 'backup_user'@'localhost';

FLUSH PRIVILEGES;

Readonly bir raporlama kullanıcısı için:

CREATE USER 'raporlama'@'10.0.0.%' IDENTIFIED BY 'şifre';
GRANT SELECT ON production_db.* TO 'raporlama'@'10.0.0.%';
FLUSH PRIVILEGES;

Docker ve Konteyner Ortamlarında Access Denied

Docker ile MySQL kullanıyorsan, bağlantı sorunları biraz farklı davranır. Container içinden container’a bağlanmak, dışarıdan bağlanmak veya host’tan container’a bağlanmak ayrı senaryolardır.

# Docker container içinden MySQL'e bağlanma
docker exec -it mysql_container mysql -u root -p

# Dışarıdan bağlanırken host IP kullan, localhost değil
mysql -h 127.0.0.1 -P 3306 -u uygulama -p
# NOT: localhost yerine 127.0.0.1 kullanmak Unix socket yerine TCP kullanılmasını sağlar

# Docker compose ile environment variable ile kullanıcı oluşturma
# docker-compose.yml içinde:
# MYSQL_USER: uygulama
# MYSQL_PASSWORD: şifre
# MYSQL_DATABASE: production_db
# Bu değişkenler container ilk başladığında kullanıcıyı otomatik oluşturur

Docker’da sık karşılaşılan bir sorun da root@localhost ile root@% farkıdır. Container başlarken oluşturulan root kullanıcısı bazen sadece localhost‘a izin verir:

docker exec -it mysql_container mysql -u root -p -e 
  "CREATE USER 'root'@'%' IDENTIFIED BY 'şifre'; GRANT ALL ON *.* TO 'root'@'%'; FLUSH PRIVILEGES;"

Gerçek Bir Olay: Production Çöküşü Nasıl Çözüldü

Geçen yıl bir e-ticaret sitesinde gece 2’de telefon geldi. Uygulama “Access denied” veriyordu, işlemler durmuştu. Sunucuya bağlandım, önce basit kontrolü yaptım:

mysql -u uygulama -p production_db
# Access denied for user 'uygulama'@'localhost' (using password: YES)

mysql -u root -p -e "SHOW GRANTS FOR 'uygulama'@'localhost';"
# ERROR 1141: There is no such grant defined for user 'uygulama' on host 'localhost'

Kullanıcı yoktu! Sonra şunu buldum:

mysql -u root -p -e "SELECT user, host FROM mysql.user WHERE user='uygulama';"
# uygulama | %

Kullanıcı % ile tanımlanmıştı ama uygulama localhost‘tan bağlanıyordu. MySQL, localhost için Unix socket kullanır ve bu % wildcard’ı ile eşleşmez. Biri yanlışlıkla eski localhost kaydını silmiş, sadece % bırakmıştı.

Hızlı çözüm:

mysql -u root -p
GRANT SELECT, INSERT, UPDATE, DELETE ON production_db.* TO 'uygulama'@'localhost' IDENTIFIED BY 'mevcut_şifre';
FLUSH PRIVILEGES;

5 dakikada çözüldü. Ama asıl önemli olan bu durumun tekrar yaşanmaması için izin değişikliklerinin versiyon kontrol sisteminde takip edilmesini sağlamaktı.

MySQL Yetki Sistemini Anlatan Faydalı Sorgular

Sistemi izlemek ve anlık durumu görmek için şu sorguları sık kullanıyorum:

# Tüm kullanıcılar ve host'lar
mysql -u root -p -e "
SELECT 
  user, 
  host, 
  plugin,
  password_expired,
  account_locked
FROM mysql.user 
ORDER BY user, host;"

# Belirli bir veritabanına kimlerin erişimi var?
mysql -u root -p -e "
SELECT user, host FROM mysql.db 
WHERE db='production_db'
UNION
SELECT user, host FROM mysql.user 
WHERE Select_priv='Y';"

# Boş şifreli kullanıcıları bul (güvenlik açığı!)
mysql -u root -p -e "
SELECT user, host FROM mysql.user 
WHERE authentication_string='' OR authentication_string IS NULL;"

Yetkileri Geri Almak ve Kullanıcı Silmek

Bir kullanıcının yetkisini kaldırmak bazen vermek kadar önemli. Özellikle ekipten ayrılan birinin veya artık kullanılmayan servis hesabının yetkilerini temizlemek gerekir.

# Belirli bir yetki kaldır
REVOKE INSERT ON production_db.* FROM 'uygulama'@'localhost';

# Tüm yetkileri kaldır
REVOKE ALL PRIVILEGES ON production_db.* FROM 'uygulama'@'localhost';
REVOKE GRANT OPTION ON *.* FROM 'uygulama'@'localhost';

# Kullanıcıyı tamamen sil
DROP USER 'eski_kullanici'@'localhost';
DROP USER 'eski_kullanici'@'%';

FLUSH PRIVILEGES;

Kullanıcıyı silmeden önce hangi veritabanlarında yetkisi olduğunu görmek istersen:

mysql -u root -p -e "SHOW GRANTS FOR 'eski_kullanici'@'localhost';"

MySQL 8.0’da Değişen Şeyler

MySQL 8.0 ile gelen bazı değişiklikler izin sorunlarını farklı şekilde yaşatabilir. Bunları bilmek önemli:

  • caching_sha2_password: Varsayılan authentication plugin değişti. Eski istemciler bağlanamayabilir.
  • IDENTIFIED BY ile CREATE USER: Artık GRANT ... IDENTIFIED BY yerine önce CREATE USER sonra GRANT kullanmalısın. Eski sözdizimi kaldırıldı.
  • Password validation: Varsayılan şifre politikası daha katı. Zayıf şifreler reddedilir.
# MySQL 8.0'da doğru kullanıcı oluşturma
CREATE USER 'kullanici'@'localhost' IDENTIFIED BY 'Güçlü@Şifre123';
GRANT SELECT, INSERT ON veritabani.* TO 'kullanici'@'localhost';

# Şifre politikasını kontrol et
SHOW VARIABLES LIKE 'validate_password%';

# Gerekirse politikayı gevşet (production'da önerilmez)
SET GLOBAL validate_password.policy=LOW;

Sonuç

MySQL izin hataları ilk bakışta kaotik görünse de sistemli bir yaklaşımla her zaman çözülür. Önce kullanıcının var olup olmadığını, sonra host eşleşmesini, ardından şifreyi ve son olarak veritabanı/tablo yetkilerini kontrol et. Bu sırayı bozmadan ilerlersen, büyük ihtimalle ikinci veya üçüncü adımda sorunu bulursun.

En önemli tavsiyem şu: izin değişikliklerini her zaman belgelemek ve mümkünse bir değişiklik yönetim sürecine dahil etmek. Gece 2’de paniklemeden değişiklik geçmişine bakıp “kim, ne zaman, neden değiştirdi” sorusunu yanıtlayabilmek paha biçilmez. Bir sonraki access denied hatası seni bulmadan sen onu bul.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir