Configuration With Google CLI
This guide explains how to prepare the Google Cloud side with the PowerShell script that the Copy PS button on the GCP Configuration page provides. The script runs on Windows with the Google Cloud CLI. It enables the required APIs, creates the service account, grants the organization-level roles, downloads the JSON key and prints the Client ID you need for Domain-Wide Delegation.
# CONFIGURATION — Fill in these variables before running
# ------------------------------------------------------------------------------
$PROJECT_ID = "your-project-id"
$ORGANIZATION_ID = "your-organization-id" # Numeric org ID (e.g. 123456789012)
$SA_NAME = "forestall-scanner-sa" # Service account short name
$SA_DISPLAY_NAME = "Forestall Scanner Service Account"
$KEY_OUTPUT_PATH = ".\forestall-scanner-sa-key.json" # Key file output path
# ------------------------------------------------------------------------------
$REQUIRED_APIS = @(
"iam.googleapis.com",
"admin.googleapis.com",
"cloudidentity.googleapis.com",
"cloudresourcemanager.googleapis.com",
"serviceusage.googleapis.com",
"policyanalyzer.googleapis.com",
"cloudasset.googleapis.com",
"groupssettings.googleapis.com"
)
$ORG_ROLES = @(
"roles/browser",
"roles/cloudasset.viewer",
"roles/resourcemanager.folderViewer",
"roles/policyanalyzer.activityAnalysisViewer",
"roles/iam.organizationRoleViewer",
"roles/resourcemanager.organizationViewer",
"roles/iam.securityReviewer",
"roles/serviceusage.serviceUsageViewer"
)
# ==============================================================================
# TRACKING
# ==============================================================================
$script:errors = [System.Collections.Generic.List[string]]::new()
$script:warnings = [System.Collections.Generic.List[string]]::new()
# ==============================================================================
# HELPERS
# ==============================================================================
function Write-Step([string]$msg) {
Write-Host "`n==> $msg" -ForegroundColor Cyan
}
function Write-Success([string]$msg) {
Write-Host " [OK] $msg" -ForegroundColor Green
}
function Write-Warn([string]$msg) {
Write-Host " [WARN] $msg" -ForegroundColor Yellow
$script:warnings.Add($msg)
}
function Write-Err([string]$msg) {
Write-Host " [ERR] $msg" -ForegroundColor Red
$script:errors.Add($msg)
}
function Invoke-Gcloud([string[]]$arguments) {
$tmpOut = [System.IO.Path]::GetTempFileName()
$tmpErr = [System.IO.Path]::GetTempFileName()
try {
$escapedArgs = $arguments | ForEach-Object {
if ($_ -match '\s') { "`"$_`"" } else { $_ }
}
$cmdLine = "/c gcloud " + ($escapedArgs -join " ")
$proc = Start-Process -FilePath "cmd.exe" `
-ArgumentList $cmdLine `
-RedirectStandardOutput $tmpOut `
-RedirectStandardError $tmpErr `
-NoNewWindow -Wait -PassThru
$exitCode = $proc.ExitCode
$stdout = (Get-Content $tmpOut -Raw -ErrorAction SilentlyContinue) -replace "`r`n", "`n"
$stderr = (Get-Content $tmpErr -Raw -ErrorAction SilentlyContinue) -replace "`r`n", "`n"
} finally {
Remove-Item $tmpOut, $tmpErr -Force -ErrorAction SilentlyContinue
}
$script:lastStderr = if ($stderr) { $stderr.Trim() } else { "" }
$script:lastExitCode = $exitCode
if ($exitCode -ne 0) { return $null }
$stdoutTrimmed = if ($stdout) { $stdout.Trim() } else { "" }
if ($stdoutTrimmed) { return $stdoutTrimmed } else { return "OK" }
}
function Get-FriendlyError([string]$stderr, [string]$context) {
if (-not $stderr) { $stderr = "" }
if ($stderr -match "PERMISSION_DENIED" -or $stderr -match "caller does not have permission") {
return "$context failed: your account ('$activeAccount') does not have permission to perform this action. " +
"Ensure the account has the required IAM role for this step."
}
if ($stderr -match "requires reauthentication" -or $stderr -match "invalid_grant" -or $stderr -match "not authenticated") {
return "$context failed: authentication has expired. Run 'gcloud auth login' manually and rerun the script."
}
if ($stderr -match "project.*not found" -or $stderr -match "does not exist" -and $stderr -match "project") {
return "$context failed: project '$PROJECT_ID' was not found. Verify the project ID and that your account has access to it."
}
if ($stderr -match "organization.*not found" -or $stderr -match "does not exist" -and $stderr -match "organ") {
return "$context failed: organization '$ORGANIZATION_ID' was not found. Verify the numeric organization ID."
}
if ($stderr -match "RESOURCE_EXHAUSTED" -or $stderr -match "quota") {
return "$context failed: quota exceeded. For service account keys, the default limit is 10 keys per service account. " +
"Delete unused keys at: https://console.cloud.google.com/iam-admin/serviceaccounts"
}
if ($stderr -match "billing" -or $stderr -match "BILLING") {
return "$context failed: billing is not enabled on project '$PROJECT_ID'. " +
"Enable it at: https://console.cloud.google.com/billing"
}
if ($stderr -match "API.*not been used" -or $stderr -match "is disabled") {
return "$context failed: a required API dependency is not yet active. This may resolve automatically on rerun once all APIs finish propagating."
}
if ($stderr -match "already exists") {
return "ALREADY_EXISTS"
}
if ($stderr -match "FAILED_PRECONDITION") {
return "$context failed: a precondition was not met. A recently enabled API may still be propagating — wait 30 seconds and rerun."
}
$detail = if ($stderr.Length -gt 400) { $stderr.Substring(0, 400) + "..." } else { $stderr.Trim() }
return "$context failed with an unexpected error:`n $detail"
}
# ==============================================================================
# PRE-FLIGHT
# ==============================================================================
Write-Step "Checking gcloud installation..."
if (-not (Get-Command gcloud -ErrorAction SilentlyContinue)) {
Write-Host "`n[FATAL] gcloud CLI is not installed or not in PATH." -ForegroundColor Red
Write-Host " Install it from: https://cloud.google.com/sdk/docs/install" -ForegroundColor Red
exit 1
}
$gcloudVersion = Invoke-Gcloud "version", "--format=value(Google Cloud SDK)"
if (-not $gcloudVersion) { $gcloudVersion = "unknown" }
Write-Success "gcloud found (SDK $gcloudVersion)"
# ==============================================================================
# STEP 1 — Authenticate
# ==============================================================================
Write-Step "Authenticating with Google Cloud..."
Write-Host " A browser window will open for sign-in." -ForegroundColor Gray
& gcloud auth login 2>&1 | Out-Null
$activeAccount = ((& gcloud config get-value account 2>&1) | Where-Object { $_ -notmatch '^\s*(WARNING|ERROR):' }) -join ""
$activeAccount = $activeAccount.Trim()
if (-not $activeAccount -or $activeAccount -eq "(unset)") {
Write-Host "`n[FATAL] Authentication failed or was cancelled by the user." -ForegroundColor Red
Write-Host " Rerun the script and complete the browser sign-in." -ForegroundColor Red
exit 1
}
Write-Success "Authenticated as: $activeAccount"
# ==============================================================================
# STEP 2 — Set active project
# ==============================================================================
Write-Step "Setting active project to: $PROJECT_ID"
& gcloud config set project $PROJECT_ID --quiet 2>&1 | Out-Null
$currentProject = ((& gcloud config get-value project 2>&1) | Where-Object { $_ -notmatch '^\s*(WARNING|ERROR):' -and $_ -notmatch '\[environment:' }) -join ""
$currentProject = $currentProject.Trim()
if ($currentProject -ne $PROJECT_ID) {
Write-Host "`n[FATAL] Failed to set project to '$PROJECT_ID'. Verify the project ID and that your account has access to it." -ForegroundColor Red
exit 1
}
Write-Success "Active project: $PROJECT_ID"
# ==============================================================================
# STEP 3 — Enable required APIs
# ==============================================================================
Write-Step "Enabling required APIs on project '$PROJECT_ID'..."
foreach ($api in $REQUIRED_APIS) {
Write-Host " $api ..." -NoNewline
$status = (& gcloud services list --enabled --filter="name:$api" --format="value(name)" --project=$PROJECT_ID 2>&1) -join ""
if ($status -match $api) {
Write-Host " already enabled." -ForegroundColor Yellow
continue
}
$result = Invoke-Gcloud "services", "enable", $api, "--project=$PROJECT_ID"
if ($null -eq $result) {
$friendly = Get-FriendlyError $script:lastStderr "Enable API '$api'"
Write-Host " FAILED." -ForegroundColor Red
Write-Err $friendly
} else {
Write-Host " enabled." -ForegroundColor Green
}
}
# ==============================================================================
# STEP 4 — Create service account
# ==============================================================================
$SA_EMAIL = "$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com"
Write-Step "Creating service account: $SA_EMAIL"
& gcloud iam service-accounts describe $SA_EMAIL --project=$PROJECT_ID 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Warn "Service account '$SA_EMAIL' already exists — skipping creation."
} else {
$result = Invoke-Gcloud "iam", "service-accounts", "create", $SA_NAME,
"--display-name=$SA_DISPLAY_NAME",
"--project=$PROJECT_ID"
if ($null -eq $result) {
$friendly = Get-FriendlyError $script:lastStderr "Create service account"
if ($friendly -eq "ALREADY_EXISTS") {
Write-Warn "Service account '$SA_EMAIL' already exists — skipping creation."
} else {
Write-Err $friendly
}
} else {
Write-Step "Waiting for service account creation 10 seconds..."
sleep 10
Write-Step "10 seconds have passed. Resuming..."
Write-Success "Service account created: $SA_EMAIL"
}
}
# ==============================================================================
# STEP 5 — Grant org-level roles
# ==============================================================================
Write-Step "Granting organization-scope roles to $SA_EMAIL (Org: $ORGANIZATION_ID)..."
foreach ($role in $ORG_ROLES) {
Write-Host " $role ..." -NoNewline
$gcloudArgs = @(
"organizations", "get-iam-policy", $ORGANIZATION_ID,
"--flatten=bindings[].members",
"--filter=bindings.role=$role AND bindings.members=serviceAccount:$SA_EMAIL",
"--format=value(bindings.members)"
)
$existingBinding = (& gcloud @gcloudArgs 2>&1) -join ""
if ($existingBinding -match [regex]::Escape($SA_EMAIL)) {
Write-Host " already bound." -ForegroundColor Yellow
continue
}
$result = Invoke-Gcloud "organizations", "add-iam-policy-binding", $ORGANIZATION_ID,
"--member=serviceAccount:$SA_EMAIL",
"--role=$role",
"--condition=None"
if ($null -eq $result) {
$friendly = Get-FriendlyError $script:lastStderr "Grant role '$role' at org scope"
Write-Host " FAILED." -ForegroundColor Red
Write-Err $friendly
} else {
Write-Host " granted." -ForegroundColor Green
}
}
# ==============================================================================
# STEP 6 — Create and download service account key
# ==============================================================================
Write-Step "Creating and downloading service account key..."
$skipKeyCreation = $false
if (Test-Path $KEY_OUTPUT_PATH) {
Write-Warn "A key file already exists at '$KEY_OUTPUT_PATH'."
Write-Host " Overwrite? This creates a NEW key on GCP (old file is replaced). (y/n): " -NoNewline -ForegroundColor Yellow
$overwrite = Read-Host
if ($overwrite -notmatch '^[Yy]$') {
Write-Host " Skipping key creation — existing file retained." -ForegroundColor Yellow
$skipKeyCreation = $true
}
}
if (-not $skipKeyCreation) {
$result = Invoke-Gcloud "iam", "service-accounts", "keys", "create", $KEY_OUTPUT_PATH,
"--iam-account=$SA_EMAIL",
"--project=$PROJECT_ID"
if ($null -eq $result) {
$friendly = Get-FriendlyError $script:lastStderr "Create service account key"
Write-Err $friendly
} else {
Write-Success "Key saved to: $KEY_OUTPUT_PATH"
}
}
# ==============================================================================
# STEP 7 — Get OAuth2 Client ID for Domain-Wide Delegation
# ==============================================================================
Write-Step "Retrieving OAuth2 Client ID for Domain-Wide Delegation..."
$CLIENT_ID = Invoke-Gcloud "iam", "service-accounts", "describe", $SA_EMAIL, "--project=$PROJECT_ID", "--format=value(oauth2ClientId)"
if ($null -eq $CLIENT_ID -or $CLIENT_ID -eq "") {
Write-Warn "Could not retrieve OAuth2 Client ID. You can find it in the GCP Console under IAM & Admin > Service Accounts."
} else {
Write-Success "Client ID: $CLIENT_ID"
}
# ==============================================================================
# FINAL SUMMARY
# ==============================================================================
Write-Host "`n============================================================" -ForegroundColor Cyan
Write-Host " GCP Onboarding — Summary" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " Authenticated As : $activeAccount"
Write-Host " Project : $PROJECT_ID"
Write-Host " Organization : $ORGANIZATION_ID"
Write-Host " Service Account : $SA_EMAIL"
if ($CLIENT_ID) {
Write-Host " Client ID (DWD) : $CLIENT_ID"
}
Write-Host " Key File : $KEY_OUTPUT_PATH"
Write-Host " APIs Targeted : $($REQUIRED_APIS.Count)"
Write-Host " Roles Targeted : $($ORG_ROLES.Count)"
if ($script:warnings.Count -gt 0) {
Write-Host "`n Warnings ($($script:warnings.Count)):" -ForegroundColor Yellow
foreach ($w in $script:warnings) {
Write-Host " - $w" -ForegroundColor Yellow
}
}
if ($script:errors.Count -gt 0) {
Write-Host "`n Errors ($($script:errors.Count)) — the following steps did not complete:" -ForegroundColor Red
foreach ($e in $script:errors) {
Write-Host " - $e" -ForegroundColor Red
}
Write-Host "`n Fix the errors above and rerun. Steps that already succeeded will be skipped." -ForegroundColor Red
Write-Host "============================================================`n" -ForegroundColor Cyan
exit 1
} else {
Write-Host "`n All steps completed successfully." -ForegroundColor Green
Write-Host "`n SECURITY REMINDER: Keep '$KEY_OUTPUT_PATH' secure. Do not commit it to source control." -ForegroundColor Yellow
if ($CLIENT_ID) {
Write-Host "`n [!] ACTION REQUIRED: Configure Domain-Wide Delegation" -ForegroundColor Cyan
Write-Host " 1. Go to Google Workspace Admin Console > Security > API Controls > Domain-wide Delegation"
Write-Host " 2. Click 'Add new'"
Write-Host " 3. Client ID: " -NoNewline; Write-Host $CLIENT_ID -ForegroundColor Yellow
Write-Host " 4. OAuth scopes (comma-separated):"
Write-Host " 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.customer.readonly,https://www.googleapis.com/auth/cloud-identity.devices.readonly,https://www.googleapis.com/auth/admin.directory.orgunit.readonly,https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly,https://www.googleapis.com/auth/admin.directory.user.security,https://www.googleapis.com/auth/apps.groups.settings" -ForegroundColor Yellow
}
Write-Host "============================================================`n" -ForegroundColor Cyan
}Prerequisites
A Windows machine with Windows PowerShell and the Google Cloud CLI (
gcloud) installed and inPATH. The script runsgcloudthroughcmd.exe, so it must run on Windows.A Google account with Organization Administrator and Project Owner permissions.
The numeric Organization ID and the Project ID where the Service Account will reside. Billing must be enabled on the project.
A Google Workspace / Cloud Identity Super Admin, for the Domain-Wide Delegation step afterwards.
Step 1: Copy the Script
In Forestall, open Configuration Center > Google Cloud Platform > Manage and click New Configuration.
Click Copy PS at the top of the GCP Configuration form. A modal shows the script.
Click Copy (or the copy icon in the corner of the code box) and click Cancel to close the modal.
Paste the script into a new file named
onboard.ps1. The script above is the same as the one in the modal.

Step 2: Edit the Configuration Section
Modify the Configuration section at the top of the script with your own values:
Save the file.
Step 3: Execute the Script
Open Windows PowerShell and go to the folder where you saved the file.
Run the script:
If the execution policy blocks the script, run it with
powershell -ExecutionPolicy Bypass -File .\onboard.ps1.A browser window opens for
gcloud auth login. Sign in with the account that has the permissions listed in the prerequisites.

Watch the output. The script:
enables the 8 required APIs on the project,
creates the service account (skipped if it already exists),
grants 8 organization-level roles,
creates a JSON key and saves it to
$KEY_OUTPUT_PATH(if the file already exists, it asks before creating a new key),retrieves the service account's OAuth2 Client ID for Domain-Wide Delegation.
Steps that already succeeded are skipped when you rerun the script.
Step 4: Read the Summary
At the end the script prints a summary: authenticated account, project, organization, service account, Client ID (DWD), key file path, and the number of APIs and roles targeted. Any warnings and errors are listed below it.
If all steps succeeded, the script prints ACTION REQUIRED: Configure Domain-Wide Delegation with the Client ID and a comma-separated list of OAuth scopes. Use the Client ID and the scope list from this output, or from the Domain-Wide Delegation page: they match.
The key file is written to the folder where you ran the script. Upload it on the GCP Configuration page.
Script Summary Table
Component
Details
Language
Windows PowerShell, with the Google Cloud CLI (gcloud)
Project Scope
Enables 8 APIs and creates the service account
Org Scope
8 IAM role bindings (Browser, Cloud Asset Viewer, Security Reviewer, etc.)
Output
forestall-scanner-sa-key.json and the OAuth2 Client ID for Domain-Wide Delegation
Security Warning: The generated JSON key file provides high-level access to your organization. Never commit this file to public repositories or share it over insecure channels.
Domain-Wide Delegation
Last updated
Was this helpful?