> For the complete documentation index, see [llms.txt](https://docs.forestall.io/forestall/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.forestall.io/forestall/configuration-center/gcp-configurations/configuration-with-google-cli.md).

# 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](/forestall/configuration-center/gcp-configurations/domain-wide-delegation.md).

```ps1
# 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](https://cloud.google.com/sdk/docs/install) (`gcloud`) installed and in `PATH`. The script runs `gcloud` through `cmd.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

1. In Forestall, open **Configuration Center > Google Cloud Platform > Manage** and click **New Configuration**.
2. Click **Copy PS** at the top of the GCP Configuration form. A modal shows the script.
3. Click **Copy** (or the copy icon in the corner of the code box) and click **Cancel** to close the modal.
4. Paste the script into a new file named `onboard.ps1`. The script above is the same as the one in the modal.

<figure><img src="https://3408039743-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FObpV44hoVkNmo5bFuVVL%2Fuploads%2Fgit-blob-4e769a95eadc0d3f9506129dd3dc903964271813%2Fgcp-copy-ps-modal.png?alt=media" alt="Copy PS modal with the GCP onboarding script"><figcaption><p>Copy PS modal</p></figcaption></figure>

### Step 2: Edit the Configuration Section

Modify the Configuration section at the top of the script with your own values:

```powershell
$PROJECT_ID       = "your-project-id"
$ORGANIZATION_ID  = "123456789012"
$SA_NAME          = "forestall-scanner-sa"
$SA_DISPLAY_NAME  = "Forestall Scanner Service Account"
$KEY_OUTPUT_PATH  = ".\forestall-scanner-sa-key.json"
```

Save the file.

### Step 3: Execute the Script

1. Open Windows PowerShell and go to the folder where you saved the file.
2. Run the script:

   ```powershell
   .\onboard.ps1
   ```

   If the execution policy blocks the script, run it with `powershell -ExecutionPolicy Bypass -File .\onboard.ps1`.
3. A browser window opens for `gcloud auth login`. Sign in with the account that has the permissions listed in the prerequisites.

<figure><img src="https://3408039743-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FObpV44hoVkNmo5bFuVVL%2Fuploads%2Fgit-blob-be22b6f12d6702367b8db74ea1319ee000bc6462%2Fonboard%20on%20terminal.png?alt=media" alt=""><figcaption><p>Run Onboard Script on Terminal</p></figcaption></figure>

4. 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](/forestall/configuration-center/gcp-configurations/domain-wide-delegation.md) page: they match.

The key file is written to the folder where you ran the script. Upload it on the [GCP Configuration](/forestall/configuration-center/gcp-configurations.md) 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.

***

{% hint style="info" %}
To complete the security handshake between Google Cloud and Google Workspace, ensure you have completed the Domain-Wide Delegation steps at the link below:
{% endhint %}

{% content-ref url="/pages/Pq05ANhIMvgqMsH30sI2" %}
[Domain-Wide Delegation](/forestall/configuration-center/gcp-configurations/domain-wide-delegation.md)
{% endcontent-ref %}
