Configuration With Google CLI
# 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
Step 1: Open Google Cloud Shell


Step 2: Create the Script File

Step 3: Execute the Script

Step 4: Download the JSON Key

Script Summary Table
Domain-Wide Delegation
Last updated
Was this helpful?