Stratify
Legal and compliance

Cryptography & Data Protection Policy

docs/compliance/isms/policies/POL-04-cryptography-data-protection.md

Source updated 03. Aug. 2026

Cryptography & Data Protection Policy

This policy covers cryptography as it is actually used in stratify today, secrets management, and the data-protection controls around information transfer, deletion, masking, and leakage prevention. Two controls here (8.10, and to a lesser extent 8.24's cryptographic core) are unusually mature for stratify's stage; three others (5.14, 8.11, 8.12) are real, undressed gaps. Both are stated as found.

1. Cryptography in use (8.24)

Stratify uses cryptography in two concrete, deliberate places rather than as a general-purpose capability scattered through the codebase:

  • Transport encryption. TLS is terminated by Traefik in front of the Hetzner Docker stack for

all production traffic. This is the expected baseline and is treated as such — not a differentiated control, but a precondition for everything else in this policy to mean anything.

  • Audit-log integrity. The audit_log table is protected by a SHA-256 chain-hash:

hash = SHA-256(prev_hash || canonical(payload)) over a deterministic, key-sorted JSON encoding of each event (apps/web/lib/audit/chain.ts). verifyChain() re-walks the table in pages of 1,000 rows and reports the first point of divergence. This is a deliberate, appropriate algorithm choice for the problem it solves — tamper-evidence for an append-only ledger, not confidentiality — and it is combined with a database-level enforcement that the table itself cannot be mutated or deleted from (a Postgres BEFORE UPDATE/BEFORE DELETE trigger, audit_log_no_mutate(), supabase/migrations/0001_init.sql). The chain-hash detects tampering after the fact; it does not by itself prevent a compromised service-role key from writing a valid next link, which is why POL-03 §5's privileged-access handling matters as a complementary control, not a redundant one.

Control 8.24 is scored partial, not because the cryptography in use is weak, but because there is no written cryptography policy: no documented standard for algorithm choices going forward, no key-lifecycle or rotation policy beyond the ad hoc practice described in §2. The gap is documentation and governance, not the cryptographic choices already made.

2. Secrets management

Stratify's secrets policy, as documented in docs/secrets.md, is: no live API keys, database URLs, or signing secrets in `.env` files, ever. Development secrets live in macOS Keychain, added via security add-generic-password and sourced into every shell session through ~/.keychain-secrets.sh. Production secrets are injected as environment variables into the Hetzner Docker containers — docs/secrets.md's current text still says "Vercel project envs in production," which is stale (register/facts.md flags this explicitly); the Hetzner Docker env-var model is the current, correct description and should be read as such until docs/secrets.md itself is corrected (tracked under GOV-13).

docs/secrets.md names the required secret variables (.env.example, values never committed): STRATIFY_SUPABASE_SERVICE_ROLE, STRATIFY_STRIPE_SECRET_KEY, STRATIFY_STRIPE_WEBHOOK_SECRET, the AWS SES credential trio, STRATIFY_VAPID_PRIVATE_KEY, STRATIFY_SENTRY_AUTH_TOKEN, STRATIFY_DATABASE_URL, STRATIFY_AI_GATEWAY_API_KEY, STRATIFY_EXPO_ACCESS_TOKEN, STRATIFY_CRON_SECRET, STRATIFY_PARTNER_WEBHOOK_SECRET, and STRATIFY_SUPABASE_WEBHOOK_SECRET.

docs/secrets.md already defines a rotation table, adopted here as policy even though its triggers are not yet automated:

TriggerAction
Engineer leavesRotate every secret they could have read. Service-role first.
Partner-key suspected compromisedRevoke in partner_api_keys, issue replacement via /admin/partners.
Payment-webhook secret leakedRotate at the provider, re-add the corresponding secret.
Audit-chain divergenceTreat as P0; freeze service-role until forensics complete.

This table is aspirational in one specific sense worth naming honestly, per register/facts.md: given the single-engineer reality, "rotate every secret an engineer could have read" has never been exercised, and no tooling automatically rotates a secret on any of these triggers today — a human has to remember and act. That doesn't make the table wrong; it makes it a documented manual procedure rather than an enforced one. Formalising this into an actual procedures/PRO-11 (Cryptographic key / secrets management) is planned but not yet written.

3. Information transfer (5.14)

Scored partial in register/controls.yaml. TLS is assumed for all traffic, as stated in §1, but there is no documented information-transfer policy for the cases that matter most: how partner API data handoffs are secured beyond TLS-in-transit and HMAC-signed webhooks (the partner webhook mechanism uses STRATIFY_PARTNER_WEBHOOK_SECRET), what happens when Antonios or Toby needs to export and share data outside the running system (e.g. for a DPA negotiation or a regulator request), and what the rule is for sending any data to a new sub-processor before that sub-processor's DPA status is confirmed. This is a real, current gap: the mechanism (TLS, HMAC signing) exists; the policy stating when and how it must be used does not.

4. Information deletion (8.10)

This is the one control in this policy that is unusually mature for stratify's stage, and it is scored implemented in register/controls.yaml on real evidence, not aspiration: eraseSubscriberData (apps/web/app/(admin)/subscribers/actions.ts) is a working, gated deletion path. It:

  • Hard-deletes web_push_subscriptions, onboarding_profile, user_active_mandate, and

subscriber_roles for the target subscriber.

  • Anonymizes the subscribers row in place — email becomes erased+<id8>@stratify.invalid,

name is nulled, status flips to banned, and Stripe ID plus referral attribution are cleared.

  • Optionally hard-deletes the corresponding Supabase Auth user.
  • Is gated by requireComplianceActor() (POL-03 §4) — an admin/compliance-triggered action, not

self-service.

  • Writes its own completion as an audit_log event (gdpr.erasure.completed) — the audit trail

of the deletion is deliberately retained even as the underlying personal data is removed or anonymized, so the fact that an erasure happened remains provable without retaining the erased data itself.

The honest caveat, consistent with dsgvo-data-flow.md's own "mid-term"/"Phase B" framing: this is admin-triggered, not a self-service /account delete button. That gap is tracked under procedures/PRO-09 and privacy/PRIV-11, not this policy.

5. Data masking (8.11)

Scored not_started. No data-masking or anonymization process exists for any non-prod environment that might use copies of real subscriber or portfolio data. This is stated as an open item to confirm, not assume: whether a staging environment with real (or real-derived) data even exists is not established in register/facts.md — control 8.31 (Separation of dev/test/prod, POL-06 §5) notes the same unconfirmed fact from the environment-separation angle. Until that's confirmed, this policy's position is that no test or staging environment should be populated with real subscriber data without an explicit masking/anonymization pass, and that determining whether such an environment currently exists is the first concrete step toward closing this gap.

6. Data leakage prevention (8.12)

Scored not_started. Stratify has no systematic DLP control — no egress monitoring, no content-inspection layer, no automated detection of bulk data exfiltration. What exists adjacent to DLP is narrower and serves a different purpose: GoTrue's OTP verify rate-limiting (GOTRUE_RATE_LIMIT_VERIFY=30, POL-03 §3) constrains brute-force authentication attempts, not data exfiltration, and RLS (POL-03 §1) constrains what an authenticated principal can read, not what a compromised or over-privileged principal can exfiltrate once it does have read access. This policy names the distinction explicitly so that RLS is not mistaken for a DLP control it was never designed to be.

7. Control summary

ControlStatus (controls.yaml)This policy's role
5.14 Information transferpartialTLS assumed; no documented transfer policy for partner/export scenarios (§3).
8.10 Information deletionimplementedReal, working eraseSubscriberData path — a genuine strength (§4).
8.11 Data maskingnot_startedNo masking process; staging-environment existence itself unconfirmed (§5).
8.12 Data leakage preventionnot_startedNo systematic DLP; rate-limiting and RLS are adjacent, not substitutes (§6).
8.24 Use of cryptographypartialTLS + SHA-256 chain-hash are real and appropriate; no written policy or rotation cadence (§1, §2).

8. Review

This policy is reviewed on the cadence set at approval, and immediately upon: confirmation (or creation) of a staging environment (triggering §5), any secret-rotation event under §2's table, or any audit-chain divergence detected by verifyChain() (triggering the P0 path in docs/runbooks/db-recovery.md §3.3 alongside this policy's own review).