Per-Feature Security Requirements
This page maps the open Features in the rag-sample backlog to testable security requirements. The system-level controls live in assessment; the boundaries live in trust boundaries; the threats live in threat model. This page is the per-feature translation layer that turns those into acceptance criteria that a story author or QA reviewer can check off.
Each requirement carries a Verify column. If a requirement cannot be verified by a test (unit, integration, IaC scan, or runbook step), it does not belong on this page.
Conventions
Section titled “Conventions”- TM references point to rows in threat model (T1, T2, T3, or a STRIDE matrix row by boundary).
- TB references point to rows in trust boundaries (B1 through B9).
- ADR references are the binding decisions; deviation requires a new ADR.
- Where a Feature has no security requirement beyond the system baseline, the row says so explicitly. Empty rows are an oversight, not a pass.
Feature 18: SPA wiring of the chat surface
Section titled “Feature 18: SPA wiring of the chat surface”Browser code consuming /query and (per RAG009) attaching the bearer token.
| Requirement | Verify | Mitigates |
|---|---|---|
SPA attaches Authorization: Bearer <key> to every /query call, never to /healthz. | Cypress or Playwright test asserting header presence on /query and absence on /healthz. | RAG009; B1. |
Key is read from localStorage, never from URL parameters or referrer-leakable channels. | Unit test on the auth hook; code review on PRs touching the auth module. | RAG009 SPA exposure note. |
| Question input is length-capped client-side at the same bound the API enforces (e.g. 2 KB), to surface 422 as a UX issue rather than as wasted Bedrock spend. | Component test on the input bound; contract test against openapi.yaml. | T2; assessment gap “confirm bound is meaningful”. |
No third-party script is loaded into the SPA without a CSP review; the bundle ships with a Content-Security-Policy header (set at CloudFront response-headers policy). | Terraform asserts the response-headers policy on the CloudFront distribution; integration test fetches index.html and asserts the header. | RAG009 XSS re-evaluation trigger. |
| The visible-key badge masks all but the last 4 characters of the secret half of the key. | Component test on the badge renderer. | RAG009 visible-key element. |
Feature 19/20/21/22/23/24/25: Infrastructure modules and IAM/secrets
Section titled “Feature 19/20/21/22/23/24/25: Infrastructure modules and IAM/secrets”Terraform modules that materialize the platform. Treat each module as a unit of compliance.
| Requirement | Verify | Mitigates |
|---|---|---|
Every Lambda execution role is scoped to specific resource ARNs (cluster ARN, secret ARN, Bedrock model IDs); wildcards on resources are forbidden outside cloudwatch:PutMetricData and X-Ray put-segments. | tfsec or checkov rule in CI; PR check fails on wildcards in aws_iam_policy_document resources. | Assessment “Least-privilege IAM”; B4, B5. |
Aurora cluster has no public endpoint and the security group denies inbound from 0.0.0.0/0; access is via RDS Data API only. | tfsec rule; integration test on terraform plan JSON. | STRIDE row “Aurora at rest”; assessment “Network minimization”. |
| Aurora and Secrets Manager use KMS encryption at rest. AWS-managed keys today, customer-managed keys when reclassification occurs (data-classification rule). | terraform plan assertion that storage_encrypted = true and a KMS key reference is present. | Assessment “Encryption at rest”; T3. |
| Secrets Manager secret ARN is referenced, never the secret value, in Terraform variables and outputs. | tfsec/checkov “no plaintext secret” rule; PR review on *.tfvars. | Assessment “Credentials”. |
| S3 buckets (SPA bundle, Terraform state) deny public access via bucket policy and block-public-access settings; CloudFront origin access uses OAC, not a public bucket. | tfsec rule; integration test on the OAC binding. | STRIDE row “CloudFront / S3 SPA”. |
The OIDC role used by GitHub Actions has an environment-scoped trust policy that names the repo and the environment (qa, prod); * in the sub claim is forbidden. | PR check parsing the trust policy JSON. | STRIDE row “CI -> AWS”; B8. |
All log groups are created with retention_in_days = 30 set explicitly. Default-infinite retention is a CI failure. | tfsec rule; terraform plan assertion. | RAG008; assessment “Observability”. |
Feature 26/27/28: Environment promotion (QA to Prod)
Section titled “Feature 26/27/28: Environment promotion (QA to Prod)”| Requirement | Verify | Mitigates |
|---|---|---|
| The Prod GitHub Environment requires a manual reviewer approval; the QA environment does not. | gh api check on the environment protection rules in CI. | RAG001; STRIDE row “CI -> AWS”. |
| Promotion artifacts (Lambda zip, SPA bundle, OpenAPI spec) are byte-identical between QA and Prod runs; the only diff is environment configuration. | Pipeline step diffs artifact hashes between QA and Prod jobs. | Supply chain integrity; B8. |
| Terraform state is per-environment with separate S3 backends with S3-native state locking; Prod state is not writable from the QA role. | Bucket policy review; one-time test that the QA OIDC role cannot s3:PutObject against the Prod state bucket. | RAG001; STRIDE row “CI -> AWS”. |
Feature 29/30: CI/CD
Section titled “Feature 29/30: CI/CD”| Requirement | Verify | Mitigates |
|---|---|---|
pip-audit runs on every PR. Findings of high or critical severity block merge once the noise floor is established; until then they post as PR comments. See sbom. | CI job; required check on the protected branch once promoted. | Assessment gap “Dependency vulnerability scanning not in CI”. |
npm audit --audit-level=high runs on every PR for the SPA workspace. | CI job. | Same as above for the SPA tree. |
Secret scanning (e.g. gitleaks) runs on every PR and fails the build on any detection that is not in the documented allowlist. | CI job; allowlist file under code review. | Assessment “Credentials”. |
Terraform plan output is posted to the PR with tfsec/checkov results inline; high-severity findings block merge. | CI job; required check. | All IaC requirements above. |
The CI role cannot read production secrets. The Prod OIDC role is only assumable from the prod environment-gated job. | Trust-policy check in CI; runbook for periodic audit. | STRIDE row “CI -> AWS”; B8. |
Feature 31/32/33/34: Cost guardrails and observability
Section titled “Feature 31/32/33/34: Cost guardrails and observability”| Requirement | Verify | Mitigates |
|---|---|---|
The Powertools Logger configuration scrubs the Authorization header from every log entry; a failing test on log output is part of the authorizer Story acceptance (per RAG009). | Unit test asserting the logged event has no authorization key under any casing. | RAG009 log-scrub follow-up; STRIDE row “Lambda -> Secrets Manager”; log-as-exfil path. |
| Question and answer bodies log at INFO with a configurable redaction list; if the data-classification rule ever fires for a non-public class, the redaction config is the single switch the operator flips. | Unit test on the redactor; runbook entry. | T3; data-classification rule. |
| The CloudWatch alarm on Lambda error rate routes to SNS with at least one subscribed email per environment. | Terraform assertion; one-time confirm of the subscription. | RAG008; assessment “Observability”. |
| The token-spend alarm (#64) fires on a defined daily ceiling; the threshold is documented per environment. | Terraform assertion; runbook entry. | T1/T2 cost-runaway shape; assessment gap “No abuse alarm on Bedrock token spend”. |
The /query route throttle (#65) caps requests at the documented rate; /healthz is exempt. | Terraform assertion on the API Gateway route settings; integration test driving 429s. | Assessment control “Route-level rate limit” (#65 closed as completed); STRIDE row “Browser -> API Gateway”. |
Feature 44: Safety Gate (parent)
Section titled “Feature 44: Safety Gate (parent)”The implementation choice (Bedrock Guardrails) is settled in Decision #55 (closed). The Feature itself carries requirements regardless of choice.
| Requirement | Verify | Mitigates |
|---|---|---|
| Retrieved chunks are delimited in the prompt with an unambiguous fence; the system prompt instructs the model to treat fenced content as data, not instructions. | Unit test inspecting the assembled prompt for the fence and the instruction. | T1; B3. |
| User-authored question is never interpolated into the system prompt block; it occupies its own delimited region with its own provenance instruction. | Unit test on prompt assembly. | T2; B3. |
| Question input strips ASCII and Unicode control characters before prompt assembly. | Unit test with adversarial fixtures. | T2. |
| Any future safety-gate implementation produces a structured verdict that is logged with the request correlation ID. | Contract test against the verdict schema. | T1, T2; observability traceability. |
Feature 45: Security (parent)
Section titled “Feature 45: Security (parent)”This Feature is the umbrella for the items already filed (#62 log scrub, #64 token alarm, #65 throttle, #66/#67 provenance/classification gate) plus the assessment gaps. No new requirements specific to the parent; the parent passes when its children pass.
Feature 46/47/48: Legal and licensing
Section titled “Feature 46/47/48: Legal and licensing”| Requirement | Verify | Mitigates |
|---|---|---|
| The corpus Apache-2.0 attribution appears in the rendered SPA (footer or About) and in the architecture overview, as already noted in compliance. | Cypress test asserting the attribution text is present in the SPA. | Compliance “Third-party license: Apache-2.0”. |
| Dependency licenses are surfaced in the SBOM (see sbom); copyleft licenses (GPL, AGPL) on production runtime dependencies block merge until reviewed. | CI step parses the CycloneDX SBOM, flags non-permissive licenses, posts to PR. | Supply-chain license risk; not covered in current threat model (gap, see report). |
Feature 49: Observability
Section titled “Feature 49: Observability”Largely covered under 31/32/33/34. The Feature-specific addition:
| Requirement | Verify | Mitigates |
|---|---|---|
| Correlation IDs propagate from API Gateway through the API Lambda and into Bedrock and Data API subsegments; logs and traces share the ID. | Integration test on a single request that asserts the same ID appears in CloudWatch Logs Insights and the X-Ray trace. | Repudiation row across STRIDE matrix; incident-response readiness. |
Feature 57: Prod readiness
Section titled “Feature 57: Prod readiness”The Prod-promotion checklist that closes the assessment gaps.
| Requirement | Verify | Mitigates |
|---|---|---|
| The incident-response runbook from the assessment gap exists and has been walked through once with the operator. | Runbook checked in under docs/.../runbooks/; dry-run notes attached. | Assessment gap “No documented incident response runbook”. |
The Bedrock token-spend alarm and the /query throttle are deployed in Prod. | Terraform plan check at Prod promotion. | Assessment gap “abuse alarm”; assessment control “Route-level rate limit”. |
pip-audit and npm audit are blocking checks on the protected branch (no longer noise-floor mode). | gh api check on branch protection. | Assessment gap “Dependency vulnerability scanning”. |
| The Hugging Face dataset revision SHA is pinned and recorded in the ingestion run metadata. | Ingestion log assertion; runbook step. | Assessment gap “Corpus integrity”; B6. |
Feature 59/60/61/62/63: Authentication
Section titled “Feature 59/60/61/62/63: Authentication”These map directly to RAG009; the Feature’s security requirements are the ADR’s acceptance criteria. Restating for traceability:
| Story | Requirement | Verify |
|---|---|---|
| #60 authorizer | Authorization: Bearer <key> is the only accepted credential; expires_at > now() AND remaining_requests > 0 AND hash match are all required; FOR UPDATE makes the decrement race-safe. | Unit and integration tests in the authorizer module. |
| #60 authorizer | Authorizer response caching is disabled on the API Gateway authorizer resource. | Terraform assertion. |
| #60 authorizer | The authorizer Lambda IAM role is scoped to rds-data:ExecuteStatement on the cluster ARN and secretsmanager:GetSecretValue on the credential secret ARN. Nothing else. | tfsec rule; PR review. |
| #61 issue-key CLI | Key generation uses a cryptographic RNG (Python secrets), prints the key once, and stores only the hash. | Unit test; CLI integration test asserting the key is not stored in plaintext. |
| #62 log scrub | The Authorization header is scrubbed from every CloudWatch log entry across the API Lambda, the authorizer Lambda, and any future Lambda that touches the request. | Unit test on each handler’s log output with a fixture request carrying a fake token. |
| #63 SPA key intake | Key is stored in localStorage keyed under a documented name; the SPA never sends it anywhere except the /query Authorization header. | Cypress test inspecting outbound network calls during a session. |
Feature 64: Token-spend alarm
Section titled “Feature 64: Token-spend alarm”Covered as an acceptance row under Feature 31/32/33/34 above. The alarm threshold is documented per environment in the runbook.
Feature 65: Route throttle
Section titled “Feature 65: Route throttle”Covered as an acceptance row under Feature 31/32/33/34 above.
Feature 66/67: Content provenance and classification gate
Section titled “Feature 66/67: Content provenance and classification gate”| Requirement | Verify | Mitigates |
|---|---|---|
The ingestion pipeline records the dataset name, revision SHA, license string, and ingestion timestamp in a corpus_provenance table or equivalent metadata file alongside the embeddings. | Unit test on the ingestion writer; runbook step on operator inspection. | T3; data-classification rule item 1. |
Any change to the corpus reference in pyproject.toml, Terraform, or the ingestion config requires a CODEOWNERS review by the data-classification owner. | CODEOWNERS rule on the relevant paths; PR check that the review fired. | Data-classification rule “process gate”. |
| The classification gate is a CI step that fails if the corpus reference changes without a matching update to data classification. | CI job parsing the diff and the classification page. | Data-classification rule items 1 through 7. |
© 2026 Benjamin Arunski