Skip to content
All case studies
InfrastructureProduction

Keyless GitOps Delivery Platform on GCP

A production CI/CD reference implementation — Spring Boot to GKE via SonarQube and Trivy gates, Helm, and ArgoCD, with Workload Identity Federation removing every long-lived credential.

Repository
Zero
JSON service account keys
Two gates
quality and vulnerability
Git
single source of truth

The constraint

Most CI/CD pipelines authenticate to their cloud with a long-lived JSON service account key pasted into a secrets store. That key does not expire, is rarely rotated, and grants its permissions to anyone who can read the secret or trick the pipeline into printing it.

It is the single most common serious misconfiguration in cloud CI/CD, and it is entirely avoidable.

Approach

Workload Identity Federation. GitHub Actions mints a short-lived OIDC token; GCP exchanges it for a scoped access token valid for minutes. No stored credential exists.

The detail that matters and is frequently missed: the WIF provider needs an attribute-condition pinning the specific repository.

--attribute-condition="assertion.repository=='org/repo'"

Without it, any repository on GitHub can mint tokens for your service account. A federation setup missing this line is arguably worse than the JSON key it replaced, because it looks secure.

Architecture

Push to main triggers: checkout, Java build, SonarQube scan with a blocking quality gate, then WIF authentication to GCP. Jib builds the container image without a Docker daemon — faster and one less privileged component in the runner.

Trivy scans the pushed image and fails on critical or high findings. Both gates are blocking; a warning nobody acts on is not a gate.

Then the GitOps step: the pipeline bumps the image tag in the Helm values.yaml and commits. ArgoCD detects the change and syncs to GKE. The pipeline never holds cluster credentials — it only writes to Git, and Git is the source of truth. Rollback is git revert.

What broke

Initially the GitOps commit went to the same repository, which created a loop: the tag-bump commit triggered the workflow, which bumped the tag again. A [skip ci] marker patched it, but that is a workaround, not a design.

The correct answer is a separate manifests repository. Application code and deployed state have genuinely different lifecycles and different reviewers. Keeping them in one repo is convenient at first and consistently regretted later.

Outcome

Zero long-lived credentials in CI. Two blocking gates before anything reaches a cluster. Deployed state visible and revertible in Git. The implementation is public — see the repository link above.

Stack

  • GitHub Actions
  • ArgoCD
  • Helm
  • GKE
  • Trivy
  • SonarQube
  • Jib