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_tokenverification validates JWKS signature, algorithm,kid, issuer, audience, expiry, andsubbefore 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_barcodeandverification_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/typewithout 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 with503(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, andSESSION_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-prodhas onlyroles/logging.viewerandroles/monitoring.viewerat project scope plusroles/run.developerscoped to theapiCloud Run service. It has no Secret Manager, IAM-admin, data-plane, or delete roles. The Pub/Sub push identity…-remediate-push@partile-prodhasroles/run.invokeronly on theremediation-responderservice. - Director prod access:
partile-controluses the non-snap Cloud SDK at/opt/agent/otto/google-cloud-sdkand the VM-attacheddirector-agent@partile-prodservice account keylessly. There are no key files for this path; the caveat is that any process running asottocan 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 backendguardrail_smokeremains 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 |
Links¶
- 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.