Amazon ElastiCache for Valkey 9.1のdb=によるデータベース単位ACLを検証してみた
こんにちは。サービス開発部の武田です。
Valkey 9.1で、ACLにデータベース単位の権限が入りました。アクセス文字列にdb=0,1のように書くと、そのユーザーが触れる論理データベースを限定できます。Amazon ElastiCacheは2026年6月23日に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 LISTやACL WHOAMIなどの参照系だけです。
代わりにcreate-userの--access-stringへdb=0,1を書きます。--access-stringはACL 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人はチャネルを指定していないのにresetchannels、db=なしの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」と書かれています。
実際に跨がせてみます。購読側には&*と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だけでなくMOVE・COPY ... DB・SWAPDB・FLUSHALLまで塞がれます。DB0を許可しない設計にするとクライアント側の対応がいりますし、範囲外のDB番号はユーザー作成時には弾かれません。拒否はDatabaseAuthorizationFailuresに出るので、監視は仕込みやすいですね。
テナントごとの分離手段として見ると、キー空間の認可としてはしっかり効きます。ただしPub/Subはdb=の対象外ですし、メモリや障害範囲は共有のままです。性能や可用性まで分けたいのであれば、クラスター自体を分ける判断になります。





