Why ArgoCD makes your GitHub Actions setup dramatically more secure
You're already using GitHub Actions. It works. So why bother adding another tool? This guide answers that question concretely โ with your security concern (Jenkins-style blast radius) at the center โ and walks you step-by-step through a realistic workflow. No marketing fluff.
git revert. ArgoCD notices and rolls back.The core insight. You don't replace GitHub Actions with ArgoCD. You split the job: GitHub Actions builds container images and pushes them (which is what it's best at). ArgoCD watches Git for changes to your Kubernetes manifests and applies them from inside the cluster (which is what it's best at). The CI never needs to talk to your cluster directly again.
What's actually wrong with "just use GitHub Actions"?
GitHub Actions is great for building, testing, and publishing. But when people use it as the deployment tool, they recreate the same architectural problem that made Jenkins dangerous โ just with a nicer UI.
Today's typical GitHub Actions deploy flow
GitHub Actions stores a KUBE_CONFIG secret and runs kubectl apply. This is a push pattern.
The hidden cost of this pattern
# .github/workflows/deploy.yml โ a typical setup name: Deploy to production on: { push: { branches: [main] } } jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up kubectl run: echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config # ๐ฉ long-lived cluster admin creds - name: Build and push image run: docker build -t myapp:${{ github.sha }} . && docker push ... - name: Deploy run: kubectl set image deploy/myapp myapp=myapp:${{ github.sha }}
- GitHub holds a
KUBE_CONFIGsecret โ often cluster-admin - Any compromised action (
tj-actions/changed-filesstyle supply-chain attack) can steal it - Any maintainer with repo access can exfiltrate it via a PR workflow
- Credential rotation means updating every workflow file
- Audit = reading GitHub Actions logs one by one
- What's actually running in the cluster? Hard to tell without kubectl
- Someone ran
kubectl edit deployat 3am. Now what? - Rollback = re-run an old workflow (hope the image still exists)
- Multi-cluster = same secret copy-pasted N times
- No single source of truth for cluster state
The blast radius. If a malicious dependency gets into one workflow, the attacker now has a kubeconfig with full cluster access. They can deploy a cryptominer, exfiltrate secrets from every namespace, or drop a persistent backdoor โ all without ever touching your servers directly. This is the exact problem you had with Jenkins, just wearing a GitHub hat.
What "GitOps" actually means (without the buzzword soup)
GitOps is not a product. It's a pattern with four rules. If you follow all four, you are doing GitOps. ArgoCD is simply a tool that makes following those four rules easy on Kubernetes.
Rule 1 ยท Declarative
Your entire system is described as data files (YAML), not as scripts. You say "I want 3 replicas of nginx", not "run kubectl scale deploy nginx --replicas=3".
Rule 2 ยท Versioned in Git
Those declarative files live in a Git repo. Git is the single source of truth. If it's not in Git, it doesn't exist. Every change is a commit, reviewable, revertible.
Rule 3 ยท Pulled automatically
An agent running inside the target system pulls from Git and applies changes. Nothing outside the cluster needs cluster credentials. This is the security superpower.
Rule 4 ยท Continuously reconciled
The agent constantly compares "what Git says" vs "what's actually running". Any drift is detected and auto-corrected (or alerted). The cluster cannot silently diverge from Git.
Push vs Pull โ visualized
CI reaches into the cluster using a stored credential. If CI is compromised, everything downstream is too.
Cluster reaches out to Git (read-only). No inbound connections. No external service holds cluster credentials.
Why this is genuinely more secure. A firewall rule on your cluster can now say: "no external service may initiate a connection to the Kubernetes API". You don't even need to trust GitHub Actions, any third-party action, or any CI vendor โ because none of them are in the path to your cluster anymore. The cluster pulls from Git, which it verifies via commit signatures and HTTPS.
How ArgoCD actually works under the hood
ArgoCD is a Kubernetes controller. That's it. Once you understand what a controller is, ArgoCD becomes obvious.
The reconciliation loop
Every ArgoCD action is a variation of this same loop, which runs forever:
In plain English
Every ~3 minutes (configurable), ArgoCD:
- Fetches the latest commit from your Git repo.
- Renders the manifests (plain YAML, or Helm charts, or Kustomize overlays).
- Compares the rendered output to what's actually running in the cluster. Any difference = "drift".
- Syncs by applying the diff โ creating, updating, or deleting K8s resources as needed.
The same loop also runs instantly when you push a commit (via webhook) or click "Sync" in the UI.
The two things ArgoCD gives you that kubectl alone can't
โ Drift detection
Someone runs kubectl edit deploy/payments --replicas=10 directly in production. ArgoCD immediately sees this doesn't match Git and marks the app OutOfSync. It can either alert you or auto-revert.
โก Continuous guarantees
Someone deletes a ConfigMap by accident. ArgoCD notices at next sync and recreates it from Git โ automatically. Your cluster self-heals back to the Git-declared state.
The Application CRD โ ArgoCD's main concept
An Application is a Kubernetes resource ArgoCD gives you. It's a declarative pointer that says "sync this Git path into this namespace".
Application.yamlapiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: snip-link namespace: argocd spec: project: default source: repoURL: https://github.com/vattanac/infra.git # where to find manifests path: apps/snip-link/production # which folder targetRevision: main # which branch destination: server: https://kubernetes.default.svc # which cluster namespace: snip-link-prod # which namespace syncPolicy: automated: prune: true # delete resources removed from Git selfHeal: true # revert manual changes to the cluster
Mental model. An Application is a subscription. It says "my cluster subscribes to this Git folder. Whatever appears there, put it in me." That's 90% of ArgoCD.
Architecture โ the five components
ArgoCD runs as a handful of pods inside your cluster. Understanding which pod does what makes debugging vastly easier.
Where the security actually lives
Direction of trust. The cluster trusts Git (read-only). Git does not trust the cluster. Nothing outside the cluster holds a cluster credential. This is the architectural property that makes ArgoCD fundamentally different from Jenkins or "GitHub Actions with kubeconfig".
The only credential ArgoCD itself needs is a read-only Git deploy key (or GitHub App with read access). Even if someone steals that key, they can only read your infra Git repo โ they cannot write to your cluster.
GitHub Actions + ArgoCD โ who does what?
This is the part people get confused about. You don't throw away GitHub Actions. You give it a clearer, smaller job.
Division of labor
| Responsibility | GitHub Actions | ArgoCD |
|---|---|---|
| Run tests on PR | โ Yes | โ No |
| Build container image | โ Yes | โ No |
| Push image to registry | โ Yes | โ No |
| Scan image for vulns | โ Yes | โ No |
| Update manifest with new image tag | โ Yes | Optional via Image Updater |
| Apply to Kubernetes | โ No | โ Yes |
| Detect/fix manual drift | โ No | โ Yes |
| Rollback on failure | โ No | โ Yes |
| Show cluster health | โ No | โ Yes |
The recommended Git structure
You need two repositories (or two folders in a monorepo). This separation is the key to making GitOps clean.
Repo 1 ยท Application code
Repo 2 ยท Infrastructure (watched by ArgoCD)
The full flow, end to end
The new GitHub Actions workflow โ no more kubeconfig
.github/workflows/build.ymlname: Build and update manifest on: { push: { branches: [main] } } jobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write # push to GHCR only โ no cluster access! steps: - uses: actions/checkout@v4 - name: Log in to registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push run: | docker build -t ghcr.io/vattanac/snip-link:${{ github.sha }} . docker push ghcr.io/vattanac/snip-link:${{ github.sha }} - name: Bump image tag in infra repo run: | git clone https://x-access-token:${{ secrets.INFRA_REPO_TOKEN }}@github.com/vattanac/infra.git cd infra/apps/snip-link/production sed -i "s/newTag:.*/newTag: ${{ github.sha }}/" kustomization.yaml git config user.email "ci@bot" && git config user.name "ci" git commit -am "deploy snip-link ${{ github.sha }}" git push
Notice what's missing. No KUBE_CONFIG. No kubectl. No cluster URL. The only secret GitHub Actions holds is a scoped PAT that can commit to the infra repo โ which even if stolen only lets an attacker open a bogus PR (auditable, reversible). ArgoCD picks up the infra commit and handles the actual cluster work.
Install ArgoCD in 5 minutes
You need a Kubernetes cluster (k3s, kind, EKS, DigitalOcean Kubernetes โ anything works) and kubectl. That's it.
Step 1 โ Install ArgoCD
# Create the namespace and apply the official install manifest kubectl create namespace argocd kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml # Wait until pods are ready kubectl -n argocd wait --for=condition=ready pod -l app.kubernetes.io/name=argocd-server --timeout=300s
Step 2 โ Access the UI
# Port-forward the server pod to your laptop kubectl port-forward -n argocd svc/argocd-server 8080:443 # Get the initial admin password kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d # Open https://localhost:8080 โ login as admin
Step 3 โ Install the CLI (optional but nice)
brew install argocd
# or
curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd && sudo mv argocd /usr/local/bin/
argocd login localhost:8080 --username admin --password <password>
Step 4 โ Point ArgoCD at your infra repo
# Register the repo (once per cluster) argocd repo add https://github.com/vattanac/infra.git \ --username vattanac --password <github-pat-with-read> # Create your first Application argocd app create snip-link \ --repo https://github.com/vattanac/infra.git \ --path apps/snip-link/production \ --dest-server https://kubernetes.default.svc \ --dest-namespace snip-link-prod \ --sync-policy automated \ --self-heal \ --auto-prune
That's it. ArgoCD will now pull from your Git repo, deploy everything in apps/snip-link/production, and keep the namespace in sync with Git forever. Every git push to infra = automatic deploy.
Live demo โ deploying "Snip.link" end-to-end
Click through the 8 steps below. This is the exact workflow you'll use every day. No placeholder content โ every step shows real files, real commands, real UI.
Step 1 ยท You write code and commit it locally
Normal developer life. You edit src/server.js in your app repo, add a feature, and commit.
# In your app repo (snip-link) vim src/server.js # add /api/v2/shorten endpoint git add . git commit -m "add v2 shorten endpoint" git push origin main
Nothing changed in your developer experience. You didn't install a new tool. You didn't learn new commands. You still git push.
Step 2 ยท GitHub receives the push and fires the workflow
Your GitHub Actions workflow is triggered by the push to main. It has exactly two jobs: build and bump.
a3f29b1Step 3 ยท The built image lands in the registry
Now ghcr.io/vattanac/snip-link:a3f29b1 exists. That's it โ just an image sitting in a registry. Nothing is running yet. Nothing talks to the cluster.
# Verify the image is there docker pull ghcr.io/vattanac/snip-link:a3f29b1 โ a3f29b1: Pulling from vattanac/snip-link โ Digest: sha256:9f7ac2ed61...
Security note. Up to this point, nothing has connected to your Kubernetes cluster. GitHub Actions held only registry push permission + an infra repo write token. No kubeconfig exists anywhere in CI.
Step 4 ยท The bump step commits to the infra repo
This is the "handoff". GH Actions edits one line in the infra repo's kustomization.yaml โ changing the image tag from the old SHA to a3f29b1 โ and pushes.
infra/apps/snip-link/production/kustomization.yamlapiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ../base images: - name: ghcr.io/vattanac/snip-link newTag: a3f29b1 # ๐ this line got updated namespace: snip-link-prod replicas: - name: snip-link count: 3
A git commit on the infra repo now exists saying "deploy snip-link a3f29b1". This commit is your audit trail โ forever.
Step 5 ยท ArgoCD detects the change
If you configured a webhook (recommended), this happens in ~2 seconds. Otherwise ArgoCD polls every ~3 minutes and catches it on the next cycle.
syncPolicy.automated is true, it syncs immediately.Step 6 ยท ArgoCD applies the change inside the cluster
The argocd-application-controller pod calls the Kubernetes API (from inside the cluster, using its own ServiceAccount โ no external credentials). It does a rolling update.
kubectl -n snip-link-prod get pods -w NAME READY STATUS RESTARTS AGE snip-link-5d8c6f7b9c-a1b2c 1/1 Running 0 12m snip-link-5d8c6f7b9c-d3e4f 1/1 Running 0 12m snip-link-5d8c6f7b9c-g5h6i 1/1 Running 0 12m snip-link-7f9a3c2d1e-x7y8z 0/1 ContainerCreating 0 2s โ new snip-link-7f9a3c2d1e-x7y8z 1/1 Running 0 8s snip-link-5d8c6f7b9c-a1b2c 1/1 Terminating 0 12m โ old ...
New version is live. ArgoCD marks the app Synced + Healthy. Total elapsed time from git push on the app repo to pods running: usually 1โ3 minutes. If anything fails (image pull error, readiness probe fails), ArgoCD marks it Degraded and alerts.
Step 7 ยท Someone makes a "quick fix" directly in the cluster
Imagine a teammate runs this at 3 AM during an incident:
kubectl -n snip-link-prod scale deploy snip-link --replicas=10 # ๐ฉ not in Git!
With a plain GitHub Actions setup, this change just... stays. Until someone notices. Until the next deploy overwrites it. Until the cluster blows up.
With ArgoCD + selfHeal: true, the controller sees the drift on its next sync:
The cluster cannot silently drift. This alone is worth the whole setup. You can even set selfHeal: false if you want ArgoCD to just alert instead of auto-revert โ great for emergency scenarios.
Step 8 ยท A bad deploy โ rollback via git revert
Commit a3f29b1 turned out to be broken. No problem. The fix is a git revert:
# In the INFRA repo (not the app repo) cd infra git revert <bump-commit-sha> git push # Done. ArgoCD sees the new commit, rolls forward (back to the old image)
Alternative: in the ArgoCD UI, click History and rollback on the app, pick the last-good revision, click Rollback. Same effect.
Why this is better than re-running a workflow. The old image is pinned in a git commit โ it cannot disappear. Your rollback path is deterministic, reversible, and auditable. Try rolling back a Jenkins or GH Actions deploy from six months ago: you'll need to hope the image, the workflow file, and the kubeconfig still exist.
The five benefits you'll actually feel
Forget marketing pages. These are the five things that make real engineers go "oh, that's nice" in the first month of using ArgoCD properly.
โ No cluster credentials in any CI system โ ever
Your GitHub Actions can be compromised by a supply-chain attack tomorrow and your production cluster is not affected. The worst an attacker can do is push a bad image tag to the infra repo โ which you'd see immediately in a diff and revert. This is a categorical security improvement, not a marginal one.
โก What's running in prod = git log
A month from now, someone asks "when did we enable the V2 API?" โ you answer in 5 seconds by grepping the infra repo. Six months from now you're audited. You hand over a git history. Done.
โข Self-healing cluster
Stop worrying about sleep-deprived 3 AM kubectl changes. ArgoCD treats unauthorized changes like any other drift and reverts them. Your cluster stays aligned with Git 24/7, with zero human effort.
โฃ Multi-cluster for free
Got a staging cluster? A DR cluster? A region cluster for EU compliance? You just add them to ArgoCD's cluster list. Each becomes a destination in your Application specs. Same infra repo, different destinations. No duplicated CI pipelines.
โค The UI is genuinely useful
Unlike Jenkins or plain kubectl, ArgoCD's UI shows you a live topological diagram of every resource in your app โ Deployments, Pods, Services, Ingresses โ with real-time health status. When something breaks, you see where in 3 seconds, not after 20 minutes of kubectl describe.
Common mistakes that kill ArgoCD adoption
Most "ArgoCD isn't giving me much" feelings come from one of these five misconfigurations. Avoid them and the value becomes obvious.
Mistake 1 ยท Keeping manifests in the same repo as your app code
Tempting โ "it's all in one place!" โ but it creates a chicken-and-egg problem. Every code change triggers a deploy, and you can't bump image tags cleanly. Fix: use a separate infra repo (or at minimum a separate top-level folder that CI is forbidden to trigger on).
Mistake 2 ยท Not enabling automated sync
People install ArgoCD and then require manual "Sync" clicks in the UI. Now it's just a slower kubectl. Fix: set syncPolicy.automated.selfHeal: true and prune: true. Let ArgoCD actually do its job.
Mistake 3 ยท Secrets in Git
Don't commit plain secrets. Use Sealed Secrets, External Secrets Operator (with Vault / AWS Secrets Manager / GCP SM), or SOPS. All of these integrate cleanly with ArgoCD. Pick one before you roll out to production.
Mistake 4 ยท Using ArgoCD as a CI runner
ArgoCD does not build code. It does not run tests. If you find yourself wanting it to โ you want Argo Workflows or Tekton. Keep ArgoCD focused on deploy; keep CI (GitHub Actions) focused on build.
Mistake 5 ยท Not using the App-of-Apps pattern at scale
Managing Applications one-by-one is fine for 5 services. At 50 services you want the "App of Apps" pattern or ApplicationSets โ a single Application that templates out N others. Lets you onboard a new microservice by adding one folder to Git.
When is ArgoCD worth it?
- A Kubernetes cluster (any flavor) you deploy to regularly
- Security team asking about CI blast radius
- More than one environment (staging + prod) or cluster
- More than two people deploying
- Any compliance requirement (SOC 2, ISO, HIPAA, etc.)
- Production incidents where "who changed what" matters
- You deploy to a single VPS with docker-compose (use Coolify/Dokploy instead)
- You're a solo dev with one small app and no Kubernetes
- You deploy once a month and nothing is security-sensitive
- You use a managed PaaS (Railway, Fly, Render) โ they handle this internally
The 60-second recommendation for your situation
You already use Kubernetes and GitHub Actions. You have a security team worried about CI-to-cluster credentials. You've partially set up ArgoCD. The setup you haven't fully felt the benefit of yet is:
- Move your Kubernetes manifests into a separate infra repo.
- Remove the
KUBE_CONFIGsecret from every GitHub Actions workflow โ have GH Actions commit to the infra repo instead of running kubectl. - Create one ArgoCD Application per service with
automated + selfHeal + pruneenabled. - Do the first
git revertrollback. That's when it clicks.
After those four steps, your infrastructure will be meaningfully more secure โ not "according to a blog post," but in a way your security team can verify by checking that no external service holds cluster credentials anymore.