Skip to content

Google Workspace

Connecting from the web app?

Follow the prerequisites and credential creation instructions below, then return to Cloud Security → Settings → Providers → Add provider. Enter the provider IDs under Configuration and save the credential using New secret under Permissions. Run Test Provider, fix required failures, and save. The LimaCharlie CLI examples below are an alternative. First-time setup and verification explains the full journey.

Collects identity posture from Google Workspace — users, groups and membership, admin roles, user security posture, devices, inbound-SSO profiles, and Gemini-in-Workspace usage — via the Admin SDK and Cloud Identity APIs.

Auth model: a Google Cloud service account, in one of two setups:

  • Domain-wide delegation (DWD) — the service account impersonates a Workspace super admin to read directory data. This is the recommended setup and the rest of this page assumes it.
  • No delegation — the service account calls the Admin SDK directly, and must itself hold a Workspace admin role in the Admin console. See Alternative: no delegation.

Prerequisites

  1. A GCP service account with a JSON key (a dedicated one, or reuse your GCP collector service account).
  2. In that service account's GCP project, enable:
    • Admin SDK API (admin.googleapis.com)
    • Cloud Identity API (cloudidentity.googleapis.com) — needed for inbound-SSO and Cloud Identity device surfaces.
    • Gmail API (gmail.googleapis.com) — needed for the mailbox settings surface.
  3. A real Workspace Super Admin account to impersonate (e.g. admin@example.com) — DWD setup only.

Create the service account if you do not have one

In Google Cloud, select the project that will own the credential. Open IAM & Admin → Service Accounts → Create service account, name it lc-workspace, and create it. Open its Keys → Add key → Create new key → JSON to download the key. No broad Google Cloud project role is needed for directory access; you authorize the Workspace scopes below. If key creation is blocked, ask your Google Cloud administrator to resolve it.

Enable the APIs

  1. In Google Cloud, select the project that owns the service account.
  2. Open APIs & Services → Library. Enable Admin SDK API (admin.googleapis.com), Cloud Identity API (cloudidentity.googleapis.com), and Gmail API (gmail.googleapis.com), one at a time.
  3. Confirm all three under Enabled APIs & services. Leave any that are already enabled on.

See Google's API enablement guide.

Open Cloud Shell in Google Cloud or use an authenticated gcloud CLI. Replace the project ID with project_id from the service-account key:

SA_PROJECT="your-service-account-project-id"
gcloud services enable admin.googleapis.com cloudidentity.googleapis.com \
  gmail.googleapis.com --project="$SA_PROJECT"

OAuth scopes

Only the user and group scopes are required. Each remaining scope unlocks one surface, and leaving it out degrades only that surface.

Capability Required Scope
Directory users (read) https://www.googleapis.com/auth/admin.directory.user.readonly
Groups & membership https://www.googleapis.com/auth/admin.directory.group.readonly
Group members https://www.googleapis.com/auth/admin.directory.group.member.readonly
User security posture https://www.googleapis.com/auth/admin.directory.user.security
Admin roles https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly
Inbound SSO profiles https://www.googleapis.com/auth/cloud-identity.inboundsso.readonly
Mobile devices https://www.googleapis.com/auth/admin.directory.device.mobile.readonly
ChromeOS devices https://www.googleapis.com/auth/admin.directory.device.chromeos.readonly
Cloud Identity devices https://www.googleapis.com/auth/cloud-identity.devices.readonly
Gemini-in-Workspace usage https://www.googleapis.com/auth/admin.reports.audit.readonly
Gmail mailbox settings (forwarding, filters, delegates) https://www.googleapis.com/auth/gmail.settings.basic

Copy-paste block for the DWD scopes field (one comma-separated line):

https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/admin.directory.group.readonly,https://www.googleapis.com/auth/admin.directory.group.member.readonly,https://www.googleapis.com/auth/admin.directory.user.security,https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly,https://www.googleapis.com/auth/cloud-identity.inboundsso.readonly,https://www.googleapis.com/auth/admin.directory.device.mobile.readonly,https://www.googleapis.com/auth/admin.directory.device.chromeos.readonly,https://www.googleapis.com/auth/cloud-identity.devices.readonly,https://www.googleapis.com/auth/admin.reports.audit.readonly,https://www.googleapis.com/auth/gmail.settings.basic

What the Gemini scope collects

admin.reports.audit.readonly reads the Admin SDK Reports audit stream to land Gemini-in-Workspace usage as an application with per-user access edges — only the set of users seen in the trailing window, never any prompt or response content. Skipping it costs only that surface: it degrades to unobserved and previously collected rows are kept, never deleted.

What the Gmail settings scope collects

gmail.settings.basic reads each mailbox's auto-forwarding setting, forwarding filters and delegates, to flag mail forwarded outside your domains and suspicious delegation. It never reads message content. The collector impersonates each mailbox owner to read their settings, so this surface requires domain-wide delegation and is unavailable in the no-delegation setup.

Register domain-wide delegation

In the Google Admin consoleSecurity → Access and data control → API controls → Manage Domain-Wide Delegation → Add new:

  • Client ID = the service account's numeric OAuth client ID (from the service-account JSON key's client_id, or GCP Console → IAM & Admin → Service Accounts → the service account → Unique ID).
  • OAuth scopes = the full comma-separated list above.

Register the entire scope list, exactly

Two Google behaviors bite here:

  • All-or-nothing token mint: when LimaCharlie requests a token, if any requested scope is not authorized for the client, Google rejects the whole request. Registering only admin.directory.user.readonly will still fail because other surfaces request additional scopes.
  • Literal string match: the broader …/admin.directory.user does not satisfy the narrower …/admin.directory.user.readonly. No typos or trailing spaces.
  • DWD changes can take ~10 minutes (occasionally up to 24h) to propagate.

Create the credentials secret

The Workspace provider record has no field for the impersonation admin — it lives inside the secret, which is a wrapper around the service-account key:

{
  "service_account_json": { "type": "service_account", "project_id": "...", "private_key": "...", "client_email": "...", "...": "..." },
  "admin_email": "admin@example.com"
}
  • service_account_json — the entire raw Google service-account key JSON, nested as this key's value.
  • admin_email — the Super Admin to impersonate. Omit it only for the no-delegation setup below.
  • domainoptional, and a filter rather than a label. Most connections should leave it out entirely.

domain narrows what is collected

Setting domain enumerates that one domain instead of the whole customer. On a tenant with secondary or additional domains, every user and group outside it is silently absent from the inventory — the sweep succeeds and the connection looks healthy. Leave domain out unless you deliberately want a single-domain inventory.

To declare which domains count as internal (for external-collaborator detection), use the provider record's internal_domains instead. That is a classification list and does not restrict collection.

Do not flatten the wrapper

admin_email and domain are siblings of service_account_json, not fields inside it and not extra keys alongside type / private_key / client_email at the top level.

A secret whose top level looks like a service-account key is read as the no-delegation form. admin_email is then ignored wherever it sits — no error, no warning — the token is minted with no subject, and every call goes to Google as the service account itself. The domain-wide delegation you registered is never used.

The symptom is not a validation error. Tokens mint normally, and every Google surface answers with an authorization denial instead:

core   HTTP 403: Not Authorized to access this resource/api
groups HTTP 403: Not Authorized to access this resource/api

That reads exactly like a missing scope, which sends people back to re-audit a delegation setup that is already correct.

Check which form your secret is in

Before storing it, look at the top-level keys:

jq 'keys' gw-secret.json
# want:  ["admin_email", "service_account_json"]   (+ "domain" if you set it)
# wrong: [..., "client_email", ..., "private_key", ..., "type", ...]

If type, private_key or client_email appear at the top level, the secret is the no-delegation form no matter what else is in it.

Reusing the same service account as the GCP provider is fine. Reusing the same secret is not — the GCP provider takes the raw key verbatim, so pointing the Workspace record at it lands in exactly this case. Give Workspace its own secret.

In LimaCharlie, open Organization Settings → Secrets Manager → Add, name the secret gw-credentials, and paste the wrapper JSON described above. Place the whole downloaded key object under service_account_json and your Workspace Super Admin's address under admin_email. Save it enabled.

Use a local text editor to assemble the JSON if needed. Keep the key's escaped line breaks unchanged. No terminal command is required to create this wrapper.

Save the wrapper JSON above as gw-secret.json, then run:

jq -Rs '{secret: .}' gw-secret.json \
  | limacharlie secret set --key gw-credentials --enabled \
  && rm -f gw-secret.json

jq -Rs supplies the outer secret-record envelope for stdin.

Email Security uses a different credential shape

Email Security expects the flat service-account key with admin_email at the top level. For delegated Cloud Security collection, use the service_account_json wrapper above instead; do not copy the Email Security credential shape.

Alternative: no delegation

Only if you chose this deliberately

This form is accepted, but it is not the recommended setup and it is the one operators land in by accident — it is what a raw service-account key, pasted verbatim, is read as. If core is failing with HTTP 403: Not Authorized to access this resource/api and you did not set this up on purpose, you are in this form by accident: go back to the wrapper envelope.

If you would rather not register domain-wide delegation, the raw service-account key JSON is accepted as the secret on its own — the file GCP hands you, stored verbatim with no wrapper and no admin_email:

jq -Rs '{secret: .}' sa-key.json \
  | limacharlie secret set --key gw-credentials --enabled \
  && rm -f sa-key.json

In this form the service account calls the Admin SDK as itself instead of impersonating an admin, so it must hold a Workspace admin role granted in the Google Admin console — assign the service account a read-only or custom admin role covering the directory surfaces you want. No DWD registration is needed, and there is no way to set domain: the whole customer is enumerated.

Everything else on this page is unchanged; provider test reports the same checks, and a surface the role does not cover fails exactly as an unregistered scope would.

Create the provider record

provider.yaml:

provider_type: google_workspace
workspace_customer_id: my_customer          # or an explicit customer ID
credentials: hive://secret/gw-credentials
internal_domains: [example.com]

In the web app: Add provider → Google Workspace, then set Customer ID (my_customer), Credentials (gw-credentials), and Refresh interval.

Use my_customer unless you have a reason not to

my_customer is an alias Google resolves to the customer the impersonated admin (or the service account) belongs to, so it is correct for every tenant and there is nothing to look up or mistype. Set an explicit ID only when a reseller-style account has to be pinned to a specific customer.

Customer ID is not your organization's name. It is an opaque ID Google issues, like C01ab2cd3. Find it in the Google Admin consoleAccountAccount settingsProfile, next to Customer ID.

Any other value is rejected — but Google rejects it in a way that reads like a permission problem, not a typo: the Admin SDK answers an unrecognized customer with a bare HTTP 400: Bad Request that never names the field or echoes the value. Every required surface fails at once, so the connection shows Failed and provider test's first check — Authenticate + read directory users — fails, even though the credential and the delegation are fine. If provider test reports a 400 on that check, it re-runs the same read against my_customer and tells you outright when the customer ID is the culprit.

Verify

limacharlie cloudsec provider test --input-file provider.yaml
Check Required Meaning if it fails
core Authentication plus the directory user read. A failure here means the delegation (or, in the no-delegation setup, the service account's admin role) is wrong — nothing else can be probed meaningfully.
groups Groups and membership unavailable — the group edges that complete the GCP IAM picture are missing.
security Per-user security posture (2SV enrolment/enforcement) unavailable.
roles Admin-role assignments unavailable.
sso Inbound-SSO profile posture unavailable.
devices_mobile Mobile device inventory unavailable.
devices_chromeos ChromeOS device inventory unavailable.
devices_ci Cloud Identity device inventory unavailable.
reports Gemini-in-Workspace usage unavailable (no AI-application rows or per-user access edges).
gmail Mailbox forwarding and delegation posture unavailable. Probed by reading the admin_email user's own Gmail settings.

An ungranted optional scope still shows the connection as Failed

The optional surfaces above degrade safely — the surface goes unobserved, its previously collected rows are kept rather than deleted, and provider test still reports OK overall, because only the required checks decide that verdict.

The Last Sync badge on the providers page works differently. It reports partial coverage: any collector left uncovered marks the sync Failed, even when every required surface succeeded. So a connection that is working as intended, minus one optional scope you chose not to grant, sits on a permanent Failed badge.

Hover the badge for the error detail — it names the collectors that were left uncovered and why. If the reason is a scope you deliberately skipped, the connection is healthy and the badge can be ignored; grant the scope to clear it.

Troubleshooting

provider test error Cause Fix
HTTP 400: Bad Request on the user check, with every other check failing too workspace_customer_id is not a customer ID Google recognizes — most often the organization's name rather than the ID. Google returns this same opaque 400 for every customer-aimed call, which reads as a permission problem and sends people back to re-audit delegation and scopes that are already correct Set workspace_customer_id to my_customer, or to the ID from Admin console → Account → Account settings → Profile. provider test names this explicitly when it can confirm it: the same read is retried against my_customer, and if that succeeds the check reports the real customer ID to use
Users or groups from a secondary domain are missing domain is set in the secret, narrowing collection to that one domain Remove domain from the secret; declare internal domains with internal_domains on the record
token mint failed (HTTP 401): scope not granted to the delegated admin DWD missing scopes (all-or-nothing mint), a non-.readonly variant, or not yet propagated Register the full scope list exactly; wait for propagation; confirm the service account's client ID matches
core fails: HTTP 403: Not Authorized to access this resource/api A token was minted (so the scopes are registered) but the caller has no Workspace admin authority. Almost always the secret is a raw service-account key with no admin_email — typically the GCP provider's secret reused verbatim, or the wrapper flattened — so nothing is impersonated and the delegation you configured is never used. It is silently accepted as the no-delegation form. Otherwise, admin_email names a user who is not a Super Admin Give Workspace its own secret in the wrapper envelope with admin_email; leave the GCP provider's secret untouched. Reusing the same service account is fine — reusing the same secret is not. Confirm the form with the jq 'keys' check above
HTTP 403: … API has not been used in project … Admin SDK / Cloud Identity / Gmail API not enabled Enable the named API in the service account's project
reports fails: HTTP 401: Access denied. You are not authorized to read activity records. The token minted (so the DWD registration itself is fine) but the impersonated admin cannot read the Reports audit stream — either admin.reports.audit.readonly is missing from the DWD scope list, or admin_email names an admin without the Reports privilege Add the scope to the delegation and impersonate a Super Admin. Optional surface: leaving it as-is drops only Gemini-in-Workspace usage — but it does leave the connection's Last Sync badge on Failed, per the note above
gmail fails: token mint failed (HTTP 401): scope not granted to the delegated admin — mailbox forwarding and delegation posture unavailable gmail.settings.basic is missing from the DWD scope list. Every other check can pass without it, but each sync then reports Gmail settings permission denied as partial coverage Add https://www.googleapis.com/auth/gmail.settings.basic to the delegation and enable the Gmail API. Optional surface, but leaving it out keeps the Last Sync badge on Failed
gmail fails: Gmail settings need domain-wide delegation (set admin_email) The secret has no admin_email, so nothing can impersonate mailbox owners Use the delegation setup, or accept that mailbox settings stay unobserved

A changed error means you fixed something

These failures mask each other, and the Last Sync badge says only Failed either way — so a real fix can look like no fix at all.

Google validates the customer ID before it checks authorization, so a wrong workspace_customer_id answers 400 on every surface and hides whatever the credential would have said. Correct the customer ID and the same connection starts answering 403 instead. That is not a new problem and not a regression: it is the next one, previously unreachable.

Read the error text, not the badge, and re-diagnose against the table above after each change. provider test is the fastest way to see it — it reports one check per surface rather than a single verdict.

Both misconfigurations can be present at once, so budget for two rounds: an unrecognized customer ID and a flattened secret are independent, and fixing either one alone still leaves the connection Failed.