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.

Last reviewed September 16, 2026Fresh

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.readonly
  • https://www.googleapis.com/auth/admin.directory.domain.readonly
  • https://www.googleapis.com/auth/admin.directory.group.readonly
  • https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly
  • https://www.googleapis.com/auth/admin.directory.device.mobile.readonly
  • https://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.

CategoryChecksWhat they read
lifecycle6Suspended 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.
authentication5Domain verification, recent OAuth token authorizations from the Reports API, secondary domain count, average aliases per user, and less secure app access.
admin4Super admin count, delegated admin count, whether custom admin roles exist, and data export controls.
mfa32-Step Verification enrollment, the security key or passkey policy, and Advanced Protection Program enrollment.
observability2Whether the admin and login audit logs are readable through the Reports API.
protocol2DKIM, SPF and DMARC configuration, and third-party API access controls.
threat_protection1Recent login_failure events from the Reports API, warning at 20 or more.
password1Password length policy.
device1Whether 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.

FieldGoes toStored as
Workspace domaingoogle_workspace_domain on shield_tenantsClear text.
Admin emailgoogle_workspace_admin_email on shield_tenantsClear text. The service account impersonates this user.
Service account emailgoogle_workspace_client_email on shield_tenantsClear text. This is client_email from the JSON key.
Private keygoogle_workspace_private_key_encrypted on shield_tenantsAES-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.

StatusBody
400Domain, service account email, private key, and admin email are all required
400Private key must be in PEM format (-----BEGIN PRIVATE KEY----- or -----BEGIN RSA PRIVATE KEY-----)
500Failed 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.

ScoreLabel in the tab
80-100Strong
60-79Moderate
40-59Needs work
0-39Critical

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>" }
StatusBodyCause
401Missing authorization header or UnauthorizedNo session JWT, or it has expired.
400Invalid JSON bodyThe body did not parse as JSON.
400tenant_id requiredtenant_id was missing from the body.
403Forbidden: tenant admin access requiredThe signed-in user is not an admin member of that tenant.
500Failed to verify tenant accessThe membership lookup itself failed.
400Unknown actionaction was not a save action, run_scan or retry_failed_checks.
400Google Workspace configuration not found. Configure your domain, service account, and admin email first.No credentials are stored for this tenant.
400Token exchange failed (...)Google rejected the signed JWT. The status and body are included.
400retry_of_scan_id requiredRetry was called without a parent scan id.
404parent scan not foundNo scan row has that id.
403parent scan does not belong to this tenant/providerThe parent scan belongs to another tenant or another provider.
400no failed or warning checks to retryEvery check in the parent scan passed or was skipped.
500Scan 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