障害分析 | エラーログからMySQLの認証メカニズムやバグの詳細分析まで

作者: 李希超

笑うのが大好きな江蘇蘇寧銀行のデータベースエンジニアで、主にデータベースの日常運用保守、自動化構築、DMPプラットフォームの運用保守を担当。サイクリングや研究技術など、MySQL、Python、Oracle が得意です。
この記事の出典: 元の寄稿

* Aikesheng オープン ソース コミュニティによって作成されたものであり、元のコンテンツを無断で使用することは許可されていません。編集者に連絡し、転載のソースを示してください。


研究開発生からは、システム性能試験環境でのMySQLデータベース関連の業務システムは正常に動作しているが、警告ログが多数発生しており、原因の分析にご協力いただく必要があるとの報告がありました。

1. 異常現象

mysql エラー ログ ファイルには、次の情報が多数含まれています。

2023-01-10T01:07:23.035479Z 13 [Warning] [MY-013360] [Server] Plugin sha256_password reported: ''sha256_password' is deprecated and will be removed in a future release. Please use caching_sha2_password instead'

主な環境情報

2.予備分析

上記の警告ログを見ると、経験的な理論によると、最初の反応は、クライアントのバージョンが低すぎるということであり、その認証プラグインはサーバーが破棄するバージョンであるため、上記の警告メッセージが生成されます。特に、一部の一般的なクライアント ツールは、更新の頻度が原因で、この問題を簡単に引き起こす可能性があります。

再現してみる

予備分析の提案に従って、研究開発学生に予備分析の提案を伝えた後、一般的なデータベース ツールを使用してデータベースにアクセスし、エラーが再現できるかどうかを確認します。ただし、データベース内の一般的なデータベース ユーザーと、さまざまなツールを介してデータベースにアクセスすることにより、アクセス時に例外がトリガーされませんでした。
したがって、再現の最初の試みは失敗しました。他の理由によるのでしょうか?

初めてアクセスしようとする過程で、データベースのエラー ログをリアルタイムで観察します。クライアントでアクセスを試みる過程で、エラーが再現されませんでした。それでも、対応する警告ログが引き続きエラー ログ ファイルに出力されます。さらに、頻度が高く、間隔時間が固定されていることも、エラーがデータベース ツールによって手動でアクセスされていないことを証明しています。

アプリケーション システムは正常に動作しており、クライアントが原因ではありません。DBA として、それをさらにどのように分析する必要がありますか?

初期のトリック

テスト環境のため、このエラーに対して、次の操作を実行して MySQL の一般ログを有効にすることができます。

-- 开通一般日志:
show variables like 'general_log';
set global general_log=on;
show variables like 'general_log';
-- 查看一般日志路径:
show variables like 'general_log_file';

ログを有効にした後、エラー ログを観察し、一般ログで次のレコードを見つけます。

ヒント: 例外を見つけたら、すぐに一般ログを閉じて、大量のログが生成されてディスク容量が使い果たされるのを防ぎます。

-- 开通一般日志:
show variables like 'general_log';
set global general_log=off;
show variables like 'general_log';

つまり、問題が発生した時点で、ユーザー dbuser2 が 10.xy43 サーバーからデータベースへのアクセス要求を開始します。異常なアクセス ユーザーとサーバーを確認した後、データベースの mysql.user テーブル、skip-grant-tables およびその他の構成を確認し、ユーザーがデータベースに存在しないこと、および承認テーブルおよびその他の構成がスキップされていないことを確認します。このユーザーを使用すると、データベースにログインできなくなります。

研究開発の学生に情報をフィードバックすると、すぐに一部のアプリケーションが不当に構成され、存在しないデータベース ユーザーを使用し、定期的にデータベースに接続してタスクを実行していることを確認しました。そのため、研究開発の学生が構成を変更した後、警告ログは生成されなくなりました。

では、この問題の分析はここまでです。これで終わりでしょうか。

構成を変更した後、警告ログは発生しなくなりました。しかし、存在しないユーザーなので、アクセス時に認証プラグインを破棄するように促されるのはなぜですか?

3. ソースコード解析

質問があると、最初に頭に浮かぶのは、データベース ユーザーが mysql.user テーブルに存在しないため、ログインしても警告が生成されるということです.このユーザーは mysql の内部ユーザーであり、ハードコードされています! そのため、対応するバージョンのソース コードを取得し、次のコマンドで確認します。

cd mysql-8.0.27/
grep -rwi "dbuser2" *

アクセス結果は空です。つまり、推測された「内部ユーザー」はありません。

通常のログイン認証ロジック

ハード コーディングがないため、内部ロジックによってのみ発生する可能性があります。ということで、まずは通常時のmysqlユーザーのログイン処理について、ソースコードを解析した結果が以下のようになります。

|—> handle_connection
  |—> thd_prepare_connection
    |—> login_connection
      |—> check_connection
        // 判断客户端的主机名是否可以登录(mysql.user.host),如果 mysql.user.host 有 '%' 那么将 allow_all_hosts,允许所有主机。
        |—> acl_check_host   
        |—> acl_authenticate
          |—> server_mpvio_initialize // 初始化mpvio对象,包括赋值 mpvio->ip / mpvio->host
          |—> auth_plugin_name="caching_sha2_password"
          |—> do_auth_once
            |—> caching_sha2_password_authenticate // auth->authenticate_user
              |—> server_mpvio_read_packet // vio->read_packet(vio, &pkt) // pkt=passwd
                |—> parse_client_handshake_packet
                  |—> char *user = get_string(&end, &bytes_remaining_in_packet, &user_len);
                  |—> passwd = get_length_encoded_string(&end, &bytes_remaining_in_packet, &passwd_len);
                  |—> mpvio->auth_info.user_name = my_strndup(key_memory_MPVIO_EXT_auth_info, user, user_len, MYF(MY_WME))
                  // 根据 user 搜索 mysql.user.host ,并与客户端的 hostname/ip 进行比较:匹配记录后,赋值 mpvio->acl_user
                  |—> find_mpvio_user(thd, mpvio) 
                    |—> list = cached_acl_users_for_name(mpvio->auth_info.user_name); // 根据 user 搜索 mysql.user.host 
                    |—> acl_user_tmp->host.compare_hostname(mpvio->host, mpvio->ip) //  与客户端的 hostname/ip 进行比较
                    |—> mpvio->acl_user_plugin = mpvio->acl_user->plugin; // 赋值 acl_user_plugin 属性为用户的plugin名
                    |—> mpvio->auth_info.multi_factor_auth_info[0].auth_string = mpvio->acl_user->credentials[PRIMARY_CRED].m_auth_string.str; 
                    |—> mpvio->auth_info.auth_string = mpvio->auth_info.multi_factor_auth_info[0].auth_string; 
                  |—> if (my_strcasecmp(system_charset_info, mpvio->acl_user_plugin.str,plugin_name(mpvio->plugin)->str) != 0)
                  |—> my_strcasecmp(system_charset_info, client_plugin,user_client_plugin_name) //检查客户端的认证插件与用户插件是否相同
              |—> make_hash_key(info->authenticated_as, hostname ? hostname : nullptr, authorization_id);  // 生成 authorization_id = user1\000% 
              |—> g_caching_sha2_password->fast_authenticate(authorization_id,*scramble,20,pkt,false) // 进行快速授权操作
                |—> m_cache.search(authorization_id, digest) // 根据 user、host 搜索密码,赋值到digest
                |—> Validate_scramble validate_scramble_first(scramble, digest.digest_buffer[0], random, random_length);
                |—> validate_scramble_first.validate(); // 校验 scramble
              // 如验证成功
              |—> vio->write_packet(vio, (uchar *)&fast_auth_success, 1)
              |—> return CR_OK;
              // 否则进行进行慢授权操作
              |—> g_caching_sha2_password->authenticate( authorization_id, serialized_string, plaintext_password);
          |—> server_mpvio_update_thd(thd, &mpvio);
          |—> check_and_update_password_lock_state(mpvio, thd, res);
          // 继续其它授权操作

つまり、主要な認証操作は関数 caching_sha2_password_authenticate() で実行され、最初に関数 find_mpvio_user() を呼び出し、ユーザーとホスト名から構成されたユーザーを見つけてから、関数 fast_authenticate() を呼び出してパスワードをすばやく検証します。

存在しないユーザー認証ロジックを使用する

ユーザーが存在しない場合、mysql ユーザーのログイン プロセス、ソース コードの解析結果は次のとおりです。

|—> handle_connection
  |—> thd_prepare_connection
    |—> login_connection
      |—> check_connection
        // 判断客户端的主机名是否可以登录(mysql.user.host),如果 mysql.user.host 有 '%' 那么将 allow_all_hosts,允许所有主机。
        |—> acl_check_host   
        |—> acl_authenticate
          |—> server_mpvio_initialize // 初始化mpvio对象,包括赋值 mpvio->ip / mpvio->host
          |—> auth_plugin_name="caching_sha2_password"
          |—> do_auth_once
            |—> caching_sha2_password_authenticate // auth->authenticate_user
              |—> server_mpvio_read_packet // vio->read_packet(vio, &pkt) // pkt=passwd
                |—> parse_client_handshake_packet
                  |—> char *user = get_string(&end, &bytes_remaining_in_packet, &user_len);
                  |—> passwd = get_length_encoded_string(&end, &bytes_remaining_in_packet, &passwd_len);
                  |—> mpvio->auth_info.user_name = my_strndup(key_memory_MPVIO_EXT_auth_info, user, user_len, MYF(MY_WME))
                  |—> find_mpvio_user(thd, mpvio) 
                    |—> list = cached_acl_users_for_name(mpvio->auth_info.user_name); // 根据 user 搜索 mysql.user.host, 由于用户不存在,搜索不到记录
                    |—> mpvio->acl_user = decoy_user(usr, hst, mpvio->mem_root, mpvio->rand, initialized); // 
                      |—> Auth_id key(user);
                      // 判断是否用户存在于 unknown_accounts
                      |—> unknown_accounts->find(key, value)
                      // 如存在:
                      |—> user->plugin = Cached_authentication_plugins::cached_plugins_names[value];
                      // 如不存在:
                      |—> const int DECIMAL_SHIFT = 1000;
                      |—> const int random_number = static_cast<int>(my_rnd(rand) * DECIMAL_SHIFT);
                      |—> uint plugin_num = (uint)(random_number % ((uint)PLUGIN_LAST));
                      |—> user->plugin = Cached_authentication_plugins::cached_plugins_names[plugin_num];
                      |—> unknown_accounts->insert(key, plugin_num)
                    |—> mpvio->acl_user_plugin = mpvio->acl_user->plugin; // 赋值 acl_user_plugin 属性为用户的plugin名
                    |—> mpvio->auth_info.multi_factor_auth_info[0].auth_string = mpvio->acl_user->credentials[PRIMARY_CRED].m_auth_string.str; // ""
                    |—> mpvio->auth_info.auth_string = mpvio->auth_info.multi_factor_auth_info[0].auth_string; // ""
                    |—> mpvio->auth_info.additional_auth_string_length = 0; // 0
                    |—> mpvio->auth_info.auth_string_length = mpvio->auth_info.multi_factor_auth_info[0].auth_string_length; // 0
                  |—> if (my_strcasecmp(system_charset_info, mpvio->acl_user_plugin.str,plugin_name(mpvio->plugin)->str) != 0)
                  |—> return packet_error;
                |—> if (pkt_len == packet_error) goto err;
                |—> return -1;
          |—> auth_plugin_name = mpvio.acl_user->plugin;
          |—> res = do_auth_once(thd, auth_plugin_name, &mpvio);
            |—> sha256_password_authenticate() //auth->authenticate_user(mpvio, &mpvio->auth_info);
              |—> LogPluginErr // Deprecate message for SHA-256 authentication plugin.
              // 打印: 2023-01-10T01:07:23.035479Z 13 [Warning] [MY-013360] [Server] Plugin sha256_password reported: ''sha256_password' is deprecated and will be removed in a future release. Please use caching_sha2_password instead'
              |—> server_mpvio_read_packet() // vio->read_packet(vio, &pkt)
              |—> if (info->auth_string_length == 0 && info->additional_auth_string_length == 0) // info -> auth_info
              |—>   return CR_ERROR;
            |—> return res; // 0
          |—> server_mpvio_update_thd(thd, &mpvio);
          |—> check_and_update_password_lock_state(mpvio, thd, res); // 直接返回
          ...
          |—> login_failed_error // 打印登录报错信息
          // 2023-01-10T02:02:44.659796Z 19 [Note] [MY-010926] [Server] Access denied for user 'user2'@'localhost' (using password: YES)
      |—> thd->send_statement_status();  // 客户端终止

つまり、存在しないユーザーがデータベースへのログインに使用されたときに関数 decoy_user() によって作成された acl_user オブジェクトです。このオブジェクトが作成されると、そのプラグイン属性が cached_plugins_enum からランダムに選択されます。したがって、PLUGIN_SHA256_PASSWORD プラグインを選択できます。ただし、関数 sha256_password_authenticate() の入り口で、認証 PLUGIN_SHA256_PASSWORD が破棄されることを示す警告レベルのプロンプトが生成されます。その後、decoy_user() で作成された acl_user オブジェクト auth_string_length の長さが 0 ではないため、後続の認証ロジックで CR_ERROR が直接返されます。つまり、認証は失敗します。

根本原因のまとめ

上記の認定分析によると、エラー ログで PLUGIN_SHA256_PASSWORD が破棄される根本的な原因は次のとおりです。現在のバージョンでは、存在しないユーザーを使用してデータベースにログインすると、mysql はユーザーのパスワード認証プラグインをランダムに選択します。現在のバージョンでは、1/3 の確率で PLUGIN_SHA256_PASSWORD プラグインが選択されます。このプラグインを選択すると、後続の認証ロジックによって警告ログの生成がトリガーされます。

4. 問題解決

上記の分析プロセスに基づくと、この問題の直接的な原因は、アプリケーションが存在しないデータベース ユーザーを構成することであり、根本的な原因は、データベース ログイン認証ロジックに特定の欠陥があることです。したがって、この問題を解決するには、次の解決策を参照してください。

1. 予備分析の解決策を参照して、アプリケーションの接続構成を正しいユーザー情報に変更します。

2. エラー ログ ファイルにアラーム情報が入力されないように、mysql データベースのパラメータを使用してアラームをフィルタリングできます。関連する構成は次のとおりです。

show variables like 'log_error_suppression_list';
set global log_error_suppression_list='MY-013360;
show variables like 'log_error_suppression_list';

このスキームを使用すると、存在し、SHA256_PASSWORD 認証プラグインを使用するアラートも発生することに注意してください。一時的な解決策として使用できます。

3. mysql コードを変更して、存在しないユーザーでデータベースにログインするときに SHA256_PASSWORD 認証プラグインを選択しないようにします。バグ #109635 は現在、このソリューションに対して提出されています。

添付:キー機能の位置

find_mpvio_user() (./sql/auth/sql_authentication.cc:2084)
parse_client_handshake_packet() (./sql/auth/sql_authentication.cc:2990)
server_mpvio_read_packet() (./sql/auth/sql_authentication.cc:3282)
caching_sha2_password_authenticate() (./sql/auth/sha2_password.cc:955)
do_auth_once() (./sql/auth/sql_authentication.cc:3327)
acl_authenticate() (./sql/auth/sql_authentication.cc:3799)
check_connection() (./sql/sql_connect.cc:651)
login_connection() (./sql/sql_connect.cc:716)
thd_prepare_connection() (./sql/sql_connect.cc:889)
handle_connection() (./sql/conn_handler/connection_handler_per_thread.cc:298)

おすすめ

転載: blog.csdn.net/ActionTech/article/details/130088608