Credentials & auth
The GCP provider authenticates to Compute Engine via Application Default
Credentials (ADC) — no token flag. On GKE that means Workload Identity
(the GKE analogue of AWS IRSA: bind the Kubernetes ServiceAccount to a Google
service account, no key files); off-GKE it means a key-file Secret via
GOOGLE_APPLICATION_CREDENTIALS.
There are two identities, and keeping them separate is the whole point:
| Identity | Who | What it does |
|---|---|---|
| Provider service account | the provider process | calls instances.insert/delete, reads instances + machine types, sets metadata/labels |
| Instance service account | the VMs the provider launches | whatever your nodes need at runtime (--instance-service-account); not the provider’s identity |
The provider’s identity must never be the node identity. A node should not be able to create or delete other nodes.
1. The provider service account & role
The provider needs exactly one predefined role on the target project:
| Role | Why | Lifecycle call |
|---|---|---|
roles/compute.instanceAdmin.v1 | create / delete instances, set metadata + labels, read instances and machine types | Create, Delete, Configure, Drain, Describe |
roles/iam.serviceAccountUser on the instance SA | lets the provider launch instances that run as --instance-service-account | Create (only when --instance-service-account is set) |
That’s the least-privilege set. instanceAdmin.v1 already covers
compute.instances.{insert,delete,setMetadata,get,list} and
compute.machineTypes.get. Map each grant to the call that needs it, and grant
nothing more.
Live pricing reads the Cloud Billing Catalog API
(cloudbilling.googleapis.com, the public SKU catalogue for service
6F81-5844-456A) on the --price-refresh cadence. The catalogue is public, so
it needs no extra project role — only that the Cloud Billing API is enabled
and the provider presents some credential: the provider’s own ADC identity, or
an API key via --pricing-api-key. If pricing cannot be reached the provider
falls back to the pinned seed table (prices go stale but never zero), so a
missing key/API only degrades cost accuracy, never the lifecycle calls.
The Terraform under
deploy/sa/
creates the provider service account, binds the role on the project, and wires
the Workload-Identity binding:
cd providers/gcp/deploy/saterraform initterraform apply \ -var 'project_id=my-gcp-project' \ -var 'name=bigfleet-gcp-us-central1' \ -var 'k8s_namespace=bigfleet' \ -var 'k8s_service_account=bigfleet-gcp' \ -var 'instance_service_account=bigfleet-node@my-gcp-project.iam.gserviceaccount.com'# -> outputs provider_service_account_email2. Workload Identity (GKE — preferred, no keys)
Workload Identity lets the Kubernetes ServiceAccount the pod runs as impersonate the Google service account — so there is no key file anywhere. Two bindings, both created by the Terraform above:
- an IAM policy binding granting the Kubernetes SA the
roles/iam.workloadIdentityUserrole on the Google SA, for the memberserviceAccount:PROJECT.svc.id.goog[NAMESPACE/KSA_NAME]; - the Kubernetes ServiceAccount annotated
iam.gke.io/gcp-service-account: <provider-sa-email>.
The Helm chart writes that annotation when you set serviceAccount.gcpServiceAccount:
serviceAccount: create: true name: bigfleet-gcp gcpServiceAccount: bigfleet-gcp-us-central1@my-gcp-project.iam.gserviceaccount.comEnsure Workload Identity is enabled on the cluster and node pool. Once bound, the provider picks up credentials from the metadata server automatically — ADC needs no env var on GKE.
3. Key-file fallback (off-GKE)
Off-GKE (or on a cluster without Workload Identity), create a key for the
provider service account, store it as a Secret, and mount it as
GOOGLE_APPLICATION_CREDENTIALS:
gcloud iam service-accounts keys create key.json \ --iam-account bigfleet-gcp-us-central1@my-gcp-project.iam.gserviceaccount.comkubectl -n bigfleet create secret generic bigfleet-gcp-key --from-file=key.jsoncredentials: secretName: bigfleet-gcp-key # mounted at /var/secrets/google/key.json # GOOGLE_APPLICATION_CREDENTIALS is set for youA ready-to-edit Secret manifest is in
deploy/secret/gcp-key.example.yaml.
Prefer Workload Identity — key files are long-lived credentials you must rotate
and protect.
4. The instance service account
--instance-service-account is the identity your nodes run as — unrelated to
the provider’s. Give it only what the workloads on the node need (often a
minimal SA, or the project default). The provider needs
roles/iam.serviceAccountUser on this SA to launch instances that run as it
(the Terraform grants it). If you omit --instance-service-account, instances
use the project default compute service account.
5. The SSH key for in-band bootstrap
Configure and Drain reach the running host over SSH (the secure, transient
delivery channel — the join blob is never persisted in instance metadata). The
provider needs an SSH private key (--ssh-key); its public key is authorised
on each instance via ssh-keys metadata at Create (with enable-oslogin=false
so metadata keys are honoured), and the instance’s SSH host key is pinned at
Create and verified on every connection. Store the private key as its own Secret:
ssh-keygen -t ed25519 -N '' -f ./id_ed25519kubectl -n bigfleet create secret generic bigfleet-gcp-ssh --from-file=id_ed25519=./id_ed25519ssh: secretName: bigfleet-gcp-ssh # mounted at /etc/bigfleet/ssh/id_ed25519 user: bigfleetUse a dedicated key for the provider, not an operator’s personal key. For
pre-pinned host keys (no trust-on-first-use window) use a cloud-init-enabled
image (e.g. Ubuntu); otherwise the provider trust-on-first-uses and pins the
observed host key. The provider reaches the host over its internal IP by
default (same VPC); set --use-external-ip only if it must use an external IP.
6. Rotate
- Workload Identity has no key to rotate — that’s its main benefit. Rotating the role binding is a Terraform change; the running process picks up new permissions immediately (no restart needed for an additive grant).
- Key files are long-lived: create a new key, update the Secret, roll the
Deployment (
kubectl -n bigfleet rollout restart deploy/…) so the process re-reads it, then delete the old key. Because the persisted--statefile is the restart path and transitions run on minute-scale timeouts, a rolling restart is safe.
What the credentials are used for
Every GCE call the provider makes, and the role permission it needs:
| Call | Permission | When |
|---|---|---|
Instances.Insert | compute.instances.create (+ iam.serviceAccounts.actAs for the node SA) | Create |
Instances.Delete | compute.instances.delete | Delete |
Instances.AggregatedList | compute.instances.list | Describe / reconcile |
Instances.SetMetadata | compute.instances.setMetadata | Configure / Drain (records/clears the cluster-binding metadata + TOFU host-key pin; the bootstrap itself goes over SSH, never into metadata) |
MachineTypes.Get | compute.machineTypes.get | allocatable resolution |
No credential is ever logged. See Security for the full trust model.