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