Scope
Full review of the serverless AWS stack: API Gateway auth, per-Lambda IAM + permissions
boundary, Cognito, secrets (SSM), storage (S3/DynamoDB), Bedrock scoping, CORS, state hygiene,
and AI/health-data privacy. Reviewed from the Terraform + Lambda source in
~/Terraform/Bobs-lifebot/.
Summary
Well-built and secure for its scope (single-user, non-HIPAA). Defence-in-depth is real: every Lambda role sits under a permissions boundary, IAM is genuinely least-privilege, secrets never touch state, and the browser surface is Cognito-gated with PKCE. No high or medium findings — only a few low-severity hardening notes.
Positive controls
- Permissions boundary on every Lambda role — a ceiling the deploy user itself cannot exceed, so a compromised deploy path still can't grant a Lambda more than the boundary.
- Least-privilege IAM — each role's inline policy is scoped to the single DynamoDB table
ARN and only the actions it needs (
ingestwrite-only;chat/statsread;advicer/w).bedrock:InvokeModelis limited to the Haiku model/inference-profile ARNs;kms:Decryptis gated bykms:ViaService = ssm(can only decrypt SSM params). - Two-tier auth — machine ingest via
x-lifebot-key(correct for changing-IP M2M callers); browser routes via Cognito JWT (audience+issuer validated at the gateway). - Cognito hardening — no public signup (
allow_admin_create_user_only), 12-char password policy, PKCE public client (no client secret), short tokens (1 h id/access). - Secrets discipline — SSM SecureString for keys; Cognito password set out-of-band;
terraform.tfstateis gitignored and contains no plaintext secrets (verified). - Storage — S3 private + versioned + AES256; DynamoDB default-encrypted; 14-day log retention.
- Cost/DoS cap — API Gateway throttle (rate 20 / burst 40) bounds Bedrock + on-demand DynamoDB spend if the ingest key leaks or is abused.
- AI/health privacy — Bedrock (Anthropic) does not train on API data; chosen deliberately over an unpaid Gemini tier. Raw HealthKit samples are aggregated before any prompt and never logged world-readable.
Findings
LOW
L1 — Static ingest key. x-lifebot-key is a single long-lived shared secret. If it leaks,
an attacker can POST fake health data (write DynamoDB/S3) and trigger /advice (Bedrock spend).
Blast radius is bounded by the throttle and by write-only, table-scoped IAM. Fix: rotate the
SSM value periodically; consider per-source keys if the userscript/Hevy paths diverge.
L2 — CORS allows https://www.udemy.com. Intentional (the study userscript posts from
udemy.com), but it lets any script on that origin attempt API calls — they still need the
key/JWT, so this only widens the browser attack surface, it doesn't bypass auth. Fix: keep
only if the userscript is still in use; drop otherwise.
L3 — kms:Decrypt on Resource: "*". Standard pattern and effectively scoped by the
ViaService = ssm condition (usable only to decrypt SSM params), so low risk. Note only.
L4 — Local Terraform state, no remote backend. State is gitignored and secret-free, but lives only on the Mac (no encryption-at-rest backend, versioning, or locking). Fix (optional): an encrypted S3 backend + DynamoDB lock table if this ever grows beyond single-user/single-machine.
INFO
- HTTP API can't use AWS WAF (REST-API-only); auth key + JWT + throttling are the compensating controls — appropriate for personal volume.
- A read-only
securityread.json/claude-assistant-policy.jsongrants an assistant profile security-ops read (IAM/CloudTrail/GuardDuty/Access-Analyzer) with no mutate — good practice.
Recommended actions (all optional / low priority)
- L1 rotate the ingest key on a cadence.
- L2 drop the udemy.com CORS origin if the userscript is retired.
- L4 move to a remote encrypted TF backend if the project grows.
Related
wiki/projects/Bobs-lifebot/security-review.md · git is the source of truth