tenant-pass

MySQL

テナントごとの MySQL のユーザーを、デュアルパスワードとスイーパーで管理する仕組みを説明します。管理者アカウントの権限、CREATE USER 権限の注意点、安全性チェック、レプリケーションを扱います。

このページの見出し
  1. 仕組み
  2. 対応するバージョン
  3. 準備
  4. 安全性チェック
  5. ノードの設定
  6. レプリケーション
  7. 自分が設定した状態の見分け方

仕組み

テナントは、それぞれ caching_sha2_password を使う MySQL のアカウント('t_acme'@'%' など)に対応します。MySQL にはパスワードごとの有効期限がない代わりに、8.0.14 以降では 1 つのアカウントにパスワードを 2 つ持たせられます(デュアルパスワード)。tenant-pass はこの 2 つを次のように使い分けます。

期間1 つ目のパスワード(プライマリ)2 つ目のパスワード(セカンダリ)
通常ウィンドウ n—
更新の期間(refresh_at から)ウィンドウ n+1ウィンドウ n
End(n) の後(スイーパーが削除)ウィンドウ n+1—

実際に実行するのは次の 3 種類の文で、いずれも GET_LOCK でロックを取ったうえで実行します。

ALTER USER 't_acme'@'%' IDENTIFIED WITH caching_sha2_password AS 0x2441243030352…;              -- プライマリを設定する
ALTER USER 't_acme'@'%' IDENTIFIED WITH caching_sha2_password AS 0x…  RETAIN CURRENT PASSWORD;  -- 入れ替える(今のパスワードをセカンダリに残す)
ALTER USER 't_acme'@'%' DISCARD OLD PASSWORD;                                                   -- セカンダリを削除する

caching_sha2_password のハッシュ(20 バイトのソルト、5000 ラウンドの SHA-crypt)は tenant-pass が自分で計算し、16 進のリテラルとして送ります。平文のパスワードは、サーバーにもバイナリログにも一般ログにも performance_schema にも残りません。

更新の期間には新旧どちらのパスワードも通るので、ローテーションのせいでクライアントの認証が失敗することはありません。ウィンドウが終わったパスワードは、rotation.sweep_interval ごとに動くスイーパーが消します。1 ウィンドウの間まったく要求されなかったアカウントには、やがて誰も知らないランダムなパスワードだけが残ります。

tenant-pass が導出したものではないパスワード(プロビジョニングで設定した最初のパスワードなど)は、そのアカウントを初めて扱うときに削除します。

対応するバージョン

MySQL 8.0.27 以降、8.4 LTS、9.x に対応しています。動作を確かめているのは 8.4 です。MariaDB は認証プラグインが違い、デュアルパスワードもないので使えません。

準備

まず、管理者のユーザーで次の SQL を実行し、目印のロールと tenant-pass 用のアカウントを作ります。

-- 目印のロール: tenant-pass はこのロールを与えられたアカウントにしか手を出さない
CREATE ROLE tenant_pass_managed;

-- tenant-pass がログインするアカウント
CREATE USER tenant_pass_admin IDENTIFIED BY '<random>' REQUIRE SSL;
GRANT CREATE USER ON *.* TO tenant_pass_admin;            -- ほかのアカウントに ALTER USER を実行するのに必要
GRANT SELECT ON mysql.user TO tenant_pass_admin;          -- authentication_string と権限を読む
GRANT SELECT ON mysql.global_grants TO tenant_pass_admin; -- 動的権限を確かめる
GRANT SELECT ON mysql.role_edges TO tenant_pass_admin;    -- ロールを確かめる
GRANT SELECT ON mysql.db TO tenant_pass_admin;            -- システムスキーマへの権限を確かめる
GRANT SELECT ON mysql.tables_priv TO tenant_pass_admin;
GRANT SELECT ON mysql.columns_priv TO tenant_pass_admin;
GRANT SELECT ON mysql.procs_priv TO tenant_pass_admin;
GRANT SELECT ON mysql.proxies_priv TO tenant_pass_admin;  -- PROXY の権限を確かめる

目印のロールは目印として使うだけなので、権限を一切与えないでください。テナントの権限は、ロール経由ではなくアカウントに直接、テナント自身のデータベースに対して与えます。

次に、テナントごとに次の SQL を実行します。

CREATE USER t_acme IDENTIFIED BY '<any initial password>';
GRANT tenant_pass_managed TO t_acme;
GRANT SELECT, INSERT, UPDATE, DELETE ON acme.* TO t_acme;   -- テナントに必要な権限

CREATE USER 権限の注意点

MySQL には、ほかのアカウントのパスワードを変えることだけを許す狭い権限がありません。そのため、CREATE USER を持つ tenant_pass_admin は、SYSTEM_USER を持たないアカウントなら何でも変更できてしまいます。対策として、次のようにしてください。

  • テナント以外の管理者のアカウントとアプリケーションのアカウントには、すべて SYSTEM_USER を持たせるか、別の方法で tenant_pass_admin から変更できないようにする(MySQL 8.0.16 以降の root には、既定で与えられています)。
  • tenant_pass_admin には SYSTEM_USER を与えない。

後で説明する tenant-pass 自身の安全性チェックは、あくまで 2 番目の守りです。それだけに頼らないでください。

もう 1 つ、mysql.user にはすべてのアカウントのパスワードのハッシュが入っています。ハッシュではログインできませんが、人が決めた弱いパスワードなら、手元で総当たりして当てられるおそれがあります。tenant_pass_admin の認証情報は、それに見合うだけ厳重に扱ってください。

安全性チェック

tenant-pass はロックを取ったまま次の条件を確かめ、1 つでも外れていれば変更を拒否します(監査ログには account_refused が残ります)。

  • ユーザー名が account_pattern と [A-Za-z0-9_.%-]{1,64} に一致する。
  • 'user'@'account_host' が存在する。
  • managed_role@managed_role_host が与えられていて、それ以外のロールは与えられていない。
  • アカウントにも管理対象のロールにも、グローバル権限がまったくない(mysql.user の *_priv の列がすべて N である。SELECT ON *.* だけでも、すべてのテナントのデータを読めてしまうためです)。
  • どちらにも動的権限(SYSTEM_USER を含む mysql.global_grants の権限)がない。
  • どちらにも、mysql、sys、performance_schema、information_schema のスキーマへの権限がなく、PROXY 権限もない。ワイルドカードを使ったデータベースの権限(GRANT … ON `%`.* や `m_sql`.* など)による権限も含みます。mysql.db のデータベース名はパターンなので、tenant-pass はシステムスキーマの名前がそのパターンに一致するかを確かめます。
  • 管理対象のロールに、どんな権限もない(目印として使うだけのためです)。
  • 管理対象のロールに、ほかのロールが与えられていない。
  • サーバーの mandatory_roles が空である(必須のロールはすべてのアカウントに適用され、アカウントごとに確かめられないためです)。
  • tenant-pass 自身のアカウントではない。
  • レプリカの時計が、サーバーの UNIX_TIMESTAMP() と rotation.max_clock_skew の範囲で一致している。

テナントのアカウントには、そのテナント自身のデータベースやテーブルへの権限(GRANT … ON acme.* TO t_acme)だけを与える想定です。

接続の枯渇にも気を付けてください。1 つのテナントが max_connections を使い切ると、tenant-pass も接続できず、ローテーションが止まります。ALTER USER t_acme WITH MAX_USER_CONNECTIONS 20 のように、テナントごとに接続数の上限を付けておきます。

ノードの設定

{
  "name": "my1",
  "type": "mysql",
  "endpoint": {
    "address": "my1.internal:3306",
    "user": "tenant_pass_admin",
    "password_file": "/run/secrets/my1-admin-password",
    "tls": {"ca_file": "/etc/tenant-pass/db-ca.pem"}
  },
  "managed_role": "tenant_pass_managed",
  "account_host": "%",
  "account_pattern": "^t_[a-z0-9_]{1,30}$",
  "connect": {"host": "my1.internal", "port": 3306, "database": "app"}
}

管理者の接続には、事実上 TLS が欠かせません。暗号化しない接続で caching_sha2_password の完全な認証をするには RSA による鍵の交換が要りますが、tenant-pass はそれを有効にしていないからです。なお、MySQL が自動で作る証明書には、利用者のホスト名に合う SAN(証明書に書かれた名前)が入っていません。正しいサーバー証明書を発行してください。

レプリケーション

ALTER USER はレプリカにも複製されます。ノードとして設定するのはソース(複製元)だけにしてください。レプリカに接続するクライアントの場合、新しいパスワードが使えるのは変更がレプリカに届いてからなので、レプリケーションの遅れの分だけ待つことになります。

自分が設定した状態の見分け方

tenant-pass は状態を別に保存していません。authentication_string(プライマリ)と User_attributes.additional_password(セカンダリ)を読み、そこに入っているソルトでハッシュを計算し直して、どのウィンドウのパスワードかを判断します。自分で計算し直せない付加情報は、一切信用しません。

この文書は、リポジトリにある docs/ja/stores/mysql.md をもとにしています(英語版は docs/en/stores/mysql.md)。