
Bedrock Meter — a self-hosted governance gateway for Amazon Bedrock
A self-hosted gateway that puts per-user API keys, hard budgets, and usage metering in front of Amazon Bedrock — so a whole team can use Bedrock without anyone holding a long-lived AWS credential. The only AWS credential is a short-lived ECS task role held by the gateway; developers get revocable, budget-capped keys. Covers the architecture, the two-credential-plane security model, admin and self-service onboarding, and an in-region deployment walkthrough.
Overview and context
Development teams increasingly want a capable AI coding assistant such as Codex. By default, Codex talks to OpenAI's hosted API, where OpenAI runs the model on its own infrastructure and determines where that processing happens. For a regulated or public-sector organization, that can mean source code, prompts, and proprietary context are processed outside your control and potentially outside your country.
Running Codex (and other OpenAI-compatible clients) against Amazon Bedrock removes OpenAI's hosted API from the request path: the model runs in your AWS account, in the region you choose — for example the India regions
ap-south-1 (Mumbai) or ap-south-2 (Hyderabad). Bedrock Meter is the governance layer that makes handing out that access manageable at scale.How this is designed to support data residency
- In-region inference — model calls are made to a Bedrock endpoint in your chosen region, so prompts and completions are processed within that region.
- In-region state and audit — the data Bedrock Meter stores (users, keys, budgets, usage, and the optional prompt/completion audit archive) is configured to reside in the same region.
- Your account, your controls — data stays in your AWS account, under your IAM, encryption (KMS), logging (CloudTrail), and retention settings.
- Reduced credential exposure — end users are not issued AWS credentials; they reach only your in-region gateway over a controlled network path.
Residency outcomes depend on your configuration and the AWS services' own regional behaviour — validate against your requirements.
Challenges this addresses
- Credential sprawl — handing each developer AWS keys or a long-lived token spreads credentials across workstations and scripts: hard to rotate and easy to leak.
- Limited per-user cost control — Bedrock billing is pooled at the account level. Native options can approximate per-user limits, but they are operationally heavy and not a simple real-time hard cap on an individual's spend.
- Limited usage visibility — native billing does not easily answer "who used what, on which model, this period?" — the attribution teams want.
- Data residency needs — regulated workloads often require requests and usage data to remain within a specific region.
- Onboarding & self-service — adding developers, issuing and rotating their keys, and letting each person see their own usage should be simple admin and self-service actions, not a ticket for every change.
- Limit-increase workflow — when someone reaches their cap, they need a clear way to request more capacity and for an admin to review and raise it, without a service restart or redeploy.
How it works
Bedrock Meter is a thin, self-hosted gateway between the client and Bedrock. Every request passes through it, so it becomes the single point where identity, budget, and metering are applied. The client is not issued an AWS credential — it carries only a Bedrock Meter key (
bm_…) that the gateway can revoke, budget, and meter.- Per-user API keys — each developer receives their own
bm_bearer token, independent of anyone else's. - Configurable budgets — requests are blocked when a user's configured budget for the period is exhausted.
- Usage metering — per-user token and cost tracking, with admin drill-down.
- In-region by configuration — state and inference are configured to stay in the chosen region.
- Admin & self-service consoles — admins onboard users and manage budgets; each user has a self-service console to view their own usage and rotate their key.
- Budget-increase requests — users request more capacity from their console; an admin reviews and raises the cap for the current period without a restart.
Architecture
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Codex / OpenAI-compatible client
| Authorization: Bearer bm_<key>
v
Application Load Balancer (restricted to an allowed CIDR)
v
ECS Fargate — Bedrock Meter service (runs as the task role)
| 1. resolve the bm_ key -> which user?
| 2. check the user's remaining budget
| 3. translate the request to Bedrock Converse
| 4. call Bedrock over SigV4 using the Fargate TASK ROLE
| 5. record token + dollar usage against the user
v
Amazon Bedrock (SigV4 - no static token, stays in-region)
State: DynamoDB (users, keys, budgets, usage) | Secrets Manager | S3 audit (private)The client never talks to Bedrock directly and never holds an AWS credential; the gateway signs the Bedrock call with its own short-lived task role.
Design approach
Bedrock Meter deploys as a single, isolated CDK stack into your existing VPC.
Request lifecycle
- Authenticate — the gateway hashes the presented
bm_key and resolves the owning user. Unknown or inactive keys are rejected first. - Budget check — the user's remaining budget for the current period is evaluated; if the cap is reached, the request is refused with a clear error and nothing is billed.
- Translate — the OpenAI
/v1/responsesrequest is mapped to Bedrock's Converse API (streaming or non-streaming). - Invoke — Bedrock is called over SigV4 using the Fargate task role; no static token is in the request path.
- Meter — token usage from the response is converted to integer micro-dollar amounts and recorded against the user; budget warnings fire at the configured threshold.
- Audit — prompt/completion records are archived to a private S3 bucket for traceability.
What runs where
- Application Load Balancer — restricts reachability to an allowed CIDR.
- ECS Fargate (container) — the gateway: auth, budgets, metering, translation.
- DynamoDB (7 tables) — users, keys, budgets, usage.
- Secrets Manager (4 secrets) — peppers, session, bootstrap.
- S3 (private) — prompt/completion audit archive.
- Amazon Bedrock — model calls via the SigV4 task role.
Authentication and access controls
Bedrock Meter uses two separate credential planes. Understanding the split explains the intended security characteristics of the design.
Plane A — User to Gateway: the
bm_ API key- Each user is issued a high-entropy, URL-safe key with a recognizable
bm_prefix. - The raw key is shown once at issuance and is not retained in the system afterwards.
- What is stored is a keyed hash (HMAC-SHA-256) under a server-side pepper held in Secrets Manager — not the raw key. A datastore compromise alone does not reveal the high-entropy keys without also obtaining the pepper.
- Keys are per-user and revocable: disabling a key cuts off that user without affecting others and without an AWS change.
- Console passwords are hashed with the scrypt KDF; sessions use stateless HMAC-signed cookies.
Plane B — Gateway to Bedrock: the Fargate task role (SigV4)
- The gateway calls Bedrock using the ECS Fargate task role via the AWS SDK's default credential provider chain. The container assumes the role automatically; the credentials are short-lived and rotated by AWS.
- There is no code path that accepts a static Bedrock bearer token, so the gateway is not designed to store or distribute one.
- The task role is configured as invoke-only:
bedrock:InvokeModel,bedrock:InvokeModelWithResponseStream,bedrock:Converse,bedrock:ConverseStream. It does not include model management,iam:*, orsts:AssumeRoleescalation. The Bedrock statement uses a wildcard resource because geographic inference profiles require it; the constraint comes from the limited action set, not a resource ARN. DynamoDB, Secrets Manager, and S3 permissions are each resource-scoped to this stack's own resources.
Why a task role at Fargate plus keys at the gateway is a sound pattern
The powerful AWS credential lives in one place — the Fargate task role, which no person holds directly. The credentials distributed to people (the
bm_ keys) are deliberately limited: they work only against your gateway, carry a configured budget, are individually revocable, and grant no direct AWS access. By contrast, issuing every developer an AWS key or a Bedrock token spreads long-lived, hard-to-rotate credentials across many workstations, each with account-level reach and no spend cap.Supporting controls
- Network — the load balancer is restricted to a specified CIDR; the service accepts traffic only from the load balancer.
- Secrets — four generated secrets reside in Secrets Manager and are injected into the task as ECS secrets rather than written to the environment in plaintext.
- Data residency — DynamoDB, S3 audit, Secrets, and inference are configured to the chosen region.
User onboarding (admin console)
A note on how developers are added, since it is the day-to-day workflow:
- An admin creates a user in the console (
/admin). If no password is given, the system generates a temporary password and sets the user to must-change-on-first-login (24-hour expiry). - A welcome email is sent with the username and temporary password (optional SES; the gateway degrades gracefully to no email if SES is not configured).
- On first login, the user is redirected to change their password; the temp flags are then cleared.
- The admin issues the user's
bm_API key, which is shown once in the console. (The key is handed over through the console, not emailed.) - Users can rotate their own key and submit budget-increase requests from their self-service console; admins review and approve, raising the cap without a restart.
Installation steps
Prerequisites: an AWS account with Bedrock model access enabled; an existing VPC with appropriate subnets and connectivity for your chosen ALB scheme; AWS CLI with CloudFormation deploy permissions; the CDK CLI (npm install -g aws-cdk), Docker, and Node.js 20+.
End-to-end deployment typically takes around 15 minutes. Replace the
<PLACEHOLDERS> with your own values.1. Clone and install dependencies
1
2
3
git clone https://github.com/tripathihsr/bedrock-meter.git
cd bedrock-meter
npm install2. Run the tests (recommended)
1
2
npm test
# Expected: service tests + infra tests, all passing3. Build and push the container image
The gateway runs as a container on Fargate. Build the image from the repo
Dockerfile and push it to ECR; the image URI is supplied to the stack at deploy time.1
2
3
4
5
6
7
8
9
10
11
12
# Create the ECR repo once
aws ecr create-repository --repository-name bedrock-meter --region ap-south-1 \
--image-scanning-configuration scanOnPush=true
# Log in to ECR
aws ecr get-login-password --region ap-south-1 | \
docker login --username AWS --password-stdin <ACCOUNT_ID>.dkr.ecr.ap-south-1.amazonaws.com
# Build (linux/amd64 for Fargate) and push
docker build --platform linux/amd64 -t bedrock-meter:v1 .
docker tag bedrock-meter:v1 <ACCOUNT_ID>.dkr.ecr.ap-south-1.amazonaws.com/bedrock-meter:v1
docker push <ACCOUNT_ID>.dkr.ecr.ap-south-1.amazonaws.com/bedrock-meter:v14. Bootstrap CDK (once per account/region)
1
npx cdk bootstrap aws://<ACCOUNT_ID>/ap-south-15. Deploy the stack
Creates the load balancer, ECS Fargate service, the DynamoDB tables, the Secrets Manager secrets, the S3 audit bucket, and the scoped task role. The load balancer is restricted via
allowedCidr; supply your VPC and subnet IDs to deploy into your network.1
2
3
4
5
6
7
8
9
10
11
cd packages/infra
npx cdk deploy BedrockMeterStack \
--region ap-south-1 --require-approval never --no-lookup \
-c albScheme=internet-facing \
-c allowedCidr="<YOUR_ALLOWED_CIDR>" \
-c vpcId=<YOUR_VPC_ID> \
-c publicSubnetIds=<SUBNET_1>,<SUBNET_2>,<SUBNET_3> \
-c privateSubnetIds=<SUBNET_4>,<SUBNET_5>,<SUBNET_6> \
-c availabilityZones=ap-south-1a,ap-south-1b,ap-south-1c \
--parameters containerImage=<ACCOUNT_ID>.dkr.ecr.ap-south-1.amazonaws.com/bedrock-meter:v1Deploy takes ~3–4 minutes. Note the load balancer DNS from the stack — that is your gateway endpoint. (Optional: add
-c sesFromAddress=<verified-sender> to enable email notifications; omit to run without email. For an internal-only deployment, set -c albScheme=internal.)6. First-run setup
A one-time bootstrap that creates your admin account, protected by a bootstrap secret that only someone with access to Secrets Manager can read.
1
2
3
4
5
6
7
8
9
10
# Open the setup page
http://<ALB_DNS>/setup
# Read the one-time bootstrap secret from Secrets Manager
aws secretsmanager get-secret-value \
--secret-id $(aws cloudformation describe-stack-resources \
--stack-name BedrockMeterStack --region ap-south-1 \
--query "StackResources[?contains(LogicalResourceId,'BootstrapSecret')].PhysicalResourceId" \
--output text) \
--region ap-south-1 --query SecretString --output textFill in the form: admin username/password, the bootstrap secret, region, model IDs, a price map (micro-dollars per input/output token), and budget defaults, then submit.
7. Create a user and issue an API key
- Sign in at
/loginwith your admin credentials. - In
/admin, create a user (username + temporary password; a welcome email is sent if SES is configured). - Set that user's budget (amount per period).
- Issue an API key — copy the
bm_key immediately; it is shown only once.
8. Configure the client (Codex)
Point the client at the gateway. The only credential it holds is the
bm_ key.1
2
3
4
5
6
7
8
9
10
model = "in.openai.gpt-5.6-terra"
model_provider = "bedrock-meter"
model_reasoning_effort = "high"
[]
name = "Bedrock Meter Gateway"
base_url = "http://<ALB_DNS>/v1"
experimental_bearer_token = "bm_<YOUR_API_KEY>"
wire_api = "responses"9. Verify
1
2
3
4
5
6
7
8
curl -s http://<ALB_DNS>/healthz
# {"status":"ok","version":"...","uptimeMs":...}
curl -s -X POST http://<ALB_DNS>/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer bm_<YOUR_KEY>" \
-d '{"model":"in.openai.gpt-5.6-terra","input":"say hi"}'
# {"status":"completed","output_text":"Hi!",...}Production network access
In production, restrict the load balancer to a stable CIDR your organization already controls, set once at deploy time via
allowedCidr — ideally the egress CIDR of your corporate VPN or Direct Connect, so access follows the organization's network path rather than a personal IP. For an internal-only deployment, -c albScheme=internal keeps the load balancer off the internet.Optional: TLS / HTTPS
For an internet-facing deployment, or any time you want encryption in transit as an additional safeguard, terminate TLS at the load balancer with an ACM certificate on a domain you control. It is optional for a VPN-fronted internal deployment but recommended as defense-in-depth.
Summary
Bedrock Meter turns "give developers Bedrock access" into a governed, metered, in-region service. The design keeps a single powerful AWS credential within the Fargate task role and distributes only limited, bounded, revocable
bm_ keys to people.- Reduce credential sprawl — users get revocable
bm_keys with no direct AWS access; the AWS credential stays in the task role. - Per-user cost control — configurable budgets block requests when a cap is reached.
- Usage visibility — per-user token and cost metering with admin drill-down.
- Bedrock access control — SigV4 via an invoke-only task role (no model management or IAM/STS escalation); no static tokens.
- Data residency — state and inference configured to the chosen region.
- Onboarding & self-service — admin consoles to onboard users and manage budgets; a per-user console for usage and key rotation.
- Capacity requests — users request a budget increase; admins review and raise the cap without a restart.
Pair this with the direct Codex on Amazon Bedrock provisioning bundle: github.com/tripathihsr/codex-on-bedrock .
Get the code: github.com/tripathihsr/bedrock-meter
Provided as-is for technical guidance; validate against your own security, privacy, and regulatory requirements. Amazon Bedrock, AWS, OpenAI Codex, and related marks are the property of their respective owners.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article