AWS Builder Center
Architecting a Zero-Trust Enterprise RAG Gateway on AWS: Multi-Tenant Vector Retrieval, Cedar Policy Enforcement, and Bedrock with Terraform

Architecting a Zero-Trust Enterprise RAG Gateway on AWS: Multi-Tenant Vector Retrieval, Cedar Policy Enforcement, and Bedrock with Terraform

A comprehensive, production-tested blueprint for stopping GenAI data leaks with Amazon Verified Permissions, OpenSearch Serverless metadata filtering, and Amazon Bedrock.

AWS Community Builder - Dev Tools | 2× AWS Certified | Devops/Cloud Consultant

It is 09:15 AM on a Monday. Forty minutes after rolling out a new internal "AI Research Copilot," a junior marketing associate asks, *"What are the projected headcount reductions and acquisition targets scheduled for Q4 in Europe?"*The AI promptly returns an unannounced $450M acquisition blueprint and confidential executive severance calculations.

The company wasn't hacked; credentials weren't leaked. The vulnerability was architectural: naive RAG treats authentication as authorization.

Vector search engines optimize for mathematical proximity in high-dimensional vector space (-NN), not security boundaries. When confidential records share the same vector index, semantically adjacent queries pull unauthorized chunks straight into the prompt context window, where prompt-engineering guardrails inevitably collapse. To prevent data leakage, authorization must occur
before vector retrieval, using cryptographic policy-as-code and mathematically enforced metadata pre-filtering.

In this guide, we engineer a production-grade Zero-Trust Enterprise RAG Gateway on AWS:
  • Enterprise RAG Gateway (FastAPI / AWS Fargate): An asynchronous microservice behind an internal ALB enforcing mTLS, validating OIDC claims, and dynamically compiling secured -NN queries with AWS SigV4.
  • Fine-Grained Authorization Engine (Amazon Verified Permissions & Cedar): Sub-millisecond policy-as-code evaluating strict tenant boundaries, clearance hierarchies (Public to Restricted), and contextual MFA constraints before any vector compute runs.
  • Hardened Vector Store (Amazon OpenSearch Serverless): An HNSW vector collection (1024-dim Titan V2) isolated within private subnets via AWS PrivateLink and encrypted at rest with a KMS Customer Managed Key.
  • Resilient Foundation Model Runtime (Amazon Bedrock): Streaming inference via the Bedrock Converse API with Claude 3.5 Sonnet, automated circuit-breaker fallback to Claude 3 Haiku, and active guardrails.
  • **Infrastructure as Code (Terraform):**100% reproducible, production-grade IaC provisioning the complete VPC, KMS, least-privilege IAM roles, policy stores, and ECS clusters.

IAM Deployment Permissions

Do not use the AWS  AdministratorAccess  managed policy. The deployment runner or engineer role requires scoped permissions for VPC, IAM, KMS, OpenSearch Serverless, Verified Permissions, Bedrock, and ECS.

Expected AWS Deployment Costs

Deploying this architecture incurs the following baseline infrastructure charges:
  • Amazon OpenSearch Serverless: Minimum 4 OCUs (2 for search, 2 for indexing for high availability). At $0.24/OCU-hour, baseline cost is $0.96/hour ($23.00/day).
  • Amazon Bedrock: Pay-per-token pricing ($0.003/1K input tokens, $0.015/1K output tokens for Claude 3.5 Sonnet). Testing with 1,000 queries will cost less than $3.50.
  • Amazon ECS Fargate: 2 tasks x 0.5 vCPU x 1GB RAM = ~$0.05/hour.
  • VPC NAT Gateways & PrivateLink Endpoints: ~$0.09/hour.
Cost Optimization Tip for Development: Follow Section 13 (Cleanup) immediately after testing to destroy the OpenSearch Serverless collection and avoid ongoing OCU baseline charges.

End-to-End System Topology

Below is the complete architectural topology of the Zero-Trust Enterprise RAG Gateway. Every communication line between components is encrypted in transit using TLS 1.3 and authenticated via AWS SigV4 or IAM roles.



Step-by-Step Request Lifecycle

Let us trace the exact deterministic sequence of events when an enterprise user issues a query:
  1. Authentication & Token Issuance: The client logs in via the corporate Identity Provider (e.g. AWS IAM Identity Center, Okta, Microsoft Entra ID). The IdP issues a signed OIDC JWT containing essential custom claims:
  •  tenant_id :  "acme-aerospace" 
  •  department :  "avionics-engineering" 
  •  clearance_level :  3  (Confidential)
  •  mfa_authenticated :  true 
  1. Ingress at API Gateway / ALB: The client submits an HTTP POST request to  https://rag.enterprise.internal/v1/query  with the Bearer JWT in the  Authorization  header. The ALB terminates TLS and forwards the request to an ECS Fargate task in a private subnet.
  1. Step 1: Identity Extraction & Context Synthesis: The FastAPI middleware decodes and cryptographically verifies the JWT signature against the IdP's JWKS endpoint. It constructs a canonical Cedar  Principal  entity and  Context  record.
  1. Step 2: Pre-Retrieval Cedar Policy Evaluation: Before computing vector embeddings or contacting OpenSearch, the Gateway calls Amazon Verified Permissions:
JSON
{
"policyStoreId": "ps-9876543210",
"principal": { "entityType": "Enterprise::User", "entityId": "usr-8821" },
"action": { "actionType": "Enterprise::Action", "actionId": "Query" },
"resource": { "entityType": "Enterprise::TenantSpace", "entityId": "acme-aerospace" },
"context": {
"contextMap": {
"clearanceLevel": { "long": 3 },
"networkSource": { "string": "corp_network" },
"mfaAuthenticated": { "boolean": true }
}
}
}
If Cedar evaluates to DENY the request is immediately rejected with HTTP 403 Forbidden. No vector compute or LLM tokens are consumed.
  1. Step 3: Query Vectorization (Amazon Titan Embeddings V2): The Gateway invokes Amazon Bedrock to generate a 1024-dimensional normalized vector embedding for the natural language query.
  1. Step 4: Filter-Injected Vector Retrieval (Amazon OpenSearch Serverless): The Gateway builds an OpenSearch Serverless query DSL. Crucially, the $k$-NN search is wrapped inside a boolean filter that forces matching on the evaluated Cedar constraints:
  •  metadata.tenant_id == "acme-aerospace" 
  •  metadata.classification_level <= 3 
  •  metadata.department IN ["avionics-engineering", "general"]  The request is signed on the fly using AWS SigV4 with the ECS task's IAM role credentials and dispatched across the VPC PrivateLink endpoint. OpenSearch Serverless returns only documents matching both the mathematical vector proximity AND the security metadata.
  1. Step 5: Post-Retrieval Verification (Defense-in-Depth): The Gateway runs a second, batch Cedar evaluation on the individual document IDs returned by OpenSearch to guarantee no index corruption or cross-tenant contamination occurred.
  1. Step 6: Bedrock Converse API Streaming: The Gateway formats a sanitized system prompt containing the verified context chunks and streams the response back to the client via Server-Sent Events (SSE).

Project Structure

Create the following directory layout on your development machine:
Plain text
zero-trust-rag-gateway/
├── terraform/
│ ├── versions.tf
│ ├── network.tf
│ ├── kms.tf
│ ├── opensearch.tf
│ ├── verified_permissions.tf
│ ├── ecs_fargate.tf
│ └── outputs.tf
├── policies/
│ ├── schema.json
│ └── rag_gateway_policy.cedar
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── config.py
│ ├── cedar_auth.py
│ ├── opensearch_client.py
│ ├── bedrock_client.py
│ ├── rag_service.py
│ └── requirements.txt
├── scripts/
│ └── seed_vector_db.py
├── tests/
│ └── test_rag.py
├── monitoring/
│ └── otel-collector-config.yaml
├── .github/
│ └── workflows/
│ └── deploy.yml
└── Dockerfile

Step 1: Terraform Infrastructure Foundation ( terraform/versions.tf ,  network.tf ,  kms.tf )

To keep this article focused on the zero-trust architecture, the complete, production-ready Terraform modules are available in the GitHub repository (Binod231/zero-trust-rag-gateway) under the terraform/ directory:
  • versions.tf: AWS provider constraints (~> 5.50) and regional defaults.
  • network.tf: Multi-AZ VPC (10.0.0.0/16), private application subnets, NAT Gateways, and route tables.
  • kms.tf: AWS KMS Customer Managed Key (CMK) with 365-day automated key rotation for envelope encryption.
  • opensearch.tf: OpenSearch Serverless vector collection, VPC PrivateLink endpoints, and SigV4 IAM data access policies.
  • verified_permissions.tf: Amazon Verified Permissions policy store with STRICT Cedar schema validation.
  • ecs_fargate.tf: ECS Fargate cluster, least-privilege IAM execution roles, security groups, and task definitions.
You can provision the entire infrastructure in minutes:
Bash
git clone https://github.com/Binod231/zero-trust-rag-gateway.git
cd zero-trust-rag-gateway/terraform
terraform init && terraform apply

Step 2: OpenSearch Serverless Vector Collection ( terraform/opensearch.tf )

OpenSearch Serverless requires three decoupled policies before creating a collection:
  1. Encryption Security Policy (must reference a KMS key).
  1. Network Security Policy (restricts collection to VPC endpoint).
  1. Data Access Policy (grants ECS IAM role access).
Create  terraform/opensearch.tf 

Step 3: Amazon Verified Permissions & Cedar Policy-as-Code

To keep this guide focused on the zero-trust evaluation logic and vector filtering, the auxiliary deployment files are available in the GitHub repository:
  • policies/schema.json: Formal Cedar namespace schema defining  User ,  Action , and  VectorDocument  entity shapes.

Create  policies/rag_gateway_policy.cedar :
cedar
// Policy 1: Zero-Trust Multi-Tenant Isolation (Default Forbid across tenants)
forbid (
principal,
action,
resource
)
when {
principal.tenantId != resource.tenantId
};

// Policy 2: Permit Query on Tenant Space if MFA is satisfied
permit (
principal,
action == Enterprise::Action::"Query",
resource
)
when {
principal.tenantId == resource.tenantId &&
context.mfaAuthenticated == true
};

// Policy 3: Clearance Hierarchy for Document Retrieval
permit (
principal,
action == Enterprise::Action::"Retrieve",
resource
)
when {
principal.tenantId == resource.tenantId &&
principal.clearanceLevel >= resource.classificationLevel &&
(
resource.department == "general" ||
resource.department == principal.department ||
principal.role in ["Executive", "ComplianceAuditor"]
)
};

// Policy 4: Hard restriction on Level 4 (Restricted) documents from non-corporate networks
forbid (
principal,
action,
resource
)
when {
resource.classificationLevel == 4 &&
context.networkSource != "corp_network"
};

Step 4: Enterprise RAG Gateway Microservice

The microservice dependencies (fastapi, boto3, opensearch-py) and environment settings are managed in app/requirements.txt and app/config.py.

Create  app/cedar_auth.py  for communicating with Amazon Verified Permissions:
Python
import boto3
import logging
from typing import Dict, Any, List
from app.config import settings

logger = logging.getLogger(__name__)

class CedarAuthorizer:
def __init__(self):
self.client = boto3.client("verifiedpermissions", region_name=settings.AWS_REGION)
self.policy_store_id = settings.POLICY_STORE_ID

def is_query_authorized(
self,
user_id: str,
tenant_id: str,
clearance_level: int,
department: str,
mfa_authenticated: bool,
network_source: str
) -> bool:
"""
Pre-authorizes whether the user is permitted to issue a RAG query
against their designated TenantSpace.
"""
try:
response = self.client.is_authorized(
policyStoreId=self.policy_store_id,
principal={
"entityType": "Enterprise::User",
"entityId": user_id
},
action={
"actionType": "Enterprise::Action",
"actionId": "Query"
},
resource={
"entityType": "Enterprise::TenantSpace",
"entityId": tenant_id
},
context={
"contextMap": {
"mfaAuthenticated": {"boolean": mfa_authenticated},
"networkSource": {"string": network_source}
}
},
entities={
"entityList": [
{
"identifier": {
"entityType": "Enterprise::User",
"entityId": user_id
},
"attributes": {
"tenantId": {"string": tenant_id},
"department": {"string": department},
"clearanceLevel": {"long": clearance_level},
"role": {"string": "Employee"}
}
},
{
"identifier": {
"entityType": "Enterprise::TenantSpace",
"entityId": tenant_id
},
"attributes": {
"tenantId": {"string": tenant_id}
}
}
]
}
)
decision = response.get("decision", "DENY")
logger.info("Cedar query authorization decision: %s for user %s", decision, user_id)
return decision == "ALLOW"
except Exception as e:
logger.error("Failed to evaluate Cedar policy via Amazon Verified Permissions: %s", str(e))
# Fail closed on zero-trust principle
return False

def filter_documents_batch(
self,
user_id: str,
tenant_id: str,
clearance_level: int,
department: str,
documents: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""
Defense-in-depth batch verification of retrieved documents.
"""
authorized_docs = []
for doc in documents:
metadata = doc.get("metadata", {})
doc_tenant = metadata.get("tenant_id")
doc_classification = metadata.get("classification_level", 4)
doc_dept = metadata.get("department", "general")

# Fast deterministic verification
if doc_tenant == tenant_id and clearance_level >= doc_classification:
if doc_dept in ["general", department]:
authorized_docs.append(doc)
else:
logger.warning("Defense-in-depth rejected doc %s due to department mismatch", doc.get("id"))
else:
logger.warning("Defense-in-depth rejected doc %s due to tenant/clearance boundary", doc.get("id"))

return authorized_docs
Create  app/opensearch_client.py  for AWS SigV4 signed vector queries:
Python
import boto3
import logging
from typing import List, Dict, Any
from opensearchpy import OpenSearch, RequestsHttpConnection, AWSV4SignerAuth
from app.config import settings

logger = logging.getLogger(__name__)

class OpenSearchVectorClient:
def __init__(self):
credentials = boto3.Session().get_credentials()
auth = AWSV4SignerAuth(credentials, settings.AWS_REGION, "aoss")
# Strip https:// if passed
host = settings.OPENSEARCH_ENDPOINT.replace("https://", "")

self.client = OpenSearch(
hosts=[{"host": host, "port": 443}],
http_auth=auth,
use_ssl=True,
verify_certs=True,
connection_class=RequestsHttpConnection,
timeout=15
)
self.index_name = settings.OPENSEARCH_INDEX

def search_similar_vectors(
self,
query_vector: List[float],
tenant_id: str,
department: str,
clearance_level: int,
top_k: int = 5
) -> List[Dict[str, Any]]:
"""
Executes a k-NN search guaranteed to enforce mathematical tenant isolation.
"""
query_dsl = {
"size": top_k,
"query": {
"bool": {
"must": [
{
"knn": {
"embedding_vector": {
"vector": query_vector,
"k": top_k
}
}
}
],
"filter": [
{"term": {"metadata.tenant_id": tenant_id}},
{"range": {"metadata.classification_level": {"lte": clearance_level}}},
{
"bool": {
"should": [
{"term": {"metadata.department": department}},
{"term": {"metadata.department": "general"}}
],
"minimum_should_match": 1
}
}
]
}
}
}

try:
response = self.client.search(
body=query_dsl,
index=self.index_name
)
hits = response.get("hits", {}).get("hits", [])
results = []
for hit in hits:
source = hit.get("_source", {})
results.append({
"id": hit.get("_id"),
"score": hit.get("_score"),
"content": source.get("content", ""),
"metadata": source.get("metadata", {})
})
return results
except Exception as e:
logger.error("OpenSearch Serverless query failed: %s", str(e))
raise
Create  app/bedrock_client.py  for Converse API streaming & resilient fallbacks:
Python
import json
import boto3
import logging
from typing import List, Generator
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from botocore.exceptions import ClientError
from app.config import settings

logger = logging.getLogger(__name__)

class BedrockRAGClient:
def __init__(self):
self.client = boto3.client("bedrock-runtime", region_name=settings.AWS_REGION)

@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type(ClientError),
reraise=True
)
def generate_embeddings(self, text: str) -> List[float]:
"""
Calls Amazon Titan Embeddings V2 to generate a 1024-dimensional normalized vector.
"""
payload = json.dumps({
"inputText": text,
"dimensions": 1024,
"normalize": True
})

response = self.client.invoke_model(
modelId=settings.BEDROCK_EMBEDDING_MODEL,
contentType="application/json",
accept="application/json",
body=payload
)
body = json.loads(response["body"].read())
return body["embedding"]

def stream_converse_rag(
self,
user_prompt: str,
context_chunks: List[str]
) -> Generator[str, None, None]:
"""
Streams inference tokens using the Converse API with automatic model fallback.
"""
formatted_context = "\n\n---\n\n".join(
[f"[Document Reference {i+1}]: {chunk}" for i, chunk in enumerate(context_chunks)]
)

system_message = (
"You are an enterprise AI assistant operating within a strict Zero-Trust enclave. "
"Answer the user query solely and strictly using the verified document references provided below. "
"If the provided documents do not contain the answer, explicitly state that the information is unavailable. "
"Do not hallucinate or disclose external knowledge."
)

messages = [
{
"role": "user",
"content": [
{"text": f"Context References:\n{formatted_context}\n\nUser Query: {user_prompt}"}
]
}
]

active_model = settings.BEDROCK_PRIMARY_MODEL
try:
yield from self._invoke_converse_stream(active_model, system_message, messages)
except ClientError as e:
error_code = e.response.get("Error", {}).get("Code")
logger.warning("Primary model %s failed with %s. Falling back to %s",
active_model, error_code, settings.BEDROCK_FALLBACK_MODEL)
# Circuit breaker fallback to secondary model
yield from self._invoke_converse_stream(settings.BEDROCK_FALLBACK_MODEL, system_message, messages)

def _invoke_converse_stream(self, model_id: str, system: str, messages: list):
response = self.client.converse_stream(
modelId=model_id,
messages=messages,
system=[{"text": system}],
inferenceConfig={
"temperature": 0.1,
"maxTokens": 2048,
"topP": 0.9
}
)

stream = response.get("stream")
if stream:
for event in stream:
if "contentBlockDelta" in event:
delta = event["contentBlockDelta"].get("delta", {})
if "text" in delta:
yield delta["text"]
Create  app/main.py  with the FastAPI application and SSE streaming endpoint:
Python
import logging
from fastapi import FastAPI, Header, HTTPException, Depends
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from app.cedar_auth import CedarAuthorizer
from app.opensearch_client import OpenSearchVectorClient
from app.bedrock_client import BedrockRAGClient

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rag-gateway")

app = FastAPI(
title="Zero-Trust Enterprise RAG Gateway",
version="1.0.0",
description="Production-grade ABAC vector retrieval and inference on AWS"
)

cedar_authorizer = CedarAuthorizer()
vector_client = OpenSearchVectorClient()
bedrock_client = BedrockRAGClient()

class QueryRequest(BaseModel):
prompt: str = Field(..., min_length=3, max_length=1000)
user_id: str = Field(..., example="usr-4091")
tenant_id: str = Field(..., example="acme-aerospace")
department: str = Field(default="avionics-engineering")
clearance_level: int = Field(default=2, ge=1, le=4)
mfa_authenticated: bool = Field(default=True)
network_source: str = Field(default="corp_network")

@app.get("/health")
def health_check():
return {"status": "HEALTHY", "service": "zero-trust-rag-gateway"}

@app.post("/v1/query")
async def execute_rag_query(request: QueryRequest):
logger.info("Received query request for user %s on tenant %s", request.user_id, request.tenant_id)

# 1. Zero-Trust Pre-Authorization with Cedar
is_allowed = cedar_authorizer.is_query_authorized(
user_id=request.user_id,
tenant_id=request.tenant_id,
clearance_level=request.clearance_level,
department=request.department,
mfa_authenticated=request.mfa_authenticated,
network_source=request.network_source
)

if not is_allowed:
logger.warning("Access DENIED by Cedar policy engine for user %s", request.user_id)
raise HTTPException(
status_code=403,
detail="Forbidden: Request rejected by enterprise Cedar security policy."
)

# 2. Vector Embedding Generation
query_vector = bedrock_client.generate_embeddings(request.prompt)

# 3. Secure k-NN Vector Search with ABAC Metadata Filtering
raw_documents = vector_client.search_similar_vectors(
query_vector=query_vector,
tenant_id=request.tenant_id,
department=request.department,
clearance_level=request.clearance_level,
top_k=4
)

# 4. Defense-in-Depth Document Verification
verified_documents = cedar_authorizer.filter_documents_batch(
user_id=request.user_id,
tenant_id=request.tenant_id,
clearance_level=request.clearance_level,
department=request.department,
documents=raw_documents
)

if not verified_documents:
return {
"answer": "No authorized documents matching your security clearance were found.",
"documents_retrieved": 0
}

context_texts = [d["content"] for d in verified_documents]

# 5. Server-Sent Events (SSE) Streaming Response
def event_stream():
for token in bedrock_client.stream_converse_rag(request.prompt, context_texts):
yield f"data: {token}\n\n"
yield "data: [DONE]\n\n"

return StreamingResponse(event_stream(), media_type="text/event-stream")

Step 5: Containerization & ECS Fargate Task Definition ( Dockerfile ,  terraform/ecs_fargate.tf )

Create  Dockerfile  (multi-stage, non-root user) and  terraform/ecs_fargate.tf For detailed code, look to the  GitHub repo :

OpenSearch Serverless Vector Index Configuration

When creating the vector index in OpenSearch Serverless, we execute an HTTP PUT with the following JSON schema:
JSON
{
"settings": {
"index": {
"knn": true,
"knn.algo_param.ef_search": 512
}
},
"mappings": {
"properties": {
"embedding_vector": {
"type": "knn_vector",
"dimension": 1024,
"method": {
"name": "hnsw",
"space_type": "cosinesimil",
"engine": "nmslib",
"parameters": {
"ef_construction": 512,
"m": 16
}
}
},
"content": { "type": "text" },
"metadata": {
"properties": {
"tenant_id": { "type": "keyword" },
"department": { "type": "keyword" },
"classification_level": { "type": "integer" },
"author_id": { "type": "keyword" }
}
}
}
}
}

Parameter Analysis:

  1.  "dimension": 1024 : Matches the output vector dimensionality of Amazon Titan Embeddings V2. Using 1024 rather than 1536 (OpenAI ada-002) achieves an optimal trade-off between retrieval precision and memory footprint, reducing OpenSearch Serverless OCU RAM consumption by 33%.
  1.  "space_type": "cosinesimil" : Cosine similarity is mandatory when embedding vectors are pre-normalized. It measures the angular orientation between queries and document chunks, preventing document length from biasing retrieval results.
  1.  "m": 16  and  "ef_construction": 512 : In HNSW, $M$ controls the maximum number of bidirectional connection links per node in the graph, while  ef_construction  governs the size of the dynamic candidate list during graph construction. An $M$ of 16 provides sub-20ms query latency while keeping RAM overhead under 6GB per 100,000 vectors.
  1.  "tenant_id": { "type": "keyword" } : Must be mapped as a  keyword  rather than  text . Text fields are tokenized by the Lucene analyzer, which could cause substring matches. Keywords enforce exact binary equality, guaranteeing that  tenant-acme  will never accidentally match  tenant-acme-corp .

Cedar Policy Evaluation Semantics

Cedar policies are evaluated with a Default-Deny architecture:
  • If no  permit  rule matches, the outcome is  DENY .
  • If an explicit  forbid  rule matches, the outcome i DENY , overriding all permit rules regardless of order.
Look at this line in  policies/rag_gateway_policy.cedar :
cedar
forbid (
principal,
action,
resource
)
when {
principal.tenantId != resource.tenantId
};
Why is this explicit  forbid  necessary if Cedar defaults to deny? In an enterprise environment where multiple teams create and append authorization policies (e.g., HR adding rules for internal promotions, Legal adding compliance exceptions), an engineer might accidentally write an overly broad rule such as  permit (principal, action, resource); . By implementing an explicit top-level  forbid  on tenant mismatch, Cedar guarantees that no future permit policy can ever breach the multi-tenant perimeter.

Automated Pytest Test Suite ( tests/test_rag.py )

We validate the gateway with an automated Pytest suite that fires synthetic queries with mismatched tenant IDs and clearance levels, verifying that unauthorized attempts immediately receive an HTTP 403 Forbidden. You can run the full test suite from  tests/test_rag.py  on  Github 

End-to-End Curl Verification Commands

  1. Verify Health Endpoint:
Bash
curl -X GET "https://rag.enterprise.internal/health" \
-H "Accept: application/json"
Expected Response:
JSON
{"status":"HEALTHY","service":"zero-trust-rag-gateway"}
  1. Verify Unauthorized Access Rejection (No MFA / Public IP):
Bash
curl -i -X POST "https://rag.enterprise.internal/v1/query" \
-H "Content-Type: application/json" \
-d '{
"prompt": "List all proprietary defense project contracts",
"user_id": "contractor-99",
"tenant_id": "acme-aerospace",
"clearance_level": 1,
"department": "Marketing",
"mfa_authenticated": false,
"network_source": "public_internet"
}'
Expected Response:
http
HTTP/1.1 403 Forbidden
content-type: application/json

{"detail":"Forbidden: Request rejected by enterprise Cedar security policy."}

Defense-in-Depth Security Matrix

This architecture systematically mitigates each major vulnerability identified in the OWASP Top 10 for Large Language Model Applications:
OWASP LLM Threat
Architectural Vector
Implemented Mitigation in This Blueprint
LLM01: Prompt Injection
Adversary attempts to override system prompt instructions via retrieved document content.
Strict separation of System instructions and Document References; temperature pinned to 0.1 ; context formatted in delimited, sanitized blocks.
LLM06: Sensitive Information Disclosure
User retrieves private documents belonging to another department or tenant.
Cedar ABAC pre-authorization in Amazon Verified Permissions + Mandatory metadata boolean filter injected into OpenSearch Serverless DSL.
LLM08: Vector and Embedding Weaknesses
Malicious vectors injected into shared embedding space to poison retrieval results.
OpenSearch Data Access Policy restricts index write operations strictly to authenticated ingestion pipelines; user query vectors are ephemeral.
LLM10: Unbounded Consumption (DoS)
Excessive token consumption exhausting AWS Bedrock quota and driving runaway costs.
Pydantic input length limits (1,000 chars); Bedrock max tokens pinned to 2,048; AWS Application Load Balancer rate limiting.

Cryptographic Boundaries & Key Management

  1. Envelope Encryption at Rest:
  • The OpenSearch Serverless collection uses an AWS KMS Customer Managed Key with automatic annual key rotation enabled.
  • All vectors, metadata, and Lucene commit logs are encrypted using AES-256 before being written to persistent storage.
  1. Zero Plaintext In-Transit:
  • All network traffic between ECS Fargate tasks, Amazon Verified Permissions, OpenSearch Serverless, and Bedrock traverses AWS PrivateLink VPC Endpoints.
  • Traffic never touches the public internet. All inter-service TLS certificates are validated using AWS Private CA or Amazon Trust Services.
  1. AWS SigV4 Authentication:
  • The OpenSearch Serverless client does not use static API keys or username/password credentials.
  • Every HTTP payload is dynamically signed with the ECS Task's temporary STS credentials  botocore.auth.SigV4Auth .

Production Roadmap Enhancements

  • Semantic Caching (Amazon ElastiCache for Redis): Cache recurring embeddings with a cosine similarity threshold of 0.96 for sub-15ms responses. Critical Guardrail: Partition cache keys by hash(tenant_id + clearance_level + department) to prevent privilege escalation.
  • Bedrock Provisioned Throughput: Purchase Model Units (MU) to eliminate peak-hour ThrottlingException errors and guarantee sub-second Time-to-First-Token (TTFT).
  • Automated Faithfulness Guardrails: Run an asynchronous AWS Lambda worker sampling 5% of queries to score answer relevance and claim faithfulness, triggering CloudWatch alerts if faithfulness falls below 0.85.

Diagnostic Reference

1. HTTP 403 Forbidden (Cedar): Verify user clearance, MFA status, identity-provider claims, and Cedar authorization policies.
2. OpenSearch 403: Ensure the ECS task IAM role has the required permissions in the OpenSearch Serverless Data Access Policy.
3. Connection Timeout: Check VPC endpoint configuration, security group rules, and port 443 connectivity between ECS and OpenSearch.
4. Bedrock Throttling: Check regional token quotas, request an increase if necessary, and configure retry and fallback logic in  app/bedrock_client.py .
5. KMS Access Denied: Verify KMS key policies and ensure authorized ECS and OpenSearch principals have the required  kms:Decrypt  and  kms:GenerateDataKey  permissions.

FinOps Cost Breakdown (100,000 Monthly Queries)

To provide realistic financial expectations for cloud leadership, we analyze a baseline enterprise deployment processing 100,000 queries per month with an average document context of 1,500 input tokens and 500 output tokens:
AWS Service
Usage Metric
Unit Rate (us-east-1)
Monthly Total (USD)
Amazon Bedrock (Claude 3.5 Sonnet)
150M Input Tokens


50M Output Tokens
$0.003 / 1K In


$0.015 / 1K Out
$450.00


$750.00



Amazon Bedrock (Titan Embeddings V2)
12M Input Tokens (Queries)
$0.00002 / 1K In
$0.24
Amazon OpenSearch Serverless
4 OCUs (2 search, 2 index)


20 GB Vector Storage
$0.24 / OCU-hour x 730h


$0.024 / GB-month
$700.80


$0.48



Amazon ECS Fargate
2 Tasks (0.5 vCPU, 1 GB RAM)
$0.04048/vCPU-hr + mem
$36.00
Amazon Verified Permissions
100K Authorization calls
Free tier / $0.0001/call
$10.00
VPC NAT Gateway & PrivateLink
2 AZs (Endpoints + Egress)
$0.045/hr + data proc
$70.00
Total Estimated Monthly Investment
—
—
$2,017.52
Effective Cost Per Query
—
—
$0.0202 (2.02¢)

Cost Optimization Opportunities:

  1. Model Cascading: Use Claude 3 Haiku ($0.00025/$0.00125 per 1K) for 80% of queries, escalating to Claude 3.5 Sonnet only for complex analysis. This slashes Bedrock inference cost from $1,200 to $180/month (an 85% reduction).
  1. OpenSearch Serverless OCU Scaling: In development environments, lower the maximum OCU limit to 1.0.

Architectural Trade-Off Analysis:

  • OpenSearch Serverless vs. Self-Managed pgvector (RDS PostgreSQL):
  • Trade-off: pgvector on RDS Aurora costs less at low volume (~$120/month for db.t4g.medium), but requires manual vacuum tuning, index rebuilding, and HA failover management. OpenSearch Serverless eliminates operational maintenance and scales automatically, but has a higher minimum baseline cost (4 OCUs).
  • Cedar in Amazon Verified Permissions vs. Open Policy Agent (OPA/Rego):
  • Trade-off: OPA is open-source and can run as a local sidecar in the same Fargate pod (zero network overhead), but lacks managed schema validation, centralized cloud audit trails, and native IAM integration. Amazon Verified Permissions offloads policy storage and governance to a fully managed AWS service.

Tear-Down Sequence

To prevent recurring costs (particularly OpenSearch Serverless OCU hourly charges), execute the cleanup runbook in the exact order below:
Bash
# 1. Navigate to the terraform directory
cd terraform

# 2. Destroy all managed AWS infrastructure
terraform destroy -auto-approve

# 3. Verify OpenSearch Serverless collection deletion
aws opensearchserverless list-collections \
--query "collectionSummaries[?name=='enterprise-rag-vectors'].status" \
--output text

Manual Checks for Stateful Resources:

  • CloudWatch Log Groups: Terraform deletes log groups if managed. If retained, delete manually:
Bash
aws logs delete-log-group --log-group-name "/ecs/zero-trust-rag-gateway"
  • KMS Key: The key is scheduled for deletion in 7 days (cannot be deleted immediately per AWS security protocols).
  • ECR Repository: If images remain in the ECR repository, force delete:
Bash
aws ecr delete-repository --repository-name "zero-trust-rag-gateway" --force

Summary of Architectural Insights

Generative AI does not require sacrificing enterprise security. By replacing naive vector retrieval with an authoritative Zero-Trust Gateway, we solve the foundational vulnerability of enterprise RAG:
  1. Authorization Precedes Retrieval: By evaluating Cedar policies in Amazon Verified Permissions before query execution, unauthorized requests are dropped with zero token expenditure.
  1. Mathematical Tenant Isolation: Injecting tenant and clearance filters directly into OpenSearch Serverless $k$-NN queries guarantees that cross-tenant vectors cannot enter the prompt context window.
  1. Resilience by Design: Using the Amazon Bedrock Converse API with token streaming and automatic model fallbacks delivers high availability even during peak demand.

Recommended Next Steps

  • Implement Amazon Bedrock Guardrails to filter PII and toxic content from output streams.
  • Integrate Amazon Kendra or hybrid lexical-vector search for keyword-heavy compliance documents.
  • Configure automated continuous evaluation of RAG responses using synthetic test benches.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article