Bedrock Managed Knowledge Base の ACL-aware 検索を試してみた — S3 データソースでの設定手順とハマりどころ

Bedrock Managed Knowledge Base の ACL-aware 検索を試してみた — S3 データソースでの設定手順とハマりどころ

Amazon Bedrock Managed Knowledge Base の ACL-aware 検索を S3 データソースで検証しました。userContext を渡すだけでユーザーごとに検索結果が変わる文書レベルのアクセス制御が実現できます。一方で、グローバル ACL とメタデータファイルを併存させると、ドキュメントの「メタデータ優先」とは異なり「両方で許可されたユーザーだけに返る AND 条件」になるという未記載の挙動を 9 パターンの実験で確認しました。
2026.07.10

はじめに

こんにちは!AI 事業本部のこーすけです。

Amazon Bedrock Managed Knowledge Base では ACL awareness(ACL-aware retrieval)という機能がサポートされました。検索をしたユーザーにデータソース上で閲覧権限を持つ文書だけを検索結果として返すというものです。

このブログでは、Amazon S3 データソース+ ACL 設定ファイルの構成で、ACL-aware 検索の設定から動作確認までをやってみたのでその手順をまとめました。

ACL awareness とは

まず、この機能が解決する課題から説明します。SharePoint や Google Drive では「このフォルダは営業部のみ」「この文書は経営層のみ」のように、文書ごとに閲覧権限が設定されていますよね。ところが、これらの文書を Knowledge Base に取り込むと権限情報は引き継がれず、検索する人が誰であっても全文書がヒットする状態になります。つまり「本人は直接開けないはずの文書の中身が、RAG チャット経由だと見えてしまう」という問題が起きます。

ACL awareness はこれを解決する機能で、仕組みは次の 3 ステップです。

  1. 取り込み時: 文書本文と一緒に「誰がこの文書を見てよいか」という権限情報(ACL)もインデックスに保存する
  2. 検索時: Retrieve API の userContext に「いま検索しているのは誰か」(ユーザーの識別子)を渡す
  3. Knowledge Base は、そのユーザーに閲覧権限がある文書だけを検索結果として返す

この「保存した ACL で検索結果を絞る」動きを Pre-retrieval filtering と呼びます。さらに SharePoint / OneDrive / Google Drive / Confluence コネクタでは、結果を返す直前にデータソース側へ最新の権限を再確認する Real-time ACL verification も行われます(S3 は非対応のため、本記事の検証は Pre-retrieval filtering のみです)。

注意: AWS ドキュメントに明記されている通り、ACL awareness は「フィルタ」であって「認可」ではありません。Bedrock はエンドユーザーを認証しないので、アプリケーション側で認証したうえで検証済みの identity を渡す前提の機能となります。

参考ドキュメント:

前提条件

  • Managed Knowledge Base が利用できる AWS アカウント・リージョン
  • ブラウザから AWS マネジメントコンソールにアクセスできること
  • Python 3.10 以降 + boto3(ステップ 5 の動作確認スクリプト用)

ステップ 1: 検証用の S3 バケットと文書を準備する

権限の違いによって取得できる文書が変わることがわかりやすいように、2 つの部署向け文書を用意します。以下の内容のファイルを 2 つ作成しておきます。

  • sales-strategy.txt営業部向け
  • budget-plan.txt経理部向け

次に S3 コンソールでバケットを作成し、以下の構成になるようにアップロードします。なお、ACL 設定ファイルは同じバケット内であれば任意の名前・場所に置くことができます。
スクリーンショット 2026-07-09 154839

フォルダ構成は以下の通りです。

my-acl-test-bucket/              # バケット名は任意の一意な名前
├── sales/
│   └── sales-strategy.txt
├── finance/
│   └── budget-plan.txt
└── acl/
    └── global-acl.json          # ステップ 2 で作成する ACL 設定ファイル

ステップ 2: ACL 設定ファイルを作成する

ACL を定義したファイルを用意します。方法は 2 つあります。

方法 特徴
グローバル ACL ファイル 1 つの JSON ファイルでフォルダ(プレフィックス)単位に権限を定義。権限構造が安定しているときに最適
文書ごとのメタデータファイル {ファイル名}.metadata.json を文書と同じパスに置く。文書単位で更新でき、ドキュメント上はグローバルより優先とされる(実挙動はステップ 6 参照)

今回はグローバル ACL ファイルの機能を検証した後、文書ごとのメタデータファイルによる機能を検証します。

まず、グローバル ACL ファイルは「sales/ 配下は user-a のみ」「finance/ 配下は user-b のみ」という設定にしました。

acl/global-acl.json
[
  {
    "keyPrefix": "s3://my-acl-test-bucket/sales/",
    "aclEntries": [
      { "Name": "user-a@example.com", "Type": "USER", "Access": "ALLOW" }
    ]
  },
  {
    "keyPrefix": "s3://my-acl-test-bucket/finance/",
    "aclEntries": [
      { "Name": "user-b@example.com", "Type": "USER", "Access": "ALLOW" }
    ]
  }
]

作成したら、ステップ 1 と同様に S3 コンソールでバケット内に acl フォルダを作成し、そこへ global-acl.json をアップロードします。

注意: ACL 設定ファイルはデータソースと同じバケットに置く必要があります。keyPrefix のバケット名は自分のバケット名に読み替えてください。

重要: ACL が有効になっている S3 データソースでは、ACL エントリが関連付けられていない文書は取り込まれません。すべての文書に、グローバル ACL ファイルまたはメタデータファイルのいずれかで ACL が定義されていることを確認してください。ACL を書いていない文書は全員に公開されるのではなく、誰にも返らなくなる挙動となります。

参考ドキュメント:

ステップ 3: Managed Knowledge Base に ACL 有効の S3 データソースを接続する

Bedrock コンソールから Managed Knowledge Base を作成し、S3 データソースを ACL 有効で接続します。

  1. Bedrock コンソールのナレッジベース一覧から「Create Managed KB」をクリックします

スクリーンショット 2026-07-07 232304

  1. データソースの種類で Amazon S3 を選択し、ステップ 1 で作成したバケットを指定します
  2. ACL awareness(アクセス制御)を有効化し、グローバル ACL ファイルの S3 URI に s3://my-acl-test-bucket/acl/global-acl.json を指定します
  3. 残りはデフォルトのまま作成を完了します

スクリーンショット 2026-07-09 160944

ステップ 4: データソースを同期する

作成したナレッジベースの詳細画面でデータソースを選択し、「同期」をクリックして完了を待ちます。同期のステータスと取り込まれた文書数はデータソースの詳細画面で確認できます。

スクリーンショット 2026-07-09 164416

同期が完了すると、文書コンテンツと一緒に ACL 情報がインデックスに取り込まれます。

ステップ 5: 動作確認 — userContext を変えて Retrieve してみる

いよいよ本題です。Retrieve API に userContext を渡して、ユーザーごとに結果が変わることを確認します。なお、執筆時点ではコンソールの「ナレッジベースをテスト」画面から userContext を指定できないため、Retrieve API を直接呼び出します。
以下の Python スクリプトで、「userContext なし」「user-a」「user-b」「user-c」の 4 パターンを順に実行します。KB_ID は作成したナレッジベースの ID(詳細画面で確認できます)に書き換えてください。

import boto3

REGION = "ap-northeast-1"
KB_ID = "XXXXXXXXXX"  # 作成した Managed KB の ID
QUERY = "2026年度の計画について教えて"

# None = userContext なし
USERS = [
    None,
    "user-a@example.com",
    "user-b@example.com",
    "user-c@example.com",
]

client = boto3.client("bedrock-agent-runtime", region_name=REGION)

def retrieve(user_id: str | None) -> None:
    params = {
        "knowledgeBaseId": KB_ID,
        "retrievalQuery": {"text": QUERY},
    }
    if user_id:
        params["userContext"] = {"userId": user_id}

    res = client.retrieve(**params)
    results = res["retrievalResults"]
    label = user_id or "(userContext なし)"
    print(f"=== {label}: {len(results)} 件 ===")
    for r in results:
        print(f"- {r['location']['s3Location']['uri']}")
    print()

for user in USERS:
    retrieve(user)

実行結果がこちらです。

=== (userContext なし): 0 件 ===

=== user-a@example.com: 1 件 ===
- https://my-acl-test-bucket.s3.ap-northeast-1.amazonaws.com/sales/sales-strategy.txt

=== user-b@example.com: 1 件 ===
- https://my-acl-test-bucket.s3.ap-northeast-1.amazonaws.com/finance/budget-plan.txt

=== user-c@example.com: 0 件 ===

結果を整理するとこうなります。
期待した通り、営業部の user-a さんは/sales 以下のファイル、経理部の user-b さんは/finance 以下のファイルのみにアクセスできることを確認できました。
また、userContext を与えない場合はどのファイルにもアクセスできない挙動、営業部でも経理部でもない user-c さんがアクセスできる資料がない挙動を確認できました。

パターン userContext 結果
1 なし 0 件(ACL 有効ソースは userContext 必須)
2 user-a@example.com sales/sales-strategy.txt のみ
3 user-b@example.com finance/budget-plan.txt のみ
4 user-c@example.com(ACL 未記載) 0 件

参考ドキュメント:

ステップ 6: 文書ごとのメタデータファイルを試す — グローバル ACL との併存は「AND 条件」だった

ステップ 2 で紹介したもう 1 つの方法である文書ごとのメタデータファイル{ファイル名}.metadata.json)での ACL 指定方法についても試してみます。ドキュメントには「文書単位のメタデータがグローバル ACL ファイルより優先される」とありますが、先に結論を言うと、実際にはそうならず、グローバル ACL とメタデータファイルの両方で許可されたユーザーだけに返る「AND 条件」の挙動でした。順を追って見ていきます。

現在、sales/sales-strategy.txt はグローバル ACL で user-a のみ許可になっています。この文書にだけ「user-b のみ許可」のメタデータファイルを置きます。ドキュメント通り「上書き」なら、sales 文書は user-a に返らなくなり、代わりに user-b に返るはずです。

sales-strategy.txt.metadata.json という名前のファイルを作成します。

sales-strategy.txt.metadata.json
{
  "metadataAttributes": {},
  "accessControlList": [
    { "Name": "user-b@example.com", "Type": "USER", "Access": "ALLOW" }
  ]
}

S3 バケット sales フォルダを開き、このファイルをアップロードします。sales-strategy.txt と同じ階層に配置する必要があることに注意してください。

スクリーンショット 2026-07-09 165841

アップロードしただけでは反映されないので、ステップ 4 と同様にナレッジベースのデータソースを再同期します。再同期後、メタデータが追加されたことを確認します。
スクリーンショット 2026-07-09 170247

再同期が完了したら、ステップ 5 のスクリプトをもう一度実行します。

結果は以下のようになりました。

=== (userContext なし): 0 件 ===

=== user-a@example.com: 0 件 ===

=== user-b@example.com: 1 件 ===
- https://my-acl-test-bucket.s3.ap-northeast-1.amazonaws.com/finance/budget-plan.txt

=== user-c@example.com: 0 件 ===

ん、想定していた挙動と一部違いますね。user-a から sales 文書が見えなくなったのは期待通りですが、メタデータファイルで許可したはずの user-b にも sales 文書が返っていません

ここで公式ドキュメントのトラブルシューティングを確認してみます。
https://docs.aws.amazon.com/bedrock/latest/userguide/kb-managed-ds-s3-acl.html

Per-document metadata takes precedence over the global ACL file. If a document has both a matching global prefix entry and a per-document metadata file, the per-document metadata is used.(文書ごとのメタデータは、グローバル ACL ファイルよりも優先されます。文書に一致するグローバルプレフィックスエントリと文書ごとのメタデータファイルの両方が存在する場合、文書ごとのメタデータが使用されます。)

スクリーンショット 2026-07-09 182654

いずれ条件も正しく設定しているように見えます……。

ちょっとよくわからない状態なので、もう少し挙動を観察してみます。

切り分けとして、メタデータファイルを「user-a と user-b の両方を ALLOW」に書き換えて再同期し、もう一度スクリプトを実行してみました。

sales-strategy.txt.metadata.json
{
  "metadataAttributes": {},
  "accessControlList": [
    { "Name": "user-a@example.com", "Type": "USER", "Access": "ALLOW" },
    { "Name": "user-b@example.com", "Type": "USER", "Access": "ALLOW" }
  ]
}

結果はこちらになりました。

=== (userContext なし): 0 件 ===

=== user-a@example.com: 1 件 ===
- https://my-acl-test-bucket.s3.ap-northeast-1.amazonaws.com/sales/sales-strategy.txt

=== user-b@example.com: 1 件 ===
- https://my-acl-test-bucket.s3.ap-northeast-1.amazonaws.com/finance/budget-plan.txt

=== user-c@example.com: 0 件 ===

sales 文書が見えるのは user-a だけですね。
メタデータファイルに書いた 2 人のうち、グローバル ACL にも載っている user-a だけが通りました。2 回の実験を整理するとこうなります。

メタデータファイルの記載 グローバル ACL(sales/)の記載 user-a に sales 文書が user-b に sales 文書が
user-b のみ user-a のみ 返らない 返らない
user-a + user-b user-a のみ 返る 返らない

どちらの結果も「グローバル ACL とメタデータファイルの両方で ALLOW されているユーザーだけに返る」で説明できます。つまり執筆時点の実挙動は、メタデータファイルが優先(上書き)ではなく、グローバル ACL とのAND 条件のようです。

本当に AND 条件なのか、条件を変えて検証してみる

2 回の実験だけで結論づけるのは早いので、条件を変えながら検証を重ねました。具体的には、① グローバル ACL の割り当てを逆転させて(sales/ を user-b、finance/ を user-a に)同じことが対称に成り立つか、② メタデータファイルの中身を変えたパターン(同一ユーザー / 別ユーザー / 両方 / DENY)、③ グローバル ACL から sales/ のエントリを削除してメタデータファイル単独にしたパターン、を順に試しました。表は sales 文書が誰に返ったかを整理したものです。

# グローバル ACL(sales/) メタデータファイル sales 文書が返ったユーザー AND 条件の仮定通りか
1 user-a なし user-a
2 user-a user-b(別ユーザー) 誰にも返らない
3 user-a user-a + user-b user-a のみ
4 user-b なし user-b
5 user-b user-b(同一ユーザー) user-b
6 user-b user-a(別ユーザー) 誰にも返らない
7 user-b user-a + user-b user-b のみ
8 user-b user-b を DENY 誰にも返らない
9 エントリなし user-b user-b

9 パターンすべてが AND 条件とした場合の仮定と一致しました。グローバル ACL を逆転させてもすべて対称な結果となりました。

まとめると、執筆時点の実挙動はこう整理できます。

  1. グローバル ACL 単独、メタデータファイル単独なら、それぞれドキュメント通りに機能する
  2. 両方が同じ文書に適用される場合は「優先(上書き)」ではなく「両方で ALLOW された人だけ通る(AND 条件)」になる
  3. DENY が ALLOW より優先されるのはドキュメント記載通り

つまり併存時には、メタデータファイルで権限を「広げる」ことはできない(広げたいならグローバル側にも同じユーザーを載せる必要がある)ことになります。逆に権限を「絞る」方向(グローバルで許可した一部ユーザーをメタデータで外す)には機能します。

注意: これは執筆時点(2026 年 7 月 9 日)の検証結果です。ドキュメントの記載と実挙動が異なっているため、将来のアップデートで挙動が変わる可能性があります。

参考ドキュメント:

まとめ

Managed Knowledge Base の ACL-aware 検索を、S3 データソース+ ACL 設定ファイルの構成で検証しました。検証を通じて分かったこと、ドキュメントを読んで大事だと思ったことを整理します。

  • Retrieve API に userContext を渡すだけで、同じクエリでもユーザーごとに見える文書が変わる、文書レベルのアクセス制御フィルタが実現できます。S3 データソースなら ACL 設定ファイルを置くだけで完結します。
  • グローバル ACL とメタデータファイルの併存は、公式ドキュメントのメタデータ優先とは異なり、執筆時点では両方で ALLOW されたユーザーだけに返る挙動(AND 条件) でした。併存させる場合は、見せたいユーザーを両方に書く必要があります。
  • ACL まわりの設定ミスや権限の不一致は、エラーではなく取得 0 件という挙動になります。トラブルシューティング通り、「userContext の渡し忘れ」「ユーザー識別子の完全一致」「ACL 未定義の文書は取り込まれない」「変更後の再同期」あたりを疑うことが大切です。
  • ACL awareness は「フィルタ」であって「認可」ではありません。アプリケーション側で認証したうえで、検証済みの identity を渡す設計が前提です。

特に AND 条件の挙動はドキュメントからは読み取れないので、同じ構成を検討している方は注意してください。この点は AWS サポートにも確認してみようと思います。

最後まで読んでいただきありがとうございました!

参考文献

この記事をシェアする

関連記事