> For the complete documentation index, see [llms.txt](https://docs.forestall.io/fsprotect/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/fsprotect/settings/gcp-configurations/configuration-with-google-cli.md).

# Configuration With Google CLI

This guide explains how to deploy the service account key using the provided PowerShell script via Google Cloud Shell.

```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"          	# Double backslash for paths
# ------------------------------------------------------------------------------

$REQUIRED_APIS = @(
    "iam.googleapis.com",
    "admin.googleapis.com",
    "cloudidentity.googleapis.com",
    "cloudresourcemanager.googleapis.com",
    "serviceusage.googleapis.com",
    "recommender.googleapis.com",
    "cloudasset.googleapis.com"
)

$ORG_ROLES = @(
    "roles/browser",
    "roles/cloudasset.viewer",
    "roles/resourcemanager.folderViewer",
    "roles/recommender.iamViewer",
    "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 {
        $proc = Start-Process -FilePath "gcloud" `
            -ArgumentList $arguments `
            -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..."

#& 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"

$currentProject = ((& gcloud config get-value project 2>&1) | Where-Object { $_ -notmatch '^\s*(WARNING|ERROR):' -and $_ -notmatch '\[environment:' }) -join ""
$currentProject = $currentProject.Trim()
Write-Success "Active project: $currentProject"

# ==============================================================================
# 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 "Checking/Creating service account: $SA_EMAIL"

# Use a direct gcloud call to check existence
$saExists = gcloud iam service-accounts list --project=$PROJECT_ID --filter="email:$SA_EMAIL" --format="value(email)"

if ($saExists -eq $SA_EMAIL) {
    Write-Warn "Service account '$SA_EMAIL' already exists — skipping creation."
} else {
    Write-Host "Creating $SA_NAME..."
    # Execute creation directly
    gcloud iam service-accounts create $SA_NAME `
        --display-name="$SA_DISPLAY_NAME" `
        --project=$PROJECT_ID

    if ($LASTEXITCODE -ne 0) {
        Write-Err "Failed to create service account. Check your permissions."
    } else {
        Write-Step "Waiting 10s for IAM propagation..."
        Start-Sleep -Seconds 10
        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

    $existingBinding = (& gcloud organizations get-iam-policy $ORGANIZATION_ID `
        --flatten="bindings[].members" `
        --filter="bindings.role=$role AND bindings.members=serviceAccount:$SA_EMAIL" `
        --format="value(bindings.members)" 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"
    }
}

# ==============================================================================
# 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"
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
    Write-Host "============================================================`n" -ForegroundColor Cyan
}

```

### Prerequisites

* A Google Cloud Account with Organization Administrator and Project Owner permissions.
* The numeric Organization ID and the Project ID where the Service Account will reside.

***

### Step 1: Open Google Cloud Shell

1. Log in to the [Google Cloud Console](https://console.cloud.google.com/).
2. Click the Activate Cloud Shell icon (`>_`) in the top-right toolbar.

<figure><img src="/files/fJo2cRCao2YlItvL67DW" alt=""><figcaption><p>Activate Cloud Shell</p></figcaption></figure>

3. Once the terminal appears, click Open Editor to launch the graphical code editor.

<figure><img src="/files/rOZTcR4gJTPJqnuEyD9A" alt=""><figcaption><p>Open Editor Button</p></figcaption></figure>

### Step 2: Create the Script File

<figure><img src="/files/LH5kSVVTn7GHKQ2v9iYf" alt=""><figcaption><p>Add Onboard File</p></figcaption></figure>

1. In the Editor’s left-hand explorer, right-click and select New File.
2. Name the file `onboard.ps1`.
3. Paste your PowerShell script into the editor.
4. Modify the Configuration section at the top of the script with your specific IDs:

   PowerShell

   ```
   $PROJECT_ID       = "your-project-id"
   $ORGANIZATION_ID  = "123456789012"
   $SA_NAME          = "forestall-scanner-sa"
   $KEY_OUTPUT_PATH  = "./forestall-scanner-sa-key.json"
   ```
5. Save the file (`File > Save` or `Ctrl+S`).

### Step 3: Execute the Script

<figure><img src="/files/eO8UnWvtlOuWZ2oVHHbb" alt=""><figcaption><p>New Terminal on Editor</p></figcaption></figure>

1. In the Cloud Shell terminal (at the bottom of the screen), switch from Bash to PowerShell Core:

   Bash

   ```
   pwsh
   ```
2. Navigate to the directory where you saved the file (usually the home directory) and run it:

   PowerShell

   ```
   ./onboard.ps1
   ```
3.

```
<figure><img src="../../.gitbook/assets/onboard on terminal.png" alt=""><figcaption><p>Run Onboard Script on Terminal</p></figcaption></figure>
```

4. Watch the output:

* The script will enable 7 required APIs
* It will create the Service Account.
* It will bind 8 organization-level roles.
* It will generate a JSON key.

### Step 4: Download the JSON Key

<figure><img src="/files/2LynDbllRENQ9I1v84Bx" alt=""><figcaption><p>Service Account Key Json File</p></figcaption></figure>

The script saves the service account key to the Cloud Shell virtual machine. To use it locally:

1. In the Cloud Shell terminal window, click the Three-dot menu (⋮) in the toolbar.
2. Select Download.
3. Type the filename: `forestall-scanner-sa-key.json`.
4. Click Download to save it to your computer.

***

### Script Summary Table

| **Component** | **Details**                                                 |
| ------------- | ----------------------------------------------------------- |
| Language      | PowerShell Core (pwsh)                                      |
| Project Scope | API Activation & SA Creation                                |
| Org Scope     | IAM Policy Bindings (Security Reviewer, Asset Viewer, etc.) |
| Output        | `forestall-scanner-sa-key.json`                             |

> 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](/fsprotect/settings/gcp-configurations/domain-wide-delegation.md)
{% endcontent-ref %}
