This project documents my experience building a production-grade AI chatbot on AWS using Amazon Bedrock with Claude Haiku as the foundation model. The focus was not just on getting a working chatbot, but on securing every layer of the stack — from network edge to model invocation — using defense-in-depth principles aligned with the OWASP Top 10 for LLM Applications.
Application Architecture
The stack uses the following AWS services and tools:
- Amazon Bedrock — managed AI service hosting Claude 3 Haiku via the Converse API
- Bedrock Guardrails — content filtering, topic blocking (competitor mentions), and prompt attack detection
- AWS Lambda — Python 3.12 function connecting API Gateway to Bedrock
- Amazon API Gateway — REST API with POST /chat endpoint, usage plans, and API key throttling
- AWS WAF — Web ACL with OWASP Core Rule Set, rate limiting, and custom regex rules for prompt injection
- AWS IAM — least-privilege policies with explicit deny rules to protect guardrail and logging configurations
- Amazon CloudWatch — model invocation logging for observability
- AWS CloudTrail — audit trail for all Bedrock control-plane API calls
IAM Policy: Least Privilege with Explicit Deny
The IAM policy restricts the application to a single model in a single region and uses explicit Deny statements to prevent tampering with guardrails or logging configuration. In IAM, an explicit Deny always overrides any Allow — even if another policy grants the permission.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSpecificBedrockModels",
"Effect": "Allow",
"Action": ["bedrock:InvokeModel"],
"Resource": [
"arn:aws:bedrock:us-east-2::foundation-model/anthropic.claude-3-haiku-*"
],
"Condition": {
"StringEquals": {"aws:RequestedRegion": "us-east-2"}
}
},
{
"Sid": "DenyDestructiveGuardrailActions",
"Effect": "Deny",
"Action": [
"bedrock:DeleteGuardrail",
"bedrock:UpdateGuardrail",
"bedrock:PutModelInvocationLoggingConfiguration"
],
"Resource": "*"
}
]
}
bedrock:InvokeModel.
Adding bedrock:Converse to your policy will cause a validation error.
Bedrock Guardrails: Content Filtering
The guardrail is configured to block competitor product mentions (ChatGPT, Gemini, Grok, Copilot) and prompt attack attempts. The guardrail intercepts both input and output transparently when attached to the Converse API call.
response = bedrock.converse( modelId="us.anthropic.claude-3-haiku-20240307-v1:0", messages=[{"role": "user", "content": [{"text": user_message}]}], system=[{"text": "You are a helpful customer support assistant."}], guardrailConfig={ "guardrailIdentifier": GUARDRAIL_ID, "guardrailVersion": "DRAFT", "trace": "enabled" # logs guardrail decisions for debugging } )
When the guardrail intervenes, the response includes a stopReason
of guardrail_intervened,
and the Lambda function returns a generic safety message instead of the blocked content.
Lambda Function: Bedrock Integration
The Lambda function serves as the glue between API Gateway and Bedrock. It handles both
direct Lambda test invocations and API Gateway proxy events by checking for the presence
of a body key in the event.
import boto3 import json bedrock = boto3.client("bedrock-runtime", region_name="us-east-2") GUARDRAIL_ID = "1fxj69ja06s4" MODEL_ID = "us.anthropic.claude-3-haiku-20240307-v1:0" def lambda_handler(event, context): # Handle both API Gateway and direct test invocations if "body" in event: body = json.loads(event["body"]) else: body = event user_message = body["message"] response = bedrock.converse( modelId=MODEL_ID, messages=[{"role": "user", "content": [{"text": user_message}]}], system=[{"text": "You are a helpful customer support assistant."}], guardrailConfig={ "guardrailIdentifier": GUARDRAIL_ID, "guardrailVersion": "DRAFT", "trace": "enabled" } ) output = response["output"]["message"]["content"][0]["text"] stop_reason = response["stopReason"] if stop_reason == "guardrail_intervened": output = "[Request blocked by safety controls]" return { "statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": json.dumps({"response": output}) }
AWS WAF: Edge Protection
The Web ACL provides three layers of protection at the network edge, before requests even reach the Lambda function:
Rule 1 — AWS Managed Core Rule Set
The AWSManagedRulesCommonRuleSet blocks SQL injection, cross-site scripting (XSS), and other OWASP web vulnerabilities automatically.
Rule 2 — Rate Limiting
A rate-based rule limits each IP address to 50 requests per 5-minute window. This prevents abuse, brute-force attacks, and cost runaway from excessive API calls.
Rule 3 — Prompt Injection Regex Filter
A custom regex pattern set blocks common prompt injection phrases in the request body:
(?i)(ignore (all |previous |your |the )?instructions|system prompt|jailbreak|DAN mode|bypass (safety|guardrail|filter))
The (?i) flag
makes the pattern case-insensitive. Matched requests return a 403 with a custom JSON error body.
Observability: Logging and Auditing
CloudWatch — Model Invocation Logging
Bedrock is configured to write all model invocation logs to a CloudWatch log group at
/aws/bedrock/model-invocations.
A dedicated IAM role (BedrockLoggingRole) with the
bedrock.amazonaws.com
trust policy grants Bedrock permission to write logs.
CloudTrail — Control-Plane Audit
CloudTrail automatically records all Bedrock control-plane API calls —
CreateGuardrail,
PutModelInvocationLoggingConfiguration,
and other administrative actions. This provides an immutable audit trail for compliance.
bedrock:PutModelInvocationLoggingConfiguration,
preventing anyone with the application role from disabling invocation logging.
API Gateway: Usage Plans and Throttling
A usage plan (chatbot-basic-plan) is attached to the API Gateway stage with the following limits:
- Rate limit: 10 requests per second
- Burst limit: 5 requests
- Monthly quota: 500 requests per month
This provides cost protection and prevents runaway usage independent of the WAF rate-based rule.
Security Controls
- Defense in depth — five layers from WAF edge filtering to Bedrock guardrails to CloudWatch logging
- Least-privilege IAM — application restricted to one model, one region, with explicit Deny on destructive actions
- Bedrock Guardrails — blocks competitor mentions, content policy violations, and prompt attacks at the model layer
- AWS WAF Core Rule Set — blocks SQL injection, XSS, and OWASP Top 10 web vulnerabilities
- Rate limiting — both WAF (50 req/5min per IP) and API Gateway (10 req/sec + 500/month quota)
- Prompt injection regex — custom WAF rule blocks known jailbreak patterns before they reach the model
- Tamper-resistant logging — explicit IAM Deny prevents disabling invocation logs or modifying guardrails
- CORS origin-restricted — API Gateway only accepts browser requests from the production domain
- Model invocation logging — all Bedrock calls logged to CloudWatch for monitoring and incident response
- CloudTrail audit — immutable record of all administrative Bedrock API calls
Lessons Learned
- bedrock:Converse is not a valid IAM action — The Converse API is covered by
bedrock:InvokeModel. Addingbedrock:Converseto an IAM policy causes a validation error. - Cross-region inference profiles required — On-demand model invocation requires the
us.prefix on the model ID (e.g.,us.anthropic.claude-3-haiku-20240307-v1:0). Using the bare model ID returns anUnsupportedModelException. - Guardrail version must match exactly — Using version
"1"when only"DRAFT"exists causes a silent validation error that looks like the guardrail doesn't exist. - Guardrail region must match client region — A guardrail created in
us-east-2is invisible to a Bedrock client configured forus-east-1. Always verify the region in both the guardrail and the boto3 client. - Lambda needs JSON parsing for API Gateway events — API Gateway wraps the request body in a
bodystring key. Direct Lambda test events do not. Handle both with anif/elsecheck. - Lambda default timeout is too short for LLM calls — The 3-second default causes
Sandbox.Timedouterrors. Increase to 30 seconds for Bedrock invocations. - Windows CMD requires different JSON escaping — Single-quoted JSON in AWS CLI commands fails on Windows. Use
file://references instead for complex JSON payloads. - WAF LockToken prevents concurrent updates — Every WAF update returns a new LockToken. You must use the current token for the next update or the request is rejected.
- CORS single-quote wrapping — API Gateway mapping expressions require the origin URL wrapped in single quotes or you get an "Invalid mapping expression" error.
- Bedrock Runtime is not in API Gateway's service dropdown — You cannot directly integrate API Gateway with Bedrock. Use a Lambda function as the intermediary.
Cost Estimate
- Low traffic (100 req/day): ~$7–10/month
- Medium traffic (1,000 req/day): ~$20–40/month
- High traffic (10,000 req/day): ~$100–200/month
The largest cost driver is Bedrock (Claude Haiku token usage). AWS WAF is the primary fixed cost at ~$6/month per Web ACL regardless of traffic volume.
References
- AWS Documentation — Amazon Bedrock Guardrails
- AWS Documentation — Bedrock Converse API
- AWS Documentation — AWS WAF Web ACLs
- OWASP — Top 10 for LLM Applications
- AWS Documentation — API Gateway Usage Plans
- AWS Documentation — CloudTrail Event Logging