tenant-pass

Redis / Valkey

テナントごとの ACL ユーザーを、複数のパスワードとスイーパーで管理する仕組みを説明します。ACL がサーバーごとに別々であること、ACL DRYRUN による安全性チェック、キーのプレフィックスの決まりを扱います。

このページの見出し
  1. 仕組み
  2. 対応するバージョン
  3. ACL はサーバーごとに別々
  4. 準備
  5. 安全性チェック
  6. ノードの設定
  7. 同時に書き込んだ場合
  8. クライアントについての注意

仕組み

テナント 1 つに、Redis の ACL ユーザーが 1 人対応します。ACL ユーザーはパスワードを複数持てるので、tenant-pass は今のウィンドウのパスワードを持たせ、更新の期間にはさらに次のウィンドウのパスワードを足します。

ACL SETUSER t_acme resetpass #<sha256 of password(n)> [#<sha256 of password(n+1)>]

パスワードは SHA-256 のハッシュ(#…)で設定するので、平文のパスワードは Redis にもスローログにも MONITOR にも出てきません。resetpass はほかのパスワードと nopass のフラグをすべて消しますが、ユーザーのそれ以外の設定(コマンド、キー、チャネル、有効か無効か)には一切触れません。

更新の期間には、新旧どちらのパスワードでもログインできます。ウィンドウが終わったパスワードと、tenant-pass が導出したものではないパスワードは、rotation.sweep_interval ごとに動くスイーパーが消します。1 ウィンドウの間まったく要求されなかったユーザーは、やがてパスワードが 1 つも残らず、ログインできなくなります。

対応するバージョン

Redis 7.0 以降と Valkey 7.2 以降に対応しています。安全性チェックに ACL DRYRUN を使うので、これより古いものでは動きません。動作を確かめているのは Redis 8 です。

ACL はサーバーごとに別々

Redis は ACL をレプリカに複製しません。プライマリとそのレプリカ、Sentinel が管理する各インスタンス、Redis Cluster の各ノードは、どれも自分だけのユーザーを持っています。ですから、それらをすべてノードの servers に並べてください。tenant-pass は全サーバーから状態を読んで、何を書き込むかを 1 回だけ決め、同じパスワードの組を各サーバーに書き込みます。

古い aclfile で再起動したなどの理由で、あるサーバーからユーザーのパスワードが消えても、次のリクエストかスイープで元に戻ります。ただし、ユーザーそのものが消えたサーバーがあると、tenant-pass はそのユーザーへの発行を拒否します。tenant-pass はユーザーを作らないので、そのサーバーでユーザーを作り直してください。

サーバーが aclfile からユーザーを読み込む設定なら、acl_save: true にしておきます。変更のたびに ACL SAVE を実行するようになります。これがないと、再起動したサーバーは、tenant-pass が直すまで古いパスワードのまま動きます。

準備

すべてのサーバーで(または aclfile に)、tenant-pass が使う ACL のコマンドだけを許した管理者ユーザーを作ります。

ACL SETUSER tenant_pass_admin on ><random> -@all +acl|getuser +acl|setuser +acl|users +acl|dryrun +acl|save +time +ping +auth +hello

+acl|setuser は、権限の付与も含めてどのユーザーでも変更できる強い権限です。Redis にはこれより狭い権限がないので、やむをえません。tenant_pass_admin の認証情報は管理者のパスワードと同じように守り、Redis にネットワークから接続できる範囲も絞ってください。

テナントのユーザーは、利用者のプロビジョニングの処理で作ります。付けるのは、テナントに必要な権限と、管理対象の目印 &tenant-pass:managed だけです。

ACL SETUSER t_acme on resetchannels &tenant-pass:managed &t:acme:* -@all +@read +@write ~t:acme:*

目印の正体はチャネルのルールです。そのユーザーが pub/sub のコマンドを使えなければ何の権限にもならず、使えたとしても、その 1 つのチャネル名に限られます。目印のないユーザーには、スイーパーも含めて tenant-pass は決して手を出しません。別の目印にしたいときは managed_marker で指定します。

安全性チェック

発行の前に、tenant-pass はすべてのサーバーで次の条件を確かめます。1 つでも外れていれば拒否し、監査ログに account_refused を残します。

  • ユーザー名が account_pattern と [A-Za-z0-9_.:-]{1,128} に一致し、default ではない。
  • ユーザーが存在し(tenant-pass はユーザーを作りません)、管理対象の目印を持つ。
  • ACL DRYRUN で試して、ユーザーが次のどのコマンドも実行できないと確かめられる。
    • 管理用のコマンド: ACL …、CONFIG …、MODULE …、DEBUG、SHUTDOWN、FLUSHALL、FLUSHDB、SWAPDB、KEYS、CLIENT KILL/LIST/PAUSE、SCRIPT FLUSH、FUNCTION LOAD/DELETE/FLUSH、REPLICAOF、SLAVEOF、FAILOVER、CLUSTER …、SAVE、BGSAVE、BGREWRITEAOF、MONITOR、SYNC、PSYNC、SLOWLOG RESET、LATENCY RESET、MEMORY PURGE
    • キーのパターンに関係なく、すべてのキー名やチャネル名が見えてしまうコマンド: SCAN、RANDOMKEY、PUBSUB CHANNELS/SHARDCHANNELS、DBSIZE、OBJECT
  • ACL DRYRUN で試して、ユーザーがランダムなキーへの GET / SET も、ランダムなチャネルへの PUBLISH / SUBSCRIBE もできないと確かめられる。つまり、キーとチャネルのパターンが自分のプレフィックスに限られている。~[a-s]* のように一部の名前にだけ一致するパターンも見つけられるように、いろいろな文字で始まる名前で試します。
  • 基本の権限とすべてのセレクターで、キーとチャネルのパターンが、どれも 2 文字以上の固定のプレフィックス(*、?、[、\ を含まない文字列)で始まっている。たとえば ~t:acme:* と &t:acme:* は受け付けますが、~*、~*:secret、~[a-s]*、&*、allkeys、allchannels は拒否します。あるテナントのパターンがほかのテナントのキーに一致しないようにできるのは、固定のプレフィックスだけです。また、どのテナントのプレフィックスも、ほかのテナントのプレフィックスの先頭部分にならないようにしてください(たとえば t:a ではなく、t:acme: と t:acme2: にします)。
  • ルールの文字列を調べて、allcommands、+@all、+@admin、+@dangerous、上に挙げたコマンドを明示的に許すルールが見つからない。
  • サーバーの TIME が、レプリカの時計と rotation.max_clock_skew の範囲で一致している。

ルールの文字列を読むだけでなく ACL DRYRUN で試すのは、@slow などのカテゴリに ACL や CONFIG が紛れ込んでいるからです。ACL DRYRUN なら、カテゴリやセレクターも含めて、ユーザーが実際に持っている権限で判定できます。テナントには、コマンドを 1 つずつ許すか、範囲の狭いカテゴリ(@read、@write、@string、@hash など)を与えてください。

スイーパーの確認は軽め

スイーパーがするのは、パスワードを消すことだけです。スイープはサーバーごとに別々に行うので、あるサーバーにユーザーがなかったり接続できなかったりしても、ほかのサーバーのスイープは止まりません。目印のないユーザーや、ルールの文字列のチェックに通らないユーザーには手を出しません。

ただし、負荷の大きい DRYRUN による確認はスイープでは省いています。そのため、目印を持っていて、DRYRUN でしか見つからない管理者の権限(+@slow を通じて持つものなど)を持つユーザーに対しては、発行は拒否する一方で、スイーパーは期限切れのパスワードを消します。テナントのアカウントではないユーザーには、目印を付けないでください。

接続の枯渇

テナントは maxclients を共有しています。管理者のユーザーのために余裕を残しておくか、tenant-pass の管理者の接続を、テナントが使い切れないサーバーに向けてください。

ノードの設定

{
  "name": "cache1",
  "type": "redis",
  "servers": [
    {"address": "cache1-a.internal:6380", "user": "tenant_pass_admin", "password_file": "/run/secrets/cache1-admin-password", "tls": {"ca_file": "/etc/tenant-pass/db-ca.pem"}},
    {"address": "cache1-b.internal:6380", "user": "tenant_pass_admin", "password_file": "/run/secrets/cache1-admin-password", "tls": {"ca_file": "/etc/tenant-pass/db-ca.pem"}}
  ],
  "account_pattern": "^t_[a-z0-9_]{1,40}$",
  "acl_save": false,
  "connect": {"host": "cache1.internal", "port": 6380}
}

Redis では TLS を有効にし(tls-port、tls-cert-file など)、暗号化しないポートは無効にしてください。

同時に書き込んだ場合

書き込みは何度やっても結果が同じです(同じウィンドウからは、いつも同じハッシュができます)。ウィンドウを過去に戻すこともありません。2 つのレプリカが同じユーザーに同時に書き込んでも、最悪の場合で前のウィンドウのパスワードが一時的に戻るだけで、それもスイーパーがまた消します。tenant-pass が導出していないパスワードが設定されることはありません。

クライアントについての注意

クライアントは AUTH <username> <password>(または HELLO 3 AUTH …)で認証します。+ping を持たないユーザーが AUTH の後に PING を送ると NOPERM が返りますが、これは認証の失敗ではありません。接続ライブラリが接続の確認に PING を使う場合は、取り違えないよう注意してください。

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