MariaDBパスワード再利用チェックプラグイン徹底解説:セキュリティ強化の鍵

2025-05-26

このプラグインは、ユーザーが新しいパスワードを設定する際に、そのパスワードが過去に使用されたものと重複していないかをチェックします。これにより、セキュリティポリシーでパスワードの使い回しを禁止している場合などに、その要件を満たすことができます。

主な機能と特徴

  1. パスワード履歴の保存:

    • このプラグインは、ユーザーが設定した過去のパスワードを mysql.password_reuse_check_history テーブルに保存します。
    • パスワードは平文ではなく、ハッシュ化された形式で保存されます。
  2. 再利用の防止:

    • ユーザーがパスワードを変更しようとすると、新しいパスワードがこの履歴テーブルに保存されているパスワードと一致しないかどうかがチェックされます。
    • 一致した場合、パスワードの変更は拒否され、エラーメッセージが表示されます。
  3. 履歴の保持期間:

    • password_reuse_check_interval というシステム変数によって、過去のパスワードをどれくらいの期間(日数)保持するかを設定できます。
    • デフォルトは 0 で、これは無期限に履歴を保持することを意味します。特定の期間(例:90日)を設定することで、その期間が経過したパスワードは再利用可能になります。
  4. 他のパスワード検証プラグインとの連携:

    • 「Simple Password Check Plugin」や「CrackLib Password Check Plugin」など、他のパスワード検証プラグインと組み合わせて使用することができます。これにより、パスワードの複雑性や強度のチェックと、再利用のチェックを同時に行うことが可能です。
  5. インストールと設定:

    • MariaDB 10.7以降で利用可能です。
    • プラグインの共有ライブラリはMariaDBに同梱されていますが、デフォルトでは有効になっていません。
    • INSTALL SONAME 'password_reuse_check'; コマンドを使って動的にインストールしたり、my.cnfなどの設定ファイルに plugin_load_add = password_reuse_check を追加してサーバー起動時にロードするように設定できます。

なぜ重要か?

パスワードの使い回しは、セキュリティ上の大きなリスクとなります。一つのサービスでパスワードが漏洩した場合、同じパスワードを使い回している他のサービスも危険に晒される可能性があるためです。このプラグインを導入することで、データベースユーザーのパスワードセキュリティを向上させることができます。

-- プラグインをインストール
INSTALL SONAME 'password_reuse_check';

-- ユーザーを作成し、パスワードを設定
CREATE USER 'testuser'@'localhost' IDENTIFIED BY 'MyStrongPwd1!';

-- 同じパスワードを再利用しようとするとエラーになる
ALTER USER 'testuser'@'localhost' IDENTIFIED BY 'MyStrongPwd1!';
-- ERROR 1819 (HY000): Your password does not satisfy the current policy requirements

-- 別のパスワードに変更
ALTER USER 'testuser'@'localhost' IDENTIFIED BY 'AnotherPwd2@';

-- 以前のパスワード(MyStrongPwd1!)を再利用しようとすると、履歴に残っているためエラーになる
ALTER USER 'testuser'@'localhost' IDENTIFIED BY 'MyStrongPwd1!';
-- ERROR 1819 (HY000): Your password does not satisfy the current policy requirements

-- パスワード履歴の保持期間を短く設定する例 (例: 1日)
SET GLOBAL password_reuse_check_interval = 1;

-- しばらく待つか、テスト環境であれば時間を進めるなどして、履歴が削除されるのを待つ
-- (本番環境では注意が必要)

-- 期間が経過すれば、古いパスワードも再利用可能になる(設定によっては)
-- ただし、通常は再利用を完全に防止したいので、intervalは0のままにするか、十分に長い期間を設定します。

-- プラグインをアンインストール
UNINSTALL SONAME 'password_reuse_check';


一般的なエラーとトラブルシューティング

「Password Reuse Check Plugin」に関連するエラーのほとんどは、プラグインのロード、設定、またはパスワードポリシー違反によるものです。

エラー: プラグインがロードされていない

エラーメッセージの例

  • ERROR 1819 (HY000): Your password does not satisfy the current policy requirements (このエラーは他のポリシー違反でも出ますが、プラグインが有効になっていないとパスワード再利用チェックが働きません)
  • ERROR 1129 (HY000): Plugin 'password_reuse_check' is not loaded

原因

  • プラグインがMariaDBインスタンスにインストールされていない、またはロードされていない。

トラブルシューティング

  1. プラグインのインストール状態を確認

    SHOW PLUGINS;
    

    password_reuse_check がリストに ACTIVE と表示されているか確認します。表示されていない場合、または INACTIVE の場合は、ロードされていません。

  2. 動的にインストール

    INSTALL SONAME 'password_reuse_check';
    

    このコマンドを実行し、エラーがなければ成功です。MariaDBサーバーを再起動しても有効にするには、設定ファイルに追加が必要です。

  3. my.cnf (または mariadb.cnf など) で設定
    MariaDBサーバーが起動するたびにプラグインをロードするには、設定ファイルに以下の行を追加します。

    [mariadb]
    plugin_load_add = password_reuse_check
    

    または、より詳細に指定する場合:

    [mariadb]
    plugin_load_add = password_reuse_check:password_reuse_check
    

    設定ファイルを変更した後は、MariaDBサーバーを再起動してください。 sudo systemctl restart mariadb (Linuxの場合)

  4. プラグインファイルの存在確認
    MariaDBのプラグインディレクトリに password_reuse_check.so (Linux/Unix) または password_reuse_check.dll (Windows) ファイルが存在するか確認します。パスは通常、plugin_dir システム変数で示されます。

    SHOW VARIABLES LIKE 'plugin_dir';
    

    このファイルが存在しない場合、MariaDBのインストールが不完全である可能性があります。

エラー: パスワード再利用ポリシー違反

エラーメッセージの例

  • ERROR 1819 (HY000): Your password does not satisfy the current policy requirements

原因

  • ユーザーが過去に設定したパスワードと全く同じパスワードを再利用しようとした。

トラブルシューティング

  1. 別のパスワードを使用する
    最も簡単な解決策は、履歴に残っていない新しいパスワードを設定することです。

  2. パスワード履歴を確認する (権限がある場合)
    mysql.password_reuse_check_history テーブルにパスワード履歴が保存されています。管理者ユーザーであれば、このテーブルを確認してどのパスワードが履歴として認識されているかを見ることができます。

    SELECT * FROM mysql.password_reuse_check_history;
    

    ただし、パスワード自体はハッシュ化されているため、元のパスワードを直接確認することはできません。

  3. password_reuse_check_interval の調整
    このシステム変数は、パスワード履歴を保持する期間(日数)を決定します。

    • 0 (デフォルト): 無期限に履歴を保持し、一度使用したパスワードは永久に再利用できません。
    • N (日数): 指定された日数より古いパスワードは、再利用可能になります。

    もし特定の期間が過ぎれば再利用を許可したい場合は、password_reuse_check_interval を適切な日数に設定します。

    SET GLOBAL password_reuse_check_interval = 90; -- 90日間履歴を保持
    

    注意
    セキュリティ上の理由から、この値を小さくしすぎないことが推奨されます。多くの企業では、パスワードの再利用を長期間(または永久に)禁止しています。

  4. 他のパスワード検証プラグインとの競合/連携
    「Simple Password Check Plugin」や「CrackLib Password Check Plugin」など、他のパスワード検証プラグインも有効になっている場合、ERROR 1819 はそれらのポリシー違反である可能性もあります。

    • パスワードの長さが足りない (simple_password_check_minimum_length)
    • パスワードが辞書攻撃に弱い (cracklib_dictpath)
    • パスワードがユーザー名と類似している (simple_password_check_user_name_block) これらの変数の設定も確認し、それらのポリシーを満たしているか確認してください。

エラー: 権限不足

エラーメッセージの例

  • ERROR 1092 (HY000): Can't load plug-in 'password_reuse_check' with a non-root user
  • ERROR 1044 (42000): Access denied for user 'your_user'@'localhost' to database 'mysql'

原因

  • mysql スキーマのテーブル(password_reuse_check_history など)にアクセスする権限がない。
  • INSTALL SONAMEUNINSTALL SONAME コマンドを実行するのに必要な権限がない。

トラブルシューティング

  • プラグインのインストール・アンインストールや、関連するシステム変数の設定変更は、root ユーザーや適切な SUPER 権限を持つユーザーで行う必要があります。

mysql.password_reuse_check_history テーブルの問題

問題

  • このテーブルが破損している、または存在しない。

トラブルシューティング

  • mysqlcheck -u root -p --check --all-databasesREPAIR TABLE mysql.password_reuse_check_history; などのコマンドでテーブルの整合性を確認・修復を試みることもできますが、本番環境では必ずバックアップを取ってから行ってください。
  • もしテーブルが存在しない場合、プラグインが正しくインストールされていないか、データベースが破損している可能性があります。プラグインを再インストールしてみるか、MariaDBのログファイルで起動時のエラーを確認してください。
  • MariaDBサーバーが正常に起動しているか確認します。
  • 詳細なログ出力の有効化
    必要に応じて、MariaDBのログレベルを上げて、より詳細なデバッグ情報を取得することを検討してください。ただし、これはパフォーマンスに影響を与える可能性があるため、問題解決後に元に戻すようにしてください。



  1. プラグインのインストールと有効化
  2. パスワードの変更と再利用チェックの挙動確認
  3. パスワード履歴の確認
  4. password_reuse_check_interval の設定変更

これらの操作は通常、SQLクライアント(mysql コマンドラインツールなど)を介して行われますが、PHPやPythonなどのプログラミング言語からMariaDBに接続して同様のSQLコマンドを実行することもできます。ここでは、SQLコマンドを中心に説明し、それらをプログラミング言語からどう実行するかについても触れます。

プラグインのインストールと有効化

これはプラグインを使用するための最初のステップです。

-- プラグインのインストール
-- MariaDBサーバーにpassword_reuse_check.so (または.dll) ファイルが存在する必要があります。
INSTALL SONAME 'password_reuse_check';

-- プラグインが正常にロードされたか確認
SHOW PLUGINS;
-- 'password_reuse_check' が 'ACTIVE' と表示されていればOKです。

-- MariaDBサーバーが再起動してもプラグインが自動的にロードされるように、
-- my.cnf (またはmariadb.cnf) に設定を追加することを推奨します。
-- [mariadb]
-- plugin_load_add = password_reuse_check

プログラミング言語での実行例(PHPのPDOの場合)

<?php
$dsn = 'mysql:host=localhost;dbname=test_db;charset=utf8mb4';
$user = 'root';
$password = 'your_root_password'; // 実際のrootパスワードに置き換えてください

try {
    $pdo = new PDO($dsn, $user, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // プラグインのインストール
    $pdo->exec("INSTALL SONAME 'password_reuse_check'");
    echo "Password Reuse Check Plugin installed.\n";

    // プラグインの状態確認
    $stmt = $pdo->query("SHOW PLUGINS");
    $plugins = $stmt->fetchAll(PDO::FETCH_ASSOC);
    foreach ($plugins as $plugin) {
        if ($plugin['Plugin'] === 'password_reuse_check') {
            echo "Plugin Status: " . $plugin['Status'] . "\n";
            break;
        }
    }

} catch (PDOException $e) {
    echo "Error: " . $e->getMessage() . "\n";
}
?>

パスワードの変更と再利用チェックの挙動確認

ユーザーのパスワードを変更し、プラグインが再利用を検知するかどうかをテストします。

-- テストユーザーの作成 (パスワード: initial_Pwd1!)
CREATE USER 'test_user'@'localhost' IDENTIFIED BY 'initial_Pwd1!';
FLUSH PRIVILEGES;

-- 最初のパスワード変更 (OK)
-- これは初回なので、パスワード履歴に 'initial_Pwd1!' が記録されます。
ALTER USER 'test_user'@'localhost' IDENTIFIED BY 'new_Pwd2@';
SELECT 'Password changed to new_Pwd2@';

-- 同じパスワードを再度設定しようとする (エラーになるはず)
ALTER USER 'test_user'@'localhost' IDENTIFIED BY 'new_Pwd2@';
-- 期待されるエラー: ERROR 1819 (HY000): Your password does not satisfy the current policy requirements

-- 以前のパスワード ('initial_Pwd1!') を再利用しようとする (エラーになるはず)
ALTER USER 'test_user'@'localhost' IDENTIFIED BY 'initial_Pwd1!';
-- 期待されるエラー: ERROR 1819 (HY000): Your password does not satisfy the current policy requirements

-- 新しい、履歴にないパスワードを設定 (OK)
ALTER USER 'test_user'@'localhost' IDENTIFIED BY 'another_Pwd3#';
SELECT 'Password changed to another_Pwd3#';

-- テストユーザーの削除
DROP USER 'test_user'@'localhost';

プログラミング言語での実行例(Pythonのmysql.connectorの場合)

import mysql.connector

try:
    conn = mysql.connector.connect(
        host="localhost",
        user="root",
        password="your_root_password", # 実際のrootパスワードに置き換えてください
        database="mysql" # mysqlデータベースに接続 (履歴テーブルが存在するため)
    )
    cursor = conn.cursor()

    # テストユーザーの作成
    cursor.execute("CREATE USER 'test_user'@'localhost' IDENTIFIED BY 'initial_Pwd1!'")
    cursor.execute("FLUSH PRIVILEGES")
    print("User 'test_user' created.")

    # 最初のパスワード変更
    cursor.execute("ALTER USER 'test_user'@'localhost' IDENTIFIED BY 'new_Pwd2@'")
    print("Password changed to new_Pwd2@")
    conn.commit()

    # 同じパスワードを再度設定しようとする
    try:
        cursor.execute("ALTER USER 'test_user'@'localhost' IDENTIFIED BY 'new_Pwd2@'")
        conn.commit()
        print("Unexpected: Password changed to new_Pwd2@ again (should have failed).")
    except mysql.connector.Error as err:
        print(f"Expected Error (reuse of current password): {err}")

    # 以前のパスワード ('initial_Pwd1!') を再利用しようとする
    try:
        cursor.execute("ALTER USER 'test_user'@'localhost' IDENTIFIED BY 'initial_Pwd1!'")
        conn.commit()
        print("Unexpected: Password changed to initial_Pwd1! again (should have failed).")
    except mysql.connector.Error as err:
        print(f"Expected Error (reuse of old password): {err}")

    # 新しい、履歴にないパスワードを設定
    cursor.execute("ALTER USER 'test_user'@'localhost' IDENTIFIED BY 'another_Pwd3#'")
    print("Password changed to another_Pwd3#")
    conn.commit()

except mysql.connector.Error as err:
    print(f"Error: {err}")
finally:
    if 'conn' in locals() and conn.is_connected():
        # クリーンアップ: テストユーザーの削除
        try:
            cursor.execute("DROP USER 'test_user'@'localhost'")
            print("User 'test_user' dropped.")
        except mysql.connector.Error as err:
            print(f"Error dropping user: {err}")
        cursor.close()
        conn.close()

パスワード履歴の確認

mysql.password_reuse_check_history テーブルには、ハッシュ化されたパスワード履歴が保存されます。

-- パスワード履歴テーブルの確認
SELECT * FROM mysql.password_reuse_check_history;

このテーブルには、ユーザーと、パスワードが設定された日時、そしてパスワードのハッシュ値が記録されます。セキュリティ上の理由から、平文のパスワードは保存されません。

パスワード履歴を保持する期間を制御するシステム変数です。

-- 現在のinterval設定を確認 (デフォルトは0で無期限)
SHOW GLOBAL VARIABLES LIKE 'password_reuse_check_interval';

-- intervalを90日に設定 (90日より古いパスワードは再利用可能になる)
SET GLOBAL password_reuse_check_interval = 90;

-- intervalを0に戻す (無期限に履歴を保持)
SET GLOBAL password_reuse_check_interval = 0;
  • SET GLOBAL で設定した内容は、MariaDBサーバーを再起動すると元に戻る可能性があります。永続的に設定を変更するには、my.cnf ファイルに記述する必要があります。
  • password_reuse_check_interval を変更すると、MariaDBサーバー全体に影響を与えます。慎重に検討し、セキュリティポリシーに合致する値を設定してください。


代替方法の考え方

代替方法は、主に以下の2つのアプローチに分類されます。

  1. アプリケーション層での実装
    データベースにパスワードを保存する前に、アプリケーションコード内でパスワードの履歴チェックを行う。
  2. オペレーティングシステム (OS) や認証システムとの連携
    MariaDB自体の認証ではなく、外部の認証システム(LDAP、Active Directoryなど)を利用し、そのシステム側でパスワードポリシーを管理する。

アプリケーション層での実装

これは最も一般的で柔軟な代替手段です。パスワードの履歴をデータベースのどこかに保存し、ユーザーがパスワードを変更しようとする際に、アプリケーションがその履歴と照合します。

必要なテーブルとデータ構造

ユーザーテーブルとは別に、パスワード履歴を保存するためのテーブルが必要です。

CREATE TABLE user_password_history (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL, -- ユーザーテーブルのIDへの外部キー
    password_hash VARCHAR(255) NOT NULL, -- ハッシュ化されたパスワード
    change_date DATETIME DEFAULT CURRENT_TIMESTAMP,
    KEY idx_user_id (user_id)
);

実装ロジック

ユーザーがパスワードを変更する際の一般的なフローは以下のようになります。

  1. 新しいパスワードの受け取り
    ユーザーから新しいパスワードを受け取ります。
  2. ハッシュ化
    受け取ったパスワードを強力なハッシュアルゴリズム(例: Argon2, bcrypt, PBKDF2)でハッシュ化します。ソルトを必ず使用し、ストレッチング(複数回の反復)を行います。
  3. 履歴チェック
    • user_password_history テーブルから、該当ユーザーの過去のパスワードハッシュを取得します。
    • 取得した各ハッシュと、新しいパスワードをハッシュ化したものが一致するか比較します。
    • 一致するものが一つでもあれば、パスワード再利用のエラーをユーザーに返します。
  4. パスワードの更新
    • 再利用でなければ、users テーブルのパスワードを新しいハッシュで更新します。
    • user_password_history テーブルに新しいパスワードのハッシュを挿入します。
    • 履歴の件数を制限する場合(例: 直近5件のみ保持)、古い履歴を削除します。
  5. 成功/失敗の通知
    ユーザーに結果を通知します。

例 (PythonとFlask/SQLAlchemy風の擬似コード)

import hashlib
import os
from datetime import datetime, timedelta
# from flask_sqlalchemy import SQLAlchemy (実際のフレームワーク依存)

# db = SQLAlchemy() # DB接続オブジェクトを仮定

# パスワードハッシュ関数 (例: bcryptを推奨だが、ここでは単純なSHA256の擬似コード)
def hash_password(password, salt=None):
    if salt is None:
        salt = os.urandom(16).hex() # ランダムなソルトを生成
    hashed_password = hashlib.sha256((password + salt).encode('utf-8')).hexdigest()
    return f"{salt}${hashed_password}" # ソルトとハッシュを結合して保存

def verify_password(stored_hash, provided_password):
    if '$' not in stored_hash:
        # 古いハッシュ形式の場合の処理も考慮する
        return False
    salt, stored_hashed_password = stored_hash.split('$', 1)
    return hash_password(provided_password, salt).split('$', 1)[1] == stored_hashed_password

# パスワード変更処理
def change_user_password(user_id, new_password, max_history_count=5, history_retention_days=365):
    # 1. パスワードポリシーチェック (長さ、複雑性など) - 省略

    # 2. 新しいパスワードをハッシュ化
    new_hashed_password = hash_password(new_password)

    # 3. 既存のパスワード履歴を取得
    # 例: SELECT password_hash, change_date FROM user_password_history WHERE user_id = :user_id ORDER BY change_date DESC
    # results = db.session.execute("SELECT ...").fetchall() # 実際のDBクエリ

    # 擬似データ
    history_records = [
        {"password_hash": hash_password("old_pass1"), "change_date": datetime.now() - timedelta(days=30)},
        {"password_hash": hash_password("old_pass2"), "change_date": datetime.now() - timedelta(days=60)},
    ]
    # DBから取得した実際の履歴データに置き換える

    for record in history_records:
        if verify_password(record['password_hash'], new_password):
            return "Error: Password has been used recently. Please choose a different one."

    # 4. パスワード再利用チェックを通過した場合、ユーザーテーブルのパスワードを更新
    # 例: UPDATE users SET password_hash = :new_hashed_password WHERE id = :user_id
    # db.session.execute("UPDATE ...")

    # 5. パスワード履歴テーブルに新しいハッシュを追加
    # 例: INSERT INTO user_password_history (user_id, password_hash) VALUES (:user_id, :new_hashed_password)
    # db.session.execute("INSERT ...")

    # 6. 古い履歴を削除 (オプション: 履歴件数または期間で制限)
    # 例: DELETE FROM user_password_history WHERE user_id = :user_id AND change_date < :threshold_date
    #     OR (SELECT COUNT(*) FROM user_password_history WHERE user_id = :user_id) > :max_history_count
    # db.session.execute("DELETE ...")


    # db.session.commit() # トランザクションコミット
    return "Password changed successfully."

# 使用例
# print(change_user_password(1, "my_new_secure_password"))
# print(change_user_password(1, "old_pass1")) # これがエラーになることを期待

アプリケーション層実装のメリット・デメリット

メリット

  • ユーザーへの詳細なフィードバック
    エラーメッセージをユーザーフレンドリーにカスタマイズできる。
  • カスタムロギング
    パスワード変更の試行や再利用エラーに関する詳細なログをアプリケーションレベルで記録できる。
  • データベース非依存
    MariaDB以外のデータベース(PostgreSQL, SQL Serverなど)を使用する場合でも同じロジックを適用できる。
  • 高度な柔軟性
    パスワード履歴の保持期間、件数、特定のユーザーグループに対するポリシーなど、細かな制御が可能。

デメリット

  • パフォーマンス
    パスワード変更のたびにDBクエリが追加で発生するため、MariaDBプラグインに比べてわずかにオーバーヘッドがある可能性がある(ただし、通常は無視できるレベル)。
  • セキュリティ責任の増大
    パスワードのハッシュ化、ソルトの管理、履歴の安全な保存など、アプリケーション側でセキュリティベストプラクティスを遵守する必要がある。
  • 開発コスト
    ゼロからロジックを実装する必要があり、開発とテストに手間がかかる。

MariaDB自体を単一の認証ポイントとするのではなく、より上位の認証システムを利用する方法です。

LDAP/Active Directory 認証

MariaDBは、LDAP (Lightweight Directory Access Protocol) や Microsoft Active Directory を利用してユーザー認証を行うことができます。

  • デメリット
    LDAP/ADサーバーのセットアップと管理が必要。小規模なアプリケーションには大袈裟な場合がある。
  • メリット
    集中管理、既存の企業インフラとの統合。
  • パスワードポリシーの管理
    この場合、パスワードの複雑性、有効期限、再利用ポリシーなどは、LDAPサーバー側(Active Directoryのグループポリシーなど)で一元的に管理されます。
  • MariaDBのLDAP認証プラグイン
    MariaDBにはauthentication_ldapプラグインがあり、これを使ってMariaDBユーザーを外部LDAPサーバーにマッピングできます。

PAM (Pluggable Authentication Modules) 認証

LinuxなどのUNIX系OSでは、PAM(Pluggable Authentication Modules)という認証フレームワークを利用できます。MariaDBはauthentication_pamプラグインを通じてPAMと連携し、OSのユーザー認証機能を利用できます。

  • デメリット
    OSの知識が必要。Windowsでは異なるアプローチが必要。
  • メリット
    OSレベルで一貫した認証ポリシーを適用できる。
  • OSレベルでのポリシー管理
    OSの /etc/security/pwquality.conf/etc/pam.d/system-auth などの設定ファイルで、パスワードの品質や履歴ポリシーを定義します。
  • PAMの設定
    PAMモジュールには、パスワード履歴をチェックする機能(例: pam_pwhistory.so)を提供するものがあります。

「Password Reuse Check Plugin」が最も直接的で効率的な方法ですが、代替手段はそれぞれ特定の要件や環境に適しています。

  • 大規模な企業環境で一元的な認証管理が必要な場合
    LDAP/Active DirectoryやPAMとの連携が有効です。
  • アプリケーションでより細かい制御やカスタムロギングが必要な場合
    アプリケーション層での実装が最適です。
  • 既存のMariaDB環境で迅速に導入したい場合
    プラグインが最も簡単です。