Bedrock Web Search in GovCloud: an ADR on grounding under compliance
Listen to article
generated on playGenerated only on first play
Powered by Amazon Polly + OmniVoice
On September 2, 2026 AWS brought the server-side Web Search tool on Amazon Bedrock to GovCloud (US-West). I wrote this ADR because the real decision is not "web search or no web search" — it is which retrieval mode you operate in, and the default of the `external_web_access` parameter pushes regulated teams to exactly the wrong side, with an HTTP 200 and no visible error.
The AWS note dated September 2, 2026 carries two lines of substance: the built-in Web Search tool on Amazon Bedrock now exists in GovCloud (US-West), and it keeps request data inside the AWS boundary by default. For anyone designing AI platforms in regulated environments — government, but equally banking, insurance, payments — this is not "one more feature". It is the difference between an agent that cites verifiable sources and an agent answering from memory about prices, service limits and regulation that changed last week. Except adoption is not a binary decision, and the path of least resistance is the wrong one. This is the decision record I would write before enabling it in production.
Context and forces
The problem that brought me here is old: LLM-assisted decision systems in regulated domains need provenance, not fluency. When an analyst asks "what is the current limit on that quota" or "is this product still authorized", an answer without an auditable link is a liability, not an asset.
Until now I solved it with one of two ugly options. First: my own index — crawler, normalization, embeddings, reindexing — which nobody wants to operate and which ages in days. Second: a third-party search API called from a Lambda, which means egress outside the account, a secret to rotate, an egress allowlist, and the tool-call loop written by hand. I ran exactly that design for months in my own content pipeline, with hand-rolled SigV4, and it broke for reasons that had nothing to do with the business problem.
The forces that weigh on this decision, hardest first: (1) query data must not leave the provider boundary without explicit control — in GovCloud that is a requirement, not a preference; (2) every claim shown to a user must carry a source; (3) cost is per query and is not negligible; (4) the team should operate neither index nor crawler; (5) authorization must be central, enforceable per Organization and per Region, with an audit trail. The tool landing in GovCloud addresses (1) and (4), but (2), (3) and (5) remain my work.
What the tool actually is — and where it lives
Before deciding, it is worth pinning down the object. Web Search is a server-side tool exposed only through the Responses API on the bedrock-mantle endpoint — it does not exist on bedrock-runtime, nor in Converse, nor in InvokeModel. You add {"type": "web_search"} to the tools array using OpenAI's own client library with a Bedrock API key, and the model decides whether to call it. That ties the decision to one model family: per the documentation, GovCloud (US-West) supports openai.gpt-5.6-terra, openai.gpt-5.6-luna and openai.gpt-5.4; commercial Regions add gpt-5.6-sol and gpt-5.5. There is no Claude, Nova or Mistral on this path.
Internally there are two operations, and that distinction is the heart of the ADR. Search returns title, URL and snippet from the web index and knowledge graph maintained by Amazon, and makes no outbound call. Fetch retrieves the content of a specific URL: with external_web_access=false it reads only the Bedrock cache; with true it checks the cache and reaches the external web only on a cache miss.
Retrieval is strictly regional — each Region runs its own search and fetch tier, and queries, fetches, index data and results are not routed across Regions. us-gov-west-1 is one of those Regions, alongside us-east-1, us-east-2 and us-west-2. Context volume per Search call is governed by search_context_size: low up to 5 observations, medium up to 11 (the default), high up to 25 — a budget shared across multiple queries in the same call. That parameter does not cap how many Search calls the model makes per turn.
Options considered
A. Native Web Search in cache-only mode (`external_web_access: false`)
- Request data does not leave the AWS boundary; Search comes from the Bedrock index and Fetch from cache only.
- Zero infrastructure: no crawler, no index, no tool-call loop, no third-party secret to rotate.
url_citationannotations with character offsets come from the model itself, ready to render as footnotes.- Requires no
ExternalWebAccesspermission, so it coexists with an SCP that denies it outright.
- Freshness is bounded by what is already in Amazon's index and cache — you do not control the refresh window.
- Locks grounding to the GPT models on the
bedrock-mantleendpoint.
Chosen.
B. Native Web Search with external web access (`true` + `ExternalWebAccess`)
- Best freshness: on a cache miss, Fetch pulls the page from origin.
- AWS's own documentation records an exfiltration risk: an agent can encode query data into a URL and have it fetched.
- Request data may leave the AWS boundary — unacceptable in the very perimeter that motivates GovCloud.
- The managed policies
...ExternalWebSearchReadOnlyand...FullAccesscurrently have identical effective permissions; "ReadOnly" restricts nothing.
Rejected for regulated workloads.
C. Bedrock AgentCore Web Search via Gateway
- Model-agnostic: serves agents on Claude, Nova or anything else, not just GPT.
- Lower per-query price — the AgentCore page lists US$ 7 per 1,000 queries.
- A surface to operate: Gateway, agent identity, request signing. I had exactly this and retired it.
- Governance lives in a different control plane, not in the three
bedrock-websearch:*actions.
Kept as the multi-model plan B.
D. Own index / search (crawler or third-party search API)
- Full source control: a real domain allowlist, which none of the native options delivers.
- Egress, secrets, retry, backoff, dedupe and the tool-call loop become your code — and that is where my incidents lived.
- Recurring operational cost with no proportional quality gain.
Rejected, unless source allowlisting is an explicit regulatory requirement.
Decision
Decision: adopt option A — native Web Search in cache-only mode — with the parameter set explicitly and the permission denied by SCP. The configuration is deliberately redundant, because the parameter and the permission fail in different ways.
In code, every caller sends tools=[{"type":"web_search","external_web_access":false,"search_context_size":"low"}]. low is my wrapper's default; medium is opt-in per research route; high requires justification, because 25 observations per call inflate input by something on the order of 10k tokens per turn.
At the perimeter, an SCP denies bedrock-websearch:ExternalWebAccess across the whole Organization. Note that IAM granularity here is short: the three actions (InvokeSearch, InvokeFetch, ExternalWebAccess) support only Resource: "*", and the sole condition key is the global aws:RequestedRegion. So the "restrict by Region" the announcement mentions is literally a StringEquals on aws:RequestedRegion — and I use it to pin us-gov-west-1 in the regulated perimeter, which is also the only way to stop a mis-routed call from being served by another regional tier.
On application roles I do not use AmazonBedrockFullAccess. I use a custom policy with InvokeSearch and InvokeFetch under the Region condition, because I want the absence of ExternalWebAccess to be a recorded decision, not a side effect of whichever managed policy somebody attached.
CloudTrail with data events enabled for bedrock-websearch is part of the decision, not a follow-up: without it, denials are reported nowhere.
One Web Search turn: two IAM gates and one boundary line
The path of a single Responses API call. Search never leaves the boundary; Fetch is the only point where egress is possible, and that is where the SCP cuts. Note the caller still gets a 200 even when Fetch is denied.
- Responses API · tools=[web_search]
- openai.gpt-5.6-terra · decides if it needs current info
- InvokeSearch · aws:RequestedRegion = us-gov-west-1
- InvokeFetch · fetchMode = USE_CACHE_ONLY
- ExternalWebAccess · DENIED by SCP
- Bedrock web index · + knowledge graph
- Bedrock page cache · title + URL + content
- External web origin · reached only on cache miss
- CloudTrail data events · fetchedSources, urlCount
- DynamoDB citation ledger · PK run#<id> / SK cite#<sha256(url)>
- UI footnotes · url_citation offsets
The consequence that bites: silent degradation with HTTP 200
external_web_access defaults to true, for compatibility with OpenAI's API. But AmazonBedrockFullAccess grants InvokeSearch and InvokeFetch and does not grant ExternalWebAccess. In that combination — the most common one — every Fetch attempt fails its backend authorization before the cache is read: you lose page content you would have had for free. And the Responses API request still returns HTTP 200, with url_citation annotations derived only from Search observations, with no guarantee the model mentions the failure. Grounding quality drops, nobody sees an error, no alarm fires. Worse: the presence of a citation does not prove the page was fetched. The only reliable evidence is the additionalEventData.fetchedSources field in CloudTrail — which exists only if you enabled data events for bedrock-websearch, which are off by default and billed separately.
Consequences: cost, latency, and what CloudTrail does not tell you
Cost. Billing is per query — a query is one search request. The AgentCore page lists US$ 7 per 1,000 queries for its variant; independent analyses put the built-in tool near US$ 12 per 1,000 (confirm the current rate for your Region on the Bedrock pricing page before modelling). The budget-breaking detail is not the unit price: it is that search_context_size caps observations per call but does not cap how many Search calls the model makes in a turn. A multi-hop turn that reformulates the query twice costs three queries. At US$ 0.012 per query, 50k turns/month at 3 queries is US$ 1,800/month in search alone, before inference — and before the input inflated by observations. Model per turn, not per query, and put a daily token and query ceiling in your own wrapper, on the application side, because IAM does not count calls.
Idempotency. The call is not idempotent: a retry re-runs the searches and bills again. My retry is max_attempts=2 with jitter plus a result cache keyed on sha256(prompt + modelId + toolConfig), TTL in hours. Caller-side timeouts must accommodate serial hops inside a single call — in my article generator I operate with a 300 s ceiling.
Audit. By design, CloudTrail does not expose query text, URLs or raw results: query text is treated like an inference prompt. You get who called, when, from where, fetchMode, urlCount, failedUrlCount and fetchedSources. If your regulator wants to know what the agent consulted, that answer is not in CloudTrail — it is in the citation ledger you write from the url_citation annotations.
Consequences by pillar
Security
Cache-only plus an SCP denying ExternalWebAccess closes the only egress path and the URL-encoded exfiltration vector AWS itself documents. In exchange, I accept that Resource is always * and the sole condition key is aws:RequestedRegion — there is no domain allowlist in IAM.
Reliability
The dominant failure mode stops being an error and becomes silence: denied Fetch with HTTP 200. I mitigate with CloudTrail data events and an alarm on the failedUrlCount / urlCount ratio, plus counting answers with zero url_citation annotations on routes that should cite.
Failure modes I watch once the decision is made
The ReadOnly illusion. AmazonBedrockExternalWebSearchReadOnly and AmazonBedrockExternalWebSearchFullAccess currently have identical effective permissions — the "ReadOnly" version does not restrict external retrieval. If your security review approves by policy name, it approved external web access believing it approved reads.
The illusion of restricting sources via IAM. Denying InvokeSearch while keeping InvokeFetch does not confine the model to your sources: Fetch applies to any URL the model produces, including one that came from the user's prompt or from its own parametric memory. Source control, if it is a requirement, must live in the application — validating each cited URL against an allowlist before display.
Prompt injection through retrieved content. Snippets and fetched pages are untrusted input entering the same context as the instruction. In regulated workloads I treat a Search observation as data, never as a command, and keep the injection filter on both sides of the turn — the same discipline I apply to any RAG.
Freshness that misleads. In cache-only mode, freshness is Amazon's index and cache, not the origin. For pricing, quotas and regulatory deadlines I do not trust what came back without checking the date in the answer itself — and I write around whatever I cannot confirm.
Model coupling. The tool exists only on the Responses API over bedrock-mantle, on GPT models. If tomorrow the model decision moves to Claude or Nova, the grounding path moves with it — which is why I kept option C alive in the record, not as a footnote.
I have lived this ADR from the wrong side. In my own content pipeline I kept a Lambda talking to a search AgentCore Gateway over hand-rolled SigV4, with a dedupe bug that always answered "this is new": no error, HTTP 200, and repeated articles on the same topic for days. I replaced it with provider-native search and the problem stopped being mine. It is precisely the same failure class as the denied Fetch returning 200 here — and the lesson I would apply again is this: whatever the HTTP response does not tell you, you have to measure from outside. Before enabling Web Search in any regulated account, I turn on CloudTrail data events for bedrock-websearch, pin external_web_access: false in code, deny ExternalWebAccess via SCP, and only then open the route — in the reverse order, you learn about the problem from a user complaint rather than an alarm.
Verdict
Adopt it — in cache-only mode, and treat the configuration as part of the control, not as a call-site detail. Web Search landing in GovCloud (US-West) closes a real gap: it gives citable grounding to compliance workloads with no crawler, no index and no egress, on GPT models that already had FedRAMP High and DoD IL-4/5 in GovCloud since June 2026. What does not solve itself is governance: external_web_access is born true, AmazonBedrockFullAccess does not grant the matching permission, and the combination degrades grounding while returning HTTP 200. Write false explicitly, deny ExternalWebAccess by SCP, pin the Region with aws:RequestedRegion, enable CloudTrail data events before the first request, and keep your own citation ledger — CloudTrail does not retain what was consulted. Done that way, it is the best grounding cost-benefit available today in a regulated environment. Done on defaults, it is a quality regression nobody will see.
References
Architecture, AWS, AI and market deep dives — straight to your inbox. Free.
No spam · unsubscribe anytime
Ask Fernando about this
Get a focused answer about this article from my AI assistant, grounded in my work.
Join the conversation
Sign in to comment
Verify your email to join in — you'll also get the newsletter. No password.
Keep reading
Architecture intelligence, in your inbox
Curated signals and original analysis on AWS, AI, distributed systems and the market — the way a solutions architect reads them.
- Curated AWS · AI · architecture · market signals
- New architecture studies & deep-dives when they ship
- Sharp summaries — depth without the noise
- No spam · double opt-in · unsubscribe anytime