Skip to content

Forms Portal — Compliance Controls Mapping

Classification: CONFIDENTIAL — Internal Use Only

Document version

v1.0 — 2026-04-18 — derived from forms-backend/compliance/cis-pci-nist-mitre.md at Phase 1 cut.

This document is the authoritative mapping of the Forms Portal (forms.greenpeace.us) implementation against CIS Controls v8, PCI-DSS v4.0, NIST 800-53 Rev 5, and MITRE ATT&CK mitigations. It is reviewed quarterly by the Director of Cyber Security and requires owner sign-off before production cutover (tracked in Cowork Task 6 of the Phase 1 runbook).

The operational/architecture description of the portal lives at Forms Portal — forms.greenpeace.us. This page covers controls and evidence only.


Data classification

Data element Classification Handling
Submission field values (default) CONFIDENTIAL AES-256-GCM envelope encryption, per-submission DEK wrapped by KMS
Searchable field mirrors (searchable: true) INTERNAL Plaintext JSONB column, indexed for admin search
Attachments CONFIDENTIAL GCS with CMEK, signed URLs, ClamAV scanned, 7-year retention lock
Submitter identity (email, username) INTERNAL Plaintext columns (operational necessity for audit)
Audit log CONFIDENTIAL Append-only (REVOKE UPDATE/DELETE), 7-year retention
DEK wrapping key RESTRICTED Cloud KMS, CMEK, software-protected, automatic 90-day rotation

Encryption — data in transit

Channel Protocol Enforcement
Browser → Cloud Run TLS 1.3, HSTS 2yr + preload Cloud Run managed cert; HSTS header set in app.py
Cloud Run → Cloud SQL TLS 1.3 + mTLS via Cloud SQL Connector IAM auth; enable_iam_auth=True in db.py
Cloud Run → KMS / GCS / Secret Manager TLS 1.3 (Google private endpoints) Service account OAuth2 via ADC
Cloud Run → HappyFox HTTPS (TLS 1.2+) Validated in requests call
Cloud Run → VPC (private IP) TLS via Cloud SQL Connector vpc-egress=private-ranges-only

Encryption — data at rest

Asset Cipher Key
Cloud SQL storage AES-256 (Google-managed layer) + CMEK (customer-managed) gpus-forms-cmek
Cloud SQL automated backups Same as instance Inherited
Submission field values AES-256-GCM Per-submission DEK, wrapped by KMS key gpus-forms-dek-wrapper
GCS attachments bucket AES-256 + CMEK gpus-forms-cmek
GCS backup bucket AES-256 + CMEK gpus-forms-cmek
Secret Manager AES-256 (Google-managed) Managed

CIS Controls v8

Control Title Implementation
3.11 Encrypt Sensitive Data at Rest CMEK on Cloud SQL + GCS; AES-256-GCM envelope on submission fields
3.12 Segment Data Processing Dedicated service account gpus-forms-backend@; private IP; VPC connector
4.4 Firewall Cloud SQL private IP only; Cloud Armor WAF at edge; Cloud Run no-allow-unauthenticated
5.1 Account Management Okta SSO via OIDC; users table mirrors Okta identity; roles admin/viewer only
6.3 Multi-factor Authentication MFA enforced by Okta policy at tenant level
6.8 Privileged Account Access Admin role granted in users table; audited via user_granted/user_revoked events
8.2 Audit Log Collection Append-only audit_log table; structured JSON to Cloud Logging → CEDAR
8.3 Log Storage 7-year retention; Cloud Logging sink to GCS with bucket lock
8.5 Centralized Log Management Cloud Logging → CEDAR Elasticsearch (existing pipeline)
8.11 Audit Log Review Wazuh rules 100020–100025 on MAPLE; SOC dashboard Tickets tab
10.6 Vulnerability Scanning Cloud Build step runs bandit + pip-audit; OAK OpenVAS weekly; Artifact Registry vuln scanning
13.1 Threat Detection Wazuh rules trigger SOC auto-ticketing at level ≥ 10
16.1 Secure Application Development Pydantic input validation; parameterized SQL via SQLAlchemy; CSRF tokens; security headers

PCI-DSS v4.0

Forms portal does not process cardholder data. PCI-DSS mapping documented for defense-in-depth alignment only.

Req Implementation
1.2.1 Inbound traffic restricted — Cloud Run auth required, Cloud Armor WAF filters
2.2.1 Secure configuration — CIS-hardened base image, distroless python, non-root container user
3.5 Key management — KMS-wrapped DEK; rule 100024 fires on decrypt failure
3.6 Key rotation — KMS automatic 90-day rotation; re-encrypt job runs on key version change
4.1 Strong cryptography for transmission — TLS 1.3, HSTS preload
6.5.9 Protect against CSRF — Flask-WTF CSRF tokens; rule 100022 logs rejects
7.1 Access to data limited to need — forms_viewer RLS role; admin actions audited
8.3.6 Password complexity — N/A (Okta SSO, no passwords in forms portal)
10.2.1–10.2.7 Audit trails — audit_log captures auth, admin, decrypt, export events
10.3 Audit trail protection — REVOKE UPDATE/DELETE on audit_log; central forwarding to CEDAR
10.7 Log retention — 7 years
11.3 Vulnerability scans — OpenVAS weekly (OAK); image scan on every build

NIST 800-53 Rev 5

Control Implementation
AC-2 Account Management — Okta + users table sync
AC-3 Access Enforcement — @require_role decorators; Postgres RLS
AC-6 Least Privilege — 2 roles only; service account scoped to Cloud SQL client + KMS encrypter/decrypter + GCS object creator/viewer
AU-2 Event Logging — audit_log table + Cloud Logging
AU-6 Audit Record Review — Wazuh rules + SOC dashboard
AU-9 Protection of Audit Information — append-only, REVOKE UPDATE/DELETE
AU-11 Audit Record Retention — 7 years
CM-7 Least Functionality — minimal container, no shell, distroless-style
IA-2 Identification & Authentication — Okta OIDC, JWT verification
IA-5 Authenticator Management — handled by Okta
SC-8 Transmission Confidentiality — TLS 1.3 everywhere
SC-12 Cryptographic Key Establishment — KMS-managed CMEK + DEK
SC-13 Cryptographic Protection — AES-256-GCM, FIPS-validated via BoringCrypto in GCP
SC-28 Protection of Info at Rest — envelope encryption + CMEK
SI-4 System Monitoring — Prometheus + Wazuh
SI-10 Information Input Validation — pydantic

MITRE ATT&CK mitigations

Technique Mitigation
T1078 (Valid Accounts) Okta MFA; role-based access; account lifecycle via Okta lifecycle management
T1190 (Exploit Public-Facing Application) Cloud Armor OWASP rules; pydantic input validation; bandit + pip-audit on build
T1213 (Data from Information Repositories) RLS on submissions; decrypt events audited; bulk export rule 100025
T1537 (Transfer Data to Cloud Account) Egress restricted to private ranges; signed URLs for attachments; bulk export alerts
T1552 (Unsecured Credentials) All secrets in Secret Manager; no passwords in code or env vars; IAM auth for DB
T1562 (Impair Defenses) Audit log append-only at DB role level; separate forms_readonly role for reports

Control ownership

Area Owner Review cadence
Cryptographic controls Director of Cyber Security Quarterly
Access controls (Okta + RLS) Director of Cyber Security Quarterly
Audit log review SOC Weekly (via SOC dashboard)
Vulnerability scanning SOC (OAK) Weekly
Key rotation verification Director of Cyber Security Quarterly
Retention policy enforcement Director of Cyber Security Monthly (purge job audit)

Sign-off before production

  • [ ] Director of Cyber Security reviews this document
  • [ ] Wazuh rules deployed and verified firing on test events
  • [ ] Prometheus alerts routed to correct email/Slack channels
  • [ ] OAK OpenVAS target added, baseline scan complete with zero criticals
  • [ ] Penetration test scope includes forms portal (update pentest-schedule.md)
  • [ ] Disaster recovery test completed (restore from GCS backup, verify decrypt works)
  • [ ] IRP (irp.md) updated with forms-portal-specific playbook entries

Rotation verification log

Quarterly execution log for Cowork Task 8 (forms-portal credential rotation check) from forms-backend/RUNBOOK-COWORK.md. Each run records one line per check with status PASS / FLAG / ACTION-NEEDED and any operator notes. This skill never rotates secrets automatically — it verifies state and raises follow-ups.

2026-04-20 — Q2 2026 check (automated run)

Executed by: Cowork scheduled task forms-portal-credential-rotation-quarterly (agent, unattended — Rajesh not present for interactive walk-through).

Verification evidence available to the agent was limited to the repo — no gcloud CLI, no GCP Console session, and no HappyFox support channel were reachable from the scheduled-task sandbox. Items that require live console state are therefore logged as ACTION-NEEDED for Rajesh to complete manually and back-fill into this log at the next available window.

# Check Status Notes
1 HappyFox credentials rotation (gpus-forms-happyfox-api-key, gpus-forms-happyfox-auth-code) ACTION-NEEDED Requires an outbound ticket/email to HappyFox support (not executable by agent). If HappyFox confirms rotation, procedure is: add new version to Secret Manager, verify /health + one test submission, disable (not delete) previous version.
2 Okta client secret rotation policy PASS (with FLAG for MFA) config.py references OKTA_CLIENT_ID only (SPA); no OKTA_CLIENT_SECRET env var or secret exists. SPA pattern has no secret to rotate (RUNBOOK-COWORK.md Task 4, Option A confirms reuse of existing "Greenpeace.US" SPA app, client id 0oadhpjktd5UfCMDm0x7). FLAG: MFA enforcement on the Okta sign-on policy needs manual confirmation in Okta Admin each quarter — not verifiable from the repo.
3 KMS key rotation timestamps (gpus-forms-cmek, gpus-forms-dek-wrapper) ACTION-NEEDED Keys were created with --rotation-period=90d per Step 3 of RUNBOOK-CLAUDE-CODE.md, so automatic rotation is configured. Per-version rotation timestamps (must be within the last 100 days) require GCP Console → Security → KMS → keyring gpus-forms (us-central1) and are not visible to the agent.
4 Cloud SQL backups (gpus-forms-db: 7 retained, PITR enabled, latest <24h) ACTION-NEEDED Requires GCP Console → Cloud SQL → gpus-forms-db → Backups tab. Not verifiable from the repo.

Overall result: 1 PASS (with secondary FLAG), 3 ACTION-NEEDED.

Follow-up: A 7-day Cowork follow-up has been scheduled (forms-portal-rotation-followup-2026-q2) to walk Rajesh through items 1, 3, and 4 live and back-fill verified statuses into this log. The MFA-enforcement FLAG on item 2 should be closed out during the same session.

2026-07-20 — Q3 2026 check (automated run)

Executed by: Cowork scheduled task forms-portal-credential-rotation-quarterly (agent, unattended — Rajesh not present for the interactive walk-through this check is designed around).

Same evidence boundary as the Q2 run: verification was limited to repo state. No gcloud/gsutil CLI, no authenticated GCP Console session, and no HappyFox support channel were reachable from the scheduled-task sandbox (the sandbox is isolated and carries none of the operator's cloud credentials). Items requiring live console state are therefore logged ACTION-NEEDED for Rajesh to complete manually and back-fill here at the next window. Repo-checkable items were re-verified this quarter rather than carried forward.

# Check Status Notes
1 HappyFox credentials rotation (gpus-forms-happyfox-api-key, gpus-forms-happyfox-auth-code) ACTION-NEEDED Requires an outbound ticket/email to HappyFox support — not an action the unattended agent takes (and the task explicitly forbids automatic rotation). If HappyFox confirms rotation: add a new version to each secret in Secret Manager, verify /health + one test submission, then disable (not delete) the prior version. Caveat re-confirmed this quarter: RUNBOOK-CLAUDE-CODE.md (line 336) records the vendor position as "we reuse current values (vendor won't rotate)" — if that still holds, this check's practical outcome is "vendor rotation not available; compensating controls (Secret Manager access scoped to gpus-forms-backend@, no secret in code/env) remain in force." Rajesh should confirm the current vendor stance and record it.
2 Okta client secret rotation policy PASS (with FLAG for MFA) Re-verified in forms-backend/config.py: only OKTA_CLIENT_ID (0oadhpjktd5UfCMDm0x7, default) and OKTA_DOMAIN/OKTA_ISSUER/OKTA_AUDIENCE are referenced — no OKTA_CLIENT_SECRET env var or Secret Manager entry exists anywhere in the backend. The forms app is a public SPA (RUNBOOK-COWORK.md Task 4, Option A — reuse of the existing "Greenpeace.US" SPA app), so there is no confidential-client secret to rotate. FLAG (unchanged from Q2): MFA enforcement on the Okta sign-on policy must be confirmed manually in Okta Admin each quarter; it is not verifiable from the repo.
3 KMS key rotation timestamps (gpus-forms-cmek, gpus-forms-dek-wrapper) ACTION-NEEDED Both keys were created with --rotation-period=90d (Step 3, RUNBOOK-CLAUDE-CODE.md), so automatic rotation is configured in code/runbook. Actual per-version rotation timestamps (must be within the last 100 days) are only visible in GCP Console → Security → KMS → keyring gpus-forms (us-central1) and cannot be read by the agent. Confirm the primary version's create time on each key is < 100 days old.
4 Cloud SQL backups (gpus-forms-db: 7 retained, PITR enabled, latest <24h) ACTION-NEEDED Requires GCP Console → Cloud SQL → gpus-forms-db → Backups tab. Not verifiable from the repo. Confirm: exactly 7 automated backups retained, point-in-time recovery enabled, and the most recent backup completed within the last 24 hours.

Overall result: 1 PASS (with a secondary MFA FLAG), 3 ACTION-NEEDED. No status regressed versus Q2 2026; the ACTION-NEEDED items are unattended-context limitations, not detected control failures.

Follow-up: A 7-day Cowork follow-up has been scheduled (forms-portal-rotation-followup-2026-q3) to walk Rajesh through items 1, 3, and 4 live and back-fill verified statuses into this log. The MFA-enforcement FLAG on item 2 should be closed out in the same session. Note whether the Q2 follow-up (forms-portal-rotation-followup-2026-q2) was completed — if those items were never back-filled, this is the second quarter running without live console verification and should be prioritized.


Retention audit log

Monthly execution log for the "Retention policy enforcement" control (control ownership table above, Director of Cyber Security, monthly cadence). Each entry records the previous calendar month's retention purge-job state with status PASS / FLAG / ACTION-NEEDED / INCIDENT. This skill never deletes data — it verifies the purge job ran, that counts are plausible, that a sampled purged row is actually gone, and that the GCS bucket retention lock is intact.

2026-06-01 — May 2026 audit (automated run)

Executed by: Cowork scheduled task forms-portal-purge-job-audit-monthly (agent, unattended — Rajesh not present for interactive walk-through).

Verification evidence available to the agent was limited to the repo. The agent had no gcloud CLI, no GCP Console session, no Cloud SQL psql/IAM-auth session, and no GCS console access from the scheduled-task sandbox. Items that require live cloud state are therefore logged as ACTION-NEEDED for Rajesh to complete manually and back-fill into this log at the next available window. The repo state was checked to confirm the purge-job code path still exists and the schema still defines the relevant fields and audit-action enum — both PASS.

# Check Status Notes
1 Purge job ran in May 2026 ACTION-NEEDED Requires Cloud Logging filter resource.type="cloud_run_revision" resource.labels.service_name="gpus-forms-backend" labels.job="retention-purge" with time range 2026-05-01T00:00:00Z..2026-06-01T00:00:00Z. Confirm ≥1 successful run. If zero runs found → escalate as INCIDENT and page on-call. Code path (admin_purge_expired audit action, retention_expires_at < NOW() predicate) is still present in forms-backend/models.py and forms-backend/schema/001_init.sql — PASS at the code-existence layer.
2 Reported counts plausible (submissions / audit rows / attachments / DEK wrappers) ACTION-NEEDED Pull structured log fields submissions_purged, audit_rows_purged, attachments_purged, dek_wrappers_retired from the May run(s). Phase-1 cutover was 2026-04-18, so the 7-year retention window (2555 days, see forms table retention_days default in schema/001_init.sql) cannot yet have elapsed for any production submission — the expected May 2026 count is zero for all four metrics. A non-zero count this month is itself a FLAG and warrants investigation (test/staging data leaked into the prod purge path, or retention_expires_at mis-stamped on a form with retention_days < 44).
3 Spot check one purged submission N/A (this month) Conditional on Check 2 returning a non-zero submission count. With the expected zero-purge result for May 2026, there is no purged submission ID to spot-check. Procedure to retain for future months: pick one purged submission_id from log, verify it is absent from submissions, submission_fields, and gpus-forms-attachments, and verify audit_log entries for that submission (submission_created, any submission_viewed, admin_purge_expired) are still present (audit log retention is independent of submission retention — see schema header comments and AU-11 control mapping).
4 GCS bucket retention lock on gpus-forms-attachments still enabled and unaltered ACTION-NEEDED Requires GCP Console → Cloud Storage → gpus-forms-attachments → Retention tab. Confirm retention policy is locked (lock icon present, not just configured) and the retention period matches the documented 7-year window. Any alteration → CRITICAL FINDING, page Rajesh immediately per task instructions.

Overall result: 0 PASS, 3 ACTION-NEEDED, 1 N/A. No INCIDENT or CRITICAL state was reachable from the sandbox — but absence of evidence is not evidence of absence, and items 1 and 4 must be closed out by Rajesh before this month's audit can be marked complete.

Expected outcome reminder: For the first ~7 years post-Phase-1 cutover (2026-04-18 → ~2033-04), monthly purge runs should report zero purged submissions/attachments and zero retired DEK wrappers in steady state. The job must still execute monthly (Check 1) and the GCS retention lock must remain intact (Check 4) — those are the two non-trivial checks during the pre-aging period. The job firing with zero rows touched is the expected PASS shape; a missing run is INCIDENT.

Follow-up: A 7-day Cowork follow-up has been scheduled (forms-portal-purge-audit-followup-2026-05) to walk Rajesh through items 1, 2, and 4 live and back-fill verified statuses into this log.

2026-06-15 — Retention value reconciliation + attachment-bucket cleanup (Gate 5)

Performed live (operator-attended) during forms 2.5(c) Gate 5.

  • Retention value corrected 2563 → 2555 days. The bucket policy had been set to 2563 days — an error with no defensible derivation (7 years = 2555 days exactly; even counting the 2 leap days in 2026–2033 yields 2557). All other sources (DB retention_days default, every forms/*.yaml, this controls doc, the IAR) say 7 years / 2555. The GCS policy was re-set to 2555 days, closing the number mismatch.
  • Noncurrent-version cleanup. Object versioning was found Enabled, so prior "deletions" had only archived objects to noncurrent versions that the retention policy preserved. Three retained-but-invisible liabilities were removed (verified with gsutil ls -a, all-versions — not live-only): the credentials-bearing pentest PDF (Exploiting Vulnerable Hosts (4e)…pdf, never actually deleted), an EICAR test-virus file, and a superseded screenshot version. Versioning was set to Suspended to stop new noncurrent versions accruing.
  • Current policy state: retention set to 2555 days, UNLOCKED. Bucket holds 7 live test artifacts, 0 noncurrent versions.

Check 4 status — UPDATED: moves from ACTION-NEEDED (failing) to "policy correct (2555d), lock scheduled." The retention lock is deliberately deferred, not silently closed. Locking is gated on three conditions, all open:

  • (a) Bucket confirmed empty of the remaining 7 test artifacts. (Sequencing note: with 2555 now set, those objects are retention-protected; emptying them requires retention clear → rm → re-set 2555 before the lock.)
  • (b) Versioning disable-vs-suspend decision + a noncurrent-version-expiration lifecycle rule resolved. Currently Suspended; the lifecycle config has only storage-class transitions, no noncurrent expiry — tracked as a T5/controls item.
  • (c) A deliberate lock call by the Director of Cyber Security. The lock is irreversible (cannot be shortened or removed once locked), so it is not taken on autopilot.

Until locked, Check 4's monthly verification asserts: policy present at 2555 days, UNLOCKED, and the three lock-preconditions still tracked. A reduction below 2555 or an unexplained clear is a FLAG.

2026-07-18 — Retention-lock review (Task B.1, automated run)

Executed by: Cowork scheduled task forms-portal-attachments-retention-lock-review (agent, unattended — Rajesh not present for the interactive walk-through this review requires). No gcloud/GCS console session was reachable from the sandbox; findings are from repo state plus the 2026-06-15 Gate 5 entry above. Decision: DO NOT LOCK. The lock was not invoked and cannot be from an unattended session. The lock is irreversible, and three of the preconditions below require a human decision.

Gating check (step 1) — NOT cleanly met.

  • Phase 2 has cut over — frontend cutover complete 2026-04-27 (priorities/gpus-it-priorities.md change log v1.3); real submission persistence began 2026-05-08 (Phase 2.5(b), was "theater" before that).
  • The 90-day production-operation bar is not met under the strict reading: 82 days since the 2026-04-27 frontend cutover, 71 days since real persistence (2026-05-08). Only the due-date driver — 90 days after the 2026-04-18 unlock decision — has elapsed (91 days). Flagged as a discrepancy between the due-date driver and the step-1 "≥90 days in production" gate; resolves in favour of hold given irreversibility.

Lock preconditions (from 2026-06-15 Gate 5) — still open. No evidence any of the three has closed since 2026-06-15: (a) bucket still not confirmed empty of the 7 live test artifacts — locking now would make test data (previously incl. an EICAR file and a credentials-bearing pentest PDF) immutable for 7 years; (b) versioning disable-vs-suspend + noncurrent-expiry lifecycle rule unresolved; (c) deliberate lock call by the Director of Cyber Security not yet made.

Category retention review (step 2). All 28 active forms/*.yaml declare retention_days: 2555 (7 years) uniformly. Categories present: hr, finance, supporter-care, it, facilities, other (the Data Team forms — data-analysis-request, data-request-form, database-* — sit under other). Assessment of whether an immutable 7-year floor fits each — all items below need Rajesh's confirmation, none is a confirmed exception yet:

  • Human Resources — 7 years is a reasonable floor; some records (pension/benefits, employment history) may warrant longer. A floor does not force deletion at 7 years, so "keep longer" is not a conflict. Likely OK.
  • Finance — 7 years aligns with IRS/tax record retention. Likely correct.
  • Supporter Care (frontline-form-request, refund-processing-form) — primary concern. An immutable 7-year floor forbids early deletion, which conflicts with data-minimization and right-to-erasure obligations (CCPA / state privacy laws / GDPR for any EU supporters) for donor PII. Refund/financial records may need 7 years, but non-financial supporter PII may need to be deletable on request. Needs a decision before an irreversible floor is applied.
  • Data Team / other operational tickets (IT, facilities, data requests) — a 7-year immutable floor likely over-retains short-lived operational attachments, enlarging breach-exposure surface. Data-minimization concern, not a legal conflict.

Recommendation. Do not lock until: (1) the three Gate-5 preconditions close (bucket emptied of test artifacts, versioning/lifecycle resolved, deliberate lock call); (2) the 90-day production-operation bar is met (~2026-07-26 by frontend cutover, ~2026-08-06 by real persistence); and (3) Rajesh confirms Supporter Care / operational-ticket retention — if either needs sub-7-year deletion, the single-bucket immutable-lock model is the wrong shape and the choice is split-buckets-per-category vs. application-level lifecycle within one bucket (the per-form retention_days metadata already supports the latter). If confirmed to lock after all preconditions clear, the exact interactive command is gsutil retention lock gs://gpus-forms-attachmentsirreversible; run only with explicit understanding that no object can be deleted before 7 years, even by an owner.

Check 4 status — unchanged: policy present at 2555 days, UNLOCKED, three preconditions still tracked (plus the new Supporter Care erasure-rights question). This review scheduled a 60-day follow-up.


Forms Portal Compliance Controls · v1.0 · 2026-04-18 · GPUS-IT · Classification: CONFIDENTIAL — Internal Use Only