Amazon ElastiCache for Valkey 9.1のdb=によるデータベース単位ACLを検証してみた

Amazon ElastiCache for Valkey 9.1のdb=によるデータベース単位ACLを検証してみた

Amazon ElastiCacheがValkey 9.1に対応し、ACLでアクセスできる論理データベースを限定できるようになりました。db=を書くとユーザーのMinimumEngineVersionが9.1に上がる点や、どのコマンドが拒否されるか、テナント分離の手段としてどこまで使えるかを確認しています。
2026.08.20

こんにちは。サービス開発部の武田です。

Valkey 9.1で、ACLにデータベース単位の権限が入りました。アクセス文字列にdb=0,1のように書くと、そのユーザーが触れる論理データベースを限定できます。Amazon ElastiCacheは2026年6月23日にValkey 9.1へ対応しました。

https://aws.amazon.com/about-aws/whats-new/2026/06/amazon-elasticache-valkey-9-1/

ただしElastiCacheはACL SETUSERを受け付けません。設定の入り口からしてセルフマネージドとは違うので、テナントごとに論理DBを割り当てる分離手段としてどこまで使えるのかを確かめました。

検証環境

東京リージョンに、Valkey 9.0と9.1のレプリケーショングループを1つずつ作りました。9.0側はバージョン互換性の確認だけに使っています。

項目 設定
ノードタイプ cache.m7g.large × 1ノード
クラスターモード 無効
レプリカ なし
転送時暗号化 有効(RBACに必須)
パラメーターグループ 9.0と9.1で共用のカスタムグループ

9.0と9.1はどちらもvalkey9パラメーターグループファミリのため、同じパラメーターグループを両方に割り当てられます。

クライアントは同一AZのEC2で、valkey-cli 9.1.0をソースからTLS付きでビルドしました。

$ valkey-cli --tls -h master.vk91blog-91... --user nodbtestuser --pass ... INFO server
valkey_version:9.1.0
redis_version:7.2.4
mem_allocator:jemalloc-5.3.0

論理DBがいくつあるかはパラメーターグループのdatabasesで決まります。ElastiCacheではCONFIG GETが使えないため、値はパラメーターグループ側で確認します。

$ valkey-cli ... CONFIG GET databases
ERR unknown command 'CONFIG', with args beginning with: 'GET' 'databases'

今回はデフォルト値の16でした。

クラスターモード有効の場合はcluster-databasesという別のパラメーターでDB数が決まります。describe-engine-default-parametersで確認したところ、デフォルト値1・許容値1〜10000でした。

$ aws elasticache describe-engine-default-parameters --cache-parameter-group-family valkey9
cluster-databases = 1 | AllowedValues: 1-10000 | IsModifiable: True

デフォルトの1のままではDBが1つしかないため、db=を書いても分ける対象がありません。クラスターモード有効でdb=を使うなら、まずcluster-databasesへ明示的に2以上を指定することになります。

ElastiCacheでdb=付きユーザーを作る

db=を設定する手順そのものが、セルフマネージドのValkeyとは違います。

ACL SETUSERは使えない

ValkeyのドキュメントにはACL SETUSER alice on +@all ~* db=0,1 nopassという例が載っています。ただしElastiCacheはACLの書き込み系コマンドを受け付けません。使えるのはACL LISTACL WHOAMIなどの参照系だけです。

代わりにcreate-user--access-stringdb=0,1を書きます。--access-stringACL SETUSERに渡すACLルールをそのまま並べた文字列で、パスワードだけは含められません。パスワードは--passwordsで別に渡します。

aws elasticache create-user \
  --user-id valkey91-dbtest \
  --user-name dbtestuser \
  --engine VALKEY \
  --passwords "Str0ngPassw0rdForTest1234" \
  --access-string "on ~* +@all db=0,1"

VALKEYエンジンのユーザーは、パスワードかIAM認証のどちらかが必須です。

db=を書くとMinimumEngineVersionが上がる

作成したユーザーのレスポンスを見ると、指定していない項目が変わっています。

{
    "UserId": "valkey91-dbtest",
    "AccessString": "on ~* resetchannels +@all db=0,1",
    "MinimumEngineVersion": "9.1"
}

db=なしで作るとMinimumEngineVersionは7.2でした。アクセス文字列の構文からエンジンバージョン要件を逆算しているようです。

いくつか作って比べた結果です。

アクセス文字列 MinimumEngineVersion
on ~* +@all 7.2
on ~* +@all db=0,1 9.1
on ~* +@all db=1 9.1
on ~tenant:* +@all db=0,1 9.1
on ~* &* +@all db=1 9.1

キーパターンやチャネル指定と組み合わせても結果は変わりません。db=が入っていれば9.1です。ユーザーグループはメンバーの最大値を引き継ぐので、db=付きユーザーを1人でも含むグループは9.1になります。

この値は、ユーザーグループをどのクラスターへ関連付けられるかを決めます。試しにdb=付きユーザーを含むグループを指定して、9.0のレプリケーショングループをcreate-replication-groupで作ってみましたが、予想どおり拒否されました。

An error occurred (InvalidParameterValue) when calling the CreateReplicationGroup operation:
User group(ug-vk91) has user(s) with access string version 9.1.0 that are not compatible
with this replication group. Please upgrade the replication group.

ACL LISTでの見え方

9.1のクラスターでACL LISTを実行すると、db=の有無が表示に出ます。

user db1onlyuser  on ... ~*        resetchannels db=1   +@all
user dbkeyuser    on ... ~tenant:* resetchannels db=0,1 +@all
user dbtestuser   on ... ~*        resetchannels db=0,1 +@all
user default      off ...          &*            alldbs -@all
user nodbtestuser on ... ~*        &*            alldbs +@all

制限のないユーザーはalldbsと表示されます。db=と対になる表記ですね。

チャネルの列も分かれています。db=付きの3人はチャネルを指定していないのにresetchannelsdb=なしのnodbtestuser&*です。db=を付けたときだけresetchannelsが入る形でした。

db=はどの操作に効くのか

以降のコマンドは、9.1のクラスターへ関連付けたユーザーで実行しています。

SELECTとDBを跨ぐ操作

まずはdb=0,1のユーザーでSELECTから試します。

SELECT 0  -> OK
SELECT 1  -> OK
SELECT 2  -> NOPERM No permissions to access database
SELECT 15 -> NOPERM No permissions to access database

許可していないDBは拒否されます。ここは想定どおりです。

DBを跨ぐ操作はSELECTだけではありません。同じユーザーでひととおり試しました。

操作 結果
MOVE src:a 1(許可DBへ) 1
MOVE src:b 2(禁止DBへ) NOPERM
COPY src:b c1 DB 0(許可DBへ) 1
COPY src:b c2 DB 3(禁止DBへ) NOPERM
SWAPDB 0 1(両方許可) OK
SWAPDB 1 2(片方が禁止) NOPERM
FLUSHDB(許可DB内) OK
FLUSHALL NOPERM

SWAPDBは片方でも禁止DBが混ざると弾かれます。FLUSHALLは全DBに対する操作ですので、一部のDBしか許可されていないユーザーからは実行できません。一方FLUSHDBは現在のDBだけが対象なため実行できます。

なおSWAPDBはクラスターモード無効でのみ使えるコマンドです。

キーパターンとDB権限はAND条件

~tenant:* db=0,1のユーザーで試します。

操作 結果
DB0でSET tenant:x OK
DB1でSET tenant:y OK
DB0でSET other:z NOPERM No permissions to access a key
DB2でSET tenant:w NOPERM No permissions to access database

両方を満たさないとコマンドは実行できません。エラーメッセージがキー違反とDB違反で書き分けられているので、どちらで弾かれたかは区別できます。

キーパターンはDBごとに指定するものではなく、許可された全DBに同じように適用されます。DBごとに違うキーパターンを当てたい場合はACLセレクタを併用することになりますが、今回は確認していません。

DB0を許可しないと接続直後が使えない

db=1だけを許可したユーザーを作って接続してみます。

PING                    -> PONG
(接続直後のDB0で)GET  -> NOPERM No permissions to access database
SELECT 1 してから GET   -> 正常
SELECT 0                -> NOPERM

認証自体は成功してPINGには応答が返ります。ところが接続直後は必ずDB0のため、そのままではコマンドが拒否されます。

アプリケーション側では接続後に明示的なSELECTを投げます。接続文字列でDB番号を指定できるクライアントならそれで済みますが、そうでなければ接続確立フックなどでSELECTを挟む作りにしておきましょう。

なお今回使用したvalkey-cli 9.1.0では、-n 2によるSELECTが失敗しても接続は終了しませんでした。SELECT 2 failed: NOPERM ...と表示したあと、DB0で後続のコマンドを実行します。禁止DBを指定したつもりでも、書き込み先はDB0です。これはクライアント実装の挙動であって、サーバー側のフォールバックではありません。

存在しないDB番号を指定できてしまう

databasesが16なので有効なDB番号は0〜15ですが、範囲外の番号を書いてもユーザーは作れます。

aws elasticache create-user ... --access-string "on ~* +@all db=0,99999"
# 成功する

ElastiCacheのユーザーはレプリケーショングループとは独立したリソースです。クラスターを消してもユーザーとユーザーグループは残りますし、作成時に対象クラスターのdatabases値との整合性も検証されません。

このユーザーで接続すると、実行時にこうなります。

操作 結果
SELECT 0 OK
SELECT 15(範囲内・許可外) NOPERM No permissions to access database
SELECT 20(範囲外) ERR DB index is out of range
SELECT 99999(範囲外) ERR DB index is out of range

SELECTについては、DB番号の範囲チェックがDB ACLチェックより先に行われる挙動でした。許可したはずの99999が、権限エラーではなく範囲エラーで返ります。

タイプミスで範囲外の番号を書いてもユーザー作成では気付けないので、アクセス文字列を組み立てる側で検証しておくほうが安全です。

db=で分離できないもの

db=が守るのは論理データベースのキー空間だけです。それ以外は同じノードを共有したままになります。

Pub/Subはdb=で分離されない

Pub/Subのチャネルはキー空間の外にあるため、db=の対象になりません。Valkeyのドキュメントにも「Publishing on db 10, will be heard by a subscriber on db 1」と書かれています。

https://valkey.io/topics/pubsub/

実際に跨がせてみます。購読側には&*db=1を付けたユーザーを用意しました。そのユーザーでDB1に入ってSUBSCRIBE crossし、別の端末では制限なしのユーザーがDB0でPUBLISH cross "hello-from-db0"を実行します。

PUBLISHの戻り値は1で、購読側にはmessage / cross / hello-from-db0が届きました。DB0で発行したメッセージが、DB1の購読者へ届いたことになります。

もっともdb=付きで作ったユーザーはresetchannels、つまりチャネル権限がない状態で返ってきます。そのままではチャネルを使えません。穴になるのは&*を足したときです。テナントごとにチャネルを分けたいなら、&*ではなく&tenant1:*のようにパターンで絞ります。

リソースと障害範囲は共有される

認可とは別に、DB単位では分けられないものもあります。

  • 同じノードを共有するので、メモリやCPUの競合は避けられない
  • ノードが落ちれば全DBが巻き込まれる
  • maxmemoryはノード単位で、容量も共有

db=は認可の境界を強くするものであって、独立したクラスターの代わりにはなりません。テナントごとに性能や可用性を分けたいのであれば、クラスター自体を分けることになります。

拒否は専用のCloudWatchメトリクスに出る

検証中に発生させた拒否をCloudWatchで確認しました。検証日の1日を期間として、Period 86400秒・Statistic Sumで取得しています。

メトリクス Sum 対応する拒否
DatabaseAuthorizationFailures 10 db=違反 10回
KeyAuthorizationFailures 1 ~tenant:*違反 1回
ChannelAuthorizationFailures 1 resetchannelsユーザーのSUBSCRIBE 1回
CommandAuthorizationFailures 0 なし
AuthenticationFailures 0 なし

拒否の種類ごとに別のメトリクスになっていて、検証中に発生させた回数と一致しました。DatabaseAuthorizationFailuresはDB権限違反専用で、ドキュメントにも「Valkey 9.1以降で利用可能」と書かれています。

アプリケーションのリリース後にこのメトリクスが増え始めたら、SELECT漏れかDB番号の設定ミスを疑いましょう。アラームを置いておくと気付きやすいです。

まとめ

db=の設定はcreate-user--access-stringから入ります。db=を書くとユーザーのMinimumEngineVersionが自動で9.1に上がり、そのユーザーを含むユーザーグループは9.1のクラスター専用になります。

実行時はSELECTだけでなくMOVECOPY ... DBSWAPDBFLUSHALLまで塞がれます。DB0を許可しない設計にするとクライアント側の対応がいりますし、範囲外のDB番号はユーザー作成時には弾かれません。拒否はDatabaseAuthorizationFailuresに出るので、監視は仕込みやすいですね。

テナントごとの分離手段として見ると、キー空間の認可としてはしっかり効きます。ただしPub/Subはdb=の対象外ですし、メモリや障害範囲は共有のままです。性能や可用性まで分けたいのであれば、クラスター自体を分ける判断になります。

この記事をシェアする

関連記事