Google Workspace Identity Security Posture Management (ISPM)
Console-only posture scanning for a Google Workspace domain: 25 weighted Admin SDK checks in nine categories, run from the dashboard Posture tab and scored 0-100.
Overview
PasskeyBridge reads a Google Workspace domain through the Admin SDK Directory and Reports APIs, runs 25 checks against its users, domains, roles, groups and audit logs, and stores each one as a finding with a weight, a status and evidence. The dashboard renders a 0-100 posture score and the findings grouped by category. Every call the scan makes is a read.
It lives in the dashboard Posture tab, under the Google Workspace provider tab. Scan rows carry provider set to google_workspace.
Who can run it. Posture is an Enterprise tab: on the starter and pro plans the dashboard shows an upgrade panel in place of the tab. The plan gate is in the dashboard only. The shield-google-ispm function enforces something different: the caller must present a valid dashboard session and be an admin member of the tenant in shield_tenant_members.
Console-only. shield-google-ispm is not in the Cloudflare worker's PUBLIC_FUNCTIONS list, so it has no https://api.passkeybridge.io/v1 route and no API key lane. The dashboard calls it directly with supabase.functions.invoke and your session JWT.
Service account and delegated scopes
In Google Cloud. Create a service account and a JSON key for it. The scan accepts the key in either PEM shape: a PKCS#8 block beginning -----BEGIN PRIVATE KEY----- or a PKCS#1 block beginning -----BEGIN RSA PRIVATE KEY-----, which it wraps in a PKCS#8 envelope before importing.
In the Admin Console. Authorise the service account's client ID for domain-wide delegation under Security > Access and data control > API controls > Domain-wide delegation, with exactly these six scopes:
https://www.googleapis.com/auth/admin.directory.user.readonlyhttps://www.googleapis.com/auth/admin.directory.domain.readonlyhttps://www.googleapis.com/auth/admin.directory.group.readonlyhttps://www.googleapis.com/auth/admin.directory.rolemanagement.readonlyhttps://www.googleapis.com/auth/admin.directory.device.mobile.readonlyhttps://www.googleapis.com/auth/admin.reports.audit.readonly
The function signs an RS256 JWT with the private key, sets iss to the service account email and sub to the admin email you configure, and exchanges it at https://oauth2.googleapis.com/token for an access token that lasts one hour. Without a super admin in sub, the Directory calls fail and their checks are skipped.
Before the checks run the function prefetches five resources in parallel and reuses them: the user list (filtered to your domain, projection=full, 500 per page, up to 5,000 users), the customer's domains, the customer's roles, the group list (200 per page, up to 5,000) and the customer's role assignments.
Checks by category
25 checks run on every scan, in nine categories.
| Category | Checks | What they read |
|---|---|---|
| lifecycle | 6 | Suspended accounts, recovery phone or email coverage, group count, organizational unit spread, users created in the last 7 days, and accounts with no sign-in for 90 days. |
| authentication | 5 | Domain verification, recent OAuth token authorizations from the Reports API, secondary domain count, average aliases per user, and less secure app access. |
| admin | 4 | Super admin count, delegated admin count, whether custom admin roles exist, and data export controls. |
| mfa | 3 | 2-Step Verification enrollment, the security key or passkey policy, and Advanced Protection Program enrollment. |
| observability | 2 | Whether the admin and login audit logs are readable through the Reports API. |
| protocol | 2 | DKIM, SPF and DMARC configuration, and third-party API access controls. |
| threat_protection | 1 | Recent login_failure events from the Reports API, warning at 20 or more. |
| password | 1 | Password length policy. |
| device | 1 | Whether any managed mobile devices are present. |
Thresholds that decide a status. 2-Step Verification passes at 90 percent of active users enrolled, warns at 50, and fails below that. Stale accounts pass at 10 percent or fewer, warn to 25 percent, then fail. Recovery information passes at 80 percent, warns at 50. Super admins pass at 5 or fewer.
Five checks always report a warning because the Admin SDK does not expose the setting: Less Secure App Access, Advanced Protection Program Enrollment, Email Authentication (DKIM/SPF/DMARC), Data Export Controls and API Access Controls. Each carries a fixed impact of -1 and a remediation pointing at the Admin Console page to check by hand.
Two more checks never pass. Security Key / Passkey Policy warns with an impact of -2 when 2-Step Verification is enforced for every active user and fails with -12 otherwise, because the API cannot confirm an "Only security key" policy. Password Length Policy always warns with an impact of 0, so it costs nothing and reminds you to verify the minimum length yourself.
Super admin detection. The check looks through role assignments for a role marked isSuperAdminRole, named _seed_admin_role, or whose name contains "super admin". When no such role is visible it falls back to counting users with the isAdmin flag and says so in the finding, since that flag covers every admin type.
Dashboard fields
Open the dashboard Posture tab, select the Google Workspace provider tab, and expand Google Workspace configuration.
| Field | Goes to | Stored as |
|---|---|---|
| Workspace domain | google_workspace_domain on shield_tenants | Clear text. |
| Admin email | google_workspace_admin_email on shield_tenants | Clear text. The service account impersonates this user. |
| Service account email | google_workspace_client_email on shield_tenants | Clear text. This is client_email from the JSON key. |
| Private key | google_workspace_private_key_encrypted on shield_tenants | AES-256-GCM under the server-held SHIELD_ENCRYPTION_KEY. This is private_key from the JSON key. |
Press Save. All four fields are required on every save, including the private key.
{ "action": "save_google_config", "tenant_id": "<tenant uuid>", "google_workspace_domain": "example.com", "google_workspace_admin_email": "admin@example.com", "google_workspace_client_email": "scanner@project.iam.gserviceaccount.com", "google_workspace_private_key": "-----BEGIN PRIVATE KEY-----..." }A successful save answers {"ok": true} and writes a save_google_workspace_config audit row.
| Status | Body |
|---|---|
| 400 | Domain, service account email, private key, and admin email are all required |
| 400 | Private key must be in PEM format (-----BEGIN PRIVATE KEY----- or -----BEGIN RSA PRIVATE KEY-----) |
| 500 | Failed to save provider configuration |
Posture score
The score is a penalty ratio over the checks that actually ran. Every check carries a hard-coded weight and a score_impact: zero when it passes, a negative number when it fails or warns. The function computes, over all non-skipped checks, 100 x (1 - total_penalty / max_penalty), where total_penalty is the sum of the absolute score_impact values and max_penalty is the sum of weight x 2. It rounds it and clamps it to 0-100. Weights run from 2 to 8. Google's heaviest checks are 2-Step Verification enrollment and the security key policy at 8 each.
There are no severity multipliers. severity (critical, high, medium, low, info) is a label on the finding row and has no effect on the arithmetic.
Skipped checks leave both sides of the ratio, so a check that could not run neither helps nor hurts. If every check is skipped the score is 0.
The tab turns the number into one of four labels.
| Score | Label in the tab |
|---|---|
| 80-100 | Strong |
| 60-79 | Moderate |
| 40-59 | Needs work |
| 0-39 | Critical |
Running a scan and retrying failures
Scans run on demand. No cron job calls any ISPM function, so a scan happens when someone presses a button in the Posture tab. Open the tab, select Google Workspace, and press Run scan in the tab header. The button is disabled while a scan is in flight and the tab reloads the scan list when it finishes.
The button invokes the function with your signed-in session.
const { data, error } = await supabase.functions.invoke("shield-google-ispm", {
body: { action: "run_scan", tenant_id: "<tenant uuid>" },
});A completed scan answers with the summary the tab renders.
{
"scan_id": "7c1f0b2e-...",
"parent_scan_id": null,
"posture_score": 68,
"checks_total": 25,
"checks_passed": 14,
"checks_failed": 5,
"checks_warning": 4,
"checks_skipped": 2,
"duration_ms": 8421,
"partial_failures": []
}Retry failed checks appears in the Findings header once a completed scan has at least one failed or warning check. It creates a second scan that re-runs only the failed and warning check ids and copies the passed and skipped findings forward, so a skipped check changes only on a full Run scan.
{ "action": "retry_failed_checks", "tenant_id": "<tenant uuid>", "retry_of_scan_id": "<scan uuid>" }| Status | Body | Cause |
|---|---|---|
| 401 | Missing authorization header or Unauthorized | No session JWT, or it has expired. |
| 400 | Invalid JSON body | The body did not parse as JSON. |
| 400 | tenant_id required | tenant_id was missing from the body. |
| 403 | Forbidden: tenant admin access required | The signed-in user is not an admin member of that tenant. |
| 500 | Failed to verify tenant access | The membership lookup itself failed. |
| 400 | Unknown action | action was not a save action, run_scan or retry_failed_checks. |
| 400 | Google Workspace configuration not found. Configure your domain, service account, and admin email first. | No credentials are stored for this tenant. |
| 400 | Token exchange failed (...) | Google rejected the signed JWT. The status and body are included. |
| 400 | retry_of_scan_id required | Retry was called without a parent scan id. |
| 404 | parent scan not found | No scan row has that id. |
| 403 | parent scan does not belong to this tenant/provider | The parent scan belongs to another tenant or another provider. |
| 400 | no failed or warning checks to retry | Every check in the parent scan passed or was skipped. |
| 500 | Scan failed: ... | The scan threw. The scan row is stored with status failed and the thrown message. |
Confirm it worked. The score card shows the band, the score out of 100, and the Passed, Failed and Warnings tiles. The card under it shows Scan, Duration, Started and Type, where Type reads Full scan or Retry, N checks. Findings are grouped by category below; expanding one shows its description, Remediation, Evidence JSON, and a line reading Weight, impact and check with that check's numbers and id. The Alerts section in the same tab can post a completed scan to a Slack webhook or to email recipients once its failed-check threshold is met.
Cross-provider comparison
Google Workspace uses the same runner, the same tables and the same scoring formula as the Okta, Entra ID, AWS, Ping Identity and OneLogin modules, so their scores are comparable. CyberArk Identity is the exception: it awards credit for passing checks instead of subtracting penalties.
What is specific to this module: a service account JWT with domain-wide delegation instead of an API token or a client secret, a device category that no other provider has, and seven checks whose result is fixed because the Admin SDK does not expose the setting.
All eight providers are side by side in the ISPM cross-provider comparison.
Related from the blog
- Roaming Steering and Identity: When the HPLMN You Trust Hands You to a VPLMN You Don'tsecurity · 20 min read
- The Identity Stack Bifurcation: Consumer Wallets vs Enterprise Attestation Layersintel · 20 min read
- Network Slicing in 5G Standalone and the Birth of Per-Slice Identity Posturentn · 15 min read