Back to Portfolio

How to Build a Secure AI Chatbot with Amazon Bedrock Guardrails and AWS WAF

User Browser / curl AWS WAF Core Rules Rate Limiting Regex Filters API Gateway REST POST /chat Usage Plans Lambda Python 3.12 Converse API Bedrock Claude Haiku Guardrails Content Filters CloudWatch Invocation Logs CloudTrail Audit Events IAM Policy Least Privilege Secure AI Chatbot Architecture AWS Cloud
AWS WAF
OWASP · Rate Limit · Regex
IAM Policy
Least Privilege · Deny Rules
Guardrails
Topic Blocks · Content Filters
Claude Haiku
Converse API · Bedrock
Logging
CloudWatch · CloudTrail

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.

Live API endpoint: The chatbot is accessible via a REST API on API Gateway, protected by AWS WAF, Bedrock Guardrails, and IAM least-privilege policies. All model invocations are logged to CloudWatch and audited via CloudTrail.

Application Architecture

The stack uses the following AWS services and tools:

  1. Amazon Bedrock — managed AI service hosting Claude 3 Haiku via the Converse API
  2. Bedrock Guardrails — content filtering, topic blocking (competitor mentions), and prompt attack detection
  3. AWS Lambda — Python 3.12 function connecting API Gateway to Bedrock
  4. Amazon API Gateway — REST API with POST /chat endpoint, usage plans, and API key throttling
  5. AWS WAF — Web ACL with OWASP Core Rule Set, rate limiting, and custom regex rules for prompt injection
  6. AWS IAM — least-privilege policies with explicit deny rules to protect guardrail and logging configurations
  7. Amazon CloudWatch — model invocation logging for observability
  8. AWS CloudTrail — audit trail for all Bedrock control-plane API calls
Bedrock Lambda API Gateway WAF IAM CloudWatch CloudTrail Python Claude Haiku OWASP LLM Top 10

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.

IAM Policy (bedrock-chatbot-policy.json)
{
  "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": "*"
    }
  ]
}
Converse API permissions: The Bedrock Converse API does not have its own IAM action. It is covered by 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.

Guardrail attached to 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.

lambda_function.py
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:

WAF Regex Pattern (prompt-injection-patterns)
(?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.

Tamper-resistant logging: The IAM policy explicitly denies 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

  1. bedrock:Converse is not a valid IAM action — The Converse API is covered by bedrock:InvokeModel. Adding bedrock:Converse to an IAM policy causes a validation error.
  2. 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 an UnsupportedModelException.
  3. 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.
  4. Guardrail region must match client region — A guardrail created in us-east-2 is invisible to a Bedrock client configured for us-east-1. Always verify the region in both the guardrail and the boto3 client.
  5. Lambda needs JSON parsing for API Gateway events — API Gateway wraps the request body in a body string key. Direct Lambda test events do not. Handle both with an if/else check.
  6. Lambda default timeout is too short for LLM calls — The 3-second default causes Sandbox.Timedout errors. Increase to 30 seconds for Bedrock invocations.
  7. Windows CMD requires different JSON escaping — Single-quoted JSON in AWS CLI commands fails on Windows. Use file:// references instead for complex JSON payloads.
  8. 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.
  9. CORS single-quote wrapping — API Gateway mapping expressions require the origin URL wrapped in single quotes or you get an "Invalid mapping expression" error.
  10. 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

  1. AWS Documentation — Amazon Bedrock Guardrails
  2. AWS Documentation — Bedrock Converse API
  3. AWS Documentation — AWS WAF Web ACLs
  4. OWASP — Top 10 for LLM Applications
  5. AWS Documentation — API Gateway Usage Plans
  6. AWS Documentation — CloudTrail Event Logging