Every Kubernetes cluster I’ve worked on had the same problem: static credentials scattered across namespaces. Docker registry secrets copied everywhere. SOPS-encrypted files in git. Service account JSON keys mounted as volumes. It works, but it’s fragile and nobody wants to rotate them.
I recently built a GKE cluster where nothing has a static credential. No gcr-regcred. No SOPS. No key files. Here’s how.
The Old Way
The typical setup:
- Create a GCP service account with Artifact Registry reader permissions
- Generate a JSON key
- Base64 encode it into a
kubernetes.io/dockerconfigjsonsecret - Copy that secret to every namespace that pulls images
- Add
imagePullSecretsto every deployment - SOPS-encrypt the secret in git so it’s not plaintext
- Hope nobody forgets to rotate the key
For 1Password integration, the pattern was:
- Store 1Password Connect credentials as SOPS-encrypted secrets in git
- External Secrets Operator reads the SOPS-encrypted credentials
- ESO connects to 1Password and pulls the actual secrets
SOPS encrypted the thing that reads secrets. Turtles all the way down.
The New Way
Image Pulls — GKE Workload Identity
GKE nodes already have a service account. If that service account has artifactregistry.reader on the project where images live, every pod on the node can pull images natively. No imagePullSecrets needed.
# One IAM binding — covers all pods on all nodes
gcloud projects add-iam-policy-binding image-project \
--member="serviceAccount:node-sa@cluster-project.iam.gserviceaccount.com" \
--role="roles/artifactregistry.reader"
Remove imagePullSecrets from every deployment. Done.
Helm Chart Pulls — Dedicated SA + Workload Identity
Flux’s source-controller needs to authenticate to pull Helm charts from private OCI registries. Create a dedicated GCP service account with only artifactregistry.reader, bind it to the Flux source-controller Kubernetes service account via Workload Identity:
# HelmRepository — use provider: gcp instead of secretRef
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: private-registry
spec:
type: oci
url: oci://us-docker.pkg.dev/my-project/my-registry
provider: gcp # ← Workload Identity, no secret needed
Patch the Flux source-controller service account:
# In flux-system/kustomization.yaml
patches:
- patch: |
apiVersion: v1
kind: ServiceAccount
metadata:
name: source-controller
namespace: flux-system
annotations:
iam.gke.io/gcp-service-account: flux-source@project.iam.gserviceaccount.com
target:
kind: ServiceAccount
name: source-controller
1Password Bootstrap — GCP Secret Manager
Instead of SOPS-encrypting the 1Password Connect credentials, store them in GCP Secret Manager. ESO reads them via Workload Identity — no static credentials anywhere.
The chain:
GKE Workload Identity
→ dedicated ESO service account
→ GCP Secret Manager (reads 1Password Connect creds)
→ 1Password Connect Server starts
→ ClusterSecretStore connects to 1Password
→ All other secrets pulled via ExternalSecrets
Create a SecretStore pointing to GCP SM:
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: gcp-secret-manager
spec:
provider:
gcpsm:
projectID: my-project
auth:
workloadIdentity:
clusterLocation: us-central1-a
clusterName: my-cluster
clusterProjectID: my-project
serviceAccountRef:
name: eso-gcp-sm # annotated with WI
The Result
| What | Old way | New way |
|---|---|---|
| Pod image pulls | gcr-regcred secret per namespace |
GKE node SA (automatic) |
| Helm chart pulls | gcr-regcred in flux-system |
provider: gcp on HelmRepository |
| 1Password bootstrap | SOPS-encrypted in git | GCP Secret Manager + Workload Identity |
| All other secrets | 1Password via ESO | Same (unchanged) |
| Static credentials in git | ~5 SOPS files | Zero |
| Rotation needed | Manual key rotation | Automatic (WI tokens rotate) |
Least Privilege
Each workload gets its own GCP service account with only the permissions it needs:
- Node SA:
artifactregistry.reader(pod image pulls) + basic logging/monitoring - Flux source SA:
artifactregistry.readeronly (Helm chart pulls) - ESO SA:
secretmanager.secretAccessoronly (reads 1P Connect creds)
No shared service accounts. No over-privileged node SA. One SA = one workload = one set of permissions.
Takeaway
The key insight: GKE Workload Identity replaces static credentials with automatic, short-lived tokens. Combined with ESO for application secrets and GCP Secret Manager for bootstrap credentials, you can run a production Kubernetes cluster with zero static credentials in git, zero SOPS files, and zero manually-managed secrets. Everything authenticates through IAM bindings.