Proxy Auth (Google IAP)
This guide covers deploying a Scion Hub behind Google Cloud Identity-Aware Proxy (IAP), using IAP for human authentication and hub-minted OIDC tokens for agent transport auth.
Authentication modes
Section titled “Authentication modes”The Hub supports three mutually exclusive human authentication modes, selected by auth.mode:
| Mode | Use case |
|---|---|
oauth (default) | Hub runs its own OAuth flows (Google / GitHub). |
proxy | Hub sits behind a trusted authenticating proxy (Google IAP, Cloudflare Access, etc.). |
dev | Single-user local development with auto-generated dev tokens. |
Only one mode is active at a time. When auth.mode is proxy, the OAuth login UI, /auth/providers, and device-flow handlers are disabled. Human identity is derived entirely from the proxy’s verified assertion.
Choose proxy / IAP when the Hub is already fronted by IAP (e.g., on Cloud Run with IAP enabled, or behind a GCE/GKE IAP-protected backend service) and you want to eliminate a separate OAuth integration.
Inbound: human IAP authentication
Section titled “Inbound: human IAP authentication”How it works
Section titled “How it works”- A user’s browser request passes through IAP, which authenticates the user and injects a signed JWT in the
X-Goog-IAP-JWT-Assertionheader. - The Hub verifies the JWT signature (ES256, via Google’s JWKS endpoint), validates
iss,aud, andexpclaims, then extracts the user’s email from the verified assertion. - On first verified request, the Hub provisions the user — applying the same access controls as the OAuth path (
user_access_mode,authorized_domains,admin_emails). If the user is not permitted, the request is rejected with 403. - Suspended users are rejected regardless of IAP status.
The unsigned convenience headers X-Goog-Authenticated-User-Email and X-Goog-Authenticated-User-Id are ignored — only the cryptographically signed assertion is trusted.
Middleware precedence
Section titled “Middleware precedence”The proxy authenticator runs after higher-priority app-layer credentials:
- Agent token (
X-Scion-Agent-Token/ agent JWT) - Broker HMAC (
X-Scion-Broker-ID) - Bearer token (dev token / PAT / user JWT)
- Proxy authenticator (IAP assertion) — runs only when no app-layer credential matched
This means agents and brokers traversing IAP are identified by their own credentials, not by the IAP service-account assertion.
Configuration
Section titled “Configuration”In settings.yaml (under the server key):
server: auth: mode: proxy proxy: provider: iap iap: # MANDATORY — the IAP audience for your backend. # GCE/GKE backend service format: # /projects/<PROJECT_NUMBER>/global/backendServices/<BACKEND_SERVICE_ID> # App Engine format: # /projects/<PROJECT_NUMBER>/apps/<PROJECT_ID> audience: "/projects/123456789/global/backendServices/987654321"
# Optional overrides (defaults are correct for production IAP): # issuer: "https://cloud.google.com/iap" # jwks_url: "https://www.gstatic.com/iap/verify/public_key-jwk"
# Optional defense-in-depth: also verify source IP is a trusted proxy. # Uses the existing trusted_proxies CIDR list. require_trusted_proxy_ip: false
# Access controls — same as for OAuth mode: user_access_mode: domain_restricted # open | domain_restricted | invite_only authorized_domains: - example.com # admin_emails is set at the hub level: hub: admin_emails: - admin@example.comIAP audience format
Section titled “IAP audience format”The audience value must match the audience claim (aud) in the IAP-signed JWT. The format depends on the backend type:
- GCE/GKE backend service:
/projects/<PROJECT_NUMBER>/global/backendServices/<BACKEND_SERVICE_ID> - App Engine:
/projects/<PROJECT_NUMBER>/apps/<PROJECT_ID>
You can find this value in the Google Cloud Console under Security → Identity-Aware Proxy → select your backend → Signed Header JWT Audience.
Issuer and JWKS overrides
Section titled “Issuer and JWKS overrides”The defaults match Google’s production IAP:
| Field | Default |
|---|---|
issuer | https://cloud.google.com/iap |
jwks_url | https://www.gstatic.com/iap/verify/public_key-jwk |
Override these only for testing with a mock IAP issuer.
User provisioning
Section titled “User provisioning”Provisioning in proxy mode works identically to OAuth — lazy, allow-list-gated, auto-create on first verified request:
open: any verified email is allowed.domain_restricted: email domain must be inauthorized_domains.invite_only: email must be pre-registered (via admin invite-code flow).- Emails in
admin_emailsare always allowed and auto-promoted to admin role. - If not permitted, the request returns 403.
- Suspended users are rejected even though IAP authenticates them upstream.
A 60-second resolution cache (keyed by verified email) avoids a database lookup on every request. The JWT signature is verified on every request — only the provisioning/store lookup is cached.
Logout behavior
Section titled “Logout behavior”In proxy mode, the Hub does not own the session. The /auth/logout endpoint:
- Browser requests: redirect to
/_gcp_iap/clear_login_cookie(IAP’s cookie-clearing endpoint). - API requests: return
200 OKwith{"success": true, "message": "proxy mode: session is managed by the authenticating proxy"}.
Outbound: agent transport auth
Section titled “Outbound: agent transport auth”When the Hub is behind IAP (or a Cloud Run invoker-only service), agents need a way to reach the Hub through the platform guard. This is solved with a dual-layer credential model:
| Layer | Header | Purpose |
|---|---|---|
| Outer (transport) | Authorization: Bearer <Google OIDC ID token> | Satisfies the platform guard (IAP or Cloud Run invoker IAM check). |
| Inner (app) | X-Scion-Agent-Token: <scion JWT> | Existing Hub agent authentication. Carried as a custom header so it never collides with the outer Authorization. |
How it works
Section titled “How it works”- Cold start (dispatch): The Hub mints an initial Google OIDC ID token (impersonating a dedicated transport service account) and includes it in the agent’s dispatch payload as environment variables.
- Steady-state refresh: The agent piggybacks on its existing scion-token refresh cycle. The refresh response includes a
tokens[]array with both the new scion access token and a fresh OIDC transport token. The agent applies each token to the appropriate layer. - Background ticker: The agent-side client drives refresh on the shortest-lived token (transport tokens have a 5-minute refresh margin vs. the ~1h Google ID token TTL).
Dispatch environment variables
Section titled “Dispatch environment variables”When transport auth is configured, the Hub injects these environment variables into the agent container at dispatch time:
| Variable | Description |
|---|---|
SCION_TRANSPORT_TOKEN | Initial Google OIDC ID token for the transport layer. |
SCION_TRANSPORT_AUDIENCE | Audience the transport token was minted for (IAP client ID or hub URL). |
SCION_TRANSPORT_TOKEN_EXPIRY | Token expiry in RFC 3339 format. |
Refresh response: tokens[] array
Section titled “Refresh response: tokens[] array”The agent token refresh endpoint (POST /api/v1/agents/{id}/token/refresh) returns a generalized tokens[] array alongside the legacy single-token fields for backward compatibility:
{ "token": "...", "expires_at": "2026-06-05T12:00:00Z", "tokens": [ { "layer": "app", "type": "scion_access", "value": "...", "expiresIn": 900 }, { "layer": "transport", "type": "google_oidc", "value": "...", "expiresIn": 3600, "audience": "1234567890.apps.googleusercontent.com" } ]}The transport entry is only present when auth.transport is configured on the Hub. Old clients ignore tokens[]; new clients consume both layers.
Agent-side token source selection
Section titled “Agent-side token source selection”The agent (pkg/sciontool/hub) selects an OIDC token source automatically:
SCION_TRANSPORT_TOKENenv var set → Injected mode: uses the hub-provided token from dispatch, refreshed viatokens[]on subsequent refresh calls.- Running on GCP (metadata server available) → Metadata mode: fetches OIDC from the GCE metadata server using the ambient SA identity (the PR #307 pattern). Audience is set via
SCION_HUB_OIDC_AUDIENCEor defaults to the hub URL. - Neither → No OIDC transport (agent uses plain HTTP).
Injected mode (option 1) is the recommended path for IAP deployments — it decouples agent transport auth from the agent’s own GCP identity.
Transport configuration
Section titled “Transport configuration”server: auth: transport: # Transport auth mode: # none (default) — no transport tokens issued # cloudrun_invoker — audience = hub URL # iap — audience = IAP OAuth client ID mode: iap
# OIDC audience for the transport token. # For IAP: the IAP OAuth client ID (e.g., "1234567890.apps.googleusercontent.com") # For cloudrun_invoker: the hub URL (auto-derived from hub.public_url if empty) oidc_audience: "1234567890.apps.googleusercontent.com"
# Dedicated service account for transport-layer auth. # The hub's runtime SA impersonates this SA to mint OIDC ID tokens. platform_auth_sa: "scion-transport@my-project.iam.gserviceaccount.com"What audience to set
Section titled “What audience to set”| Transport mode | oidc_audience value |
|---|---|
iap | The IAP OAuth client ID (found in Cloud Console → Security → IAP → your backend → OAuth client). Format: <client-id>.apps.googleusercontent.com |
cloudrun_invoker | The Hub’s URL (e.g., https://hub.example.com). If left empty, derived from hub.public_url. |
Hub-managed transport SA (Option C)
Section titled “Hub-managed transport SA (Option C)”The Hub uses a dedicated service account solely for transport-layer auth. The Hub’s runtime SA impersonates this SA via the IAM Credentials API (generateIdToken) to mint OIDC ID tokens for agents. This design:
- Keeps the auth-grade minting capability in the Hub only — agents hold no SA credential.
- Works regardless of the agent’s GCP metadata mode (
block,passthrough, orassign). - Avoids distributing service account key files.
Required IAM bindings:
| Principal | Role | Target |
|---|---|---|
| Hub’s runtime SA | roles/iam.serviceAccountTokenCreator | Transport SA (platform_auth_sa) |
| Transport SA | IAP-secured web user or Cloud Run invoker | The Hub’s backend service |
Security notes
Section titled “Security notes”- Only the signed assertion is trusted. The unsigned
X-Goog-Authenticated-User-EmailandX-Goog-Authenticated-User-Idheaders are completely ignored. - Audience binding is mandatory. Without it, a JWT minted for a different IAP-protected service would be accepted. The
auth.proxy.iap.audiencefield must always be set. - The Hub must be reachable only through IAP for the human surface. Any path that reaches the Hub directly could bypass proxy authentication. The verified-JWT path is safe against header spoofing (forged assertions fail the signature check), but direct access bypasses IAP entirely. Use VPC networking, firewall rules, or Cloud Run ingress settings to enforce this.
- JWKS key rotation is handled automatically: keys are cached with hourly background refresh and on-miss refresh for rotated key IDs. Transient JWKS endpoint failures are tolerated by serving the last-good key set.
- Clock skew of ±30 seconds is allowed on
expandiatclaims. - Suspended users are rejected at the provisioning layer even though IAP still authenticates them upstream.
End-to-end GCP setup checklist
Section titled “End-to-end GCP setup checklist”Prerequisites
Section titled “Prerequisites”- A GCP project with billing enabled.
- The Hub deployed on Cloud Run (or behind a GCE/GKE load balancer).
gcloudCLI configured with appropriate permissions.
1. Enable IAP and create an OAuth consent screen
Section titled “1. Enable IAP and create an OAuth consent screen”# Enable the IAP APIgcloud services enable iap.googleapis.com
# Configure the OAuth consent screen (if not already done)# Go to: Console → APIs & Services → OAuth consent screen2. Enable IAP on the backend service
Section titled “2. Enable IAP on the backend service”# For Cloud Run behind a load balancer:gcloud iap web enable \ --resource-type=backend-services \ --service=YOUR_BACKEND_SERVICE_NAMENote the IAP OAuth client ID (found in Console → Security → IAP → your backend → click the three dots → Edit OAuth Client). You will need it for both auth.proxy.iap.audience and auth.transport.oidc_audience.
Note the signed header JWT audience (found in Console → Security → IAP → your backend). This goes into auth.proxy.iap.audience.
3. Create the transport service account
Section titled “3. Create the transport service account”# Create a dedicated SA for transport authgcloud iam service-accounts create scion-transport \ --display-name="Scion Transport Auth"
# Grant the Hub's runtime SA permission to impersonate the transport SAgcloud iam service-accounts add-iam-policy-binding \ scion-transport@PROJECT_ID.iam.gserviceaccount.com \ --member="serviceAccount:HUB_RUNTIME_SA@PROJECT_ID.iam.gserviceaccount.com" \ --role="roles/iam.serviceAccountTokenCreator"4. Grant the transport SA access to the platform guard
Section titled “4. Grant the transport SA access to the platform guard”For IAP:
# Grant IAP-secured web user access to the transport SAgcloud iap web add-iam-policy-binding \ --resource-type=backend-services \ --service=YOUR_BACKEND_SERVICE_NAME \ --member="serviceAccount:scion-transport@PROJECT_ID.iam.gserviceaccount.com" \ --role="roles/iap.httpsResourceAccessor"For Cloud Run invoker:
gcloud run services add-iam-policy-binding YOUR_SERVICE_NAME \ --member="serviceAccount:scion-transport@PROJECT_ID.iam.gserviceaccount.com" \ --role="roles/run.invoker" \ --region=YOUR_REGION5. Configure the Hub
Section titled “5. Configure the Hub”Create or update the settings.yaml:
schema_version: "1"server: mode: hosted hub: public_url: "https://hub.example.com" admin_emails: - admin@example.com auth: mode: proxy proxy: provider: iap iap: audience: "/projects/123456789/global/backendServices/987654321" transport: mode: iap oidc_audience: "1234567890.apps.googleusercontent.com" platform_auth_sa: "scion-transport@my-project.iam.gserviceaccount.com" user_access_mode: domain_restricted authorized_domains: - example.com database: driver: postgres url: "postgres://..."6. Verify
Section titled “6. Verify”- Access the Hub URL in a browser — IAP should prompt for Google login, then the Hub should show your identity.
- Dispatch an agent and verify it can communicate back to the Hub (check agent logs for OIDC transport messages).
- Check Hub logs for
Proxy auth configured: provider=iapandTransport auth configured: mode=iapat startup.
Reference scripts
Section titled “Reference scripts”The scripts/cloudrun/ directory on the pr/cloudrun-hub branch contains reference deployment scripts (deploy.sh, entrypoint.sh, hub-settings-template.yaml) for a Cloud Run + IAP topology that can serve as a starting point.