---
id: "9.9"
title: "Security practices"
dept: security-privacy-and-legal
status: draft
owner: Param
branded: false
last_verified: "2026-10-26"
sources:
  - "brew origin/main d6154b3: .github/workflows/test.yml:1-16, db/brand-wipe.js:7-32, 187-195, docs/production.md:112-115"
  - "brew/auth.js:4-6, 46-47, 54-64, 66, 73, 221-247, 250"
  - "brew/sessions.js:1-36"
  - "brew/vault.js:1-30"
  - "brew/handoff.js:18"
  - "brew/reader/util.js:10-40"
  - "brew/reader/stages/site.js:103"
  - "brew/pdf/render.js:26"
  - "brew/tools/prod-env.sh:1-12"
  - "brew/docs/production.md:9-15, 25-33, 35-45, 112-115"
  - "brew/docs/handover/engineering/09-security-and-access.md:1-60"
  - "brew/docs/handover/engineering/audits/E-accounts-demo-security.md:1-115"
  - "memory brew-prod-backend, brew-audit-2026-10-15, brew-qa-audit-2026-10-20, prod-clean-only, brew-access-rules"
  - "handbook content/engineering/changelog.md:81, 93, 97, 886-896"
---

# Security practices

## Answer

How we keep brand data safe today, sign in, sessions, secrets, least access, the network edge and the audits already done, and what is still open before brands beyond the five partners arrive.

## Today

### Sign in and sessions

- Google sign in with PKCE and a nonce; the id token's audience, issuer, expiry, nonce and verified email are checked. [src: brew/auth.js:4-5, 221-239]
- Sessions are an HMAC signed cookie, HttpOnly, SameSite Lax, Secure on Cloud Run, for 30 days. [src: brew/auth.js:54-64, 66, 245]
- Every token carries its type, so a connect state never works as a session. [src: brew/auth.js:56-64]
- Each sign in has a session row; sign out ends it on every instance, and a root admin can end every session of one email. [src: brew/sessions.js:1-4, 27-32]
- Redirects after sign in go only to our own paths. [src: brew/auth.js:73]
- Local test sign in exists only off Cloud Run. [src: brew/auth.js:250]

### Secrets

- All keys come from Secret Manager; production uses its own `*_PROD` secrets for the session, vault and share links. [src: brew/tools/prod-env.sh:10] [src: brew/docs/production.md:14]
- Connected account keys are AES-256-GCM in the vault, never sent to a page and never logged. [src: brew/vault.js:1-3]
- On production, share links are signed with `HANDOFF_SECRET` only, never the AI key. [src: brew/handoff.js:18] [src: handbook content/engineering/changelog.md:895]
- Without a session secret on Cloud Run, sign in turns itself off instead of using a default. [src: brew/auth.js:46-47]

### Least access

- Production runs as `brew-prod-runtime@`; staging's account cannot read production's secrets or data. [src: brew/tools/prod-env.sh:4-6] [src: memory brew-prod-backend]
- Web ingress is the load balancer only; the worker is internal; the database has a private IP and deletion protection. [src: brew/docs/production.md:9-15] [src: memory brew-prod-backend]
- Cloud Armor limits 3000 and 6000 requests a minute; load balancer logging is on; the CDN is off. [src: memory brew-prod-backend]
- Every answer not marked public is private, so a cache never serves one viewer's data to another. [src: handbook content/engineering/changelog.md:97]
- The only root admin on production is `param@joinsquareup.com`; every address at the two staff domains, `joinsquareup.com` and `shopos.ai`, gets full staff access. [src: brew/tools/prod-env.sh:9]

### Outside input

- Every fetch of a pasted address resolves DNS and refuses private, loopback and metadata ranges on each redirect. [src: brew/reader/util.js:10-40]
- Headless Chromium runs with `--no-sandbox` in the reader and the PDF renderer. [src: brew/reader/stages/site.js:103] [src: brew/pdf/render.js:26]
- The demo site wraps `fetch` so it can never call a sending host or write to an account. [src: brew/docs/handover/engineering/09-security-and-access.md:48]

### Release and recovery

- Only `tools/promote.sh` releases production: backup, migrations, a clean gate that refuses test data, dummies and wrong settings, then traffic, with rollback on failure. [src: brew/docs/production.md:25-33] [src: memory prod-clean-only]
- Point in time recovery for 7 days, 14 daily backups, a nightly export to a separate bucket; a restore test passed on 25 Oct. [src: brew/docs/production.md:35-45]
- Alerts email param@ and hello@: server errors, slow requests, worker lease lost, release failed, disk, connections, CPU, export failed, site down. [src: brew/docs/production.md:112-115]
- Since 26 Oct GitHub Actions runs the whole test suite on every push and pull request, with every AI call mocked, so no key is needed in CI. [src: brew/.github/workflows/test.yml:1-16]
- A data request deletion is a dry run first, refuses partners and protected brands, never revokes a platform grant and leaves one audit row. [src: brew/db/brand-wipe.js:7-9, 24-32, 187-195]

### Audits done

| Date | Audit | Outcome | Source |
|---|---|---|---|
| 15 Oct | Handover audit E: accounts, demo, security; 11 findings S-1 to S-11 and 5 fix-now items N-1 to N-5 | Shared links, team notes, mic, facts, PDF path, service account and share secret fixed the same day | [src: brew/docs/handover/engineering/audits/E-accounts-demo-security.md:57-115] [src: handbook content/engineering/changelog.md:886-896] |
| 15 and 16 Oct | Full audit | Onboarding stream and Play titles made team only; notify takes only your own address; photo fetches through the guard; Slack actions and GA4 by property flagged | [src: memory brew-audit-2026-10-15] |
| 20 Oct | QA audit, 4 auditors, about 86 findings | P0s and most P1s live | [src: memory brew-qa-audit-2026-10-20] |
| 25 Oct | Three independent reviews on storage, roles and security, about 45 findings, then QV rounds 2 and 3 | All fixed with tests; sign out ends sessions; Arata guard no longer bypassable | [src: memory brew-prod-backend] |

### Still open

- The default compute service account keeps project wide secret access and `run.admin`, which affects about 15 other services. [src: memory brew-prod-backend]
- `STAFF_REQUIRE_HD` is not set on production. [src: brew/auth.js:240-242] [src: brew/tools/prod-env.sh:9]
- Claiming by email domain is weak proof of ownership. [src: brew/docs/handover/engineering/09-security-and-access.md:40]
- Chromium without its sandbox. [src: brew/reader/stages/site.js:103]

## Tomorrow

| Item | Version | Tag |
|---|---|---|
| Strip the default compute account to what Cloud Run needs | v1 | New, proposed |
| `STAFF_REQUIRE_HD=1` and named staff on production | v1 | New, proposed |
| A security review before every new power, v2 to v4 | v1 | New, proposed |
| Store admin or DNS proof for claims | v2 | New |
| Write scopes held in their own table, used only after a person's yes | v2 | New |
| Chromium in its own sandboxed job | v2 | New, proposed |
| Ad rules with spend limits checked on the server | v4 | New |

## How we do it

1. Never print, log or send a key to a page. New keys go to Secret Manager and `tools/prod-env.sh`.
2. Every new route goes through sign in, `genOk` and the closed gate; test the outsider paths.
3. Run the security and access tests before every deploy:

```bash
cd /Users/param/Documents/ChatGPT/brew && AI_MOCK=1 node --test test/security-fixes.test.js test/access.test.js test/qv1-roles-security.test.js test/qa-final-access.test.js
```

4. Release only with `tools/promote.sh`; never edit it while it runs.
5. Run the restore test monthly, as written in `docs/production.md`.
6. After any leak of a key in chat or a file, rotate it the same day.

## Numbers

| Metric | Definition | Where measured | Status |
|---|---|---|---|
| Open security findings | Items in Still open | This page | 4 on 26 Oct |
| Days since restore test | Days since the last passed restore test | `docs/production.md` | 1 on 26 Oct |
| Clean gate refusals | Promotes the gate stopped | `brew-prod-clean` job logs | Proposed |

## Open

1. **Fix the default compute account now?** It touches other company services. Proposal: yes, this week, one service at a time, each moved to its own account first.
2. **An outside penetration test?** Proposal: one after v1 ships to the first 20 brands, not before.
3. **Rotate the keys pasted in chat on 28 Sep?** The SquareUp tracking keys share this Google project. Proposal: rotate them now. [src: memory squareup-tracking-structure]
