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. After setup, verify both identities and reject an anonymous principal:


runtime_gsa="$NAVIGATOR_GCP_SERVICE_ACCOUNT_ID@$NAVIGATOR_GCP_PROJECT_ID.iam.gserviceaccount.com"
operator_account="$(gcloud config get-value account)"
case "$operator_account" in
  *.gserviceaccount.com) operator_member="serviceAccount:$operator_account" ;;
  *) operator_member="user:$operator_account" ;;
esac

for bucket_name in \
  "$NAVIGATOR_ASSETS_BUCKET" \
  "$NAVIGATOR_DOCUMENTS_BUCKET" \
  "$NAVIGATOR_EXPORTS_BUCKET" \
  "$NAVIGATOR_LOGS_BUCKET"
do
  iam_rows="$(
    gcloud storage buckets get-iam-policy "gs://$bucket_name" \
      --flatten='bindings[].members[]' \
      --format='value(bindings.role,bindings.members)'
  )"
  printf '%s\n' "$iam_rows" |
    awk -v runtime="serviceAccount:$runtime_gsa" -v operator="$operator_member" '
      $1 == "roles/storage.objectAdmin" && $2 == runtime { runtime_ok = 1 }
      $1 == "roles/storage.objectAdmin" && $2 == operator { operator_ok = 1 }
      $2 == "allUsers" { public = 1 }
      END { if (!runtime_ok || !operator_ok || public) exit 1 }
    '
done