Service Accounts and IAM

Why IAM matters for data engineering

Most platform failures that look like “auth problems” are actually one of four different issues:

  • The wrong principal is being used.
  • The right principal has the wrong role or scope.
  • A conditional binding no longer evaluates to true.
  • A higher-order control such as deny, VPC Service Controls, or secret-level IAM blocks the request.

Data engineers hit this constantly: Cloud Run jobs that can write BigQuery but not read a bucket, GitHub Actions that can authenticate through WIF but cannot impersonate the deployment service account, or local scripts that work with user ADC but fail under the production service account. IAM is the decision layer that separates those cases.

Conceptual Model

The control path below is the minimum model to keep in your head when debugging access:


flowchart TD
    A["Principal<br/>user or service account"] --> B["Allow binding<br/>role on project or resource"]
    B --> C["Permission set<br/>from role"]
    C --> D["Request to resource"]
    D --> E{"Condition true?"}
    E -->|yes| F{"Deny policy?"}
    E -->|no| G["Access not granted"]
    F -->|no deny| H["Access granted"]
    F -->|deny matches| I["Access denied"]
    A --> J["Impersonation path"]
    J --> K["Short-lived token"]
    K --> D

Service Accounts and Keys

Service accounts are the machine identities that your pipelines actually run as. The first questions to answer are: which service accounts already exist, which ones are high privilege, and whether any of them still rely on long-lived user-managed keys.

PowerShell / Linux | gcloud iam service-accounts | inspect service accounts and keys

This subsection validates the current service-account estate and shows the real difference between system-managed keys and user-managed keys.

List the current project service accounts

At the start of any IAM review, incident-response triage, or least-privilege cleanup. It is typically triggered by you need to know which machine identities already exist in the project. Read-only command against the IAM API. Requires permission to list service accounts in the project. Establish the current machine-identity inventory before changing any bindings.

List the live service accounts in bq-wh-nb with display names and disabled state.

gcloud iam service-accounts list \
  --project=bq-wh-nb \
  --format="table(displayName,email,disabled)"
DISPLAY NAME                            EMAIL                                                   DISABLED
Pipeline State Writer                   pipeline-state-writer@bq-wh-nb.iam.gserviceaccount.com  False
GitHub Actions (git-lab)                github-actions-sa@bq-wh-nb.iam.gserviceaccount.com      False
Codex Security Lab                      codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com   False
BQ WH SA                                bq-wh-sa@bq-wh-nb.iam.gserviceaccount.com               False
Compute Engine default service account  348557092514-compute@developer.gserviceaccount.com      False

The important operational point is that bq-wh-nb already has dedicated identities for GitHub Actions and pipeline state writes. That is a healthier pattern than reusing the default Compute Engine service account everywhere.

Current product note: default service-account guardrails

Current Google Cloud guidance is stricter about default service accounts than many older runbooks assume. Stronger organization defaults now push teams away from privileged basic-role grants on default service accounts, so the safer design is still one dedicated service account per workload boundary.

Describe the primary high-privilege service account

Before auditing roles, keys, or impersonation rights on a production service account. It is typically triggered by A workload identity appears central to the project or carries broad permissions. Read-only metadata lookup on a service account resource. Capture the stable resource name, unique ID, and client ID for the principal you are about to audit.

Describe bq-wh-sa, the broadest data-platform service account in this project.

gcloud iam service-accounts describe \
  bq-wh-sa@bq-wh-nb.iam.gserviceaccount.com \
  --project=bq-wh-nb \
  --format=json
{
  "displayName": "BQ WH SA",
  "email": "bq-wh-sa@bq-wh-nb.iam.gserviceaccount.com",
  "etag": "MDEwMjE5MjA=",
  "name": "projects/bq-wh-nb/serviceAccounts/bq-wh-sa@bq-wh-nb.iam.gserviceaccount.com",
  "oauth2ClientId": "108539674431524446365",
  "projectId": "bq-wh-nb",
  "uniqueId": "108539674431524446365"
}

This confirms that bq-wh-sa is a user-managed service account owned by project bq-wh-nb, not a Google-managed service agent.

Inspect key risk on a production service account

During any least-privilege review, credential leak investigation, or migration away from JSON key files. It is typically triggered by you need to know whether a service account still has long-lived downloadable credentials. Read-only key inventory lookup on the service account. Separate short-lived Google-managed signing keys from user-managed keys that can be copied and leaked.

User-managed keys are long-lived bearer credentials

A user-managed service-account key remains valid until it is explicitly deleted. If it lands in source control, an artifact store, or a chat paste, the attacker holds the same effective identity as the service account.

Prefer impersonation, WIF, or metadata-backed tokens

Use --impersonate-service-account for operator testing, Workload Identity Federation for external CI/CD, and the metadata server for Cloud Run or GCE. Keep JSON keys as a migration exception, not the steady-state design.

Current product note: key creation guardrails

Current IAM guidance recommends enforcing organization policies that block new user-managed key creation and key upload wherever possible. The operational takeaway is simple: JSON keys should now be treated as an explicit exception path, not as normal machine-auth plumbing.

List the current key inventory on bq-wh-sa.

gcloud iam service-accounts keys list \
  --iam-account=bq-wh-sa@bq-wh-nb.iam.gserviceaccount.com \
  --project=bq-wh-nb \
  --format="table(keyType,keyOrigin,validAfterTime,validBeforeTime)"
KEY_TYPE        KEY_ORIGIN       CREATED_AT            EXPIRES_AT
SYSTEM_MANAGED  GOOGLE_PROVIDED  2026-04-04T07:33:18Z  2026-04-21T07:33:18Z
SYSTEM_MANAGED  GOOGLE_PROVIDED  2026-04-13T07:33:18Z  2026-04-29T07:33:18Z
USER_MANAGED    GOOGLE_PROVIDED  2026-03-22T16:27:38Z  9999-12-31T23:59:59Z
USER_MANAGED    GOOGLE_PROVIDED  2026-04-05T07:34:04Z  9999-12-31T23:59:59Z

This is a real risk signal. The two SYSTEM_MANAGED rows are normal Google-managed rotation artifacts. The two USER_MANAGED rows are the credentials that should be migrated away from.

Create, disable, and re-enable a disposable service account

During controlled IAM testing, onboarding of a new workload, or break-glass rehearsal. It is typically triggered by you need a new machine identity with no inherited assumptions and no existing key history. State-changing IAM commands on the project. Requires service-account create, disable, and enable permissions. Validate the service-account lifecycle without touching production identities.

Create the disposable service account used for the rest of the note, then disable and re-enable it.

gcloud iam service-accounts create codex-sec-lab-260413 \
  --project=bq-wh-nb \
  --display-name="Codex Security Lab" \
  --description="Disposable security walkthrough principal for April 13 2026"
Created service account [codex-sec-lab-260413].
gcloud iam service-accounts disable \
  codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com \
  --project=bq-wh-nb
Disabled service account [codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com].
gcloud iam service-accounts enable \
  codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com \
  --project=bq-wh-nb
Enabled service account [codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com].

Disabling is the safer containment action during an incident because it is reversible and immediate. Deletion is a cleanup or retirement step, not the first response to suspicious activity.

FlagSyntaxDescription
--project--project=bq-wh-nbProject that owns the service account resource.
--display-name--display-name="Codex Security Lab"Human-readable label shown in Console and list output.
--description--description="..."Free-text operational purpose for the service account.
--format--format=jsonRestricts output to a machine-readable shape for auditing or scripting.
--iam-account--iam-account=bq-wh-sa@bq-wh-nb.iam.gserviceaccount.comSelects the service account whose keys you want to inspect.

Project Bindings, Custom Roles, and Conditional Access

Project-level grants are where most over-privilege starts. This section keeps the scope intentionally narrow: one custom role, one conditional predefined role, and live policy-analysis outputs that show how those bindings behave.

PowerShell / Linux | gcloud iam roles and projects | create and validate least-privilege bindings

This subsection creates a minimal project custom role, validates a time-based IAM condition, grants the lab principal a temporary browser role, and then proves the condition changes the result over time.

Create a project-scoped custom role

When no predefined role matches the exact machine permissions you want. It is typically triggered by A service account needs less than a predefined role but more than one isolated permission. State-changing IAM role administration on the project. Replace broad predefined roles with a tiny permission set that can be reasoned about.

Create a custom role that can only read Secret Manager metadata, not secret payloads.

gcloud iam roles create codexSecretMetaViewer \
  --project=bq-wh-nb \
  --title="Codex Secret Metadata Viewer" \
  --description="Temporary custom role for secret metadata walkthroughs" \
  --permissions="secretmanager.secrets.get,secretmanager.secrets.list" \
  --stage=GA
description: Temporary custom role for secret metadata walkthroughs
etag: BwZPV9LzrWk=
includedPermissions:
- secretmanager.secrets.get
- secretmanager.secrets.list
name: projects/bq-wh-nb/roles/codexSecretMetaViewer
stage: GA
title: Codex Secret Metadata Viewer
Created role [codexSecretMetaViewer].

This role is intentionally weak: it can inventory secret containers but cannot read secret values.

Lint a temporary conditional binding before adding it

Before applying any IAM condition that could unexpectedly lock out a workload. It is typically triggered by you are about to add time-based or context-aware access. Read-only validation call against the IAM condition linter. Catch malformed CEL or unsupported references before changing the project policy.

Lint the CEL expression used for the temporary browser binding.

gcloud alpha iam policies lint-condition \
  --resource-name='//cloudresourcemanager.googleapis.com/projects/bq-wh-nb' \
  --expression='request.time < timestamp("2026-12-31T23:59:59Z")' \
  --title='Expiry2026' \
  --description='Temporary browser access for lab principal' \
  --format=json
{}

The empty JSON object means the linter found no issues with this expression in the project context.

Add project bindings to the lab principal

After the principal exists and the access requirement has been reduced to a minimal role set. It is typically triggered by A new workload needs live access to one project surface. State-changing project IAM policy update. Grant the lab principal one small custom role and one temporary predefined role.

Grant the custom role unconditionally and roles/browser under a time-bound condition.

gcloud projects add-iam-policy-binding bq-wh-nb \
  --member='serviceAccount:codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com' \
  --role='projects/bq-wh-nb/roles/codexSecretMetaViewer' \
  --condition=None
Updated IAM policy for project [bq-wh-nb].
gcloud projects add-iam-policy-binding bq-wh-nb \
  --member='serviceAccount:codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com' \
  --role='roles/browser' \
  --condition='expression=request.time < timestamp("2026-12-31T23:59:59Z"),title=Expiry2026,description=Temporary browser access for lab principal'
WARNING: Adding binding with condition to a policy without condition will change the behavior of add-iam-policy-binding and remove-iam-policy-binding commands.
Updated IAM policy for project [bq-wh-nb].

The custom role handles metadata inventory. The browser role is the temporary project-level read grant that expires on December 31, 2026.

Inspect the resulting project bindings for the lab principal

Immediately after any IAM policy change. It is typically triggered by you need to confirm the project policy now contains exactly the intended membership and condition. Read-only IAM policy inspection with filtering. Verify that the binding landed with the expected role and condition.

Filter the project policy down to only the bindings that mention the lab principal.

gcloud projects get-iam-policy bq-wh-nb \
  --flatten='bindings[].members' \
  --filter='bindings.members:codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com' \
  --format='table(bindings.role,bindings.condition.title,bindings.condition.expression)'
ROLE                                           TITLE       EXPRESSION
projects/bq-wh-nb/roles/codexSecretMetaViewer
roles/browser                                  Expiry2026  request.time < timestamp("2026-12-31T23:59:59Z")

This is the exact shape you want after a change: one row for the unconditional custom role and one row for the temporary browser grant.

Analyze the effective allow path with Cloud Asset

After a grant exists but before you rely on it in production. It is typically triggered by you want to know which binding is responsible for a permission. Read-only analysis against Cloud Asset Inventory. Prove which IAM binding contributes resourcemanager.projects.get for the lab principal.

Ask Cloud Asset which binding explains project-read access for the lab principal.

gcloud asset analyze-iam-policy \
  --project=bq-wh-nb \
  --identity='serviceAccount:codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com' \
  --permissions='resourcemanager.projects.get' \
  --format='table(policy.binding.role,policy.binding.condition.title,ACLs.accesses.permission,ACLs.conditionEvaluationValue)'
ROLE           TITLE       PERMISSION                          CONDITION_EVALUATION_VALUE
roles/browser  Expiry2026  [['resourcemanager.projects.get']]  ['CONDITIONAL']
Your analysis request is fully explored. The ACLs matching your requests are listed per IAM policy binding, so there could be duplications.

Cloud Asset correctly points to the conditional browser binding. That is the binding that currently makes project metadata readable.

Troubleshoot the permission before and after the expiry time

Before cutover, before an expiration window ends, or whenever a conditional binding is suspected. It is typically triggered by A workload has a conditional grant and you need to know whether the condition evaluates to true right now. Read-only call to Policy Troubleshooter. Show that the same binding grants access on April 13, 2026 and stops granting access after January 1, 2027.

Troubleshoot the project-read permission while the condition is still true.

gcloud policy-intelligence troubleshoot-policy iam \
  //cloudresourcemanager.googleapis.com/projects/bq-wh-nb \
  --principal-email=codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com \
  --permission=resourcemanager.projects.get \
  --request-time='2026-04-13T12:00:00Z' \
  --format='yaml(overallAccessState,allowPolicyExplanation.allowAccessState,denyPolicyExplanation.denyAccessState)'
allowPolicyExplanation:
  allowAccessState: ALLOW_ACCESS_STATE_GRANTED
denyPolicyExplanation:
  denyAccessState: DENY_ACCESS_STATE_NOT_DENIED
overallAccessState: CAN_ACCESS

Troubleshoot the same permission after the condition has expired.

gcloud policy-intelligence troubleshoot-policy iam \
  //cloudresourcemanager.googleapis.com/projects/bq-wh-nb \
  --principal-email=codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com \
  --permission=resourcemanager.projects.get \
  --request-time='2027-01-01T00:00:00Z' \
  --format='yaml(overallAccessState,allowPolicyExplanation.allowAccessState,denyPolicyExplanation.denyAccessState)'
allowPolicyExplanation:
  allowAccessState: ALLOW_ACCESS_STATE_NOT_GRANTED
denyPolicyExplanation:
  denyAccessState: DENY_ACCESS_STATE_NOT_DENIED
overallAccessState: CANNOT_ACCESS

This is the cleanest possible conditional-access proof: same principal, same permission, same resource, different request time, different answer.

FlagSyntaxDescription
--permissions--permissions='secretmanager.secrets.list'Permission or permissions to analyze in Cloud Asset.
--identity--identity='serviceAccount:...'Principal whose effective access you want to analyze.
--member--member='serviceAccount:...'Principal receiving a project binding.
--role--role='roles/browser'Predefined or custom role to grant.
--condition--condition='expression=...,title=...,description=...'Adds a CEL-based conditional binding.
--condition=None--condition=NoneExplicitly states that the binding is unconditional when the project policy already contains conditional bindings.
--request-time--request-time='2027-01-01T00:00:00Z'Forces Troubleshooter to evaluate the binding at a specific timestamp.
--resource-name--resource-name='//cloudresourcemanager.googleapis.com/projects/bq-wh-nb'Resource context for linting a condition.
--format--format='yaml(...)'Restricts output to only the decision fields you need.

PowerShell / Linux | gcloud policy-intelligence simulate | verify the current simulation boundary

The prompt for this chapter called out Policy Simulator explicitly, so it matters to be precise about what the local SDK can simulate here. In this environment the live simulate command group is for Organization Policy simulation, not for project-level IAM allow-policy dry runs.

Inspect the current Policy Intelligence simulate surface

Before assuming the CLI can preview the exact IAM change you are about to make. It is typically triggered by you want to know whether simulation is available for the policy family you care about. CLI help inspection. Distinguish the live simulation surface from the IAM-validation tools that are actually usable in this project.

gcloud policy-intelligence simulate --help
NAME
    gcloud policy-intelligence simulate - simulate changes to organization
        policies
 
DESCRIPTION
    Simulate changes to organization policies.
 
COMMANDS
    COMMAND is one of the following:
 
     orgpolicy
        Understand how changes to organization policies could affect your
        resources.

The practical meaning is:

  • use condition linting before writing a conditional binding
  • use Cloud Asset analysis to see which binding grants a permission
  • use Policy Troubleshooter to ask whether access is granted right now
  • use the simulate family only where Organization Policy simulation is the relevant control surface

Impersonation and Secret-Scope IAM

Keyless operator testing is the practical bridge between IAM policy review and workload validation. If impersonation works and the workload can only reach the intended resource, the design is usually in good shape.

PowerShell / Linux | gcloud auth and secrets | validate access through impersonation

This subsection grants the operator short-lived impersonation rights on the lab principal, then proves that the principal can list secret metadata project-wide and read the payload of one secret only because of a secret-scope accessor binding.

Grant token-creator on the lab principal to the operator

Before testing a workload identity from your own authenticated session. It is typically triggered by you need a short-lived token for a service account but do not want to download a key file. State-changing IAM policy update on the service account resource itself. Authorize the human operator to mint access tokens for the lab principal.

Grant roles/iam.serviceAccountTokenCreator on the lab service account to the current user.

gcloud iam service-accounts add-iam-policy-binding \
  codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com \
  --project=bq-wh-nb \
  --member='user:alexper.recovery@gmail.com' \
  --role='roles/iam.serviceAccountTokenCreator'
Updated IAM policy for serviceAccount [codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com].

Read the service-account IAM policy back and confirm the token-creator grant.

gcloud iam service-accounts get-iam-policy \
  codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com \
  --project=bq-wh-nb \
  --format=json
{
  "bindings": [
    {
      "members": [
        "user:alexper.recovery@gmail.com"
      ],
      "role": "roles/iam.serviceAccountTokenCreator"
    }
  ],
  "etag": "BwZPV9VD1Yc=",
  "version": 1
}

This binding is what makes impersonation possible. Without it, gcloud auth print-access-token --impersonate-service-account=... fails immediately.

Mint a short-lived token through impersonation

During safe identity testing, CLI-based debugging, or REST API reproduction. It is typically triggered by you need to prove that you can authenticate as the service account without exporting a key. Read-only token-minting call through IAM Credentials. Demonstrate the keyless authentication path for the lab principal.

Print an impersonated OAuth token for the lab principal.

gcloud auth print-access-token \
  --impersonate-service-account=codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com
ya29.c.c0AZ4bNpYV_7mbGBvxXimByD4p5WPRPX5qRNHpE_...[redacted]...2e6v-8g6dXXoip
WARNING: This command is using service account impersonation. All API calls will be executed as [codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com].

The warning is important: the token belongs to the service account, but the audit trail still points back to the impersonating user.

Prove project-scope metadata access and secret-scope payload access

After the service account has both project-scope and resource-scope grants. It is typically triggered by you need to prove that the principal can see what it should see and nothing more. The first command relies on the custom project role plus the temporary browser role. The second relies on roles/secretmanager.secretAccessor at secret scope. Validate least privilege with a real secret list and a real secret read.

List secrets as the lab principal.

gcloud secrets list \
  --project=bq-wh-nb \
  --limit=5 \
  --format='table(name)' \
  --impersonate-service-account=codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com
NAME
codex-api-token-260413
WARNING: This command is using service account impersonation. All API calls will be executed as [codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com].

Read the current version of the secret payload as the lab principal.

gcloud secrets versions access current \
  --secret=codex-api-token-260413 \
  --project=bq-wh-nb \
  --impersonate-service-account=codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com
ghp_codex_v2_20260413
WARNING: This command is using service account impersonation. All API calls will be executed as [codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.com].

This is the intended split:

  • The project custom role lets the principal enumerate secret containers.
  • The secret-level accessor role lets the principal read exactly one secret’s payload.
FlagSyntaxDescription
--impersonate-service-account--impersonate-service-account=codex-sec-lab-260413@bq-wh-nb.iam.gserviceaccount.comRuns the command with a short-lived token for the target service account.
--limit--limit=5Restricts list output while validating access.
--secret--secret=codex-api-token-260413Chooses the secret whose version you want to access.
--project--project=bq-wh-nbProject context for Secret Manager and IAM calls.
--format--format='table(name)'Keeps the output focused on the access proof rather than full metadata.

Modern Guardrails: Deny Policies and Principal Access Boundary Policies

Deny policies and PAB policies are now core parts of the Google Cloud authorization model, but they sit above ordinary project allow bindings. The important distinction for this environment is that the CLI surface exists locally, while the control-plane authority needed to apply those policies does not.

PowerShell / Linux | gcloud iam policies | inspect deny-policy support at the current project boundary

This subsection shows the difference between “the command exists” and “the environment can author the policy.”

List current deny policies attached to the project

Before assuming a permission is blocked only by allow policy. It is typically triggered by you are auditing a project for higher-order IAM guardrails. Read-only deny-policy inventory on the project attachment point. Determine whether any explicit deny policies already apply to this project.

List deny policies attached to project number 348557092514.

gcloud iam policies list \
  --attachment-point='cloudresourcemanager.googleapis.com/projects/348557092514' \
  --kind=denypolicies \
  --format=yaml
{}

There are no deny policies currently attached to this project.

Current product note: deny attachment points

Deny policies are a real product feature at project, folder, and organization attachment points. The blocker in this archived environment was delegated authority on the target project, not a product-level restriction that deny policies only exist at the organization layer.

Attempt project-scope deny-policy creation

When you need to verify whether project-scope deny administration is actually available to the current principal. It is typically triggered by you want to block a dangerous permission even if an allow binding grants it. State-changing deny-policy create attempt on the project attachment point. Validate whether this environment can author deny policies at project scope.

Attempt to create a deny policy that would block secret reads for the disposable lab principal.

gcloud iam policies create deny-codex-secret-read \
  --attachment-point='cloudresourcemanager.googleapis.com/projects/348557092514' \
  --kind=denypolicies \
  --policy-file='deny-secret-access.json'
ERROR: (gcloud.iam.policies.create) [alexper.recovery@gmail.com] does not have permission to access policies instance [cloudresourcemanager.googleapis.com%252Fprojects%252F348557092514] (or it may not exist): Permission iam.googleapis.com/denypolicies.create denied on resource cloudresourcemanager.googleapis.com/projects/348557092514.

The practical result is clear: deny-policy authoring is not available from the current project-only authority surface in bq-wh-nb.

Check whether deny-policy creation can be delegated through a custom role

After a deny-policy create attempt fails and you need to know whether a custom role could close the gap. It is typically triggered by the current principal lacks iam.denypolicies.create. Read-only permission capability check. Determine whether iam.denypolicies.create is eligible for project custom roles in this environment.

Query the permission metadata for iam.denypolicies.create on this project resource.

gcloud alpha iam list-testable-permissions \
  //cloudresourcemanager.googleapis.com/projects/bq-wh-nb \
  --filter='name=iam.denypolicies.create' \
  --format='table(name,stage,customRolesSupportLevel)'
NAME                     STAGE  CUSTOM_ROLES_SUPPORT_LEVEL
iam.denypolicies.create  GA     NOT_SUPPORTED

This explains why the project cannot self-bootstrap deny authoring through a temporary custom role. The permission is real, but it is not custom-role-delegable at this project scope.

FlagSyntaxDescription
--attachment-point--attachment-point='cloudresourcemanager.googleapis.com/projects/348557092514'Resource where the deny policy would attach.
--kind--kind=denypoliciesSelects the deny-policy policy family.
--policy-file--policy-file='deny-secret-access.json'JSON or YAML document that defines the deny rule.
--filter--filter='name=iam.denypolicies.create'Restricts testable-permission output to the exact permission you care about.
--format--format='table(...)'Keeps the result focused on the capability decision.

PowerShell / Linux | gcloud iam principal-access-boundary-policies | understand the organization boundary

PAB policies are not a project-local feature. They depend on organization-level policy objects and organization-level bindings, which this project does not currently expose.

Confirm that bq-wh-nb has no visible organization parent

Before planning any PAB or VPC Service Controls rollout. It is typically triggered by you are deciding whether a project can host organization-scoped security controls. Read-only organization and project metadata lookup. Prove whether this project sits inside an organization that can hold org-scoped controls.

List visible organizations for the current credentials, then describe the project itself.

gcloud organizations list --format=json
[]
gcloud projects describe bq-wh-nb --format=json
{
  "createTime": "2026-03-22T16:26:19.672Z",
  "lifecycleState": "ACTIVE",
  "name": "BQ Database",
  "projectId": "bq-wh-nb",
  "projectNumber": "348557092514"
}

There is no visible organization in the current credential context, and the project metadata shows no parent object. That is the reason PAB remains conceptual here.

Live boundary

The local SDK exposes gcloud iam principal-access-boundary-policies commands, but those commands require --organization and organization-level policy bindings. In this environment, the absence of a visible organization is the blocker, not missing CLI support.

Current product note: PAB control plane

Principal Access Boundary policy objects are paired with policy bindings that target principal sets. In practice this keeps PAB administration organization-centric even when the runtime question you are solving feels project-local.

Recommendations and Production Rules

  • Create one service account per workload boundary. GitHub deployment, Cloud Run execution, and state-writing pipelines should not share one broad identity.
  • Prefer resource-scope roles over project-scope roles wherever the product supports them: buckets, datasets, secrets, and service accounts are the important examples in this chapter.
  • Treat every user-managed key as a migration candidate. The live bq-wh-sa inventory shows why: key sprawl is easy to create and easy to forget.
  • Lint IAM conditions before adding them, then prove them with Policy Troubleshooter using real request times.
  • Use custom roles for metadata-only or narrowly scoped workflows, but remember that some permissions, including iam.denypolicies.create, are not custom-role-eligible.
  • Use impersonation to test machine identity behavior from an operator session. It gives you a real answer with short-lived credentials and better auditability than a JSON key file.

Data-Engineering Scenarios

ScenarioCorrect IAM patternWhat to avoid
GitHub Actions deploys to GCPWIF provider + roles/iam.workloadIdentityUser on one deployment service accountExporting a JSON key into repository secrets
Cloud Run job reads one secret and writes one bucketService account with secret-scope accessor on that secret and bucket-scope object role on that bucketProject-wide roles/editor or roles/storage.admin
Local operator tests a workload identity--impersonate-service-account plus Policy Troubleshooter and Cloud Asset analysisDownloading a key file to a workstation
Temporary analyst accessTime-bound conditional binding with a documented expiryPermanent project-wide viewer/editor grant
Incident response on suspicious machine identityDisable the service account, review token-creator grants, remove broad bindings, rotate or delete user-managed keysDeleting the service account first and losing visibility into what it was bound to

Troubleshooting and Incident Response

SymptomLikely causeFirst checkSafe next action
PERMISSION_DENIED during impersonationMissing roles/iam.serviceAccountTokenCreatorgcloud iam service-accounts get-iam-policy on the target SAGrant token creator narrowly to the operator or CI identity
Permission works yesterday but not todayConditional binding expiredPolicy Troubleshooter with --request-timeExtend or replace the conditional binding intentionally
Workload can see secret names but not valuesMetadata role only, no secret accessorSecret-level IAM policy and Troubleshooter on secretmanager.versions.accessAdd roles/secretmanager.secretAccessor at secret scope, not project scope
Service account suddenly exposes broad blast radiusUser-managed keys or broad project rolesgcloud iam service-accounts keys list and project IAM filterRemove unused keys, replace with impersonation/WIF, narrow bindings
Deny policy design exists on paper but cannot be appliedScope or permission boundary issuegcloud iam policies create error and list-testable-permissions resultEscalate to the organization-level security admin who can author deny policies

Quick Reference

ControlLayerBest useArchived status in removed project
Service accountIdentityOne machine identity per workloadIn active use
Custom roleAllow policyMinimal nonstandard permission bundleProven live with codexSecretMetaViewer
Conditional bindingAllow policyTemporary or context-aware accessProven live with Expiry2026
ImpersonationAuthentication pathKeyless operator and CI testingProven live
User-managed keyCredential materialMigration exception onlyStill present on bq-wh-sa
Deny policyHard authorization guardrailExplicitly block dangerous permissionsCLI available, authoring blocked here
PAB policyPrincipal-side resource boundaryRestrict which resources principals can ever accessConceptual only here because no visible org scope

GCP Service Accounts and IAM References