> 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 %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.forestall.io/fsprotect/settings/gcp-configurations/configuration-with-google-cli.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
