Configuration With Google CLI

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

# 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.

  2. Click the Activate Cloud Shell icon (>_) in the top-right toolbar.

Activate Cloud Shell
  1. Once the terminal appears, click Open Editor to launch the graphical code editor.

Open Editor Button

Step 2: Create the Script File

Add Onboard File
  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

  5. Save the file (File > Save or Ctrl+S).

Step 3: Execute the Script

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

    Bash

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

    PowerShell

  1. 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

Service Account Key Json File

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.


To complete the security handshake between Google Cloud and Google Workspace, ensure you have completed the Domain-Wide Delegation steps at the link below:

Domain-Wide Delegation

Last updated

Was this helpful?