I tried validating document-level ACL for S3 knowledge base in Amazon Bedrock Managed Knowledge Base from Amazon Q - Serving confidential documents differently for each user

I tried validating document-level ACL for S3 knowledge base in Amazon Bedrock Managed Knowledge Base from Amazon Q - Serving confidential documents differently for each user

I tried connecting Amazon Bedrock Managed Knowledge Base as a knowledge source for Amazon Quick and tested document-level access control lists (ACLs). I will summarize the differences from the ACL settings on the Quick side that I verified previously, as well as the methods for defining and verifying ACLs on the Bedrock side.
2026.09.14

This page has been translated by machine translation. View original

Ishikawa here from the Cloud Business Division. I tried connecting Amazon Bedrock Managed Knowledge Base as a knowledge source for Amazon Quick, and tested per-user access control using document-level access control lists (ACLs).

https://docs.aws.amazon.com/quick/latest/userguide/quick-byo-bedrock-kb.html

Recently, I verified the document-level ACL feature built into Amazon Quick's own S3 knowledge base. This is a feature that lets you configure ALLOW / DENY on a per-user and per-group basis for individual documents, rather than at the bucket or prefix level.

https://dev.classmethod.jp/articles/20260911-amazon-quick-s3-acl/

This time I tried a different approach. Amazon Quick can connect an Amazon Bedrock Managed Knowledge Base as a "bring-your-own knowledge source." In this case, ACL definition and evaluation are both handled on the Bedrock side, and Quick's role is to pass the user's ID to Bedrock at query time.

Even though the goal is the same — "attach ACLs to documents on S3 and serve different results to different users" — the place where you configure things and the features available change. I used documentation covering the same configuration as a reference to understand what differs and how.

https://docs.aws.amazon.com/bedrock/latest/userguide/kb-managed-acl.html

https://docs.aws.amazon.com/quick/latest/userguide/byo-bedrock-kb-acl.html

Also, for an overview of the flow for making natural-language queries in Japanese from Amazon Quick to an Amazon Bedrock Managed Knowledge Base, please see the following blog post.

https://dev.classmethod.jp/articles/20260813-amazon-mkb-with-quick/

What is Document-Level ACL in Bedrock Managed Knowledge Base

Bedrock Managed Knowledge Base is a knowledge base you can use without setting up a vector store or configuring chunking. You create it by specifying MANAGED for knowledgeBaseConfiguration.type.

In this Managed Knowledge Base, ACL support varies by data source connector.

Connector Pre-filtering Real-time ACL validation
SharePoint Supported Supported
OneDrive Supported Supported
Google Drive Supported Supported
Confluence Supported Supported
Amazon S3 Supported Not supported
Custom Supported Not supported
Web Crawler Not supported

https://docs.aws.amazon.com/bedrock/latest/userguide/kb-managed-acl.html

Note that the only connector I verified in this testing was the Amazon S3 connector. The table above and the explanations of real-time validation and identity models that follow are based on the official documentation; I did not verify the behavior of SharePoint or Google Drive.

The reason S3 and Custom do not support real-time validation is that the source of ACL information is a "configuration file prepared by the user." Since there is no permission system to query — unlike SharePoint or Google Drive — the content read during ingestion is treated as authoritative as-is.

Email addresses are used for identity matching. The email address passed at query time must exactly match the email address written in the ACL. Alias resolution and cross-identity-provider mapping are not performed.

There are two ways to define ACLs.

Global ACL Configuration File

This is a JSON file listing ACL entries for each S3 key prefix. When creating the data source, you specify the S3 URI of this file using aclConfiguration.globalAccessControlListS3Uri.

[
  {
    "keyPrefix": "s3://BUCKETNAME/finance/",
    "aclEntries": [
      {
        "Name": "user1@example.com",
        "Type": "USER",
        "Access": "ALLOW"
      }
    ]
  }
]

Name is the user's email address, Type is USER, and Access is either ALLOW or DENY. DENY takes precedence over ALLOW.

This is the first difference from Quick's S3 knowledge base that I verified previously. In Quick's ACL, you could specify GROUP for Type and write a Quick group name, but in Bedrock's S3 connector, only USER is supported according to the documentation. I will actually confirm this in a later step.

Per-Document Metadata File

This method involves placing a <filename>.metadata.json file at the same S3 path as each document and writing ACL entries in accessControlList.

{
  "metadataAttributes": {},
  "accessControlList": [
    {
      "Name": "user1@example.com",
      "Type": "USER",
      "Access": "ALLOW"
    }
  ]
}

The documentation explicitly states that "per-document metadata takes precedence over the global ACL file." If both are configured, the metadata side is used.

Also, documents with no associated ACL entries will not be ingested. This behavior is the same as Quick's S3 knowledge base from last time.

https://docs.aws.amazon.com/bedrock/latest/userguide/kb-managed-ds-s3-acl.html

Trying It Out

Prerequisites

  • AWS account: Amazon Quick ENTERPRISE subscription already in place (authentication type: IDENTITY_POOL)
  • Verification region: us-east-1
  • AWS CLI: aws-cli/2.36.40

There are regional constraints. Managed Knowledge Base connectivity for Quick is only supported in four regions: US East (N. Virginia), US West (Oregon), EU West (Ireland), and Asia Pacific (Sydney). The Tokyo region is not included. In addition, the Managed Knowledge Base and the Quick instance must be in the same region.

The number of Managed Knowledge Bases that can be connected per Quick instance is limited to two.

https://docs.aws.amazon.com/quick/latest/userguide/byo-bedrock-kb-limitations.html

Verification Configuration

I placed eight documents and an ACL configuration file in a single S3 bucket and created two Managed Knowledge Bases that differ only in whether ACL is enabled or disabled.

The identifiers for the two knowledge bases are as follows. These IDs are used in the commands throughout.

MKB-A (ACL Enabled) MKB-B (For Comparison · ACL Disabled)
Knowledge base name mkb-acl-blog-acl-987164eb mkb-acl-blog-noacl-987164eb
Knowledge base ID O3VLSO3CS9 VZCVVT2YJL
Data source name s3-acl-connector s3-noacl-connector
Data source ID YBNLLH2UBM 61AKBSB7TR
aclEnabled true Not specified (false)

Each document has an embedded management number for identification. The ACL configuration is as follows. other-user@example.com is a dummy email address that does not exist in Quick, used as a counterpart for DENY.

Document Management number Defined in Self other-user
global/allow/holiday-policy.md ALPHA-GLOBAL-ALLOW Global ACL ALLOW -
global/deny/exec-compensation.md BRAVO-GLOBAL-DENY Global ACL DENY ALLOW
global/noacl/uncontrolled-memo.md CHARLIE-GLOBAL-NOACL None - -
meta/allow/incident-report.md DELTA-META-ALLOW Metadata ALLOW -
meta/deny/ma-valuation.md ECHO-META-DENY Metadata DENY ALLOW
meta/noacl/orphan-note.md FOXTROT-META-NOACL None - -
group/board-minutes.md GOLF-GROUP-TYPE Metadata (Type: GROUP) - -
override/override-doc.md HOTEL-OVERRIDE Global DENY + Metadata ALLOW Conflict -

group/ was prepared to observe the behavior when Type: GROUP is written, and override/ was prepared to observe the precedence when global and metadata settings conflict.

docs
├── acl
   └── global-acl.json
├── global
   ├── allow
   └── holiday-policy.md
   ├── deny
   └── exec-compensation.md
   └── noacl
       └── uncontrolled-memo.md
├── group
   ├── board-minutes.md
   └── board-minutes.md.metadata.json
├── meta
   ├── allow
   ├── incident-report.md
   └── incident-report.md.metadata.json
   ├── deny
   ├── ma-valuation.md
   └── ma-valuation.md.metadata.json
   └── noacl
       └── orphan-note.md
└── override
    ├── override-doc.md
    └── override-doc.md.metadata.json

Step 1: Place Documents and ACL File in S3

Prepare the global ACL file as global-acl.json and place it under the acl/ prefix. The contents are as follows. Files under meta/ are defined using metadata files, so they are not included here. override/ is set to DENY to create a conflict with the ALLOW on the metadata side. When creating the data source, specify the S3 URI using globalAccessControlListS3Uri.

[
  {
    "keyPrefix": "s3://mkb-acl-blog-987164eb/global/allow/",
    "aclEntries": [
      { "Name": "xxxxxxxxxx@classmethod.jp", "Type": "USER", "Access": "ALLOW" }
    ]
  },
  {
    "keyPrefix": "s3://mkb-acl-blog-987164eb/global/deny/",
    "aclEntries": [
      { "Name": "xxxxxxxxxx@classmethod.jp", "Type": "USER", "Access": "DENY" },
      { "Name": "other-user@example.com", "Type": "USER", "Access": "ALLOW" }
    ]
  },
  {
    "keyPrefix": "s3://mkb-acl-blog-987164eb/override/",
    "aclEntries": [
      { "Name": "xxxxxxxxxx@classmethod.jp", "Type": "USER", "Access": "DENY" }
    ]
  }
]

group/board-minutes.md.metadata.json includes Type: GROUP, which should be unsupported.

{
  "metadataAttributes": {},
  "accessControlList": [
    { "Name": "mkb-acl-blog-group", "Type": "GROUP", "Access": "ALLOW" },
    { "Name": "xxxxxxxxxx@classmethod.jp", "Type": "USER", "Access": "ALLOW" }
  ]
}

Create the bucket and upload the files.

% aws s3api create-bucket --bucket mkb-acl-blog-987164eb --region us-east-1
% aws s3 sync docs/ s3://mkb-acl-blog-987164eb/ --region us-east-1
% aws s3 ls s3://mkb-acl-blog-987164eb/ --recursive --region us-east-1
2026-09-12 00:20:59        621 acl/global-acl.json
2026-09-12 00:20:59        460 global/allow/holiday-policy.md
2026-09-12 00:20:59        435 global/deny/exec-compensation.md
2026-09-12 00:20:59        332 global/noacl/uncontrolled-memo.md
2026-09-12 00:20:59        273 group/board-minutes.md
2026-09-12 00:20:59        219 group/board-minutes.md.metadata.json
2026-09-12 00:20:59        423 meta/allow/incident-report.md
2026-09-12 00:20:59        145 meta/allow/incident-report.md.metadata.json
2026-09-12 00:20:59        327 meta/deny/ma-valuation.md
2026-09-12 00:20:59        221 meta/deny/ma-valuation.md.metadata.json
2026-09-12 00:20:59        315 meta/noacl/orphan-note.md
2026-09-12 00:20:59        453 override/override-doc.md
2026-09-12 00:20:59        145 override/override-doc.md.metadata.json

The ACL configuration file must be placed in the same bucket as the data source target bucket.

Step 2: Create an IAM Service Role

With a Managed Knowledge Base, no permissions for a vector store are needed. All that is required is a trust policy that allows AssumeRole from Bedrock, and read permissions for the S3 data source.

Save the trust policy as trust-policy.json.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "bedrock.amazonaws.com" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "aws:SourceAccount": "123456789012" },
        "ArnLike": { "AWS:SourceArn": "arn:aws:bedrock:us-east-1:123456789012:knowledge-base/*" }
      }
    }
  ]
}

The permissions policy needs s3:ListBucket, s3:GetObject, and permissions to invoke an embedding model. Save this as kb-permissions.json.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "S3ListBucketStatement",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": ["arn:aws:s3:::mkb-acl-blog-987164eb"],
      "Condition": { "StringEquals": { "aws:ResourceAccount": "123456789012" } }
    },
    {
      "Sid": "S3GetObjectStatement",
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": ["arn:aws:s3:::mkb-acl-blog-987164eb/*"],
      "Condition": { "StringEquals": { "aws:ResourceAccount": "123456789012" } }
    },
    {
      "Sid": "BedrockModelStatement",
      "Effect": "Allow",
      "Action": ["bedrock:ListFoundationModels", "bedrock:ListCustomModels", "bedrock:InvokeModel"],
      "Resource": "*"
    }
  ]
}

Create the role using the two files.

% aws iam create-role \
  --role-name mkb-acl-blog-role-987164eb \
  --assume-role-policy-document file://trust-policy.json \
  --query 'Role.Arn' --output text
arn:aws:iam::123456789012:role/mkb-acl-blog-role-987164eb

% aws iam put-role-policy \
  --role-name mkb-acl-blog-role-987164eb \
  --policy-name mkb-acl-blog-policy \
  --policy-document file://kb-permissions.json

Step 3: Create the Bedrock Managed Knowledge Base

Pass type: MANAGED and managedKnowledgeBaseConfiguration to knowledgeBaseConfiguration. Setting embeddingModelType to MANAGED uses a service-managed embedding model, eliminating the need to specify a model ARN or configure a vector store.

% aws bedrock-agent create-knowledge-base \
  --name "mkb-acl-blog-acl-987164eb" \
  --description "Bedrock Managed Knowledge Base with ACL enabled (blog verification)" \
  --role-arn arn:aws:iam::123456789012:role/mkb-acl-blog-role-987164eb \
  --knowledge-base-configuration '{"type":"MANAGED","managedKnowledgeBaseConfiguration":{"embeddingModelType":"MANAGED"}}' \
  --region us-east-1
{
    "knowledgeBase": {
        "knowledgeBaseId": "O3VLSO3CS9",
        "name": "mkb-acl-blog-acl-987164eb",
        "knowledgeBaseArn": "arn:aws:bedrock:us-east-1:123456789012:knowledge-base/O3VLSO3CS9",
        "roleArn": "arn:aws:iam::123456789012:role/mkb-acl-blog-role-987164eb",
        "knowledgeBaseConfiguration": {
            "type": "MANAGED",
            "managedKnowledgeBaseConfiguration": {
                "embeddingModelType": "MANAGED"
            }
        },
        "status": "CREATING"
    }
}

I created the comparison knowledge base with ACL disabled using the same command (ID: VZCVVT2YJL). The only changes were setting --name to mkb-acl-blog-noacl-987164eb and updating --description; --role-arn and --knowledge-base-configuration are identical. Whether ACL is enabled or not is determined by the data source in the next step, not the knowledge base itself, so there is no difference between the two at this point.

Step 4: Create the S3 Data Source (ACL Enabled)

For a Managed Knowledge Base data source, specify MANAGED_KNOWLEDGE_BASE_CONNECTOR for type and write connector-specific settings in connectorParameters. Save this configuration as ds-acl.json.

{
  "type": "MANAGED_KNOWLEDGE_BASE_CONNECTOR",
  "managedKnowledgeBaseConnectorConfiguration": {
    "connectorParameters": {
      "type": "S3",
      "version": "1",
      "aclEnabled": true,
      "connectionConfiguration": {
        "bucketName": "mkb-acl-blog-987164eb",
        "bucketOwnerAccountId": "123456789012"
      },
      "aclConfiguration": {
        "globalAccessControlListS3Uri": "s3://mkb-acl-blog-987164eb/acl/global-acl.json"
      }
    }
  }
}

Set aclEnabled to true and specify the S3 URI of the global ACL file for aclConfiguration.globalAccessControlListS3Uri. If this path is omitted, only the metadata file approach is used. In this verification, both methods are used together in a single data source.

% aws bedrock-agent create-data-source \
  --knowledge-base-id O3VLSO3CS9 \
  --name "s3-acl-connector" \
  --data-source-configuration file://ds-acl.json \
  --region us-east-1
{
    "dataSource": {
        "knowledgeBaseId": "O3VLSO3CS9",
        "dataSourceId": "YBNLLH2UBM",
        "name": "s3-acl-connector",
        "status": "CREATING",
        "dataSourceConfiguration": {
            "type": "MANAGED_KNOWLEDGE_BASE_CONNECTOR",
            "managedKnowledgeBaseConnectorConfiguration": {
                "connectorParameters": "{\"type\":\"S3\",\"aclEnabled\":true,\"aclConfiguration\":{\"globalAccessControlListS3Uri\":\"s3://mkb-acl-blog-987164eb/acl/global-acl.json\"},\"connectionConfiguration\":{\"bucketName\":\"mkb-acl-blog-987164eb\",\"bucketOwnerAccountId\":\"123456789012\"},\"filterConfiguration\":{\"maxFileSizeInMegaBytes\":\"500\"},\"version\":\"1\"}"
            }
        },
        "dataDeletionPolicy": "DELETE"
    }
}

Looking at the response, connectorParameters is stored as a string, and the default value of 500 has been filled in for filterConfiguration.maxFileSizeInMegaBytes, which was not specified.

Step 5: Create the S3 Data Source (ACL Disabled)

As a baseline for the comparison experiment, create a data source in MKB-B that does not read ACLs. The only difference from the ACL-enabled side is removing aclEnabled and aclConfiguration from connectorParameters; the connectionConfiguration bucket is the same. Save this as ds-noacl.json.

{
  "type": "MANAGED_KNOWLEDGE_BASE_CONNECTOR",
  "managedKnowledgeBaseConnectorConfiguration": {
    "connectorParameters": {
      "type": "S3",
      "version": "1",
      "connectionConfiguration": {
        "bucketName": "mkb-acl-blog-987164eb",
        "bucketOwnerAccountId": "123456789012"
      }
    }
  }
}

Keeping the same bucket target, create the data source pointing to MKB-B (VZCVVT2YJL).

% aws bedrock-agent create-data-source \
  --knowledge-base-id VZCVVT2YJL \
  --name "s3-noacl-connector" \
  --description "S3 connector with ACL disabled (for comparison)" \
  --data-source-configuration file://ds-noacl.json \
  --region us-east-1

In this response, aclEnabled is explicitly set to false.

{"type":"S3","connectionConfiguration":{...},"filterConfiguration":{"maxFileSizeInMegaBytes":"500"},"aclEnabled":false,"version":"1"}

This completes the setup where the same documents in the same S3 bucket are ingested from both a data source that reads ACLs and one that does not.

Step 6: Compare Ingestion Results

Start ingestion on both knowledge bases.

% aws bedrock-agent start-ingestion-job \
  --knowledge-base-id O3VLSO3CS9 \
  --data-source-id YBNLLH2UBM \
  --region us-east-1

It completed in about two minutes. There is a clear difference in the results.

% aws bedrock-agent get-ingestion-job \
  --knowledge-base-id O3VLSO3CS9 --data-source-id YBNLLH2UBM \
  --ingestion-job-id FWZKF0FLMX --region us-east-1

{
    "status": "COMPLETE",
    "stats": {
        "numberOfDocumentsScanned": 8,
        "numberOfMetadataDocumentsScanned": 3,
        "numberOfNewDocumentsIndexed": 5,
        "numberOfDocumentsFailed": 3,
        "numberOfDocumentsSkipped": 0
    },
    "fail": [
        "[\"The sync completed with partial failures. Some documents could not be crawled.\"]"
    ]
}

The ACL-disabled side is as follows.

{
    "status": "COMPLETE",
    "stats": {
        "numberOfDocumentsScanned": 9,
        "numberOfMetadataDocumentsScanned": 4,
        "numberOfNewDocumentsIndexed": 9,
        "numberOfDocumentsFailed": 0
    }
}

Putting the numbers side by side makes the differences clear.

MKB-A (ACL Enabled) MKB-B (ACL Disabled)
Documents scanned 8 9
Metadata scanned 3 4
Documents indexed 5 9
Documents failed 3 0

The reason the ACL-enabled side scanned 8 documents while the disabled side scanned 9 is the difference in how acl/global-acl.json is handled. On the ACL-enabled side, it is interpreted as an ACL configuration file and excluded from documents, but on the disabled side it is ingested as just a plain JSON file.

The split in metadata scan counts — 3 versus 4 — is for the same reason: board-minutes.md.metadata.json, which contains Type: GROUP, is not counted as valid metadata on the ACL-enabled side.

Check which documents were ingested.

% aws bedrock-agent list-knowledge-base-documents \
  --knowledge-base-id O3VLSO3CS9 --data-source-id YBNLLH2UBM --region us-east-1

INDEXED    global/allow/holiday-policy.md
INDEXED    global/deny/exec-compensation.md
INDEXED    meta/allow/incident-report.md
INDEXED    meta/deny/ma-valuation.md
INDEXED    override/override-doc.md
Total: 5

The three documents that were dropped were the following:

  • global/noacl/uncontrolled-memo.md (not listed in the global ACL)
  • meta/noacl/orphan-note.md (no metadata file)
  • group/board-minutes.md (Type: GROUP specified)

That documents with no ACL entries are not ingested is as documented. In addition, documents with Type: GROUP specified were also not ingested. Even though the same metadata file also included a USER ALLOW entry for my own email address, it still did not pass. The behavior is that the document is rejected as a whole, not that individual entries are ignored.

The DENY-configured documents exec-compensation.md and ma-valuation.md are ingested. ACL operates in two stages — "whether to ingest" and "whether to return in search results" — and DENY is processed in the latter.

On the other hand, the ACL-disabled knowledge base ingested all nine documents.

% aws bedrock-agent list-knowledge-base-documents \
  --knowledge-base-id VZCVVT2YJL --data-source-id 61AKBSB7TR --region us-east-1

INDEXED    acl/global-acl.json
INDEXED    global/allow/holiday-policy.md
INDEXED    global/deny/exec-compensation.md
INDEXED    global/noacl/uncontrolled-memo.md
INDEXED    group/board-minutes.md
INDEXED    meta/allow/incident-report.md
INDEXED    meta/deny/ma-valuation.md
INDEXED    meta/noacl/orphan-note.md
INDEXED    override/override-doc.md
Total: 9

There are 13 files total in the bucket, but only 9 appear here. The remaining 4 are metadata files such as incident-report.md.metadata.json, which are not indexed as documents regardless of whether ACL is enabled or disabled. In the statistics as well, they are counted under numberOfMetadataDocumentsScanned, not numberOfDocumentsScanned.

The reason acl/global-acl.json appears as the 9th entry is that it does not match the metadata file naming convention (<filename>.metadata.json). From a data source that does not read ACLs, it is treated as just a plain JSON file.

Step 7: Verify the ingested ACL

With get-ingested-document-acl, you can verify the ACL stored in the index. Pass the S3 URI as-is to --document-id.

Here are the results for a document with DENY configured.

% aws bedrock-agent-runtime get-ingested-document-acl \
  --knowledge-base-id O3VLSO3CS9 --data-source-id YBNLLH2UBM \
  --document-id "s3://mkb-acl-blog-987164eb/global/deny/exec-compensation.md" \
  --region us-east-1
{
    "documentAcl": {
        "allowList": {
            "conditions": [
                {
                    "conditionOperator": "OR",
                    "users": [
                        { "id": "other-user@example.com", "type": "KNOWLEDGE_BASE" }
                    ]
                }
            ],
            "memberRelation": "AND"
        },
        "denyList": {
            "conditions": [
                {
                    "conditionOperator": "OR",
                    "users": [
                        { "id": "xxxxxxxxxx@classmethod.jp", "type": "KNOWLEDGE_BASE" }
                    ]
                }
            ],
            "memberRelation": "AND"
        }
    }
}

The ALLOW / DENY written in JSON is broken down and stored as allowList and denyList.

Looking at override/override-doc.md here gives us the answer about priority. This document had DENY set in the global ACL file and ALLOW set in the metadata file.

% aws bedrock-agent-runtime get-ingested-document-acl \
  --knowledge-base-id O3VLSO3CS9 --data-source-id YBNLLH2UBM \
  --document-id "s3://mkb-acl-blog-987164eb/override/override-doc.md" \
  --region us-east-1
{
    "documentAcl": {
        "allowList": {
            "conditions": [
                {
                    "conditionOperator": "OR",
                    "users": [
                        { "id": "xxxxxxxxxx@classmethod.jp", "type": "KNOWLEDGE_BASE" }
                    ]
                }
            ],
            "memberRelation": "AND"
        }
    }
}

The denyList does not exist. The DENY from the global side was discarded at ingestion time, and only the ALLOW from the metadata side remains. The rule that "DENY takes precedence over ALLOW" only applies within the same ACL source. When the source itself is replaced, the metadata file wins.

Step 8: Verify access permissions

Using check-ingested-document-acl, you can directly query access permissions per user. This is the CLI equivalent of the Permission Checker feature verified in Quick last time.

Here are the results of checking 5 items with my own email address.

% for key in global/allow/holiday-policy.md global/deny/exec-compensation.md \
             meta/allow/incident-report.md meta/deny/ma-valuation.md \
             override/override-doc.md; do
    aws bedrock-agent-runtime check-ingested-document-acl \
      --knowledge-base-id O3VLSO3CS9 --data-source-id YBNLLH2UBM \
      --document-id "s3://mkb-acl-blog-987164eb/${key}" \
      --user-context "userId=xxxxxxxxxx@classmethod.jp" --region us-east-1
  done

global/allow/holiday-policy.md      {"hasAccess": true}
global/deny/exec-compensation.md    {"hasAccess": false}
meta/allow/incident-report.md       {"hasAccess": true}
meta/deny/ma-valuation.md           {"hasAccess": false}
override/override-doc.md            {"hasAccess": true}

With other-user@example.com, who was given ALLOW on the DENY side, the results are reversed.

global/allow/holiday-policy.md      {"hasAccess": false}
global/deny/exec-compensation.md    {"hasAccess": true}
meta/deny/ma-valuation.md           {"hasAccess": true}
override/override-doc.md            {"hasAccess": false}

I also tried with an email address that doesn't appear in the ACL at all.

% aws bedrock-agent-runtime check-ingested-document-acl \
  --knowledge-base-id O3VLSO3CS9 --data-source-id YBNLLH2UBM \
  --document-id "s3://mkb-acl-blog-987164eb/global/allow/holiday-policy.md" \
  --user-context "userId=unknown@example.com" --region us-east-1
{"hasAccess": false}

If you are not on the ALLOW list, access is denied. There is no need to explicitly write a DENY.

Step 9: Verify ACL effectiveness with retrieve

Let's verify with an actual search. I'll ask a question about "executive compensation revision rate and holidays," which involves both an ALLOW document and a DENY document.

% aws bedrock-agent-runtime retrieve \
  --knowledge-base-id O3VLSO3CS9 \
  --retrieval-query '{"text":"役員報酬の改定率と休業日について教えてください"}' \
  --user-context "userId=xxxxxxxxxx@classmethod.jp" \
  --region us-east-1

score=0.4403  global/allow/holiday-policy.md
Hit count: 1

Only the holidays document is returned, and the executive compensation document with DENY does not appear.

Running the same query with other-user@example.com reverses the result.

score=0.3646  global/deny/exec-compensation.md
Hit count: 1

I also checked what happens when userContext is omitted.

% aws bedrock-agent-runtime retrieve \
  --knowledge-base-id O3VLSO3CS9 \
  --retrieval-query '{"text":"役員報酬の改定率と休業日について教えてください"}' \
  --region us-east-1

Hit count: 0

Zero results. The documentation explicitly states that "if userContext is not specified in a Retrieve request, a data source with ACL enabled returns zero results," and the behavior matched exactly. Rather than returning results without filtering when ACL evaluation is not possible, the design returns nothing at all.

https://docs.aws.amazon.com/bedrock/latest/userguide/kb-test-retrieve-acl.html

Step 10: Compare with a knowledge base without ACL

To confirm that ACL is truly in effect, I'll send the same query to a knowledge base without ACL (MKB-B, VZCVVT2YJL). Note that --knowledge-base-id differs from the steps up to this point.

% aws bedrock-agent-runtime retrieve \
  --knowledge-base-id VZCVVT2YJL \
  --retrieval-query '{"text":"役員報酬の改定率と休業日について教えてください"}' \
  --region us-east-1

score=0.6565  global/deny/exec-compensation.md
score=0.3101  global/allow/holiday-policy.md
score=0.1837  acl/global-acl.json
score=0.1686  group/board-minutes.md
score=0.1664  meta/deny/ma-valuation.md
Hit count: 5

5 results were returned without userContext. With the same bucket, same documents, and same query, the results differed, so the only difference is the ACL configuration.

There is one more notable point in these results. acl/global-acl.json itself is included in the search results. Since the ACL definition file explicitly lists who can access which prefix, creating a knowledge base without ACL pointing to the same bucket makes the permission design contents searchable.

I also tried passing userContext to the knowledge base without ACL, but the result was still 5 hits. As stated in the documentation, a data source that doesn't support ACL returns results as normal.

In Step 7, we verified priority by examining the ACL contents. Let's see if the same result occurs with a search.

% aws bedrock-agent-runtime retrieve \
  --knowledge-base-id O3VLSO3CS9 \
  --retrieval-query '{"text":"OVERRIDE-VALUE-7788 優先順位検証用ドキュメント"}' \
  --user-context "userId=xxxxxxxxxx@classmethod.jp" --region us-east-1

score=0.4346  override/override-doc.md
score=0.1608  meta/allow/incident-report.md
Hit count: 2

Despite being DENY in the global ACL, the document was returned. The ALLOW from the metadata file won.

Step 12: Connect to Amazon QuickSight

From here, the work moves to the QuickSight side. I only connected the ACL-enabled MKB-A (O3VLSO3CS9) to QuickSight. The limit is 2 Managed Knowledge Bases per QuickSight instance, and the account used for verification already had one other Bedrock integration. MKB-B without ACL was not registered in QuickSight and was left only for the CLI comparison in the previous steps.

Create it from the console. Open "Knowledge" from "See more" in the left menu of the QuickSight screen, and a list of connectable applications will be displayed.

QuickSight knowledge screen

Select "Amazon Bedrock Managed Knowledge Base" in the upper left and enter the name and the knowledge base ARN on the Bedrock side.

Enter the Bedrock KB ARN

A warning appeared when I entered the ARN.

Amazon Quick needs access to this knowledge base before the connection can read from it.
An account administrator can add it under AWS resources.

From the "Add this knowledge base to AWS resources" link, you are taken to a screen to grant access to this knowledge base to the QuickSight service role.

QuickSight access to AWS services

QuickSight requires bedrock:Retrieve and bedrock:GetDocumentContent for the Bedrock knowledge base. Registering on this screen causes QuickSight to attach an IAM policy scoped to the relevant ARN to the service role. After saving, I entered the knowledge base name and description and created it.

Let's verify the settings of the created knowledge base.

% aws quicksight describe-knowledge-base \
  --aws-account-id 123456789012 \
  --knowledge-base-id ced09646-bb81-4528-8bd0-89682a7c8ffc \
  --region us-east-1

Name        : mkb-acl-blog-acl-enabled
Type        : FULLY_MANAGED_KNOWLEDGE_BASE
Status      : ACTIVE
Template    : {"templateConfiguration": {"template": {"type": "BEDROCKFMKB", "retrievalScope": "ALL", ...}}}
ACL config  : {"isACLEnabled": false}

What is notable is that isACLEnabled is false. In the QuickSight S3 knowledge base verified last time, it was necessary to set this flag to true to enable ACL. When going through a Managed Knowledge Base, since both the ACL definition and evaluation are on the Bedrock side, the QuickSight flag remains false.

The documentation also states that "no additional configuration on the Amazon QuickSight side is required to enable ACL support," which is consistent with this display.

Step 13: Verify with QuickSight chat

In the chat screen's data source selection, specify the created knowledge base. First, I ask about ALPHA-GLOBAL-ALLOW, which was given ALLOW.

ALLOW document is answered

holiday-policy.md was cited, and the holidays were returned in table format.

Next, I ask about BRAVO-GLOBAL-DENY, where I set DENY for myself.

DENY document is not answered

No information was found regarding the document with management number BRAVO-GLOBAL-DENY.
Access to the relevant document may be restricted, or it may not be included in the currently available data sources.

Please check the following:
- Whether the management number is correct
- Whether you have access permissions to the relevant document

The 12.5% revision rate was not returned. Despite not having configured any ACL settings on the QuickSight side, the DENY on the Bedrock side is affecting the chat responses.

I asked about the ALLOW / DENY with the metadata method and the priority verification document all in one question.

Verifying metadata method and priority

1) Management number DELTA-META-ALLOW — Date and time of occurrence in the incident report
✅ Was able to reference. The date and time of occurrence for Security Incident Report #4210 is August 24, 2026 at 14:32.

2) Management number ECHO-META-DENY — Estimated valuation
❌ Could not reference. The relevant document was not found. Access may be restricted or it may not be included in the available data sources.

3) Management number HOTEL-OVERRIDE — Verification value
✅ Was able to reference. The verification value listed in the priority verification document is OVERRIDE-VALUE-7788.

The result is the same as the CLI retrieve. HOTEL-OVERRIDE, which was DENY in the global ACL, is accessible because the ALLOW from the metadata file took effect.

Finally, I ask about the 2 documents that were not ingested.

Documents that were not ingested

Neither CHARLIE-GLOBAL-NOACL nor GOLF-GROUP-TYPE could be referenced. Both a missing ACL entry and an unsupported Type: GROUP end up at the same result of "cannot be referenced."

Step 14: Verify ACL from the console

Opening the data source details in the Bedrock console displays the ACL configured via CLI as-is.

Data source ACL settings

"Crawl access control list (ACL): Enabled" and "Global ACL file S3 URI" can be confirmed.

In the sync history, the breakdown of 8 scanned, 5 added, and 3 failed is visible in a list.

Sync history

The content displayed by "Show warnings" was only a single line message for the entire operation.

The sync completed with partial failures. Some documents could not be crawled.

In the QuickSight S3 knowledge base verified last time, per-document failure reasons (No ACL entries found for document in ACL-enabled data source.) were displayed in the sync report. With this Managed Knowledge Base, which file failed and why is not discernible from this screen. Even querying individually with get-knowledge-base-documents only returns NOT_FOUND.

% aws bedrock-agent get-knowledge-base-documents \
  --knowledge-base-id O3VLSO3CS9 --data-source-id YBNLLH2UBM \
  --document-identifiers '{"dataSourceType":"S3","s3":{"uri":"s3://mkb-acl-blog-987164eb/group/board-minutes.md"}}' \
  --region us-east-1

  status: NOT_FOUND
  reason: (none)

At the bottom of the same screen is "Document access control," where the two CLI APIs are available directly as a GUI.

Check document access

Enter the Document ID and user's email address, then press "Check access" to get the verdict.

ishikawa.satoru@classmethod.jp does not have access to this document.

"Get document access list" displays the list of ACLs set for the document. Here are the results run on override/override-doc.md.

Get document access list

Access identifier Type Access
xxxxxxxxxx@classmethod.jp User Allow

There is only one entry. The DENY written in the global ACL file does not appear here either. This is consistent with what was confirmed via CLI.

Discussion

Let me organize what I learned from the verification.

The main arena of ACL moves from QuickSight to Bedrock

When using a Managed Knowledge Base, there are no ACL-related settings on the QuickSight side at all. QuickSight's isACLEnabled remains false, and the global ACL file and metadata files are referenced by the Bedrock data source configuration. What QuickSight handles is only passing the user's ID at query time.

This structure has advantages. When using the same Bedrock knowledge base from applications other than QuickSight, ACL definitions can be consolidated in one place. Conversely, control using QuickSight groups becomes unavailable.

Groups cannot be used with the S3 connector

Documents with Type: GROUP written were not ingested. The result is the same even when USER ALLOW is written alongside it in the same metadata file. In the previous QuickSight S3 knowledge base, QuickSight group names could be specified, so this is a clear step back.

When you want to control by group unit with Bedrock Managed Knowledge Base, you would use one of the SharePoint, OneDrive, Google Drive, or Confluence connectors. These crawl group membership from the connected source and resolve user affiliations at query time. As long as you target files placed in S3, permissions can only be expressed by enumerating email addresses.

Metadata files override the global ACL file

As documented, it was a gain to actually confirm the behavior. Since the denyList itself disappears, the operation of "broadly DENY globally, then individually ALLOW for exceptions" works.

However, the flip side is that placing a single metadata file can invalidate the global-side restrictions. Even if you restrict write permissions to the ACL file, anyone who can place a .metadata.json in the same location as the document can circumvent it. The write permissions to the bucket itself need to be part of the design scope.

There is a pathway for the ACL definition file to leak

When you create a knowledge base without ACL pointing to the same bucket, global-acl.json appears in search results. This single file enumerates who can access which prefix. Even if the confidential documents themselves cannot be read, the organization's permission structure can be read.

As a countermeasure, the straightforward approach is to exclude the ACL file from the data source's inclusionPrefixes or explicitly exclude it with exclusionPrefixes. However, since the ACL file must be placed in the same bucket as the data source, the option of using separate buckets is not available.

Investigating ingestion failure causes takes more effort than before

In the previous QuickSight S3 knowledge base, per-document failure reasons were explicitly shown in the sync report. With this Managed Knowledge Base, there is no information beyond "partial failure" in either the console or the API. Even if you suspect a missing ACL entry, you need to manually take the difference between "the list of files placed in S3" and "the list of indexed files" to find out which files failed.

The sync history screen has a link to CloudWatch Logs, so if log delivery is configured, there is a possibility of tracing it. It was not configured this time, so this could not be confirmed.

ACL can now be verified from the CLI

Previously the only means was the console's Permission Checker, but this time check-ingested-document-acl and get-ingested-document-acl are available from the CLI. I think it is significant that it has become possible to mechanically verify "whether the intended permissions are actually applied" in CI.

Only knowledge base creation cannot be completed via CLI

On the QuickSight side, data source creation passes via CLI but knowledge base creation alone is rejected. The error message says "Other published knowledge base APIs support this connector type," so I look forward to creation support in the future.

Frequently Asked Questions

Q. Which should I use: the previous QuickSight S3 knowledge base ACL or this one?

A. It depends on the use case. Organizing the differences between the two:

Aspect QuickSight S3 knowledge base Via Bedrock Managed Knowledge Base
ACL configuration location QuickSight knowledge base Bedrock data source
QuickSight's isACLEnabled true required Remains false
Type: GROUP Usable Not usable (S3 connector)
Changing ACL enable/disable Cannot be changed after creation Unverified
Access permission verification Console Permission Checker Both CLI and console
Ingestion failure reason Displayed per document in sync report Only an overall warning
Tokyo Region Available Not available
Reuse from other apps QuickSight-exclusive Any application that can call Bedrock

If you are working exclusively within QuickSight and need group-level control, the former is appropriate. If you plan to use the same knowledge base from applications other than QuickSight, the latter is suitable. If you want to use it in the Tokyo Region, you can only choose the former.

Q. How should I verify that ACL is correctly configured?

A. You can check per-user access permissions with check-ingested-document-acl and examine the contents of the ingested ACL with get-ingested-document-acl. Since both look at the content stored in the index, you can see the "ACL that is actually in effect" rather than the "ACL you intended to write."

ACL configuration mistakes do not appear as errors at search time. Results just decrease or become zero. When results differ from expectations, the quickest path is to first check the ACL itself with these two APIs.

Q. Can ACL settings be changed later?

A. For changes to ACL file contents, they are reflected by re-syncing the affected prefix. Changing one line in the global ACL file targets everything under that prefix. Keeping frequently changing permissions in the metadata file side allows you to keep the re-indexing scope smaller.

On the other hand, whether you can switch ACL itself between enabled and disabled after the fact was not verified this time. In the previous QuickSight S3 knowledge base, it was rejected with the error ACL enablement cannot be changed. Since Bedrock's Managed Knowledge Base has ACL settings on the data source side, the situation may differ, but I will avoid asserting this since it has not been confirmed. Until your approach is finalized, it is safer to test with a verification knowledge base before creating a production one.

Q. How much of the authentication does Bedrock handle?

A. Nothing at all. The documentation repeatedly states "ACL awareness is not authorization." Bedrock does not verify whether the provided email address is genuine. The prerequisite is that authentication is completed on the application side and verified IDs are passed.

In this configuration, QuickSight plays that role. Since the ID of the user signed in to QuickSight is passed to Bedrock, users cannot impersonate arbitrary email addresses. On the other hand, when building an application that directly calls the retrieve API, controlling what to put in userContext is the responsibility of the implementer.

Closing Thoughts

I connected Amazon Bedrock Managed Knowledge Base as a knowledge source for Amazon QuickSight and tested the document-level ACL of the S3 connector from both the AWS CLI and the console.

For the same documents in the same bucket, the DENY document was not returned from the knowledge base with ACL enabled, while it was returned from the knowledge base without ACL — a difference confirmed via the CLI's retrieve. Connecting the ACL-enabled one to QuickSight and asking questions via chat also did not return the contents of DENY documents. Despite not having configured any ACL settings on the QuickSight side, the permissions on the Bedrock side take effect as-is.

Compared to the QuickSight S3 knowledge base verified last time, you lose the ability to use groups, but you gain the ability to verify ACL from the CLI, and the knowledge base itself can be reused from applications other than QuickSight. Rather than one being superior to the other, the difference between holding the knowledge base as a QuickSight-exclusive asset versus holding it as a shared asset across multiple applications seems to directly manifest as a difference in where the ACL is designed.

We look forward to availability in the Tokyo Region, but for those considering configurations that use the same knowledge base from applications other than QuickSight, I think it would be good to start designing with the understanding that permission expression is limited to enumerating email addresses.

Share this article

AWSのお困り事はクラスメソッドへ