Project · Bobs-lifebot

Bob's Lifebot — Security Review (2026-07-03)

type referencestatus activeaws · security · review · health

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 (ingest write-only; chat/stats read; advice r/w). bedrock:InvokeModel is limited to the Haiku model/inference-profile ARNs; kms:Decrypt is gated by kms: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.tfstate is 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.json grants an assistant profile security-ops read (IAM/CloudTrail/GuardDuty/Access-Analyzer) with no mutate — good practice.
  1. L1 rotate the ingest key on a cadence.
  2. L2 drop the udemy.com CORS origin if the userscript is retired.
  3. L4 move to a remote encrypted TF backend if the project grows.
Compiled from wiki/projects/Bobs-lifebot/security-review.md · git is the source of truth