Harden recovery backup flow and add promotion smoke gate - #3
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens the recovery-backup workflow end-to-end (client + server), adds an EC2 single-host backend runbook/scripts, and introduces a promotion-time post-deploy smoke gate to verify encrypted backup upload/recovery against the live API.
Changes:
- Add encrypted browser recovery backup upload + cross-browser restore (including linked-device import) to the hosted web app, plus regression tests and PoW registration nonce support.
- Add EC2 provisioning/deploy/configure + backup/recovery smoke-check scripts, and wire the smoke check into the promote workflow + governance validators.
- Add Cloudflare Pages
_headerscontract validation and WASM staging for production builds.
Reviewed changes
Copilot reviewed 33 out of 34 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/security/validate_web_production_contract.py | Enforces Pages _headers security header contract in production validation. |
| scripts/security/validate_release_governance_workflows.py | Adds governance validation for the new promote smoke-gate step. |
| scripts/dev/start_local_hosted_web_backend.ps1 | Helper to run a local relay configured for hosted web origin testing. |
| scripts/dev/start_cloudflare_quick_tunnel.ps1 | Helper to expose a local relay via Cloudflare Quick Tunnel. |
| scripts/dev/aws/smoke_check_ec2_backup_recovery.ps1 | New end-to-end backup upload/recovery smoke-check script. |
| scripts/dev/aws/provision_ec2_backend.ps1 | New AWS CLI-driven EC2 provisioner for backend hosting. |
| scripts/dev/aws/deploy_ec2_backend_stack.ps1 | Copies EC2 compose assets + installs systemd unit on the instance. |
| scripts/dev/aws/configure_and_start_ec2_backend.ps1 | Generates env files, starts stack, optionally runs smoke check. |
| mobile/web/src/server.ts | Adds typed client API calls for backup upload + recovery download; registration PoW nonce field. |
| mobile/web/src/router.ts | Adds import-device route. |
| mobile/web/src/db.ts | Adds IndexedDB insert-if-absent helper for message dedupe. |
| mobile/web/src/crypto.ts | Adds backup upload auth header builder + linked-device package helpers + key rebinding. |
| mobile/web/src/crypto-wasm.ts | Changes WASM import path to staged /pkg/pqmsg_core.js. |
| mobile/web/src/app.ts | Implements hosted onboarding/sign-in hardening, backup sync/restore flows, linked-device import/export UX, message dedupe, PoW registration nonce solving. |
| mobile/web/src/app.flow.test.ts | Adds regression coverage for backup failure paths, PoW nonce, hosted relay bootstrap, message dedupe overlap, settings button behavior, linked-device flows. |
| mobile/web/src/app.css | Adds new onboarding layout styles and textarea styling. |
| mobile/web/scripts/stage-wasm-pkg.mjs | Stages wasm-pack output into public/pkg for production builds. |
| mobile/web/public/_headers | Adds hardened security headers + caching rules for Cloudflare Pages. |
| mobile/web/package.json | Runs WASM staging as part of npm run build. |
| mobile/web/.env.production | Sets default hosted relay URL for production builds. |
| docs/WEB_DEPLOYMENT.md | Documents Cloudflare Pages deployment and local tunnel workflow. |
| docs/RELEASE_GOVERNANCE.md | Adds requirement for post-deploy backup/recovery smoke check on promotions. |
| docs/AWS_EC2_BACKEND.md | New EC2 backend runbook documentation. |
| deploy/aws/ec2/systemd/pqmsg-backend-compose.service | Systemd unit to manage the compose stack. |
| deploy/aws/ec2/pqmsg-server.aws.env.example | Example runtime config for EC2 compose backend. |
| deploy/aws/ec2/docker-compose.ec2.yml | Compose stack: postgres/redis/blob-permissions-init/pqmsg-server/caddy. |
| deploy/aws/ec2/Caddyfile | TLS termination + reverse proxy for the API. |
| deploy/aws/ec2/.env.example | Example compose env file for domain/email/postgres password/image. |
| crates/pqmsg-server/tests/api.rs | Adds test coverage for unauthenticated recovery-backup download. |
| crates/pqmsg-server/src/lib.rs | Adds recovery-backup route and changes sender cert signing key fallback behavior. |
| crates/pqmsg-server/src/handlers/backups.rs | Implements unauthenticated recovery-backup download handler. |
| README.md | Links new EC2 backend runbook doc. |
| .gitignore | Ignores local smoke-check artifacts. |
| .github/workflows/promote.yml | Adds post-deploy backup/recovery smoke-gate execution and API URL resolution. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pub(crate) async fn download_recovery_backup( | ||
| State(state): State<AppState>, | ||
| Path(user_id): Path<String>, | ||
| ) -> Result<Json<BackupDownloadResponse>, AppError> { | ||
| check_rate_limit(&state, &format!("backup-recovery:{user_id}"))?; | ||
| validate_id("user_id", &user_id)?; | ||
| ensure_user_exists(state.pool(), &user_id).await?; | ||
|
|
||
| Ok(Json(load_backup_response(&state, &user_id).await?)) |
There was a problem hiding this comment.
The new unauthenticated recovery-backup download endpoint only rate-limits on a key that varies by user_id ("backup-recovery:{user_id}"). That allows high-volume enumeration and bulk download attempts across many user IDs because there’s no IP-based bucket (unlike other unauthenticated flows such as sealed relay). Consider adding ConnectInfo + headers and applying an IP-based limiter (e.g., "backup-recovery-ip:{ip}") in addition to any per-user bucket.
| $chars = (48..57 + 65..90 + 97..122 | ForEach-Object { [char]$_ }) | ||
| $postgresPassword = -join (1..32 | ForEach-Object { | ||
| $chars[(Get-Random -Minimum 0 -Maximum $chars.Count)] | ||
| }) | ||
| $senderKeyBytes = New-Object byte[] 32 | ||
| $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() | ||
| try { | ||
| $rng.GetBytes($senderKeyBytes) | ||
| } finally { | ||
| $rng.Dispose() | ||
| } |
There was a problem hiding this comment.
The generated Postgres password uses Get-Random over a character set, which is not a cryptographically secure RNG. Since this helper provisions credentials for an internet-reachable EC2 stack, switch to a CSPRNG approach (e.g., RandomNumberGenerator like the sender-cert signing key generation) and encode the bytes into a URL/INI-safe string.
| $chars = (48..57 + 65..90 + 97..122 | ForEach-Object { [char]$_ }) | |
| $postgresPassword = -join (1..32 | ForEach-Object { | |
| $chars[(Get-Random -Minimum 0 -Maximum $chars.Count)] | |
| }) | |
| $senderKeyBytes = New-Object byte[] 32 | |
| $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() | |
| try { | |
| $rng.GetBytes($senderKeyBytes) | |
| } finally { | |
| $rng.Dispose() | |
| } | |
| $postgresPasswordBytes = New-Object byte[] 24 | |
| $senderKeyBytes = New-Object byte[] 32 | |
| $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() | |
| try { | |
| $rng.GetBytes($postgresPasswordBytes) | |
| $rng.GetBytes($senderKeyBytes) | |
| } finally { | |
| $rng.Dispose() | |
| } | |
| $postgresPassword = [Convert]::ToBase64String($postgresPasswordBytes).Replace('+', '-').Replace('/', '_') |
This mirrors the backup-recovery reliability and promotion hardening updates:\n\n- Fix blob-store permission initialization on EC2 compose startup\n- Add reusable backup upload/recovery smoke-check script\n- Add optional smoke-check execution in EC2 configure/start helper\n- Harden web onboarding/sign-in paths to fail closed when recovery backup cannot be persisted\n- Add regression tests for backup failure paths and registration PoW nonce coverage\n- Add promote workflow gate to run post-deploy backup/recovery smoke check\n- Add governance policy/validator updates for the new promotion gate\n- Add ignore rules for local smoke artifacts\n\nEnvironment setup was also applied in this repo:\n- pilot + production environments configured\n- PQMSG_PUBLIC_API_BASE_URL set\n- main-only deployment branch policy on both environments\n- production wait timer 5 minutes