Subchapter 36.25
references/bedrock-kb-migration/safety-and-iam.mdMarkdown28 KBView on GitHub
The safety contract for the whole migration: what it may and may not do, how it reads documentation, the nine non-negotiable guardrails, the owned-resource ledger, the pre-Implement gate, the refusal list, and the least-privilege IAM policies. Every phase inherits this file.
Scripts
Fetch Bedrock AgentAssets
Kb Shim PyThe migration provisions only. It builds a new Managed KB (MANAGED) alongside the user’s
self-managed KB (VECTOR; the host skill calls this a Customer-managed KB). It never ingests
data, cuts over traffic, decommissions, or mutates the original in any way. Ingestion, retrieval
validation, cut-over and decommission are user-run handoff steps.
On the IaC path this is stricter still: every phase provisions nothing — it reads IaC and writes a migrated IaC file for the user to review and deploy themselves.
Read/verify AWS docs with curl from the doc’s Markdown endpoint — never web_fetch, a browser,
or any other render tool. This is the only sanctioned method and applies to every phase. Every
docs.aws.amazon.com page has a plain-Markdown twin: swap the trailing .html for .md (same
path). It is clean, greppable text with stable anchors and no JS to render — which is why
web_fetch and browser tools fail on these pages. A few pages have no .md twin (a 404 on the .md
URL); fetch the .html page then.
Doc and price fetches are plain GET requests to AWS documentation and pricing hosts
(docs.aws.amazon.com, aws.amazon.com, pricing.us-east-1.amazonaws.com) only. Never add -d, -F,
-T, -X, -H or -K to a fetch, and never send anything from the customer’s files or account in one.
curl -sSL "https://docs.aws.amazon.com/bedrock/latest/userguide/<page>.md" # preferred
curl -sSL "https://docs.aws.amazon.com/bedrock/latest/userguide/<page>.html" # fallback (grep-able)If curl fails (network, 404, empty), report it and treat the fact as unverified — never guess.
“I couldn’t fetch it” ≠ “it isn’t supported.” Prefer the user’s Region’s behavior when a doc is
Region-specific.
create-*, update-*, put-*, tag-*) may target only an owned resource — never the user’s
original KB, its data sources, or any pre-existing/discovered resource. The agent provisions
only; it does not ingest, retrieve, or validate.aws sts get-caller-identity, aws bedrock-agent list-*/get-*. Every
mutating command and anything that could expose sensitive content (retrieve, reading
credential/secret files) must be displayed, labeled, and individually approved. Running the
provisioning script --execute is mutating and needs approval of that run; writing the script
and its DRY_RUN preview do not.main.tf line 2, password, has a
token in plain text”), and recommend moving it to AWS Secrets Manager — you do not have to mask it
or avoid repeating it. But the new deployable files you write (migrated IaC, provisioning
script, any note or plan that will be deployed or committed) must reference a secret only by its
Secrets Manager ARN and never contain the value — so a raw credential found inline is replaced
by a connectionConfiguration.secretArn reference, never copied forward. And never read a
secret’s value out of Secrets Manager yourself (granting the KB role secretsmanager:GetSecretValue
is a role permission, not the agent reading the value). Reference resources, keys and ARNs by
name/ARN. Recommend a least-privilege profile (read-only to discover/plan; scoped write to build).curl + .md) and/or aws bedrock-agent <command> --generate-cli-skeleton,
preferring the user’s Region. For an uncertain managed-KB setting, name the concrete path:
“validate via skeleton” (confirm the shape, no AWS change) or “validate via creation” (the
gated --execute create either succeeds = validated, or returns a precise error mapped via the
managed KB API (opens in a new tab)). Never resolve uncertainty
with a speculative throwaway call.Append every resource the migration creates to a ledger as it is created (KB id, data-source ids, tags). Mutation is permitted only against resources on this ledger. An ambiguous resource is not owned — do not mutate it. Reconcile the ledger against what the plan expected before any create, so the migration provisions exactly the expected set and no unplanned resources. The source KB and its data sources never appear on this ledger.
Before the first mutating action, confirm out loud that:
plan_generated is true — an advisory plan exists for this migration;If any is missing, produce it first. The gate is reported, never worked around silently.
Refuse, and hand back as a user step or redirect, when asked to (say the reason; do not ask for names or values first, and do not offer to do it later — a side request “unrelated to the migration” is covered too):
Small side requests. A small request outside the migration (general coding, a question about an unrelated service) that changes nothing in the customer’s account may be answered briefly, for example a short snippet the customer runs themselves. Then return to the migration step in progress. A side request never replaces or stalls the migration, and anything on the list above stays refused however it is framed.
Any read that returns document content — retrieve, retrieve-and-generate, get-document-content, or
s3:GetObject (or aws s3 cp) on the corpus documents — needs the customer’s explicit yes to that read
before it runs. Ask for the yes and give both reasons: the text lands in this conversation and its logs, and
the call runs under your role rather than an end user’s, so the document-level ACLs and metadata filters the
application enforces per user do not apply. A specific request for the text (“show me the full text of the
top document”), a query string, a Region or a general “go ahead” is not that yes; ask once and wait. The same
holds when you are the one offering the read: an option such as “give me a query and I’ll run Retrieve“
must ask the yes in the same breath (“do you want me to run it and show the text here?”), and a reply that
only supplies a query does not answer it. Configuration reads (get-* / list-* on the KB and its
data sources, object listings with sizes) return no document text and stay pre-approved. Policy A grants no
bedrock:Retrieve, so an approved retrieve also needs credentials that allow it; say so rather than
broadening the profile yourself.
Flag the credential posture at Identify, before any work. Use the least privilege each phase needs:
*FullAccess / service:* permissions — this governs what to request, not
a reason to refuse work. If the only session available is over-privileged (admin or full access),
flag it once and continue with the read-only phases; the read-only calls Discover and Plan make
are the same either way. Do not refuse discovery or planning because the session can do more than
they need. Only the KB-path build must run under the scoped build policy — request it at that
point, never earlier.A partial grant is a supported outcome, not a failure. Never respond to a denied action by asking for write or admin permissions.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BedrockKnowledgeBaseList",
"Effect": "Allow",
"Action": ["bedrock:ListKnowledgeBases"],
"Resource": "*"
},
{
"Sid": "BedrockKnowledgeBaseRead",
"Effect": "Allow",
"Action": [
"bedrock:GetKnowledgeBase", "bedrock:GetDataSource", "bedrock:GetIngestionJob",
"bedrock:ListDataSources", "bedrock:ListIngestionJobs"
],
"Resource": "arn:aws:bedrock:<REGION>:<ACCOUNT_ID>:knowledge-base/<SOURCE_KB_ID>"
},
{
"Sid": "VectorStoreConfigRead",
"Effect": "Allow",
"Action": [
"aoss:ListCollections", "aoss:BatchGetCollection", "aoss:BatchGetCollectionGroup",
"s3vectors:GetIndex"
],
"Resource": "*"
},
{
"Sid": "DiscoveryCostAnalysisReadOnly",
"Effect": "Allow",
"Action": [
"cloudwatch:ListMetrics", "cloudwatch:GetMetricStatistics", "cloudwatch:GetMetricData",
"pricing:DescribeServices", "pricing:GetAttributeValues", "pricing:GetProducts",
"ce:GetCostAndUsage"
],
"Resource": "*"
},
{
"Sid": "ServiceQuotasBedrockRead",
"Effect": "Allow",
"Action": ["servicequotas:GetServiceQuota"],
"Resource": "arn:aws:servicequotas:<REGION>:<ACCOUNT_ID>:bedrock/*"
},
{
"Sid": "DiscoverySourceBucketReadOnly",
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetBucketLocation"],
"Resource": "arn:aws:s3:::<SOURCE_BUCKET>"
},
{
"Sid": "PlannedRoleNameCheck",
"Effect": "Allow",
"Action": ["iam:GetRole"],
"Resource": "arn:aws:iam::<ACCOUNT_ID>:role/<PLANNED_ROLE_NAME>"
},
{ "Sid": "Identity", "Effect": "Allow", "Action": ["sts:GetCallerIdentity"], "Resource": "*" }
]
}Least privilege — scope the resource, not just the action (IAM best practices: apply least-privilege permissions (opens in a new tab)). Only
bedrock:ListKnowledgeBasesrequiresResource: "*"; the other Bedrock reads and scoped list actions in this policy support the Knowledge Base ARN according to the Amazon Bedrock Service Authorization Reference (opens in a new tab), so bind them toarn:aws:bedrock:<REGION>:<ACCOUNT_ID>:knowledge-base/<SOURCE_KB_ID>. If the source ID is not known yet, useknowledge-base/*only for the initial discovery pass, then narrow it. The AOSS configuration reads and the optional CloudWatch, Pricing, Cost Explorer and STS actions stay onResource: "*": Pricing and STS take no resource, and the resource types thatcloudwatch:GetMetricData/ListMetrics(dataset) andce:GetCostAndUsage(billingview) list cannot narrow a read to one knowledge base. Service Quotas reads are bound to the account’s Bedrock quotas (servicequotas:<REGION>:<ACCOUNT_ID>:bedrock/*), and source-bucket reads to the bucket ARN.PlannedRoleNameChecklets the plan confirm each planned role name is free (plan → “Planned names must be free”); list one ARN per planned role, or omit it when the plan creates no role.
The IaC path reads the source IaC — CloudFormation templates and any S3-stored source data — which
the statements above do not cover. Without these, a session provisioned with only the reads above hits a
source-read denial on the IaC path and ends BLOCKED. Add the applicable statement(s) below inside
policy A’s Statement array (omit both on the live-KB path, which never reads CloudFormation or S3 objects):
[
{
"Sid": "CloudFormationSourceReadOnly",
"Effect": "Allow",
"Action": [
"cloudformation:GetTemplate",
"cloudformation:DescribeStackResources"
],
"Resource": "arn:aws:cloudformation:<REGION>:<ACCOUNT_ID>:stack/<SOURCE_STACK_NAME>/*"
},
{
"Sid": "CloudFormationSchemaValidateReadOnly",
"Effect": "Allow",
"Action": [
"cloudformation:DescribeType",
"cloudformation:ValidateTemplate"
],
"Resource": "*"
},
{
"Sid": "S3SourceObjectReadOnly",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::<SOURCE_BUCKET>/<SOURCE_OBJECT_OR_PREFIX>*"
}
]Replace every placeholder before use. Include only the statement for the supplied source type. The IAM
action name is cloudformation:DescribeStackResources, not the CLI spelling describe-stack-resources.
The
DiscoveryCostAnalysisReadOnlystatement powers the optional cost-analysis gate (procedure in cost analysis (opens in a new tab)). This whole statement is optional — omit it when the user declines the cost-analysis gate. Every action is read-only.VectorStoreConfigReadis not optional: discovery reads the store’s configuration (its encryption key, for the cost path its collection group’s capacity limits, and for an S3 Vectors store its index dimension) whatever the cost gate says.
Adds exactly the create/poll permissions the KB-path Implement phase uses and, as defense in depth,
denies everything except six configuration reads on every source KB’s ARN. Never request admin/full
access. The agent’s mutating call set is exactly CreateKnowledgeBase then CreateDataSource; polling the
new KB uses the tag-gated OwnedKBCreateDataSourceAndPoll statement (policy A is scoped to the source KB,
so it cannot poll the new one). It therefore grants no UpdateDataSource (the script never updates a data source — an
interrupted build resumes via the idempotency token, it does not patch) and no StartIngestionJob
(ingestion is a user-run handoff step). It does grant bedrock:TagResource and iam:PassRole, but only
as the dependent permissions CreateKnowledgeBase requires — the ownership tag travels inline in the
create request’s tags field and the execution role is passed to the service on create; the script makes
no separate TagResource call.
Scope to ARNs where the action supports it, and tag-gate the one create that cannot be. The
Amazon Bedrock Service Authorization Reference (opens in a new tab)
(verified) states: bedrock:CreateKnowledgeBase has no resource type, so it takes Resource: "*", and
it supports the aws:RequestTag/${TagKey} and aws:TagKeys condition keys — so constrain it by
requiring an ownership tag on create and restricting the tag set. CreateKnowledgeBase also lists two
dependent permissions: bedrock:TagResource (the permission that authorizes applying the inline
create-time tag — there is still no separate TagResource API call; tags travel in the create request’s
tags field) and iam:PassRole (to pass the new KB’s execution role to Bedrock). bedrock:CreateDataSource
requires a knowledge-base* resource and supports aws:ResourceTag/${TagKey} — so scope it to the new
KB ARN and require the matching ownership tag on that KB. This policy is generated for one migration:
render the same fixed value into every occurrence of the <MIGRATION_ID> placeholder below (the ownership
tag value) and into the CreateKnowledgeBase inline tags map, so the created KB carries exactly the tag
the policy requires. The ownership tag key is bedrock-kb-migration-id (choose your own stable key and use
it everywhere). If your organization mandates additional tag keys on create, add each of them to the
aws:TagKeys list below so the tag-restricted creates are not denied.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BuildManagedKBCreate",
"Effect": "Allow",
"Action": ["bedrock:CreateKnowledgeBase"],
"Resource": "*",
"Condition": {
"StringEquals": { "aws:RequestTag/bedrock-kb-migration-id": "<MIGRATION_ID>" },
"ForAllValues:StringEquals": { "aws:TagKeys": ["bedrock-kb-migration-id"] }
}
},
{
"Sid": "TagManagedKBOnCreate",
"Effect": "Allow",
"Action": ["bedrock:TagResource"],
"Resource": "arn:aws:bedrock:<REGION>:<ACCOUNT_ID>:knowledge-base/*",
"Condition": {
"StringEquals": { "aws:RequestTag/bedrock-kb-migration-id": "<MIGRATION_ID>" },
"ForAllValues:StringEquals": { "aws:TagKeys": ["bedrock-kb-migration-id"] }
}
},
{
"Sid": "PassKBExecutionRole",
"Effect": "Allow",
"Action": ["iam:PassRole"],
"Resource": "<NEW_KB_EXEC_ROLE_ARN>",
"Condition": {
"StringEquals": { "iam:PassedToService": "bedrock.amazonaws.com" }
}
},
{
"Sid": "OwnedKBCreateDataSourceAndPoll",
"Effect": "Allow",
"Action": [
"bedrock:CreateDataSource", "bedrock:GetKnowledgeBase", "bedrock:GetDataSource",
"bedrock:ListDataSources", "bedrock:ListTagsForResource"
],
"Resource": "arn:aws:bedrock:<REGION>:<ACCOUNT_ID>:knowledge-base/*",
"Condition": {
"StringEquals": { "aws:ResourceTag/bedrock-kb-migration-id": "<MIGRATION_ID>" }
}
},
{
"Sid": "CMKGrantForNewKB",
"Effect": "Allow",
"Action": ["kms:CreateGrant"],
"Resource": "<KB_CMK_ARN>",
"Condition": {
"StringEquals": { "kms:ViaService": "bedrock.<REGION>.amazonaws.com" },
"Bool": { "kms:GrantIsForAWSResource": "true" }
}
},
{
"Sid": "SimulateOwnBuildPolicy",
"Effect": "Allow",
"Action": ["iam:SimulatePrincipalPolicy"],
"Resource": "<BUILD_PRINCIPAL_ROLE_ARN>"
},
{
"Sid": "ProtectOriginalKB",
"Effect": "Deny",
"NotAction": [
"bedrock:GetKnowledgeBase", "bedrock:GetDataSource", "bedrock:GetIngestionJob",
"bedrock:ListDataSources", "bedrock:ListIngestionJobs", "bedrock:ListTagsForResource"
],
"Resource": [
"<ORIGINAL_KB_ARN_1>",
"<ORIGINAL_KB_ARN_2>"
]
}
]
}Notes on the policy, all from the verified SAR:
bedrock:TagResource is a dependent permission of CreateKnowledgeBase, not a second call. Tags are
supplied inline in the create request’s tags field; the service authorizes applying them by checking
TagResource. The script issues no standalone TagResource call.<MIGRATION_ID> is one fixed value for this migration, rendered identically into all three
conditions (the two aws:RequestTag create conditions, the owned-KB aws:ResourceTag) and into the
CreateKnowledgeBase inline tags map — so the tag the policy demands is exactly the tag the KB is
created with. The key name bedrock-kb-migration-id is likewise a placeholder — pick one clear stable
key and use it in all four places (the two create conditions, the tag permission, and the data-source
aws:ResourceTag).iam:PassRole is scoped to the single new execution-role ARN and constrained with
iam:PassedToService: bedrock.amazonaws.com, so the role can be passed only to Bedrock.OwnedKBCreateDataSourceAndPoll is gated on aws:ResourceTag, so data sources can be created, and
the build polled, only on a KB carrying this migration’s ownership tag. It can be rendered before the run
(the new KB ID is not needed).ProtectOriginalKB is a NotAction deny: on every source KB it denies every action except the six
configuration reads it lists, so mutations, document ingest/delete, tagging and Retrieve on the original
are explicitDeny even for a principal that also holds broad permissions. It lists every source KB in
this migration, one ARN per source KB; a source KB left out of the list is not protected.CMKGrantForNewKB is needed only when the new KB uses a customer-managed KMS key; omit it otherwise.SimulateOwnBuildPolicy lets the build principal run the pre-execute check below on itself.--execute command, run a read-only
aws iam simulate-principal-policy --policy-source-arn <BUILD_PRINCIPAL_ROLE_ARN> --action-names bedrock:UpdateKnowledgeBase bedrock:DeleteKnowledgeBase bedrock:CreateDataSource bedrock:TagResource bedrock:Retrieve --resource-arns <ORIGINAL_KB_ARN_1> ... and show the result. Every action must return
explicitDeny on every original KB; if any does not, the build is BLOCKED (say which action and KB).iam:PassRole, pass the condition keys its statement tests
with --context-entries (for example aws:RequestTag/bedrock-kb-migration-id, aws:TagKeys,
iam:PassedToService, and any aws:PrincipalTag/... the policy reads). Without them a conditioned
allow returns implicitDeny, which does not show that the build would be refused. A session tag is
set when the session is issued: it does not appear in GetRole or ListRoleTags, so ask the customer
which tag the session carries instead of reading the role.BLOCKED before any write: say why, and offer this policy B for a
new build profile. Discovery and planning still run under it, read-only.knowledge-base/* with the same aws:ResourceTag condition. Check each ID with
list-tags-for-resource before deleting it. A build profile that already carries that tag-scoped
delete statement still meets this policy: it can delete only what this migration created, so it is not
a reason to block the build. Run no delete without the customer’s approval of that teardown.Building also needs an execution role for the new KB plus S3 / embedding-model permissions per the public create-KB prerequisites (the role passed via
iam:PassRoleabove). Confirm exact IAM action names in the public Service Authorization Reference for Amazon Bedrock before use.
The new KB’s execution role is assumed by the Bedrock service, and it must exist before
CreateKnowledgeBase runs (the create passes it via iam:PassRole). Because the role is created first,
its trust policy cannot yet name the new KB’s ARN — that id does not exist until the create returns. Its
trust policy is still where the service-principal confused-deputy guard belongs: aws:SourceAccount and
aws:SourceArn condition keys go on the sts:AssumeRole action in the role’s trust (assume-role) policy,
not scattered across identity or resource policies. For bootstrap, scope aws:SourceArn with ArnLike
over the account’s knowledge-base namespace so the role can be assumed for the KB about to be created, while
still refusing another account’s Bedrock resource:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "bedrock.amazonaws.com" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "aws:SourceAccount": "<ACCOUNT_ID>" },
"ArnLike": { "aws:SourceArn": "arn:aws:bedrock:<REGION>:<ACCOUNT_ID>:knowledge-base/*" }
}
}
]
}Put these two keys only in the trust policy, scoped to the service principal that assumes the role —
adding them indiscriminately to unrelated identity policies does not add protection and can silently deny
legitimate calls. After the KB is created, the customer may tighten aws:SourceArn to the specific
knowledge-base/<NEW_OWNED_KB_ID> ARN using ArnEquals; this skill does not mutate the customer’s
trust policy — recommend the tightening as a customer step. The role’s permissions (its identity
policy) separately grant the KB’s runtime needs — invoke-model on the embedding model only when the KB uses a
CUSTOM embedding model, S3 read on the source
bucket, and for auth connectors secretsmanager:GetSecretValue (+ kms:Decrypt) on the secret ARN — per
the public create-KB prerequisites; confirm each action name in the SAR.
| Need | Source |
|---|---|
| Read-only discovery calls | discovery (opens in a new tab) |
| Owned-only build (KB-path Implement) | implement KB (opens in a new tab) |
| IaC emission (provisions nothing) | implement IaC (opens in a new tab) |
| Cost analysis (measurement, provenance) | cost analysis (opens in a new tab) |
| API request shapes, error→cause→fix, connector auth | managed KB API (opens in a new tab) |
| self-managed ↔ Managed capability map | mapping (opens in a new tab) |
| IaC schema (CloudFormation + Terraform) | iac schema (opens in a new tab) |