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.
Two verified proxy providers are available, selected by auth.proxy.provider:
iap— Google IAP. Covered by most of this guide.jwt— any authenticating proxy that forwards a signed JWT. See Generic JWT proxy provider.
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 / UAT / 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. # Cloud Run format: # /projects/<PROJECT_NUMBER>/locations/<REGION>/services/<SERVICE_NAME> # GCE/GKE backend service (GCLB) format: # /projects/<PROJECT_NUMBER>/global/backendServices/<BACKEND_SERVICE_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:
- Cloud Run:
/projects/<PROJECT_NUMBER>/locations/<REGION>/services/<SERVICE_NAME> - GCE/GKE backend service (GCLB):
/projects/<PROJECT_NUMBER>/global/backendServices/<BACKEND_SERVICE_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).- Roles: Emails in
admin_emailsare granted theadminrole automatically. Other new users getserver.auth.default_user_role(memberorviewer). An admin granted byadmin_emailswho is removed from the list is demoted to the default role; admins promoted through the Admin UI/API are not. See Hub roles and AdminEmails and UI-promoted admins. - If not permitted, the request returns 403. Deleted users are rejected with 401 or 403, and suspended users are rejected with 403 (even though upstream IAP may authenticate them).
- Database-Backed Token Refresh: The token refresh endpoint reads the user’s active role directly from the database rather than relying on stale session cache, ensuring UI-based role promotions take effect immediately.
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"}.
The browser redirect is the same for every proxy provider. With provider: jwt, /_gcp_iap/clear_login_cookie is a path on your own proxy, so sign-out depends on how that proxy handles it.
Generic JWT proxy provider
Section titled “Generic JWT proxy provider”Use provider: jwt when the Hub sits behind a bespoke authenticating proxy rather than Google IAP. The proxy must forward a JWT signed with an asymmetric key. The Hub verifies the signature, validates the claims, and then provisions the user exactly as described in User provisioning. The middleware precedence is the same as for IAP.
server: auth: mode: proxy proxy: provider: jwt jwt: header: X-Auth-Proxy-JWT # default algorithm: RS256 # required; asymmetric algorithms only issuer: https://auth.example.com audience: scion-hub
# Exactly one key source: jwks_url: https://auth.example.com/.well-known/jwks.json # jwks_file: /etc/scion/proxy-jwks.json # public_key_file: /etc/scion/proxy-signing-key.pem
# Optional claim mapping (OIDC-standard defaults shown): claims: email: email subject: sub display_name: name domain: hd user_access_mode: domain_restricted authorized_domains: - example.comKey sources
Section titled “Key sources”Configure exactly one key source. If none or more than one is set, the Hub refuses to start.
| Key source | Behavior |
|---|---|
public_key_file |
A single PEM public key (PKIX, PKCS1, or an X.509 certificate), read at startup and applied to every token regardless of kid. |
jwks_url |
A remote JWKS endpoint. Keys are matched by kid and cached. The cache refreshes hourly in the background and immediately when a token carries an unknown kid. Fetches are debounced to at most one attempt every 5 seconds. If a refresh fails, the last-good key set is still served. This matches the IAP provider. |
jwks_file |
A local JWKS JSON document, read at startup. Keys are matched by kid, and keys without a kid are ignored. The file must contain at least one usable keyed entry. Changes take effect only after a restart. |
Missing or malformed key files are reported at startup, not on the first request.
Validation rules
Section titled “Validation rules”- Algorithm pinning. Only the configured
algorithmis accepted. Symmetric (HS*) algorithms are rejected at startup. - Issuer and audience are checked only when
issuerandaudienceare set. Set both in production. Without an audience, the Hub accepts any token the proxy’s key has signed, including tokens minted for other services. - Expiry.
expis required.expandiat(if present) are checked with ±30 seconds of clock skew. - Identity claims. The email claim is required and is lowercased. If the subject claim is missing, the email is used as the subject. Display name and domain are optional.
- A request without the configured header falls through to the normal unauthenticated handling. A request whose header holds an invalid JWT is rejected.
As with IAP, the Hub must be reachable only through the proxy. Use network controls to keep clients from reaching it directly.
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> or Proxy-Authorization: Bearer <Google OIDC ID token> |
Satisfies the platform guard (IAP or Cloud Run invoker IAM check). Cloud Run native IAP fully supports Proxy-Authorization. |
| Inner (app) | X-Scion-Agent-Token: <scion JWT> |
Existing Hub agent authentication. Carried as a custom header so it never collides with the outer header. |
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 |
Expiry of the initial token, in RFC 3339 format. Bootstrap only: sciontool init removes it together with SCION_TRANSPORT_TOKEN, because it goes stale after the first refresh. |
SCION_TRANSPORT_TOKEN only bootstraps the agent. At startup, sciontool init writes it to ~/.scion/transport-token (mode 0600, owned by the agent user) and removes it from the environment that the harness and its child processes inherit. Children get SCION_TRANSPORT_TOKEN_FILE, which points at that file. Each token refresh rewrites the file, and every in-agent hub client (hooks, sciontool subcommands, the in-agent scion CLI) re-reads it when it changes. So these clients keep working after the initial token expires, which takes about an hour.
If sciontool init cannot write the file, it logs an error and leaves SCION_TRANSPORT_TOKEN in the environment. In-agent clients then use that value until it expires, and hub calls from child processes fail after about an hour. A file is used only when the agent was given a transport token (SCION_TRANSPORT_TOKEN or SCION_TRANSPORT_TOKEN_FILE is set); a stale file left in a persisted home is removed at startup when neither is set.
A root shell opened with docker exec or kubectl exec does not inherit SCION_TRANSPORT_TOKEN_FILE. Run . ~/.scion/scion-env (as the agent user, or with the agent’s home path) to get the same hub environment as the harness. That file exports SCION_TRANSPORT_TOKEN_FILE.
On the Kubernetes runtime, SCION_TRANSPORT_TOKEN comes from the agent’s per-agent Secret through secretKeyRef, not from a plain value in the Pod spec. See Hub Transport Credential. Other runtimes set it as a regular environment variable.
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.
If the Hub is configured to mint transport tokens but cannot mint one (for example, the Hub’s service account has lost roles/iam.serviceAccountTokenCreator on the transport SA), the refresh still succeeds with the app token, the transport entry is omitted, and the response carries a transportError field with a fixed, generic description. The underlying error stays in the Hub’s logs. An agent that uses a hub-provided transport token logs the failure and records the transport outcome of each refresh (refreshed, failed or absent) in ~/.scion/transport-token.status, which sciontool doctor reports. Agents that do not use a hub-provided transport token (metadata mode, or no transport) ignore transport entries and record nothing. An agent in a proxy mode that started without a transport token counts as using one: it records outcomes, and it adopts the first transport entry it receives. The file never contains a token, and it is removed with the transport token file when an agent starts without a transport token.
Recovering with reset-auth
Section titled “Recovering with reset-auth”scion agent reset-auth <agent> also pushes a fresh transport token when the Hub mints them. The broker writes it to ~/.scion/transport-token next to the agent token, and sciontool init reloads it straight away and records the outcome reset, so doctor no longer shows an earlier failed refresh as the latest event. A value that cannot be parsed is not adopted: the agent keeps its current credential, restores the file from it, and records the reset as failed. If the agent has no credential yet (it started without a transport token in a proxy mode), the file is removed instead. This recovers an agent whose transport token has already expired, since that agent’s own refresh can no longer get through the platform guard. If the Hub cannot mint a transport token, the reset still replaces the agent token.
Agents that started without a transport token
Section titled “Agents that started without a transport token”If the Hub cannot mint a transport token at dispatch time, the agent starts without one. The Hub still sets SCION_TRANSPORT_MODE whenever it is configured to mint transport tokens. In a proxy mode (iap or cloudrun_invoker), such an agent adopts the first transport token it receives later, either from a token refresh or from reset-auth. That token is written to ~/.scion/transport-token through the same path as a normal refresh, and it is picked up without a restart: sciontool’s hub clients (including the long-lived one in the agent’s init process) re-read the file, and every new in-agent hub client uses it, including clients created by processes that were already running. Until then, requests carry no transport header, and sciontool doctor reports that no transport credential has been received. Because the platform guard usually blocks the agent’s own refresh until it has a credential, reset-auth is the usual way to recover. Without a proxy mode, the agent ignores a transport token that arrives after start.
Diagnosing with sciontool doctor
Section titled “Diagnosing with sciontool doctor”Inside the agent, sciontool doctor has a Transport Auth section that shows:
- the header mode (
SCION_TRANSPORT_MODE) and the header it uses, plus a shortened form of the audience (enough to spot a mismatch); - which credential is in use (the refreshed file or the bootstrap value) and when it expires. The check fails if that credential has expired, cannot be parsed, or none is available, and warns when it is within the refresh margin;
- the expiry of the bootstrap value and of the file side by side, and when the file was last written;
- the transport outcome of the last refresh, or of the last
reset-auth.
When ~/.scion/transport-token is replaced by rename during a read, the file reader retries up to three reads in total; if all three encounter an unlinked file during back-to-back refreshes, it keeps the value from its last successful read if that read is at most two seconds old and has a parsed expiry in the future, and sciontool doctor reports that state (FileRetained).
Doctor never prints token values. Its authentication checks tell a rejection by the platform proxy (a non-JSON 401/403, or a redirect to Google sign-in) apart from a rejection by the Hub (a JSON error), and the remediation differs: for a proxy rejection, run reset-auth and check the transport mode and audience; for a Hub rejection, the agent token itself is invalid. Doctor does not follow redirects. Redirects are shown only as their scheme and host, never with their query string. A redirect to any other host means authentication could not be confirmed, and it counts as a failed check; it usually means SCION_HUB_ENDPOINT is not the hub’s final URL. A 404 on /healthz can come from the platform rather than the Hub, since some platforms (for example Cloud Run) reserve that path.
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_TOKEN_FILEorSCION_TRANSPORT_TOKENset → Injected mode: reads the refreshed file, with the env value as bootstrap fallback. The hub-provided token from dispatch is refreshed viatokens[]on subsequent refresh calls and shared with other processes through the file. Whichever of the file and the env value expires later is used. Outside a proxy mode, the file alone does not select this mode.- 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. SCION_TRANSPORT_MODEis a proxy mode (iaporcloudrun_invoker) → File-backed mode with no bootstrap value, for an agent that started without a transport token. Thesciontoolhub client always selects this mode in a proxy mode. It sends no transport header until a refresh orreset-authdelivers a token, which is then written to~/.scion/transport-token. Other clients built withtransportauth.FromEnv, such as the in-agentscionCLI and doctor, select it only once that file exists. See Agents that started without a transport token.- None of the above → No OIDC transport (agent uses plain HTTP).
The header follows SCION_TRANSPORT_MODE in every in-agent client: iap sends Proxy-Authorization, cloudrun_invoker sends X-Serverless-Authorization, and anything else sends Authorization.
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 |
Seeing requests IAP rejects
Section titled “Seeing requests IAP rejects”When IAP enforces access directly on a Cloud Run service (for example, the IAP proxy in front of a single-node Hub), a request IAP rejects never reaches the container. It does not appear in the Cloud Run service’s logs or the Hub’s logs. To see IAP’s own authorization decisions, enable Data Access audit logs (Data Read) for the iap.googleapis.com service, then filter Cloud Logging on protoPayload.serviceName="iap.googleapis.com" and protoPayload.authorizationInfo.granted=false. A mismatched auth.proxy.iap.audience is different: IAP passes the request through and the Hub rejects it, so it does show up in the Hub’s logs. For step-by-step Console and auditConfigs instructions, see Requests IAP rejects never reach Cloud Run or the Hub logs in the repository.
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 this client ID for auth.transport.oidc_audience (it is used for minting OIDC tokens).
Note the signed header JWT audience (found in Console → Security → IAP → your backend). This goes into auth.proxy.iap.audience (used for validating incoming human and browser requests).
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, or run
sciontool doctorinside the agent). - 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.
Cloud Run IAP reverse proxy (VPC-fronting)
Section titled “Cloud Run IAP reverse proxy (VPC-fronting)”If your Scion Hub runs on a private GCE VM (or any internal VPC service) and you want to front it with IAP via Cloud Run, the extras/cloudrun-iap-proxy/ directory provides a lightweight Go reverse proxy purpose-built for this topology:
[User Browser / Client] │ (HTTPS)[Cloud Run (IAP Proxy)] │ (Direct VPC Egress / Private IP) [Scion Hub (GCE VM)]Key features:
- IAP header preservation — forwards
X-Goog-IAP-JWT-Assertionand authenticated-user headers to the backend Hub. - HTTP/2 cleartext (h2c) support — uses
h2cfor multiplexed streaming over Cloud Run, which is critical for SSE connections (agent events, live dashboard updates). - Health probe — exposes
/proxy-healthzfor Cloud Run health checks without proxying to the backend. - Direct VPC Egress ready — deploy with Cloud Run Direct VPC Egress to reach internal compute instances on private RFC 1918 IPs.
The directory includes a Dockerfile, deploy script, and unit tests. See the README.md in extras/cloudrun-iap-proxy/ for configuration and deployment instructions.
Interactive CLI sessions (scion attach) via IAP
Section titled “Interactive CLI sessions (scion attach) via IAP”Connecting an interactive terminal to a running agent’s session (scion attach <agent-name>) when the Hub is behind IAP requires tunneling through the Google platform guard.
Dual-layer Authentication Bypass
Section titled “Dual-layer Authentication Bypass”Because IAP is a transport-layer security mechanism, authenticating via the CLI in IAP-protected environments utilizes a dual-layer authentication model:
- Outer Transport Layer: Google OIDC ID token matching the IAP Client ID audience. This satisfies the Google IAP proxy check and allows the WebSocket request to reach the Hub.
- Inner App Layer: Existing Hub authentication (such as a User Access Token (UAT) / PAT) carried as a custom WebSocket protocol header.
App-Token Gate Bypassing
Section titled “App-Token Gate Bypassing”Previously, scion attach would enforce the existence of an application-level token before attempting transport-auth resolution. Since proxy/IAP mode operates by validating credentials at the transport level (with the Hub deriving identity from the X-Goog-IAP-JWT-Assertion header inserted by IAP), requiring a separate application-level token at the CLI gate blocked fully-authenticated IAP connections.
The scion attach flow resolves transport-layer authentication before checking the application-level token gate:
- If a valid transport auth token source is configured and resolved (e.g., your local Google Cloud SDK identity or GKE Workload Identity can authenticate to IAP), the application-level token requirement is bypassed.
- If no transport source is configured, the command falls back to requiring a standard Hub access token (via
scion hub auth loginorSCION_HUB_TOKEN).
This unblocks seamless interactive attachments to agents executing under IAP-secured Hubs.
Brokers behind IAP
Section titled “Brokers behind IAP”When the Hub is behind IAP, Runtime Brokers must also carry a transport-layer OIDC token on every request — just like agents do. However, brokers are long-lived originators: nothing injects a transport token into them, so they must mint their own OIDC tokens from their runtime identity (GKE Workload Identity or ambient GCE service account).
This section covers the deployment and configuration steps to connect brokers to an IAP-protected Hub.
Custom OAuth 2.0 Client ID requirement
Section titled “Custom OAuth 2.0 Client ID requirement”The Google-managed OAuth client that Cloud Run auto-provisions when IAP is enabled does not support programmatic (service-account) authentication. This means brokers (and any other machine client) cannot use it to mint OIDC tokens.
You must:
- Create a custom OAuth 2.0 Client ID in the Google Cloud Console under APIs & Services → Credentials → Create Credentials → OAuth client ID (application type: Web application).
- Bind the custom client ID to the IAP settings for your Hub’s backend service (Console → Security → IAP → select backend → Edit OAuth Client → Use custom client).
The custom client ID (e.g., 1234567890-abc.apps.googleusercontent.com) becomes the OIDC audience for all machine clients — both agents and brokers.
Broker Workload Identity setup
Section titled “Broker Workload Identity setup”Brokers running in GKE use Workload Identity to mint OIDC tokens from the GCE metadata server.
-
Create a Google Service Account (GSA) for brokers:
Terminal window gcloud iam service-accounts create scion-broker \--display-name="Scion Broker" -
Grant the GSA access to traverse the platform guard. The required role depends on the transport mode:
Transport mode Role Target iaproles/iap.httpsResourceAccessorHub backend service cloudrun_invokerroles/run.invokerHub Cloud Run service For IAP:
Terminal window gcloud iap web add-iam-policy-binding \--resource-type=backend-services \--service=YOUR_BACKEND_SERVICE_NAME \--member="serviceAccount:scion-broker@PROJECT_ID.iam.gserviceaccount.com" \--role="roles/iap.httpsResourceAccessor" -
Bind the broker’s Kubernetes Service Account (KSA) to the GSA via the Workload Identity annotation:
Terminal window # Allow the KSA to impersonate the GSAgcloud iam service-accounts add-iam-policy-binding \scion-broker@PROJECT_ID.iam.gserviceaccount.com \--member="serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/BROKER_KSA_NAME]" \--role="roles/iam.workloadIdentityUser"# Annotate the KSAkubectl annotate serviceaccount BROKER_KSA_NAME \--namespace NAMESPACE \iam.gke.io/gcp-service-account=scion-broker@PROJECT_ID.iam.gserviceaccount.com
The broker can now mint OIDC ID tokens for the configured audience via the metadata server, which the transport layer uses automatically.
Broker transport configuration
Section titled “Broker transport configuration”Broker transport auth has two configuration layers: environment variables (for containerized brokers in Kubernetes) and credentials-file fields (for per-connection config).
Environment variables
Section titled “Environment variables”Set these on the broker’s Kubernetes Deployment:
| Variable | Description |
|---|---|
SCION_TRANSPORT_MODE |
Transport mode: iap or cloudrun_invoker |
SCION_TRANSPORT_AUDIENCE |
OIDC audience — the custom OAuth 2.0 Client ID (for iap mode) or the Hub URL (for cloudrun_invoker mode) |
env: - name: SCION_TRANSPORT_MODE value: "iap" - name: SCION_TRANSPORT_AUDIENCE value: "1234567890-abc.apps.googleusercontent.com"Credentials-file fields
Section titled “Credentials-file fields”The broker credentials file (~/.scion/hub-credentials/<name>.json, written by scion runtime-broker register) can also store transport settings per hub connection:
{ "brokerId": "...", "secretKey": "...", "hubEndpoint": "https://hub.example.com", "transportMode": "iap", "transportAudience": "1234567890-abc.apps.googleusercontent.com"}Environment variables override credentials-file values. This allows Kubernetes Deployment manifests to set transport config declaratively while the credentials file retains the values persisted at registration time.
Broker registration without PAT (proxy-auth mode)
Section titled “Broker registration without PAT (proxy-auth mode)”With transport auth configured, scion runtime-broker register works through IAP natively — no Personal Access Token (PAT) or hub token is needed. The broker authenticates via the IAP assertion of its service account identity.
When the Hub is in proxy auth mode and the broker has a valid transport token source (Workload Identity), the registration command:
- Sends the OIDC transport token in the
Authorizationheader to traverse IAP. - IAP verifies the token and passes the request through to the Hub.
- The Hub identifies the caller via the IAP assertion (
X-Goog-IAP-JWT-Assertion) and completes the registration.
This retires the manual install-broker.sh curl-from-a-pod workaround.
The register command also persists transportMode and transportAudience into the credentials file automatically, so the broker daemon inherits them on startup.
Registering the broker Deployment
Section titled “Registering the broker Deployment”Instead of manual shell scripts, run the registration in the broker pod itself. scion runtime-broker register first checks that the broker server answers on its local port, so it has to run where the broker is reachable on localhost. The broker pod already has the broker’s KSA (with Workload Identity) and the transport environment variables, and writes the credentials to the broker’s own ~/.scion volume:
kubectl -n scion exec -it deploy/scion-broker -- \ scion runtime-broker register --global \ --hub https://hub.example.com \ --name my-broker \ --transport-mode iap \ --transport-audience "1234567890-abc.apps.googleusercontent.com"Answer the confirmation prompts in the terminal. (--yes accepts every prompt, including adding the broker as a provider for a Hub project named global, which is created if it does not exist.) The CLI flags persist the transport values to the credentials file, so the broker inherits them on later starts. Add --port <port> if the broker does not listen on 9800.
Keep the broker’s ~/.scion on a persistent volume, so the credentials survive pod restarts. If the broker started without a Hub endpoint configured, restart it after registering (kubectl -n scion rollout restart deploy/scion-broker) so it connects with the new credentials.
GKE deployment summary
Section titled “GKE deployment summary”| Step | What |
|---|---|
| 1 | Create a custom OAuth 2.0 Client ID; bind it to the Hub service’s IAP settings |
| 2 | Create a broker GSA; grant roles/iap.httpsResourceAccessor on the Hub backend service |
| 3 | Bind KSA ↔ GSA via Workload Identity annotation on the broker’s Kubernetes service account |
| 4 | Broker Deployment env: SCION_TRANSPORT_MODE=iap, SCION_TRANSPORT_AUDIENCE=<custom client id> |
| 5 | One-time scion runtime-broker register in the broker pod (same KSA) — no curl scripts |