Claude via Amazon Bedrock na conta AWS do cliente — onboarding IAM
BYOK 3 · ADR 0002, opção C · plano Enterprise (
FEATURE_LLM_BYOK_CLOUD)Clientes GCP: a mesma rota existe no Vertex AI, com impersonação de SA e sem chave JSON —
docs/BYOK_VERTEX.md. Um workspace tem uma rota cloud (Bedrock ou Vertex).
Com esta rota, relatórios, sugestões, sinais, assistente, base de conhecimento e varredura de
engenharia do BeryX correm no Claude da conta AWS da organização, numa região da UE.
Os dados e a fatura dos tokens ficam na conta do cliente. A BeryxTech não guarda segredos: guarda
o ARN de um role IAM e um ExternalId gerado por workspace, e obtém credenciais temporárias
(15 min, só em memória) com sts:AssumeRole.
Como funciona
BeryX (runtime da BeryxTech) Conta AWS do cliente (eu-*)
─────────────────────────── ───────────────────────────────────────────
ID token Google da SA do Cloud Run
│ sts:AssumeRoleWithWebIdentity
▼
role broker da BeryxTech ── sts:AssumeRole ─▶ role DataVaultBedrock
(LLM_BEDROCK_BROKER_PRINCIPAL) + ExternalId trust: principal da BeryxTech + ExternalId
policy: bedrock:InvokeModel* em eu.anthropic.*
credenciais temporárias ── SigV4 ──────────▶ bedrock-runtime.<eu-região>.amazonaws.com
(só em memória, ≤15 min) inference profile eu.anthropic.claude-…
- Residência: só são aceites regiões
eu-*e ids de modeloeu.anthropic.*(inference profiles da UE). A política é verificada no domínio e na infra (providers/bedrock.py), por isso uma configuração fora dela nunca chega a assumir o role. - Confused deputy: o
ExternalIdé aleatório e único por workspace (dv-…). Sem ele na trust policy, o AssumeRole é recusado, mesmo que alguém saiba o ARN. - Falha: se o role deixar de funcionar (trust policy alterada, modelo sem acesso, quota), a IA
degrada para os modos offline (heurísticas e templates) e o evento fica como
llm.byok_error. Nunca caímos para a conta da BeryxTech contra a escolha do cliente. - Precedência: uma rota Bedrock ativa substitui a chave Anthropic própria (BYOK 1), se existir.
- Model policy (BYOK 2): «IA desligada» e as restrições de modelos por tarefa continuam a aplicar-se. A policy pode recusar o modelo Bedrock numa tarefa, mas nunca o troca por outro.
Passo a passo
Em Equipa & Acessos → Claude via Amazon Bedrock (só administradores):
- Gerar ExternalId. A UI mostra o
ExternalIddo workspace e o principal da BeryxTech (o ARN que vai na trust policy). - Criar o role na conta AWS (Terraform abaixo) e colar na UI o ARN do role, a região e o modelo. Ao guardar, fazemos um health-check: AssumeRole + um pedido de 1 token ao modelo (o Bedrock não tem Models API, e esta é a única prova de que o role pode invocá-lo nessa região). O custo é de ~10 tokens, na conta do cliente. Se falhar, nada é guardado e a mensagem diz o que corrigir (trust policy, permissões, acesso ao modelo, região).
- Correr o golden set. O golden set das sugestões (13 casos, GOLDEN v1) corre no modelo
Bedrock escolhido, no worker (fila
llm.golden.requested). A rota só fica ativa se passar (limiarDATAVAULT_LLM_CLOUD_GOLDEN_THRESHOLD, por omissão 100%). Reprovado, fica emgolden_failed, com os casos que falharam, e a IA continua como estava. Os tokens do golden set são faturados na conta AWS e aparecem no FinOps (kind=golden). ComDATAVAULT_LLM_CLOUD_GOLDEN_JUDGE_MIN_SCORE> 0, o gate junta o juiz LLM do GOLDEN v2: 5 sugestões amostradas avaliadas pela cadeia dos relatórios, com a chave da BeryxTech, e a rota só ativa se a média das notas chegar a esse mínimo (sem fornecedor, reprova). O custo do juiz fica no FinOps como da BeryxTech (kind=golden,provider=anthropic).
Mudar o role, a região ou o modelo invalida a aprovação: o golden set tem de passar outra vez nesse modelo. Remover a rota é sempre possível, mesmo depois de um downgrade de plano.
Terraform (conta do cliente)
variable "datavault_principal_arn" {
description = "Principal AWS da BeryxTech (mostrado na UI, passo 1)"
type = string
}
variable "datavault_external_id" {
description = "ExternalId do workspace BeryX (mostrado na UI, passo 1)"
type = string
}
variable "region" {
description = "Região da UE onde o BeryX invoca o Claude"
type = string
default = "eu-central-1"
}
data "aws_caller_identity" "current" {}
data "aws_iam_policy_document" "datavault_trust" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = [var.datavault_principal_arn]
}
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [var.datavault_external_id]
}
}
}
resource "aws_iam_role" "datavault_bedrock" {
name = "DataVaultBedrock"
assume_role_policy = data.aws_iam_policy_document.datavault_trust.json
max_session_duration = 3600
}
# Mínimo: invocar os inference profiles Claude da UE (e os modelos de base para
# onde o profile encaminha, nas regiões da UE). Nada de listagens nem de gestão.
data "aws_iam_policy_document" "datavault_invoke" {
statement {
sid = "InvokeClaudeEuInferenceProfiles"
actions = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
resources = [
"arn:aws:bedrock:${var.region}:${data.aws_caller_identity.current.account_id}:inference-profile/eu.anthropic.claude-*",
"arn:aws:bedrock:eu-*::foundation-model/anthropic.claude-*",
]
}
}
resource "aws_iam_role_policy" "datavault_invoke" {
name = "DataVaultBedrockInvoke"
role = aws_iam_role.datavault_bedrock.id
policy = data.aws_iam_policy_document.datavault_invoke.json
}
output "datavault_role_arn" {
value = aws_iam_role.datavault_bedrock.arn # colar na UI, passo 2
}
Antes de aplicar, garantir no console do Bedrock (região escolhida) que o acesso ao modelo
Claude está concedido. Sem isso, o health-check devolve «O role não pode invocar este modelo».
Para restringir mais, trocar eu.anthropic.claude-* pelo id exato do modelo escolhido. O
CloudTrail do cliente regista cada sessão como datavault-<workspace_id>.
Modelos disponíveis
A allowlist é mantida pela BeryxTech (DEFAULT_BEDROCK_MODELS em
backend/app/infra/llm/providers/bedrock.py). Por omissão tem Claude Haiku 4.5, Sonnet 5 e
Opus 4.8, cada um com o seu inference profile eu.anthropic.…. Os ids mudam com as versões no
Bedrock, e por isso podem ser substituídos sem deploy com LLM_BEDROCK_MODELS (JSON
{"família": "eu.anthropic.…"}). Entradas fora de eu.anthropic.* são ignoradas. Confirmar
os ids no console (Cross-region inference) antes de oferecer um modelo novo. Um modelo
retirado da lista desliga a rota dos workspaces que o usavam (offline), até o admin escolher
outro e o golden set passar.
Operação (BeryxTech)
Imagem: o extra
[bedrock](boto3) entra nas builds (CONNECTOR_EXTRAS), e os casos do golden set são copiados para/app/golden. Semboto3, o health-check devolvesdk_unavailable(503 na API). O broker usa também ogoogle-auth, que obackend/requirements.lockda imagem já traz.Identidade de origem (role broker): o Cloud Run não tem identidade AWS e não põe o token da service account num ficheiro, por isso o
AWS_WEB_IDENTITY_TOKEN_FILEdo boto3 não serve. A api e o worker fazem a federação no código (backend/app/infra/llm/providers/bedrock_broker.py):- pedem ao servidor de metadados um ID token Google da SA do serviço (
datavault-apioudatavault-worker), com a audiênciaLLM_BEDROCK_BROKER_AUDIENCE; - trocam-no por credenciais de 1 h do role broker da BeryxTech, com
sts:AssumeRoleWithWebIdentityno STS deeu-central-1(uma região ativa por omissão, mesmo que o cliente use uma região opt-in). Ficam só em memória e renovam-se 20 min antes de expirarem; - assinam com elas o
sts:AssumeRolecomExternalIdno role do cliente, que dá as credenciais de 15 min (backend/app/infra/llm/providers/bedrock.py).
A trust policy do broker só aceita
accounts.google.com, com essa audiência e o uniqueId das duas SAs de runtime. O role só pode fazersts:AssumeRoleem roles de outras contas e com umExternalIddo BeryX (dv-*). Não há chaves de acesso AWS em lado nenhum, nem por workspace. O ARN do broker vai emLLM_BEDROCK_BROKER_PRINCIPALe é o principal que os clientes põem na trust policy (passo 1). Um broker que recuse deixa a rota emunavailable(503) e nunca emassume_role_denied, porque o erro está do lado da BeryxTech e não na trust policy do cliente. SemLLM_BEDROCK_BROKER_AUDIENCE(dev) não há federação, e o STS usa a cadeia por omissão do boto3 (AWS_PROFILE).- pedem ao servidor de metadados um ID token Google da SA do serviço (
Custo: o preço Bedrock depende da região e do contrato do cliente, e por isso o FinOps mostra os tokens com custo
—(nunca um número inventado) e remete para a fatura AWS.Rotulagem art. 50.º: os outputs saem com «Claude …, Anthropic via Amazon Bedrock».
Sem Message Batches: o Bedrock não tem a Batches API. Os relatórios agendados desta rota correm pelo pedido síncrono.
Ligar o broker em produção (owner, uma vez)
Com as credenciais AWS do owner na conta da BeryxTech (o script não guarda nenhuma) e o gcloud
com acesso ao projeto de produção:
AWS_PROFILE=<perfil do owner> ./deploy/aws_broker_bootstrap.sh --project <projeto de produção>
LLM_BEDROCK_BROKER_PRINCIPAL=<ARN que o script imprime> ./deploy/gcp_deploy.sh
bash deploy/validate_prod.sh
- O
deploy/aws_broker_bootstrap.shlê o uniqueId das SAsdatavault-apiedatavault-worker(gcloud iam service-accounts describe), cria ou atualiza o roleberyx-bedrock-broker(MaxSessionDuration de 1 h), confere-o e imprime o ARN. Com--checksó lê, e sai ≠ 0 se faltar alguma coisa ou se o role tiver outras permissões. Não cria OIDC provider, porque a AWS já conhece oaccounts.google.com. - O
gcp_deploy.shpõeLLM_BEDROCK_BROKER_PRINCIPALeLLM_BEDROCK_BROKER_AUDIENCEno env da api e do worker. A audiência éberyx-bedrock-brokerpor omissão, a mesma que o bootstrap põe na trust policy. Como o deploy repõe o env inteiro, a variável tem de vir em cada execução, tal como os price ids do Stripe. - O check «Bedrock: broker AWS» do
validate_prod.shlê o ARN e a audiência do env dodatavault-workere executa compython -m app.infra.llm.providers.bedrock_brokero jobdatavault-llm-models, que corre com a SA do worker: o job obtém o ID token e assume o broker. Não chama o Bedrock nem a conta de nenhum cliente. Sem o broker no worker, o check dá SKIP «broker por configurar». Configurado, um STS que recuse dá FAIL de postura. - Prova ponta a ponta, numa conta AWS de teste do owner. Com o acesso ao Claude pedido no
Bedrock numa região
eu-*, cria o role do Terraform com o ARN do broker como principal e oExternalIdque a UI mostra. Depois, num workspace Enterprise, segue o passo a passo até guardar a rota: o health-check (AssumeRole assinado pelo broker e 1 token ao modelo, ~10 tokens na conta de teste) tem de passar. Ovalidate_prod.shnão faz esta prova, porque precisava de uma conta de cliente.
A trust policy segue as fontes primárias lidas a 19/10/2026. O ID token de uma service account
traz o azp, e por isso a AWS compara accounts.google.com:aud com o azp (o uniqueId da SA),
accounts.google.com:oaud com o aud (a audiência) e accounts.google.com:sub com o sub (o
uniqueId): chaves de condição do IAM
e AWS Security Blog.
O role broker nunca se apaga. As trust policies dos clientes guardam o id interno do role, e um
role recriado com o mesmo nome já não lhes serve (elemento Principal do IAM).
API (admin)
| Método | Caminho | O quê |
|---|---|---|
GET |
/api/v1/workspace/llm-bedrock |
Estado, ExternalId, principal da BeryxTech, allowlist, último golden |
POST |
/api/v1/workspace/llm-bedrock/prepare |
Gera o ExternalId (idempotente) |
PUT |
/api/v1/workspace/llm-bedrock |
{role_arn, region, model}: grava se o health-check passar |
POST |
/api/v1/workspace/llm-bedrock/golden |
Pede o golden set (fila; inline sem mensageria) |
DELETE |
/api/v1/workspace/llm-bedrock |
Remove a rota (sem gate de plano) |
Estados: draft → pending_golden → golden_running → active | golden_failed. Erros
com code estável: region_not_allowed, invalid_role_arn, model_not_allowed (422),
assume_role_denied, model_denied, model_not_found (422), unreachable (502),
unavailable (503), not_prepared, not_configured, golden_running, already_active (409),
e 402 fora do Enterprise. Tudo fica em admin-events (llm_byok_bedrock_*, categoria sensível
byok), com a conta AWS, a região e o modelo, mas nunca o ExternalId.