Private assets and domain restricted sharing

Every deployment bucket remains private. navigator ops gcp setup grants the deployment Google service account roles/storage.objectAdmin on its assets, documents, exports, logs, and applications buckets, then maps both Kubernetes service accounts (navigator-web and workflows-service) to that Google identity through Workload Identity. It never adds allUsers and never changes constraints/iam.allowedPolicyMemberDomains. The applications bucket holds each Project's published client-portal bundle at {project-code}/portal/, which web streams same-origin through /app/projects/{code}/portal so the session and Project-participation gate stay on every request.

The public website receives only marketing bytes through GET /assets/*. web reads that object from NAVIGATOR_ASSETS_BUCKET with the deployment identity and returns it with its stored content type, X-Content-Type-Options: nosniff, and a one-hour public cache. Unsafe keys and missing objects return 404; storage failures return 502. There is no parallel anonymous route for documents, exports, or logs. This is an application delivery boundary, not a Navigator persons.role or Project-participation grant.

Set NAVIGATOR_ASSET_BASE_URL=$NAV_BASE_URL/assets in each deployment's config.toml — it is a plaintext coordinate the ops ship preflight requires. Setup also gives the active gcloud identity the same bucket-scoped object CRUD role so the operator and the Kubernetes runtime can inspect and repair objects without a public or project-wide grant.

Publish the public asset inventory before ops ship. A full or image-only roll reads NAVIGATOR_ASSETS_BUCKET and stops before any rollout when a required object is missing or the bucket cannot be read. The preflight and the post-roll check use one inventory. On a source checkout that inventory is every markdown image under server/content, every responsive gallery variant, the brand media keys, and every licensed webfont face. On a deploy-only checkout (--deployments-dir with no server/content directory) blog references are absent, and the inventory is the workshop images embedded in the CLI plus those same gallery variants, brand media keys, and fonts. Restart-only skips both checks.

assets build then assets upload publishes the gallery variants. assets fonts upload publishes one licensed family. site asset upload publishes one finished image, including brand media and a workshop slide. Run the publish that covers the inventory, then ops ship.

After setup, verify both identities and reject an anonymous principal: