Skip to content

Security

Purpose

The security-relevant facts about this repository's infrastructure, gathered from direct inspection - not a generic checklist. Includes findings that are worth a deliberate decision rather than a silent fix.

Secrets and credentials

See CI/CD → Secrets for the full GitHub-secret-vs-Worker-secret breakdown, the CLOUDFLARE_API_TOKEN rotation procedure, and least-privilege scoping.

Environment separation (or lack of it)

There is no staging tier - every credential capable of wrangler deploy/--remote is a production credential. See Environments. This raises the bar on how carefully CLOUDFLARE_API_TOKEN access is granted: anyone who can trigger deploy-cloudflare.yml or land a commit on main can affect production.

D1 and R2 access

  • D1: no row-level access control - the API Worker's own route handlers are the only authorization boundary. See Cloudflare → D1.
  • R2: object access is gated by an application-level allowlist, not a bucket ACL - the central API Worker's /api/files/:key handler (workers/api/src/public-file-access.ts) only serves keys matching a recognized public shape: freelancer-avatar-<userId>-<uuid>.<ext>, and, since feature/commerce-production-backend, product-image-<productId>-<uuid>.<ext> (Commerce catalog photos, requireAdmin-gated to upload). Everything else, including any private/-prefixed key, gets the same 404 a missing key would. Hardened as part of fix/platform-hardening: this was previously a blocklist (serve anything except a key starting with private), which meant a future R2 write landing outside the private/ prefix - by a bug, a copy-paste, or a new feature - would have been silently exposed with no authentication at all. Adding the product-image shape later was a minimal, deliberate addition to the allowlist, not a loosening of it - proven by a test asserting the new category doesn't affect any other key's rejection. The genuinely private object category (embroidery reference images, private/embroidery-references/...) was already, independently, only retrievable through apps/admin-web/worker/index.ts's /api/reference route, gated behind requireAdmin() session auth - that path is unchanged. See Cloudflare → R2 for the full detail.

Commerce customer identity bridge

New in feature/commerce-production-backend. Commerce needs to know which authenticated customer is checking out without trusting a browser-supplied user id, and without a second customer database. The bridge: an authenticated Corporate session mints a short-lived (5 minute), signed Commerce token (POST /api/customer/commerce-token, requireCustomer-gated); the Commerce Worker verifies it and attaches the trusted user id to the central operations API call itself, server-side - the browser never talks to the operations API directly, and the token itself never carries email/mobile/password. Signing/verification is hand-rolled HS256 (Web Crypto, no JWT library dependency) - see Applications → Commerce "Customer identity architecture" for the full flow and file references, and CI/CD → Secrets for the CUSTOMER_SSO_SECRET Worker secret this depends on. This reuses, rather than duplicates, the existing customer-session/CSRF/redirect machinery already built for the Freelancer Marketplace (checkCustomerMutationOrigin's origin-allowlist-plus-header pattern, and packages/ui's customerPortalHref sign-in redirect).

CORS

workers/api/wrangler.jsonc's vars.ALLOWED_ORIGINS allowlists calling origins for the central API Worker: Corporate, Software, Logistics, Commerce, Embroidery. Admin does not call this Worker at all (it has its own separate Worker/D1 access at admin.akshayagroupglobal.com) and correctly has no entry. Investors (investors.akshayagroupglobal.com) also has no entry - verified: apps/investors-web currently makes zero fetch calls to any API, including a backend POST /api/investors/enquiries route that already exists in workers/api/src/index.ts but is not yet wired up from the Investors Contact page. Per an explicit platform-scope decision, Investors is currently excluded / inactive for the active platform - ALLOWED_ORIGINS intentionally does not include it, and this documentation effort does not add it. Revisit if/when Investors re-enters active scope (at which point the existing backend endpoint would need front-end wiring too, not just a CORS entry).

Deployment permissions

  • main branch pushes trigger automatic production deployment (see CI/CD → Deployment) - branch protection on main (required reviews, status checks) is the practical control point for "who can cause a production deploy," since the workflow itself has no additional approval gate.
  • deploy-cloudflare.yml's workflow_dispatch trigger is available to anyone with write access who can run workflows in this repository - it is not restricted to a smaller group via GitHub Environments' required-reviewers feature (not configured here).

Secret rotation

See CI/CD → Secrets "Rotating CLOUDFLARE_API_TOKEN" - sourced from docs/DEPLOYMENT.md §6.

Findings from this portal's initial review, and their resolution

Per this portal's own ground rules (document actual infrastructure, do not silently alter it), the initial review recorded findings rather than changes. fix/platform-hardening (a separate, deliberate follow-up PR) resolved the ones with a clear, minimal, non-production-affecting fix, and left the ones requiring a real product/scope decision as an explicit, documented choice:

  1. ~~Root README.md claims deployment is manual~~ - fixed: README.md now describes the actual automatic-on-push-to-main pipeline (see CI/CD).
  2. ALLOWED_ORIGINS omits the Investors domain - not a defect: Investors is currently excluded / inactive for the active platform by explicit scope decision (see "CORS" above); no code change made.
  3. ~~R2 access control was an application-level blocklist string check~~ - fixed: hardened to an explicit allowlist (see "D1 and R2 access" above); the actually-private object category was independently already authenticated elsewhere and needed no change.
  4. ~~The two deploy workflows have asymmetric app coverage~~ - fixed for active apps: deploy-cloudflare.yml's matrix now also includes Admin and Embroidery, matching ci.yml's automatic coverage. Investors intentionally remains asymmetric (manual-only, pre-existing) - see CI/CD → Deployment.
  5. ~~docs/DEPLOYMENT.md states migrations are numbered "0001-0005"~~ - fixed: corrected to describe running every ordered file in the migrations directory rather than a hardcoded range, so the instruction stays valid as migrations increase (see Architecture → Data Platform).

Before committing anything to this repository

Always grep a diff for password|secret|token|api_key|account_id|private_key before staging - this portal's own contribution followed that same check before its own commit (see CI/CD → Secrets "Security considerations").