仕組み
テナントは、それぞれ 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)。