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.
Controls in place
Section titled “Controls in place”| Control | Mechanism | Source |
|---|---|---|
| Environment isolation | Separate VPCs per environment in one AWS account | RAG001 |
| Production change gate | GitHub Environment manual-approval rule on Prod apply | RAG001 |
| Network minimization | No IGW, no NAT, Lambda outside VPC; Aurora reached via RDS Data API (HTTPS, IAM-authed) | RAG010, RAG011 |
| Encryption in transit | HTTPS at API Gateway, CloudFront, Bedrock, RDS Data API, Secrets Manager (all AWS-managed TLS) | Infrastructure |
| Encryption at rest | Aurora storage encryption, S3 default encryption, Secrets Manager (AWS-managed KMS) | Infrastructure |
| Credentials | DB credentials in Secrets Manager, read by Lambda at cold start; no static secrets in code or env | Infrastructure |
| Least-privilege IAM | Lambda execution role scoped to bedrock:Invoke*, rds-data:* on one cluster ARN, secretsmanager:GetSecretValue on one secret ARN | Infrastructure |
| Input validation | Pydantic on /query; HTTP 422 on empty/oversized question | specs/retrieval |
| Output grounding | System prompt instructs Claude to answer only from provided chunks and cite by index | specs/retrieval |
| Supply chain (Python) | Pinned lower bounds in pyproject.toml; ingestion tree minimized to huggingface-hub + pyarrow | RAG002, RAG003 |
| Test floor | 90% aggregate / 70% per-file coverage hard-fail in CI | RAG006 |
| API contract control | openapi.yaml committed, CI diff-checked, CODEOWNERS on spec and routes | RAG005 |
| Observability | Structured logs (30-day retention), X-Ray traces, EMF metrics, error-rate alarm per environment | RAG008 |
| Route-level rate limit | POST /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 unthrottled | Story #65 (closed as completed), Infrastructure |
| Cost guardrails | Infracost 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 |
Accepted risks
Section titled “Accepted risks”| Risk | Why accepted |
|---|---|
| Shared AWS account boundary between QA and Prod | RAG001 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 verbatim | Apache-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 today | Modeled as future component (Capability #44); Bedrock Guardrails approved per Decision #55 (closed), gated on #44 entering Stage 3. |
| Self-review on OpenAPI changes | RAG005 acknowledges single-operator consumer rep; process exists for when roles diverge. |
| Prompt content leaves the calling region via Bedrock cross-region inference | Generation 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. |
Resolved risks
Section titled “Resolved risks”Items previously listed as accepted risks or open gaps that have since been superseded by an explicit decision. Kept here for audit history.
| Risk | Resolved by |
|---|---|
/query is unauthenticated and public | Previously 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 /query | Previously 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 LikeC4 | Previously 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. |
Gaps to close before Prod gate
Section titled “Gaps to close before Prod gate”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.
| Gap | Recommendation | Owner |
|---|---|---|
| No abuse alarm on Bedrock token spend | Add 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 implemented | Treat 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 body | Cap 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 Face | One-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 CI | Add pip-audit (or equivalent) as a non-blocking PR check; promote to blocking once noise level is known. | Operator |
| No documented incident response runbook | One-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 |
Verification
Section titled “Verification”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.
© 2026 Benjamin Arunski