Onboarding the Google Cloud plugin (keyless, Workload Identity Federation)
This guide lets you authorize Linro to scan your Google Cloud without
handing over a service-account key. Linro's managed sensor proves its
identity with a short-lived Kubernetes token, exchanges it for a Google STS
token, and impersonates a read-only service account you control. Nothing
long-lived ever leaves your project.
It works even when your org enforces
iam.disableServiceAccountKeyCreation (which makes SA keys impossible).
This document is written to be followed by a human operator or executed
by an automation agent. Section For automation / agents
restates every step as structured parameters.
How it works (30-second version)
managed sensor pod Google Cloud (your project)
────────────────── ───────────────────────────
projected K8s token ──STS──▶ Workload Identity Pool + OIDC provider
iss = Linro cluster (trusts Linro's cluster issuer,
aud = linro-managed-sensor restricted to YOUR install namespace)
ns = linro-<your-install> │
▼
impersonate linro-scanner@<project>
(roles/viewer + iam.securityReviewer)
│
▼
read-only inventory scan
The trust anchor is your install's namespace (linro-<slug>). Any managed
sensor Linro runs in that namespace can scan; no other Linro install — on the
same shared cluster, same issuer, same audience — can, because the OIDC
provider is pinned to your namespace. There is no per-sensor configuration and
no secret to rotate.
Step 0 — Get your install's exact values
Three values describe your install. Do not guess or copy them from this
doc — read them from your own install, which is the source of truth:
| Value | What it is | Example |
|---|
| namespace | The Kubernetes namespace your install runs in (the trust anchor) | linro-acme |
| OIDC issuer | Linro's cluster issuer URL (same for every install on a cluster) | https://oidc.eks.eu-west-1.amazonaws.com/id/240C82FA... |
| audience | The token audience (fixed) | linro-managed-sensor |
Two equivalent ways to read them:
-
UI: in your Linro install, open System → Security. The "Cloud
federation" panel shows all three and a pre-filled snippet.
-
CLI: run
linro oidc info --address https://<your-install-host>
It prints the namespace, issuer, and audience, and (with --fetch) the
issuer's live OpenID discovery + JWKS documents so you can confirm the
issuer is reachable before configuring GCP.
Both are backed by the same API (SystemService.GetCloudFederationInfo), so
they always reflect the live install.
Step 1 — Create the GCP trust (Terraform, recommended)
Use the example module at
deploy/terraform/examples/onboarding-wif/.
It creates the read-only service account, the workload-identity pool + OIDC
provider (pinned to your namespace), and the impersonation binding — then
outputs the exact external_account config to hand back to Linro.
cd deploy/terraform/examples/onboarding-wif
terraform init
terraform apply \
-var project_id=YOUR_PROJECT_ID \
-var org_id=YOUR_ORG_ID \ # omit for project-level read only
-var linro_namespace=linro-acme # from Step 0
# linro_oidc_issuer / linro_audience default to Linro prod; override only
# if Step 0 reported different values.
When it finishes:
terraform output -raw external_account_config
Copy that JSON — it is not secret (it carries no key material) — and give
it to your Linro operator for Step 2.
What to grant, and why
The plugin calls per-service GET/LIST APIs directly (Compute, Cloud
Storage, IAM, BigQuery, KMS, Cloud SQL, GKE, …), so the scanner SA needs:
roles/viewer — broad per-service read, and
roles/iam.securityReviewer — read IAM policies/bindings (Viewer does
not fully cover getIamPolicy).
Grant at the organization (or folder) level for full multi-project
coverage; project level works but must be repeated per project and misses new
projects.
Do not substitute roles/cloudasset.viewer. The plugin does not call
the Cloud Asset Inventory API; the per-service calls would 403.
Enable the service APIs
A disabled API returns 403 regardless of role. Enable the services you want
scanned on each target project, e.g.:
gcloud services enable \
compute.googleapis.com container.googleapis.com sqladmin.googleapis.com \
cloudkms.googleapis.com storage.googleapis.com iam.googleapis.com \
--project YOUR_PROJECT_ID
Disabled-API 403s are isolated — they skip that one service and do not abort
the scan — but you only get inventory for enabled services.
Step 2 — Linro operator wires it up
The operator sets one sealed sensor-profile environment variable on the GCP
sensor profile:
GOOGLE_CREDENTIALS = <the external_account JSON from Step 1>
The value is AES-GCM sealed, materialized into a per-profile Kubernetes Secret,
and mounted into the sensor pod (envFrom). The plugin reads it, validates it
is an external_account config, and authenticates via the projected token the
pod already mounts at /var/run/secrets/linro/federation/token. No plugin
config and no per-sensor setup are required.
Step 3 — Verify
Trigger a reconcile for the GCP plugin (UI: the plugin's sync action; CLI:
linro reconciliation ...). Within a minute, GCP resources (buckets, compute
instances, IAM service accounts, …) appear in your inventory. If nothing
appears, see Troubleshooting.
Troubleshooting
| Symptom | Cause | Fix |
|---|
PERMISSION_DENIED ... has not been used in project ... or it is disabled | The service API is off | gcloud services enable <api> --project ... (Step 1) |
| All resources of a service 403 | Missing role | Confirm both roles/viewer and roles/iam.securityReviewer are granted to the scanner SA |
Token exchange fails / unauthorized_client | Namespace/audience/issuer mismatch | Re-read Step 0 from the install; the attribute_condition namespace must equal your install namespace exactly; allowed_audiences must include the audience |
iam.allowedPolicyMemberDomains blocks the binding | Org policy restricts principal domains | Add an exception for principalSet/principal members of the workload-identity pool, or apply the binding from a project exempted by the policy |
| Issuer not reachable from GCP | Private/non-public cluster issuer | WIF requires a publicly reachable issuer (GCP fetches JWKS itself); confirm with linro oidc info --fetch. Linro production uses a public EKS issuer |
For automation / agents
Goal: produce a GCP external_account JSON and have the Linro operator set it
as the GOOGLE_CREDENTIALS sealed sensor-profile env var.
Inputs to fetch from the live install (never hardcode):
| Param | Source of truth |
|---|
namespace | GetCloudFederationInfo().namespace (UI System→Security, or linro oidc info) |
issuer | GetCloudFederationInfo().oidc_issuer_url |
audience | GetCloudFederationInfo().audience |
linro oidc info --output json returns these fields machine-readably; add
--fetch to also include the issuer's openid_configuration and jwks.
GCP objects to create (project = customer project; PROJECT_NUMBER from
gcloud projects describe):
- Service account
linro-scanner@<project>.iam.gserviceaccount.com, granted
roles/viewer + roles/iam.securityReviewer (org or project level).
- Workload-identity pool, e.g.
linro-pool.
- OIDC provider in the pool:
issuer-uri = issuer
allowed-audiences = [audience]
attribute-mapping = google.subject=assertion['kubernetes.io']['namespace']
attribute-condition = assertion['kubernetes.io']['namespace'] == "<namespace>"
- Binding:
roles/iam.workloadIdentityUser on the SA for member
principal://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<pool>/subject/<namespace>
Output to hand to the operator (external_account, no secret):
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<pool>/providers/<provider>",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/linro-scanner@<project>.iam.gserviceaccount.com:generateAccessToken",
"credential_source": {
"file": "/var/run/secrets/linro/federation/token",
"format": { "type": "text" }
}
}
The Terraform module emits this exact JSON as the external_account_config
output — preferred over hand-assembly. The plugin validates that token_url
and service_account_impersonation_url are *.googleapis.com and that
credential_source is file-based, so keep these endpoints verbatim.
See docs/authentication.md for the credential
resolution + validation details.