MySQLを8系へ更新したあと、PHPのPDO接続でThe server requested authentication method unknown to the client [caching_sha2_password]と表示されたら、最初に確認するのはアプリを実行しているPHPとMySQLクライアントライブラリです。DSNの文字コードを変えるだけでは、クライアントが知らない認証方式には対応できません。
基本の対処は、アプリとの互換性を確認しながら、サポート中のPHPと対応するpdo_mysql・mysqlndへ更新することです。更新後も接続できない場合は、パスワードや接続先と、TLS・RSAの認証経路を分けて調べます。
2054・1045・2002を、同じ接続エラーとして直さない
SQLSTATE[HY000]だけでは原因を特定できません。その後ろのドライバーエラー番号と、管理用ログに残ったメッセージを一緒に確認します。次の表は調べ始める場所を選ぶための目安で、番号だけで原因が一つに決まるわけではありません。
| エラーや表示 | 先に確認すること | 変更を急がないもの |
|---|---|---|
| 2054・unknown authentication method | 実行PHP、mysqlnd、pdo_mysqlが認証方式に対応しているか | SQL本文やテーブルの文字コード |
| 1045・Access denied | ユーザー名、パスワード、接続元に一致するアカウント、TLS要求 | 全ユーザーの認証方式 |
| 2002・Connection refused / No such file | ホスト、ポート、ソケット、サーバー稼働。SSL関連の文なら証明書も確認 | パスワードの再発行 |
| could not find driver | そのPHPでpdo_mysqlがロードされているか | MySQLのユーザー設定 |
| Authentication requires secure connection、RSA関連 | TLSか、信頼済みRSA公開鍵でフル認証できるか | 証明書検証を無効にする設定 |
| 1524・Plugin ‘mysql_native_password’ is not loaded | MySQLのバージョンと、無効な旧方式を指定していないか | 別のユーザーも旧方式へ戻す操作 |
本記事の隔離テストでは、PHP 8.3.30のPDO/mysqlndからMySQL 8.4.11へ接続し、パスワード違いで1045、接続先ポート違いで2002を確認しました。証明書と接続先名の不一致も、この環境では2002でした。古いPHPでの2054自体は再現しておらず、対応履歴はPHP公式マニュアルで確認しています。
CLIとWebで、実際に使われるPHPを確かめる
ターミナルのphp -vが新しくても、WebサーバーのPHP-FPM、Apacheモジュール、別コンテナ、cronが同じPHPを使っているとは限りません。エラーを出している実行経路に合わせて、次の情報を確認します。
runtime.php
<?php
// 認証情報を出さず、実際にアプリが使うPHPで実行する。
echo json_encode([
'php' => PHP_VERSION,
'sapi' => PHP_SAPI,
'pdo_drivers' => class_exists(PDO::class) ? PDO::getAvailableDrivers() : [],
'pdo_mysql' => extension_loaded('pdo_mysql'),
'mysqlnd' => phpversion('mysqlnd') ?: null,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), PHP_EOL;
CLIならphp runtime.phpで実行できます。Web経由の実行環境を調べる場合は、管理者だけが見られる診断画面やログで一時的に確認し、一般公開のURLへ置き続けないでください。php -mでも拡張の一覧を確認できますが、それはそのコマンドが起動したPHPの一覧です。
pdo_driversにmysqlがなければ、まず拡張の導入・有効化を確認します。mysqlndがnullなら、別のクライアントライブラリでビルドされている可能性もあるので、その構成を確認してください。接続後にはPDO::ATTR_CLIENT_VERSIONで使われたクライアント、PDO::ATTR_DRIVER_NAMEでドライバーを確認できます。接続自体に失敗している段階では、この二つを取得できません。
PHP公式のPDO_MYSQL説明には、caching_sha2_passwordの完全対応はPHP 7.4.4からとあります。これは対応開始の履歴であり、今から7.4へ更新する推奨ではありません。アプリ・拡張の互換性を確認し、サポート中のバージョンを選びます。PHP-FPMなどを使う構成では、パッケージ更新後に実際のワーカープロセスへ反映されたかも確認してください。
管理者側では、ユーザー名と接続元の組を確認する
MySQLのアカウントは、ユーザー名とHostの組で管理されます。同じapp_userでも、localhost用と別ホスト用では認証方式や条件が異なる場合があります。管理者は、パスワードハッシュを表示する必要のない列だけで確認できます。
inspect-account.sql
-- 管理者用。ユーザー名は実際のアプリ用ユーザーに合わせる。
SELECT VERSION();
SELECT User, Host, plugin, ssl_type
FROM mysql.user
WHERE User = 'app_user';
-- 接続できる同じアプリ用接続で実行する。
SELECT USER(), CURRENT_USER();
SHOW SESSION STATUS LIKE 'Ssl_cipher';
上半分はmysql.userを参照できる管理者用です。接続できた場合のCURRENT_USER()は、認証に使われたアカウントを示します。USER()の接続ユーザー情報と並べることで、想定したHostの行に一致しているかを調べられます。ssl_typeがANYなら、アカウントにSSL接続の要求があります。
PHPのPDO_MYSQLでは、Unix系環境のhost=localhostは通常Unixソケット接続になります。TCPで確認したいなら、実際のDBホスト名や127.0.0.1を指定します。ただし、DockerのPHPコンテナ内の127.0.0.1はそのPHPコンテナ自身です。DBが別コンテナならDBサービス名を使います。TLSでは、指定するホスト名がサーバー証明書と一致することも必要です。
基本的なユーザー作成と権限付与は、MySQLのユーザーと権限の設定を参照できます。認証エラーの対処として、アプリのユーザーへ管理者権限をまとめて付ける必要はありません。
TLSとCA検証を付けて、最小のPDO接続を試す
対応済みクライアントからTCP接続する場合、まずTLSで接続する構成を確認します。以下はCLI用の例です。DB_HOST・DB_PORT・DB_NAME・DB_USER・DB_PASSWORD・DB_SSL_CAを実行環境へ設定してから、php connect-tls.phpで動かします。パスワードをコマンド履歴へ直接書かず、既存の秘密情報管理から渡してください。
DB_SSL_CAには、DB管理者やサービス提供元から信頼できる経路で入手したCA証明書のファイルパスを指定します。サーバーのホスト名と証明書も一致させます。接続後のsetAttribute()でTLSを有効にすることはできないため、オプションはPDOの生成時に渡します。
connect-tls.php
<?php
// CLI専用の接続確認。認証情報は環境変数から渡す。
if (PHP_SAPI !== 'cli') { http_response_code(404); exit; }
function requiredEnv(string $name): string {
$value = getenv($name);
if ($value === false || $value === '') throw new RuntimeException("Missing: $name");
return $value;
}
try {
$host = requiredEnv('DB_HOST');
$port = getenv('DB_PORT') ?: '3306';
$database = requiredEnv('DB_NAME');
// DSNへ渡す値はアプリの設定。区切り文字の混入を防ぐ。
if (preg_match('/[;\x00-\x20]/', $host . $database) || !ctype_digit($port)) {
throw new RuntimeException('Invalid connection settings');
}
$ca = requiredEnv('DB_SSL_CA');
if (!is_readable($ca)) throw new RuntimeException('CA file is not readable');
// PHP 8.4以降はPdo\Mysqlの定数、それ以前はPDOの定数を使う。
$sslCa = defined('Pdo\\Mysql::ATTR_SSL_CA')
? constant('Pdo\\Mysql::ATTR_SSL_CA') : PDO::MYSQL_ATTR_SSL_CA;
$verify = defined('Pdo\\Mysql::ATTR_SSL_VERIFY_SERVER_CERT')
? constant('Pdo\\Mysql::ATTR_SSL_VERIFY_SERVER_CERT') : PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT;
$pdo = new PDO(
"mysql:host={$host};port={$port};dbname={$database};charset=utf8mb4",
requiredEnv('DB_USER'), requiredEnv('DB_PASSWORD'),
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
$sslCa => $ca, $verify => true]
);
$tls = $pdo->query("SHOW SESSION STATUS LIKE 'Ssl_cipher'")->fetch();
if (empty($tls['Value'])) throw new RuntimeException('TLS was not negotiated');
$result = $pdo->query('SELECT 1 AS ok, VERSION() AS server_version, CURRENT_USER() AS account')->fetch();
$result['client'] = $pdo->getAttribute(PDO::ATTR_CLIENT_VERSION);
$result['driver'] = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
$result['tls_cipher'] = $tls['Value'];
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), PHP_EOL;
} catch (PDOException $e) {
// メッセージ全文・DSN・パスワードを公開画面やログへ出さない。
fwrite(STDERR, json_encode([
'sqlstate' => $e->errorInfo[0] ?? (string) $e->getCode(),
'driver_code' => $e->errorInfo[1] ?? null,
]) . PHP_EOL);
exit(1);
} catch (RuntimeException $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
exit(1);
}
成功時にはok: 1、認証に使われたアカウント、クライアント、TLS暗号スイートが表示されます。Ssl_cipherが空ならTLS接続として成功扱いにしません。この診断結果にも内部の構成情報が含まれるため、公開画面へそのまま出さないでください。
PHP 8.4以降にはPdo\Mysqlのドライバー固有定数があります。上のコードはその存在を確認し、PHP 8.3では従来のPDO::MYSQL_ATTR_*を使います。実測したのは8.3側です。8.5では従来定数が非推奨になっているため、新しい環境ではPdo\Mysql側を使う設計にしています。
RSA公開鍵は、TLSの代わりに通信全体を暗号化するものではない
caching_sha2_passwordのフル認証は、TLSなどの保護された接続か、RSA鍵を使うパスワード交換に対応した接続で行えます。TLSが使えない構成でRSAを検討するなら、管理者から信頼できる経路で得た認証用のRSA公開鍵をクライアントへ配置します。これはTLS用のCA証明書とは別のファイルです。
PDOにはPdo\Mysql::ATTR_SERVER_PUBLIC_KEY(PHP 8.3ではPDO::MYSQL_ATTR_SERVER_PUBLIC_KEY)があります。ただし、RSAで保護するのはパスワード交換であり、その後のSQLや結果全体ではありません。今回のテストでも、公開鍵指定で認証は成功しましたが、Ssl_cipherは空でした。ネットワーク全体の暗号化が必要な構成を、RSAだけで満たしたことにはできません。詳しい条件はMySQL公式のCaching SHA-2認証の説明で確認できます。
一度だけ接続できても、認証キャッシュを疑う
caching_sha2_passwordには認証キャッシュがあります。TLSなどで一度認証できたあと、キャッシュを使う接続だけが通る場合があります。サーバー再起動やパスワード変更などでキャッシュがなくなれば、再びフル認証の経路が必要です。
隔離テストでは、mysqlクライアントをTLS無効・公開鍵指定なしで接続させると、初回は2061で失敗しました。同じアカウントで公開鍵を指定したPDO接続を一度成功させると、前のmysql接続も成功し、キャッシュを消すと再び2061になりました。一方、CA検証付きのPDO/TLS接続はキャッシュ消去後も成功しました。
この確認は専用DBで行っています。本番全体の認証キャッシュを消して試すのではなく、検証環境や新しい検証用アカウントで、初回認証が通ることを確かめてから変更を反映します。
mysql_native_passwordへ戻す前に、MySQLの版を確認する
古い解説のdefault_authentication_plugin=mysql_native_passwordや、全ユーザーを旧方式へ戻す対処を、そのままMySQL 8.4以降へコピーしないでください。MySQL公式のNative認証の説明では、次の違いが示されています。
| MySQL | mysql_native_passwordの扱い | 対処を考える順序 |
|---|---|---|
| 8.0.34以降の8.0系 | 非推奨 | クライアント更新を先に検討。一時的な旧方式が必要なら対象アカウントと移行期限を限定する |
| 8.4 | 既定で無効 | ALTER USERだけで旧方式へ戻せるとは限らない。検証環境では1524を確認 |
| 9.0以降 | 削除 | 旧方式へ戻す方針は使えない。対応クライアントと認証方式へ移行する |
アカウントをcaching_sha2_passwordへ移す場合のSQLの形は、ALTER USER 'app_user'@'接続元' IDENTIFIED WITH caching_sha2_password BY '新しいパスワード';です。Hostは確認した実在アカウントへ合わせ、パスワード変更とアプリ側の秘密情報更新を一緒に計画します。先に同じ構成の検証用アカウントで接続を確かめてください。まだ対応していないクライアントがある状態で、本番ユーザーだけを先に変更しないことが重要です。
直ったかどうかは、実際のWeb・ジョブの実行環境から、想定したアカウントで接続し、必要なTLSと初回認証が成立するかで判断します。接続後のデータ取得や更新へ進む場合は、PHPからMySQLへ接続して操作する基本へ戻れます。