Skip to content

Security Assessment

This is a personal demo. The threat model is shaped by that: no user data, no compliance scope, a single operator, and an Apache-2.0 third-party corpus reaching the LLM prompt verbatim. The controls are sized accordingly. Production-grade items that are not warranted at this scale are listed as accepted risks rather than gaps.

  • In scope: the AWS deployment described in Infrastructure (QA + Prod, single account), the FastAPI Lambda, Aurora Serverless v2 with pgvector, AWS Bedrock (Titan embeddings + Haiku-class generation; generation runs through a cross-region inference profile that may execute the request in us-east-1, us-east-2, or us-west-2, so prompt content does not stay in the calling region — RAG007), the CloudFront/S3 SPA, the one-off ingestion Lambda, and the corpus pipeline from Hugging Face.
  • Out of scope: the local Docker dev environment (operator’s machine; not a production surface), upstream Hugging Face Hub controls, AWS managed-service internals.
ControlMechanismSource
Environment isolationSeparate VPCs per environment in one AWS accountRAG001
Production change gateGitHub Environment manual-approval rule on Prod applyRAG001
Network minimizationNo IGW, no NAT, Lambda outside VPC; Aurora reached via RDS Data API (HTTPS, IAM-authed)RAG010, RAG011
Encryption in transitHTTPS at API Gateway, CloudFront, Bedrock, RDS Data API, Secrets Manager (all AWS-managed TLS)Infrastructure
Encryption at restAurora storage encryption, S3 default encryption, Secrets Manager (AWS-managed KMS)Infrastructure
CredentialsDB credentials in Secrets Manager, read by Lambda at cold start; no static secrets in code or envInfrastructure
Least-privilege IAMLambda execution role scoped to bedrock:Invoke*, rds-data:* on one cluster ARN, secretsmanager:GetSecretValue on one secret ARNInfrastructure
Input validationPydantic on /query; HTTP 422 on empty/oversized questionspecs/retrieval
Output groundingSystem prompt instructs Claude to answer only from provided chunks and cite by indexspecs/retrieval
Supply chain (Python)Pinned lower bounds in pyproject.toml; ingestion tree minimized to huggingface-hub + pyarrowRAG002, RAG003
Test floor90% aggregate / 70% per-file coverage hard-fail in CIRAG006
API contract controlopenapi.yaml committed, CI diff-checked, CODEOWNERS on spec and routesRAG005
ObservabilityStructured logs (30-day retention), X-Ray traces, EMF metrics, error-rate alarm per environmentRAG008
Route-level rate limitPOST /query has an API Gateway route-level throttle at 1 req/s steady-state, burst 5, as defense-in-depth behind RAG009’s per-key lifetime request quota; ANY /{proxy+} (which serves /healthz) is unthrottledStory #65 (closed as completed), Infrastructure
Cost guardrailsInfracost diff on every PR, covering Terraform-managed infrastructure only — it prices a plan, so usage-driven Bedrock spend (the largest line item at volume) is invisible to it. The two AWS Budgets that exist are inert and provide no coverage: hand-created, notification-only, and filtered on user:Environment$<env> while no cost allocation tag is active on the account (aws ce list-cost-allocation-tags --status Active empty, verified 2026-08-11), so they match no spend and cannot fire — see the bootstrap guide, step 6. Between the two, no cost-visibility or alerting control covers Bedrock spend today; the preventive caps that do bound it are RAG009’s per-key lifetime request quota and the Lambda concurrency limit (risks.md R2).RAG016, Infrastructure
RiskWhy accepted
Shared AWS account boundary between QA and ProdRAG001 weighs this against Organizations overhead; isolation at VPC + IAM is adequate for a demo. Migration path documented.
Third-party corpus content reaches the LLM prompt verbatimApache-2.0 public dataset; we trust the upstream maintainer no more than necessary. Prompt-injection is the threat-model consequence, not a code defect — see threat model.
No Safety Gate on responses todayModeled as future component (Capability #44); Bedrock Guardrails approved per Decision #55 (closed), gated on #44 entering Stage 3.
Self-review on OpenAPI changesRAG005 acknowledges single-operator consumer rep; process exists for when roles diverge.
Prompt content leaves the calling region via Bedrock cross-region inferenceGeneration runs through a us. inference profile, so the request may execute in any region of that profile’s group (RAG007 requires the IAM grant to name every region the profile can route to; the region list itself lives in the API module’s Terraform). Demo scale, no personal or sensitive data expected in the question, and the AWS trust boundary is unchanged — the region of execution moves, the call stays inside AWS under the same IAM grant. See trust boundaries B5.
Lambda reaches AWS services over the public internet (RDS Data API, Bedrock, Secrets Manager)RAG001: PrivateLink/NAT cost is disproportionate at demo scale. All calls are TLS + SigV4.

Items previously listed as accepted risks or open gaps that have since been superseded by an explicit decision. Kept here for audit history.

RiskResolved by
/query is unauthenticated and publicPreviously listed under Accepted risks. Superseded by RAG009: bearer-token Lambda authorizer backed by an Aurora api_keys table, with a per-key lifetime request quota. Realized by Feature #59 (Stories #60 authorizer, #61 key issuance, #62 log scrub, #63 SPA key intake, all closed). The authorizer is deployed and POST /query returns 401 without a bearer token; /healthz remains unauthenticated by design. See per-feature security requirements.
No rate limit on /queryPreviously listed under Gaps. Resolved by Story #65 (closed as completed): API Gateway route-level throttle on POST /query at 1 req/s steady-state, burst 5, behind RAG009’s per-key lifetime request quota. ANY /{proxy+} (which serves /healthz) is left unthrottled by design. See Controls in place.
Trust-boundary documentation has no home in LikeC4Previously listed under Gaps. Resolved by Decision #51 (closed): #trustBoundary tag added to the LikeC4 specification and applied to container relationships in architecture/_systems.c4; narrative lives at trust boundaries.

These are the items the architect-reviewer should weigh at the Prod promotion step. They are not blockers for the Prod deploy; they are blockers for treating Prod as more than a demo.

GapRecommendationOwner
No abuse alarm on Bedrock token spendAdd a CloudWatch alarm on bedrock-runtime input-token EMF metric (or AWS Budgets per-service action filter) tied to the existing SNS topic. Tracked as Story #64.Operator
Prompt-injection mitigations not implementedTreat retrieved chunks as untrusted: delimit clearly in the prompt, instruct the model to ignore embedded instructions, log responses for review. Full Safety Gate is Capability #44 / Decision #55 (closed).Architect, Operator
Direct prompt-injection via /query bodyCap question length (already 422 on oversized — confirm bound is meaningful, e.g. 2KB), strip control characters, never echo question into system prompt.Operator
Corpus integrity is implicit trust in Hugging FaceOne-off ingestion is acceptable today. If ingestion ever becomes scheduled or unattended, pin dataset revision SHA and record it in run metadata.Operator
/healthz polled every 30s from every SPA tab/healthz is unauthenticated by design under RAG009 (no bearer required). Confirm it does not exercise Bedrock. It is already exempt from the POST /query throttle, which is scoped to that route and leaves ANY /{proxy+} unthrottled (see Controls in place).Operator
Dependency vulnerability scanning not in CIAdd pip-audit (or equivalent) as a non-blocking PR check; promote to blocking once noise level is known.Operator
No documented incident response runbookOne-page runbook: revoke Lambda IAM role, rotate DB secret, disable API Gateway stage, expected dwell time. Demo-scale but worth having before public link.Operator

The architect-reviewer closes the Stage 3 gate against this assessment plus the companion documents in this section. The Prod manual-approval step (RAG001) is the operational checkpoint where the gaps above are re-checked at every release.