Skip to content

Security Architecture

Field Value
Status live (current-state; application controls plus GCP production controls)
Owner INFRA (application) / OPS (infra/secrets) / DIR (governance)
Applies to Authentication, authorization, input handling, abuse/safety controls, secrets, IAM, network isolation, transport security, and shadow remediation across the backend, Cloud Run jobs/responder, GCP managed stores, and mobile seam.
Last updated 2026-06-14
Evidence partile-infra through PR-30 (app/auth/*, repositories/*, app/models.py, app/rate_limit.py, app/ai/*, retention_jobs.py, seed/purge tooling, smoke suite); partile-mobile PR-11 through PR-26; partile-ops: infra/envs/prod-gcp/ and infra/modules/gcp-*; selfheal PR-1 deployed remediation responder facts (infra/remediation/, atelier/selfheal/pr-1); partile-ops: infra/edge/cloudflare/; readiness/threat-model-and-auth-review.md; ../control-register.md (SEC-, SAF-, PRIV-04/06/15, GOV-06/07, OPS-06).

Current State

For an MVP the implemented posture is strong: opaque hashed sessions, atomic CSRF handling, no-token-in-URL mobile handoff, active-only auth, SQL-enforced object authorization, boarding-pass-gated presence, symmetric block enforcement, shared Redis rate limiting, signed LinkedIn id_token verification, account erasure, and consent-gated event emission. Production now adds GCP managed controls: Cloud Run, private Cloud SQL, private Memorystore, Secret Manager, dedicated service accounts, GCS remote state, Cloudflare edge/origin TLS, and scheduled Cloud Run Jobs. The self-healing ops surface is deliberately constrained: remediation-responder is private, receives selected alert notifications, and runs with ENFORCE=false, so it records decisions without changing production.

Known weaknesses are recorded rather than hidden: no real user launch yet, no restore drill/on-call/SLO, no HA database/cache posture, no admin plane, no third-party pentest, and a documented public-invoker exception for the consumer API.

Identity, sessions, and authorization

  • Opaque session tokens use secrets.token_urlsafe(32) and are stored only as SHA-256 hashes. Plaintext is returned once and never persisted (SEC-01).
  • OAuth CSRF state is random, Redis-backed, atomic GETDEL, single-use, and short-lived (SEC-02).
  • Mobile auth handoff uses a one-time Redis code with no token in any URL and a fixed redirect target, preventing open redirect token leakage (SEC-03).
  • LinkedIn id_token verification validates JWKS signature, algorithm, kid, issuer, audience, expiry, and sub before identity is trusted (SEC-10).
  • Object-level authorization is enforced in SQL; non-visible records return 404 with no existence leak (SEC-04/15).
  • Account lifecycle revokes sessions immediately on deletion and hard-deletes after the erasure grace window; deleted/suspended LinkedIn subjects cannot revive accounts during the grace window (PRIV-04).
sequenceDiagram
  autonumber
  participant M as Mobile app
  participant API as Cloud Run API
  participant R as Memorystore Redis
  participant L as LinkedIn OIDC
  participant DB as Cloud SQL

  M->>API: Start LinkedIn sign-in
  API->>R: Store single-use CSRF state
  API-->>M: Fixed LinkedIn redirect
  M->>L: User consents
  L-->>API: Callback with code and state
  API->>R: Atomic consume state
  API->>L: Exchange code and fetch JWKS
  API->>API: Verify signed id_token
  API->>R: Store one-time mobile handoff code
  M->>API: Exchange handoff code
  API->>R: Atomic consume handoff code
  API->>DB: Store hashed session token
  API-->>M: Opaque session token

  M->>M: Decode boarding-pass barcode locally
  M->>API: Submit derived trip fields only
  API->>API: Require boarding_pass_barcode and document_verified
  API->>DB: Store minimal flight_presences row

Input intake and minimization

  • Boarding-pass intake requires verification_source=boarding_pass_barcode and verification_level=document_verified; manual/self-reported/unverified product presence is rejected (SEC-06, PRIV-06).
  • The backend accepts only derived trip/presence fields and rejects raw pass artifact fields. Validation errors are sanitized to loc/msg/type without echoed values.
  • Mobile scan/import paths parse locally, submit only derived fields, clean up cache copies best-effort, and fail closed for unsupported/PDF/non-image states.
  • Seed test data is allowed only as pre-launch mock data tagged users.is_seed = true. It is not real PII and must be purged and verified before GA (PRIV-15).

Abuse and safety controls

  • Rate limiting is Redis-backed and applied to auth, presence, match, directory, connection request, message send, and AI interview paths. Over limit returns 429 + Retry-After; Redis unavailable fails closed with 503 (SEC-07).
  • Block separation is symmetric and applied at read/derive paths and candidate generation; block tears down live candidate/conversation state (SAF-01).
  • Report capture uses a closed reason enum and durable report rows; mobile exposes Block / Report / Report & block on the relevant cards/details (SAF-02/03).
  • No exact-location reveal: public match/conversation models omit terminal and gate (SAF-04).

Automated remediation posture

Cloud Monitoring observe alert policies for Uptime check failing (…/alertPolicies/4068533737113267562) and API elevated 5xx rate (…/alertPolicies/4053361739307914661) now also notify projects/partile-prod/notificationChannels/7003417658785730995. That channel delivers to Pub/Sub topic partile-prod-remediation-alerts, whose push subscription calls the private Cloud Run remediation-responder at POST /pubsub.

The responder is a bounded control, not an autonomous production mutator today: it is default-deny, allowlists only api_down and api_5xx_spike_post_deploy, applies <=3 action decisions per hour per resource plus repeat-fingerprint thrash protection, and logs every decision as structured JSON. With ENFORCE=false, all decisions are shadow/audit records. The change to ENFORCE=true is an explicit future trust gate in the action register.

Secrets, IAM, and transport

  • Secrets are in GCP Secret Manager for production, never in code, logs, or this repo. Six secret labels exist: ANTHROPIC_API_KEY, DATABASE_URL, LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET, REDIS_URL, and SESSION_SIGNING_SECRET.
  • Terraform generates the database app-user password and session signing secret, composes DATABASE_URL/REDIS_URL, and stores secret payloads in Secret Manager. Because Terraform state can contain generated secret material, the GCS remote-state bucket is private Tier 2 operational evidence.
  • Operator-supplied secret values are limited to the LinkedIn client credentials and Anthropic API key. Only labels appear in Tier 1 docs.
  • Least privilege: production uses dedicated API, worker, and scheduler service accounts. API/worker identities receive only runtime roles for image read, Cloud SQL client, logging, metrics, and per-secret Secret Manager access. Scheduler invokes only Cloud Run Job run endpoints.
  • Responder least privilege: the responder service account …-remediation@partile-prod has only roles/logging.viewer and roles/monitoring.viewer at project scope plus roles/run.developer scoped to the api Cloud Run service. It has no Secret Manager, IAM-admin, data-plane, or delete roles. The Pub/Sub push identity …-remediate-push@partile-prod has roles/run.invoker only on the remediation-responder service.
  • Director prod access: partile-control uses the non-snap Cloud SDK at /opt/agent/otto/google-cloud-sdk and the VM-attached director-agent@partile-prod service account keylessly. There are no key files for this path; the caveat is that any process running as otto can use the attached identity via the metadata server until per-agent identity is implemented.
  • Transport and network isolation: mobile to Cloudflare and Cloudflare to Cloud Run are HTTPS. Cloud Run reaches Cloud SQL over private IP/socket and Redis over the private VPC through the Serverless VPC Access connector; managed stores are not public.
  • Production environment guardrail: GCP Cloud Run plain env includes APP_ENV=production, so /internal/dev/* is unmounted in production. The backend guardrail_smoke remains the application-level proof for this contract (SEC-09).

Public API accepted risk

Cloud Run API has public ingress and an allUsers Run Invoker binding. To allow that binding, the GCP organization policy iam.allowedPolicyMemberDomains was relaxed for the production project. This is a documented accepted risk, not a silent exception:

  • the public surface is the consumer API, not an admin plane;
  • application endpoints remain protected by LinkedIn OIDC sessions and object-level authorization;
  • Cloud SQL and Redis are private-network only;
  • the exception is scoped to partile-prod;
  • the risk and review trigger are tracked in ../registers/action-register.md.

STRIDE-lite Summary

The full model is readiness/threat-model-and-auth-review.md. Boundaries are mobile -> Cloudflare -> Cloud Run -> private GCP data stores, plus outbound LinkedIn/Anthropic and operator/deploy access to GCP. The highest-weighted Partile-specific threat remains physical-safety abuse: matching moves strangers from online to co-located. The first controls are block enforcement, no precise location reveal, rate limits, report capture, and first-contact constraints.

Known Gaps / Next Work

Gap Control Status Note
Cloudflare API token rotation SEC-05 / GOV-07 open action Operator note says a token appeared in chat transcript; rotate without recording value
Public Cloud Run invoker / DRS relaxation GOV-07 accepted risk Project-scoped exception, app auth, private DB/cache, no admin plane
Seed data purge before GA PRIV-15 open gate PR-30 seed/purge tooling exists; purge must be run and verified before real-user launch
Remediation shadow-to-enforce OPS-06 open gate Responder is deployed with ENFORCE=false; enablement needs trust-gate evidence and change approval
Control-plane per-agent identity SEC-16 / GOV-07 open action Keyless VM-attached SA path avoids key files, but otto process separation is by convention
Restore drill not run OPS-01 designed Cloud SQL backups/PITR exist; drill evidence does not
No operational SLO/on-call OPS-02 designed Initial observe alerts exist; formal SLOs, runbooks, and on-call ownership remain required before production SLA
Zonal DB / low-cost Redis posture OPS-03 not started Upgrade to regional Cloud SQL / HA Redis before production SLA
No audit logging for security/admin actions SEC-11 not started No admin plane exists yet
Cross-user IDOR regression not CI-enforced SEC-15 implemented/manual Smoke exists; CI wiring remains future
No dependency/SAST scanning; no pre-launch pentest SEC-14 not started M5/private-beta gate
Incident response not yet exercised SEC-13 not started Tabletop before private beta
Session idle timeout / rotation; optional OIDC PKCE/nonce SEC-12 designed Expiry sweep exists; idle/rotation and PKCE/nonce future
Moderation queue + enforcement ladder SAF-06 not started Mobile can capture reports; review/admin tooling absent
  • Policies: ../policies/secure-development-policy.md, ../policies/access-control-policy.md, ../policies/cryptography-and-secrets-policy.md, ../policies/vulnerability-management-policy.md, ../policies/incident-response-policy.md.
  • Procedures: ../procedures/vulnerability-triage-procedure.md, ../procedures/incident-response-playbook.md, ../procedures/release-review-procedure.md.
  • Controls: ../control-register.md (SEC-, SAF-, PRIV-15, GOV-07, OPS-06).
  • Open items / accepted risks: ../registers/action-register.md.
  • Threat model / auth review: readiness/threat-model-and-auth-review.md.