Complete Phase 3, Phase 5 polish and hardening
Phase 3 — Investments: - Multi-currency support: holdings track purchase currency, FX rates convert to base for totals - Capital gains report using UK Section 104 pool method, grouped by tax year - Capital Gains tab added to Reports page Phase 5 — Polish & Hardening: - Mobile-responsive layout: bottom nav, sidebar hidden on mobile, logo in TopBar, compact header buttons, hover-only actions now always visible on touch - Backup system: encrypted GPG backups via backup.sh, nightly scheduler job, admin API (list/trigger/download/restore), Settings UI with drag-to-restore confirmation - Docker entrypoint with gosu privilege drop to fix bind-mount ownership on fresh deployments - OWASP fixes: refresh token now bound to its session (new refresh_token_hash column + migration), CSRF secure flag tied to environment, IP-level rate limiting on login, TOTPEnableRequest Pydantic schema replaces raw dict - AES-256-GCM key rotation script (rotate_keys.py) with dry-run mode and atomic DB transaction - CLAUDE.md added for AI-assisted development context - README updated: correct reverse proxy port, accurate backup/restore commands, key rotation instructions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
74e57a35c0
commit
fe4e69b9ad
40 changed files with 2079 additions and 127 deletions
58
README.md
58
README.md
|
|
@ -80,7 +80,7 @@ Ten independent security layers:
|
|||
3. **Security headers** — CSP, HSTS, X-Frame-Options DENY, X-Content-Type-Options, form-action, Permissions-Policy
|
||||
4. **CSRF** — double-submit cookie on all mutating endpoints
|
||||
5. **Authentication** — Argon2id password hashing; RS256 JWT (15-min access + 7-day HttpOnly refresh cookie); TOTP (RFC 6238) with backup codes; brute-force lockout (exponential backoff via Redis)
|
||||
6. **Rate limiting** — Redis sliding window: 10/min on auth, 10/min on TOTP, 20/min on predictions, 300/min general API
|
||||
6. **Rate limiting** — Redis sliding window: 20/min on login (per IP), 10/min on TOTP, 20/min on predictions, 300/min general API
|
||||
7. **Field-level encryption** — AES-256-GCM on all PII fields (account names, transaction descriptions, merchant, notes, TOTP secret); IV ‖ ciphertext ‖ tag stored in `bytea`
|
||||
8. **Database encryption** — pgcrypto as a second layer on backup dumps
|
||||
9. **Row-level security** — PostgreSQL RLS policies enforce user isolation at the DB layer; buggy queries cannot leak another user's data
|
||||
|
|
@ -168,44 +168,62 @@ docker compose up -d backend
|
|||
|
||||
### 5. Point your reverse proxy
|
||||
|
||||
Forward your domain to `http://<host>:8090`. The frontend is served from port `4000` internally but the backend proxies frontend assets — point everything at `8090`.
|
||||
Forward your domain to `http://<host>:4000`. The frontend nginx serves the React app and proxies all `/api/` requests to the backend internally.
|
||||
|
||||
---
|
||||
|
||||
## Backups
|
||||
|
||||
Encrypted nightly backup runs automatically at 3 AM:
|
||||
Encrypted backups run automatically every night at 3 AM (GPG AES-256 symmetric encryption). Backups are stored in `./data/backups/` and retained for 30 days.
|
||||
|
||||
**Via the web UI:** Settings → Backups — trigger a manual backup, download a copy, or restore from any listed backup file.
|
||||
|
||||
**Manual backup from the command line:**
|
||||
|
||||
```bash
|
||||
# Manual backup
|
||||
docker compose exec -e BACKUP_PASSPHRASE="$BACKUP_PASSPHRASE" backend bash scripts/backup.sh
|
||||
|
||||
# List backups
|
||||
docker compose exec backend bash scripts/restore.sh --list
|
||||
|
||||
# Restore
|
||||
docker compose exec -e BACKUP_PASSPHRASE="$BACKUP_PASSPHRASE" \
|
||||
-e DATABASE_URL="$DATABASE_URL" \
|
||||
backend bash scripts/restore.sh 20240101_030000.sql.gz.gpg
|
||||
docker compose exec backend bash /app/scripts/backup.sh
|
||||
```
|
||||
|
||||
Backups are stored in `./data/backups/` and retained for 30 days.
|
||||
**Manual restore** (stop the backend first to avoid lock contention):
|
||||
|
||||
```bash
|
||||
docker compose stop backend
|
||||
|
||||
docker compose run --rm backend bash -c '
|
||||
export GNUPGHOME=/tmp/.gnupg; mkdir -p $GNUPGHOME && chmod 700 $GNUPGHOME
|
||||
PG_URL=${DATABASE_URL/postgresql+asyncpg/postgresql}
|
||||
gpg --batch --yes --no-symkey-cache --pinentry-mode loopback \
|
||||
--passphrase "$BACKUP_PASSPHRASE" \
|
||||
--decrypt $(ls /app/backups/*.sql.gz.gpg | tail -1) \
|
||||
| gunzip | psql "$PG_URL"
|
||||
'
|
||||
|
||||
docker compose start backend
|
||||
```
|
||||
|
||||
## Key Rotation
|
||||
|
||||
To rotate the AES-256-GCM encryption key without data loss:
|
||||
|
||||
```bash
|
||||
# Generate new key
|
||||
# Generate a new key
|
||||
NEW_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
||||
echo "New key: $NEW_KEY"
|
||||
|
||||
# Stop the app, rotate, update .env, restart
|
||||
docker compose stop backend
|
||||
NEW_ENCRYPTION_KEY="$NEW_KEY" ./scripts/rotate_keys.sh
|
||||
# Update ENCRYPTION_KEY in .env to $NEW_KEY
|
||||
# Dry-run first — validates decryption works, no DB changes
|
||||
docker compose exec backend python /app/scripts/rotate_keys.py \
|
||||
--old-key "$ENCRYPTION_KEY" --new-key "$NEW_KEY" --dry-run
|
||||
|
||||
# If dry-run passes, rotate for real (atomic — rolls back on any failure)
|
||||
docker compose exec backend python /app/scripts/rotate_keys.py \
|
||||
--old-key "$ENCRYPTION_KEY" --new-key "$NEW_KEY"
|
||||
|
||||
# Update ENCRYPTION_KEY in .env, then restart
|
||||
docker compose up -d backend
|
||||
```
|
||||
|
||||
Do not lose your current `ENCRYPTION_KEY` — without it, all encrypted fields are permanently unreadable.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
|
@ -242,7 +260,7 @@ MyMidas/
|
|||
│ ├── pages/ # Route-level page components
|
||||
│ └── store/ # Zustand state (auth, UI/theme)
|
||||
├── postgres/init/ # PostgreSQL init SQL (extensions, RLS policies)
|
||||
├── scripts/ # backup.sh, restore.sh, rotate_keys.sh
|
||||
├── backend/scripts/ # backup.sh, rotate_keys.py, entrypoint.sh
|
||||
├── secrets/ # JWT keys (git-ignored, generate locally)
|
||||
└── docker-compose.yml
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue