Skip to content

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.

The Hub supports three mutually exclusive human authentication modes, selected by auth.mode:

ModeUse case
oauth (default)Hub runs its own OAuth flows (Google / GitHub).
proxyHub sits behind a trusted authenticating proxy (Google IAP, Cloudflare Access, etc.).
devSingle-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.

  1. A user’s browser request passes through IAP, which authenticates the user and injects a signed JWT in the X-Goog-IAP-JWT-Assertion header.
  2. The Hub verifies the JWT signature (ES256, via Google’s JWKS endpoint), validates iss, aud, and exp claims, then extracts the user’s email from the verified assertion.
  3. 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.
  4. 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.

The proxy authenticator runs after higher-priority app-layer credentials:

  1. Agent token (X-Scion-Agent-Token / agent JWT)
  2. Broker HMAC (X-Scion-Broker-ID)
  3. Bearer token (dev token / PAT / user JWT)
  4. 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.

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.com

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.

The defaults match Google’s production IAP:

FieldDefault
issuerhttps://cloud.google.com/iap
jwks_urlhttps://www.gstatic.com/iap/verify/public_key-jwk

Override these only for testing with a mock IAP issuer.

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 in authorized_domains.
  • invite_only: email must be pre-registered (via admin invite-code flow).
  • Emails in admin_emails are 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.

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 OK with {"success": true, "message": "proxy mode: session is managed by the authenticating proxy"}.

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:

LayerHeaderPurpose
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.
  1. 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.
  2. 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.
  3. 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).

When transport auth is configured, the Hub injects these environment variables into the agent container at dispatch time:

VariableDescription
SCION_TRANSPORT_TOKENInitial Google OIDC ID token for the transport layer.
SCION_TRANSPORT_AUDIENCEAudience the transport token was minted for (IAP client ID or hub URL).
SCION_TRANSPORT_TOKEN_EXPIRYToken expiry in RFC 3339 format.

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.

The agent (pkg/sciontool/hub) selects an OIDC token source automatically:

  1. SCION_TRANSPORT_TOKEN env var setInjected mode: uses the hub-provided token from dispatch, refreshed via tokens[] on subsequent refresh calls.
  2. 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_AUDIENCE or defaults to the hub URL.
  3. 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.

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"
Transport modeoidc_audience value
iapThe IAP OAuth client ID (found in Cloud Console → Security → IAP → your backend → OAuth client). Format: <client-id>.apps.googleusercontent.com
cloudrun_invokerThe Hub’s URL (e.g., https://hub.example.com). If left empty, derived from hub.public_url.

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, or assign).
  • Avoids distributing service account key files.

Required IAM bindings:

PrincipalRoleTarget
Hub’s runtime SAroles/iam.serviceAccountTokenCreatorTransport SA (platform_auth_sa)
Transport SAIAP-secured web user or Cloud Run invokerThe Hub’s backend service
  1. Only the signed assertion is trusted. The unsigned X-Goog-Authenticated-User-Email and X-Goog-Authenticated-User-Id headers are completely ignored.
  2. Audience binding is mandatory. Without it, a JWT minted for a different IAP-protected service would be accepted. The auth.proxy.iap.audience field must always be set.
  3. 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.
  4. 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.
  5. Clock skew of ±30 seconds is allowed on exp and iat claims.
  6. Suspended users are rejected at the provisioning layer even though IAP still authenticates them upstream.
  • A GCP project with billing enabled.
  • The Hub deployed on Cloud Run (or behind a GCE/GKE load balancer).
  • gcloud CLI configured with appropriate permissions.
Section titled “1. Enable IAP and create an OAuth consent screen”
Terminal window
# Enable the IAP API
gcloud services enable iap.googleapis.com
# Configure the OAuth consent screen (if not already done)
# Go to: Console → APIs & Services → OAuth consent screen
Terminal window
# For Cloud Run behind a load balancer:
gcloud iap web enable \
--resource-type=backend-services \
--service=YOUR_BACKEND_SERVICE_NAME

Note 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.

Terminal window
# Create a dedicated SA for transport auth
gcloud iam service-accounts create scion-transport \
--display-name="Scion Transport Auth"
# Grant the Hub's runtime SA permission to impersonate the transport SA
gcloud 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:

Terminal window
# Grant IAP-secured web user access to the transport SA
gcloud 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:

Terminal window
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_REGION

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://..."
  1. Access the Hub URL in a browser — IAP should prompt for Google login, then the Hub should show your identity.
  2. Dispatch an agent and verify it can communicate back to the Hub (check agent logs for OIDC transport messages).
  3. Check Hub logs for Proxy auth configured: provider=iap and Transport auth configured: mode=iap at startup.

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.