Google Cloud Platform — Integration Setup
GCP had no configuration evidence in CMDash before this integration. The only Google Cloud integration was Chronicle, which is a SIEM — it reads detections and alerts and cannot answer "is this bucket public?". AWS and Azure at least had a paid findings product that could be connected; GCP had nothing. This reads your project's configuration directly, with a read-only service account.
Prerequisites
- A GCP project
- Permission to create a service account and grant project-level roles
- The following APIs enabled on the project: Cloud Resource Manager, IAM, Cloud Storage, and Cloud KMS (only if you want key-rotation checks)
- CMDash Essentials tier or above
Step 1: Create a service account
- IAM & Admin → Service Accounts → Create service account.
- Name it
cmdash-config-reader. - Skip the "Grant this service account access" step for now — Step 2 does it more precisely.
- Done.
Step 2: Grant read-only roles
Grant these three predefined roles at the project level:
| Role | Why |
|---|---|
roles/viewer | Lists buckets and their IAM configuration |
roles/iam.securityReviewer | Reads the project IAM policy (for audit-log configuration) and lists service-account keys |
roles/cloudkms.viewer | Lists KMS key rings and keys. Omit this if you do not want key-rotation checks |
Least privilege: all three are read-only. Do not grant
roles/editororroles/owner. Note thatroles/viewerdoes not grant access to object contents in Cloud Storage — CMDash reads bucket settings, never your data.
Step 3: Create a JSON key
- Select the service account → Keys → Add key → Create new key → JSON.
- Download the file. It contains
project_id,client_email, andprivate_key.
This key is a long-lived credential. CMDash's own GCP check flags user-managed service-account keys as a finding, and that includes this one. That is not a contradiction — it is the honest trade-off of the integration, and it is the reason the credential is scoped as narrowly as Step 2 allows. Store it as you would any production secret, and rotate it on the schedule below.
Step 4: Connect in CMDash
- Navigate to Integrations in your CMDash dashboard.
- Find Google Cloud Platform and click Connect.
- Enter, from the JSON key file:
- Project ID —
project_id. Required. - Client email —
client_email - Private key —
private_key, the entire PEM block including the-----BEGIN PRIVATE KEY-----and-----END PRIVATE KEY-----lines - KMS locations — optional list (e.g.
global,us-central1). See below.
- Project ID —
- Click Save & Test Connection.
Why a per-connection service account: GCP service accounts authenticate by signing a JWT with their own RSA private key and exchanging it for a token. The key belongs to your project, so it lives on the connection rather than in CMDash's server-wide config.
CMDash requests the token with the cloud-platform.read-only scope. Even if the service account is over-granted in IAM, the token CMDash holds cannot mutate anything — the scope ceiling applies on top of the role. That is a stronger promise than "we only send GET requests".
What Syncs
| Check | GCP API | Control it evidences |
|---|---|---|
| Bucket enforces public access prevention | storage.buckets.list | CC6.6, SC-7, A.5.10 |
| Bucket uses uniform bucket-level access | storage.buckets.list | CC6.6, A.5.10 |
| Data Access audit logs enabled for all services | projects.getIamPolicy | AU-2, A.8.15, CC7.2 |
| Service account has no user-managed keys | serviceAccounts.list → keys.list | CC6.1, AC-2, A.5.15 |
| Cloud KMS key has a rotation period | keyRings.list → cryptoKeys.list | SC-12, A.8.24 |
How the numbers are computed
- Every check is tri-state: pass, fail, or unknown. An API that returned 403, or a field GCP did not report, is
unknown— never a failure. Unknown is excluded from both sides of every percentage, so a missing role produces not measured, not 0%. publicAccessPrevention: inheritedis recorded as unknown, not as a failure.inheritedmeans the bucket defers to the organisation policy, which may well enforce prevention — but the buckets API does not say. Calling it a failure would flag buckets that an org policy already protects, which is a fabricated finding.- Absent Data Access audit configuration is a genuine failure, not unknown. GCP disables Data Access logs by default for everything except BigQuery, so the absence is the default state, and the default is the finding. All three of
ADMIN_READ,DATA_READandDATA_WRITEmust be enabled onallServicesto pass. - A user-managed service-account key is a failure. These never expire and are the most common source of leaked GCP access. An empty key list is the passing state.
- KMS rotation is only checked in the locations you list. Key rings are per-location and there is no cross-location list API. If you configure no locations, no KMS check runs at all and
keyRotationCoveragereads as unmeasured. CMDash does not emit a passing record in that case — claiming your keys were inspected and found rotating would be a claim nobody made. - Asymmetric KMS keys are skipped, not failed. Only
ENCRYPT_DECRYPTkeys support automatic rotation; asking a signing key to rotate is a category error, not a finding.
Troubleshooting
- "GCP private key could not be used to sign the assertion": the private key was pasted without its PEM header/footer, or the newlines were mangled. Paste the whole
private_keyvalue from the JSON file. CMDash handles the\nescaping in the JSON automatically. - Token endpoint returns 400
invalid_grant: the service-account key was deleted or disabled in GCP, or the clock on your side is significantly wrong (the signed JWT carries a timestamp). - Connection test fails with 403: the service account exists but has no
roles/vieweron the project, or the Cloud Resource Manager API is not enabled. - Only the audit-log check is unknown:
roles/iam.securityRevieweris missing —roles/vieweralone cannot read the project IAM policy. - Only the KMS checks are missing: either KMS locations is empty (by design — nothing is claimed) or
roles/cloudkms.viewerwas not granted. - Service-account key checks are unknown for every account:
roles/iam.securityRevieweris missing. - Bucket count is lower than the console shows: the 500-bucket ceiling truncated the walk.
Rate Limits & Sync Behavior
| Setting | Value |
|---|---|
| Sync frequency | Every 4 hours by default (syncInterval is configurable per connection) |
| Auth | Self-signed JWT (RS256) exchanged for a 1-hour access token, per sync |
| Request volume | ~3 calls, plus one per service account (key list) and one per KMS key ring |
| Ceilings | 500 buckets, 200 service accounts per sync; beyond either the walk truncates and the sync log carries a warning |
| Rate limit handling | Routed through CMDash's shared token-bucket limiter at a conservative 300 req/min |
| Circuit breaker | Repeated failures open the breaker and the sync is skipped |
| Failed sync retry | Job queue retries 3 times with exponential backoff (starting at 30s) — see docs/developers/dead-letter.md |
Credential Rotation
Service-account JSON keys do not expire. Rotate at least annually.
- In GCP: Service Accounts → cmdash-config-reader → Keys → Add key. Both keys work simultaneously.
- In CMDash: Settings → Integrations → Google Cloud Platform → Edit, enter the new client email and private key, Save.
- Confirm a successful sync, then delete the old key in GCP.
Known Limitations
- One project per connection. Folders and organisations are not walked. A multi-project estate needs one connection per project, and coverage numbers are per connection.
- KMS coverage is limited to the locations you list. See above. This is the limitation most likely to mislead, because an empty list produces no failures — which looks like a pass and is not one. CMDash reports it as unmeasured; read it that way.
- No encryption-at-rest check. GCP encrypts all data at rest by default with no way to turn it off, so a check would be a guaranteed pass that tells you nothing.
encryptionAtRestCoveragereads as unmeasured for GCP rather than as a vacuous 100%. - No password-policy signal. GCP delegates that to Google Workspace or your external IdP; it is not a project-level setting.
- No firewall-rule analysis. Open-ingress auditing is an unbounded walk whose only possible answer is a truncated one.
- Bucket IAM policies are not evaluated. The bucket's public-access-prevention and uniform-access settings are read, not the individual IAM bindings on it.