This guide covers setting up Google OIDC authentication with optional group-based RBAC using Google Workspace groups.
In Google auth mode, users authenticate via Google OAuth2. The backend:
- Verifies the Google ID token against Google's public keys
- Optionally resolves the user's Google Workspace group memberships
- Issues a backend token (opaque, stored in SQLite) that the Pulumi CLI uses for subsequent requests
- Enforces RBAC based on group membership (if configured)
- A Google Cloud project
- A Google Workspace domain (for group-based RBAC)
gcloudCLI installed and authenticated
# Create OAuth consent screen (internal = Workspace users only)
# This must be done in the GCP Console:
# APIs & Services > OAuth consent screen > Internal
# Create OAuth client ID (Desktop type for CLI-based login)
# APIs & Services > Credentials > Create Credentials > OAuth client ID
# Application type: Desktop appAfter creation, download the client secret JSON. The file will be named client_secret_CLIENT_ID.apps.googleusercontent.com.json.
Note the Client ID and Client Secret from the downloaded file.
Minimal configuration (no groups, no RBAC):
./pulumi-backend \
-auth-mode=google \
-google-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
-google-allowed-domains=yourdomain.com \
-token-ttl=24hOr via environment variables:
export PULUMI_BACKEND_AUTH_MODE=google
export PULUMI_BACKEND_GOOGLE_CLIENT_ID=YOUR_CLIENT_ID.apps.googleusercontent.com
export PULUMI_BACKEND_GOOGLE_ALLOWED_DOMAINS=yourdomain.com
export PULUMI_BACKEND_TOKEN_TTL=24h
./pulumi-backendTo enable browser-based login (sign in with Google button), also provide the OAuth2 client secret:
./pulumi-backend \
-auth-mode=google \
-google-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
-google-client-secret=YOUR_CLIENT_SECRET \
-google-allowed-domains=yourdomain.comThen open http://localhost:8080/login in a browser. After signing in with Google, the page shows your access token with ready-to-use commands:
export PULUMI_ACCESS_TOKEN=pul-...
pulumi login http://localhost:8080The browser login flow:
- User clicks "Sign in with Google" on
/login - Google OAuth consent screen → user approves
- Backend exchanges the authorization code for an ID token (server-side)
- ID token is validated and a backend access token is minted
- Token is displayed on
/login/callbackwith copy-to-clipboard
Without the client secret, the /login page is not available — users must obtain an ID token externally (e.g., via gcloud auth print-identity-token) and POST it to /api/auth/token-exchange.
The backend implements the same browser login protocol as Pulumi Cloud. When configured, pulumi login will automatically open your browser for Google sign-in:
# Set PULUMI_CONSOLE_DOMAIN to point back to your backend
export PULUMI_CONSOLE_DOMAIN=localhost:8080
# Login — browser opens automatically, no token copy-paste needed
pulumi login http://localhost:8080The flow:
pulumi loginstarts a temporary local HTTP server and openshttp://localhost:8080/cli-login?cliSessionPort=PORT&cliSessionNonce=NONCE- Backend redirects to Google OAuth consent screen
- After Google sign-in, backend mints a token and redirects to
http://localhost:PORT/?accessToken=TOKEN&nonce=NONCE - The Pulumi CLI picks up the token automatically — done
For non-localhost deployments, set the domain accordingly:
export PULUMI_CONSOLE_DOMAIN=pulumi.internal.example.com
pulumi login https://pulumi.internal.example.comYou can add PULUMI_CONSOLE_DOMAIN to your shell profile to make it permanent.
To use Google Workspace groups for RBAC, the backend's service account needs permission to read group memberships. Two approaches:
The simplest approach — assign the SA a Workspace admin role that grants group read access. No domain-wide delegation, no SA key files, no admin email configuration.
-
Create a service account (or use an existing one):
PROJECT_ID=$(gcloud config get project) SA_EMAIL=pulumi-backend@${PROJECT_ID}.iam.gserviceaccount.com gcloud iam service-accounts create pulumi-backend \ --display-name="Pulumi Backend" \ --project=$PROJECT_ID
-
In Workspace Admin Console → Account → Admin roles:
- Find the Groups Reader role (or create a custom role with Groups > Read permission)
- Assign
pulumi-backend@PROJECT_ID.iam.gserviceaccount.comto this role
-
No
--google-admin-emailflag needed. The backend uses the SA's own credentials to call the Admin SDK.
Important: Admin-role mode requires -google-allowed-domains — the first domain is used to scope the Admin SDK Groups.List API call. Without it, the API returns no results because the SA has no implicit Workspace identity.
Use DWD only when the Groups Reader admin role is not available (e.g., Workspace org policy restrictions preventing admin role assignment to service accounts).
Keyless DWD (preferred for GCE/GKE):
SA_EMAIL=pulumi-backend@${PROJECT_ID}.iam.gserviceaccount.com
gcloud services enable iamcredentials.googleapis.com --project=$PROJECT_ID
# Grant the SA permission to impersonate itself (needed for signJwt)
gcloud iam service-accounts add-iam-policy-binding $SA_EMAIL \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/iam.serviceAccountTokenCreator" \
--project=$PROJECT_IDSA Key File (if keyless is not possible):
gcloud iam service-accounts keys create sa-key.json \
--iam-account=$SA_EMAIL \
--project=$PROJECT_IDEnable DWD in Workspace Admin Console:
- Go to admin.google.com → Security → API Controls → Domain-wide Delegation
- Click "Add new"
- Enter the SA's Client ID (numeric, found in GCP Console under the SA details)
- Add scope:
https://www.googleapis.com/auth/admin.directory.group.readonly - Click "Authorize"
gcloud identity groups create \
--organization=yourdomain.com \
--group-email-address=platform-admins@yourdomain.com \
--display-name="Platform Admins"
gcloud identity groups create \
--organization=yourdomain.com \
--group-email-address=developers@yourdomain.com \
--display-name="Developers"
# Add members
gcloud identity groups memberships add \
--group-email=platform-admins@yourdomain.com \
--member-email=admin@yourdomain.com
gcloud identity groups memberships add \
--group-email=developers@yourdomain.com \
--member-email=dev@yourdomain.comCreate rbac.yaml:
defaultPermission: read
groupRoles:
- group: "platform-admins@yourdomain.com"
permission: admin
- group: "developers@yourdomain.com"
permission: write
stackPolicies:
- group: "developers@yourdomain.com"
stackPattern: "myorg/*/dev-*"
permission: admin
- group: "platform-admins@yourdomain.com"
stackPattern: "myorg/*/prod-*"
permission: adminPermission levels: none < read < write < admin.
Groups Reader admin role (no DWD):
./pulumi-backend \
-auth-mode=google \
-google-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
-google-allowed-domains=yourdomain.com \
-rbac-config=rbac.yamlKeyless DWD:
./pulumi-backend \
-auth-mode=google \
-google-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
-google-sa-email=pulumi-backend@PROJECT_ID.iam.gserviceaccount.com \
-google-admin-email=admin@yourdomain.com \
-google-allowed-domains=yourdomain.com \
-rbac-config=rbac.yamlWith SA key file:
./pulumi-backend \
-auth-mode=google \
-google-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
-google-sa-key=sa-key.json \
-google-admin-email=admin@yourdomain.com \
-google-allowed-domains=yourdomain.com \
-rbac-config=rbac.yamlWhen running on GKE with Workload Identity:
- Create a Kubernetes service account and bind it to the GCP SA
- The pod's ADC will automatically be the GCP SA
# Bind KSA to GSA
gcloud iam service-accounts add-iam-policy-binding $SA_EMAIL \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:${PROJECT_ID}.svc.id.goog[NAMESPACE/KSA_NAME]"With admin-role mode (recommended): No additional flags needed beyond -google-allowed-domains. The SA auto-detects its email from the GCE metadata server and self-impersonates to obtain the Admin SDK scope.
With DWD: Add -google-sa-email so the backend can impersonate the SA for keyless DWD via IAM signJwt. No SA key file needed.
| Flag | Env | Description |
|---|---|---|
-google-client-id |
PULUMI_BACKEND_GOOGLE_CLIENT_ID |
OAuth2 client ID (required) |
-google-client-secret |
PULUMI_BACKEND_GOOGLE_CLIENT_SECRET |
OAuth2 client secret (required for browser login) |
-google-sa-key |
PULUMI_BACKEND_GOOGLE_SA_KEY |
Path to SA JSON key for Admin SDK groups |
-google-sa-email |
PULUMI_BACKEND_GOOGLE_SA_EMAIL |
SA email (auto-detected on GCE for admin-role; required for keyless DWD) |
-google-admin-email |
PULUMI_BACKEND_GOOGLE_ADMIN_EMAIL |
Workspace super-admin email for DWD subject |
-google-allowed-domains |
PULUMI_BACKEND_GOOGLE_ALLOWED_DOMAINS |
Comma-separated allowed hosted domains |
-google-transitive-groups |
PULUMI_BACKEND_GOOGLE_TRANSITIVE_GROUPS |
Resolve nested group memberships |
-token-ttl |
PULUMI_BACKEND_TOKEN_TTL |
Backend-issued token lifetime (default 24h) |
-groups-cache-ttl |
PULUMI_BACKEND_GROUPS_CACHE_TTL |
Group membership cache TTL (default 5m) |
Backend tokens are issued during Google OAuth login and stored in SQLite.
- TTL: Configurable via
-token-ttl(default24h). Expired tokens are rejected on use. - Refresh token re-validation: When users log in via the browser (
/login) or CLI (/cli-login), the backend stores Google's OAuth2 refresh token alongside the backend token. On each authenticated request past half the token's TTL, the backend asynchronously re-validates against Google by exchanging the refresh token for a new ID token. If Google rejects the refresh (user deactivated, consent revoked), the backend token is immediately deleted. This follows the same pattern as Dex's Google connector. - Deactivated users: With refresh token re-validation, deactivated users are detected within half the token TTL. Without a refresh token (programmatic
POST /api/auth/token-exchangeflow), existing tokens remain valid until they expire. - Admin revocation: Admins can immediately revoke all tokens for a user via
DELETE /api/admin/tokens/{userName}. List a user's tokens viaGET /api/admin/tokens/{userName}. - Groups cache invalidation: Admins can force-refresh group memberships via
POST /api/admin/groups-cache/invalidate. Useful after adding/removing users from Workspace groups — takes effect immediately instead of waiting for the cache TTL. - Short TTL: Use a short
-token-ttl(e.g.1h) for tighter security.
Requires RBAC admin permission (or single-tenant mode).
# List a user's tokens
curl -H "Authorization: token $ADMIN_TOKEN" \
https://pulumi.example.com/api/admin/tokens/user@example.com
# Revoke all tokens for a user
curl -X DELETE -H "Authorization: token $ADMIN_TOKEN" \
https://pulumi.example.com/api/admin/tokens/user@example.com
# Invalidate groups cache for a specific user (immediate RBAC re-evaluation)
curl -X POST -H "Authorization: token $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"userName":"user@example.com"}' \
https://pulumi.example.com/api/admin/groups-cache/invalidate
# Invalidate entire groups cache
curl -X POST -H "Authorization: token $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{}' \
https://pulumi.example.com/api/admin/groups-cache/invalidateThe groups resolution mode is inferred from the flags you provide:
- No
-google-admin-email→ admin-role mode (recommended). SA self-impersonates to get Admin SDK scope. Domain is taken from-google-allowed-domains. -google-admin-email+-google-sa-key→ dwd-keyfile mode. SA key file with DWD, Subject set to admin email.-google-admin-email+-google-sa-email→ dwd-keyless mode. ADC + IAM impersonate API for keyless DWD.-google-admin-emailonly → dwd-adc mode. Plain ADC with DWD Subject (works only when ADC is a SA key file).