Who Approved That Deploy? Audit Trails and RBAC Across Multi-Cluster ArgoCD
ArgoCD records what synced and when, not who approved it. Here is how to join Git PR reviews, ArgoCD RBAC and sync events, and Kubernetes API audit logs into one audit trail across every cluster - plus what each option costs you.
ArgoCD cannot answer "who approved this deploy?" on its own. The approval lives in your Git provider (PR reviews, CODEOWNERS, signed commits). The sync lives in the ArgoCD Application status. The identity lives in your SSO provider, and the cluster-side write lives in the Kubernetes API audit log. A defensible trail joins all four on one key: the commit SHA.
Kubernetes API server auditing is off by default. Upstream, no events are logged unless you pass --audit-policy-file. EKS control plane logs are off, GKE Data Access logs are off, and AKS collects nothing until you create a diagnostic setting. The clusters you created after the first one are usually the ones that got missed.
Kubernetes audit logs never name the human behind an ArgoCD sync. They name ArgoCD's service account. That attribution gap is why Git has to be your system of record, and why ArgoCD sync events (initiatedBy.username plus sync.revision) must be exported off-cluster.
With one ArgoCD managing many clusters, the AppProject becomes your security boundary. Lock sourceRepos, destinations, and clusterResourceWhitelist. In policy.csv, grant sync separately from override and delete, disable the local admin account, and back every login with SSO.
Qovery is one way to get the outcome without building the pipeline. It deploys into your own AWS, GCP, Azure, Scaleway, or existing Kubernetes cluster, with per-environment RBAC and a deployment history that records which user deployed which commit. It is not a drop-in ArgoCD replacement if your platform contract is the Application CRD.
ArgoCD will tell you what is running and when it synced. It cannot tell you who approved it, because that answer lives in three other systems. I have interviewed a lot of CTOs and run Kubernetes in production for years, and the audit request always lands the same way: security asks "show me who approved every production deploy across our clusters", and the platform team discovers the answer is spread across GitHub, ArgoCD, and logs that were never turned on.
What does ArgoCD actually record about a deploy - and what does it miss?
ArgoCD records the sync, not the approval. Each Application stores the applied revision (status.sync.revision), the destination cluster and namespace, the result, the start and finish times, and in status.operationState.operation.initiatedBy either the username of the person who clicked sync or automated: true when the controller did it (Application types). With auto-sync on, which is how most teams run, there is no human actor at all.
Past deployments, capped by revisionHistoryLimit (default 10)
What is missing: PR approvals, reviewer identity, who edited policy.csv, and the link between a mutable image tag and the commit that built it. The UI history tab keeps 10 entries and lives inside the cluster you are auditing. ArgoCD's Kubernetes Events disappear faster still, because kube-apiserver's --event-ttl defaults to one hour (kube-apiserver reference).
In the Kubernetes API audit log, every ArgoCD write is attributed to a service account, never to a human: system:serviceaccount:argocd:argocd-application-controller for the cluster ArgoCD runs in, and the argocd-manager service account in kube-system for remote clusters added with argocd cluster add.Sync impersonation (beta, off by default) swaps in a per-project service account, which helps least privilege but still names no person.
The cheapest durable export is ArgoCD Notifications. The built-in triggers include on-sync-succeeded, on-sync-failed, and on-deployed, and a custom webhook template can emit the actor and revision (template shown in the pipeline section below).
Where does the "who approved it" signal actually come from?
The approval record lives in your Git provider and nowhere else. In GitOps, the only legitimate proof of approval is a pull request review on a protected branch with code owner enforcement and ideally a signed commit, streamed out of the Git provider, because deleting a repo or branch takes the evidence with it.
Require pull request reviews before merging, with a minimum number of approvals
Dismiss stale pull request approvals when new commits are pushed
Require approval of the most recent reviewable push, so nobody approves their own last commit
Require review from code owners, via a CODEOWNERS file covering production paths
Require signed commits
Do not allow bypassing the above settings. Without it, repository admins skip every rule
On the ArgoCD side, an AppProject can refuse to sync commits that fail signature checks. The older signatureKeys field is now deprecated in favour of Source Integrity Verification under .spec.sourceIntegrity. When a criterion fails, the application cannot sync.
Then get the evidence out of the provider. GitHub Enterprise Cloud keeps general audit events for 180 days and Git events for only seven (about the enterprise audit log). It can stream to Amazon S3, Azure Blob Storage, Azure Event Hubs, Datadog, Google Cloud Storage, and Splunk. GitLab audit event streaming is an Ultimate feature with HTTP, Google Cloud Logging, and AWS S3 destinations.
With that in place, every audit question reduces to one lookup: given SHA abc123, who authored it, who approved it, when did it sync, to which clusters, and is it still running?
It breaks in four predictable places:
Bot or CI tokens with direct push to the GitOps repo, skipping review entirely
Self-approved PRs, when the most-recent-push rule is off
Mutable image tags like latest, which cut the SHA link between the manifest and the code that actually runs
Out-of-band kubectl apply by a human, visible only in the Kubernetes audit log
How do you configure ArgoCD RBAC so it holds up across many clusters?
Disable the local admin account, send every login through OIDC or SAML, map SSO groups to roles in policy.csv, and scope every permission to an AppProject with locked destinations. When one ArgoCD manages many clusters, the AppProject replaces the cluster as your security boundary.
Turn off local accounts. Set admin.enabled: "false" in argocd-cm. The ArgoCD docs recommend using admin only for initial setup. Avoid local accounts.<name>: apiKey users, which mint long-lived tokens outside your IdP.
Write policy.csv by project. The RBAC syntax is p, <role/user/group>, <resource>, <action>, <object>, <effect> and g, <user/group>, <role>. For applications, applicationsets, logs, and exec, the object is <app-project>/<app-name>.
Split sync from override.override lets a user sync arbitrary manifests or a different revision, which means changing what is deployed without Git. Since v3.2, application.sync.requireOverridePrivilegeForRevisionSync: 'true' also treats passing a revision as an override. Withhold override, delete, and action/* from delivery teams.
Treat exec, create as production shell access. It is kubectl exec through the UI. Grant it to nobody by default.
Keep policy.default minimal. Every authenticated user gets the default role, and a deny rule cannot take it away.
Harden every AppProject (Projects docs). The built-in default project allows any repo, any cluster, and every resource kind, so strip it. Then give each team a project with an explicit sourceRepos list, destinations pinned to a server plus namespace (no '*'), a narrow clusterResourceWhitelist, and a namespaceResourceBlacklist. For CI, use project roles with tokens from argocd proj role create-token instead of a shared admin token. The CLI issues those tokens with no expiry unless you set one.
Put policy.csv under its own review. RBAC drift is the silent failure. Manage argocd-rbac-cm from Git with its own CODEOWNERS, and alert on any change to it in the Kubernetes audit log.
Your topology decides how much of this you repeat:
Topology
Blast radius
RBAC complexity
Audit consolidation effort
SSO / group management
Upgrade overhead
Single central ArgoCD
One compromise reaches every cluster
One policy.csv, many AppProjects
Low: one event stream
One Dex/OIDC config
One instance
ArgoCD per cluster
Limited to one cluster
Same policy duplicated N times, drifts
High: N event streams to merge
N OIDC clients
N instances
Hub-and-spoke with ApplicationSets
Hub compromise reaches all spokes
Central policy, cluster generators
Medium: hub stream plus per-cluster audit logs
One per hub
Hub plus spoke agents
How do you enable Kubernetes audit logging on every cluster and keep it tamper-evident?
Enable API server auditing per cluster with a policy that records metadata for everything and full request bodies for writes to workloads and RBAC. Then ship the logs off-cluster to an append-only store right away, because whoever compromises a cluster can delete logs that stay inside it. Upstream Kubernetes logs nothing at all if --audit-policy-file is omitted (Kubernetes auditing).
The four levels are None, Metadata, Request, and RequestResponse, and the first matching rule wins. A starting policy:
YAML
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
rules:
# Who touched Secrets and ConfigMaps, never their contents
- level: Metadata
resources:
- group: ""
resources: ["secrets", "configmaps"]
# Full bodies for writes to workloads and RBAC
- level: RequestResponse
verbs: ["create", "update", "patch", "delete"]
resources:
- group: "apps"
resources: ["deployments", "statefulsets", "daemonsets"]
- group: "rbac.authorization.k8s.io"
resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
# Drop read noise
- level: None
verbs: ["get", "list", "watch"]
# Everything else, including pods/exec, at Metadata
- level: Metadata
Keep Secrets at Metadata. RequestResponse on Secrets writes secret values into your log store.
Provider by provider:
Amazon EKS. The control plane log typesapi, audit, authenticator, controllerManager, and scheduler are each off by default, and enabled logs bill standard CloudWatch Logs ingestion and storage. Turn on at least audit and authenticator, which maps IAM principals to Kubernetes users.
Google GKE.Admin Activity audit logs are always on and cannot be disabled. Data Access logs are off by default. The _Required bucket keeps logs for 400 days and the _Default bucket for 30 by default (Logging quotas).
Azure AKS. Control plane resource logs are not collected until you create a diagnostic setting. Microsoft warns kube-audit can be expensive and recommends kube-audit-admin, which excludes get and list events.
Self-managed. Set --audit-policy-file and --audit-log-path, rotate with --audit-log-maxage, --audit-log-maxbackup, and --audit-log-maxsize, and forward with Fluent Bit or Vector.
Capture the identity layer too: AWS CloudTrail, GCP Cloud Audit Logs, and the Azure Activity Log tell you who assumed the role that got cluster-admin. Land everything in a bucket with S3 Object Lock in compliance mode, where no user, including the root user, can delete or overwrite an object version during retention. Set retention to cover your whole audit observation period, not the provider default.
Ship faster on infrastructure you control.
Qovery gives your team self-service deployments on your own AWS, GCP, Azure, or Scaleway account - or your existing Kubernetes cluster - with per-environment RBAC and a deployment audit trail out of the box. Start deploying in under 10 minutes.
How do you join Git, ArgoCD, and cluster logs into one trail security can read?
Normalise every source into one five-field event (actor, action, target, commit SHA, timestamp) and land it in a single store. Security can then answer "who approved and shipped this" with one query instead of four tools and a Slack thread.
The pipeline is Git audit stream plus ArgoCD Notifications webhooks plus Kubernetes audit logs, into Fluent Bit or Vector, into S3, Loki, Splunk, or Datadog. The ArgoCD half fits in argocd-notifications-cm:
Send the same events as deployment markers to your observability tool, so incidents line up against deploys.
Auditors ask for two reports. "Every change to cluster X in the last 90 days with its approver" is a join of ArgoCD events to Git reviews on revision, plus Kubernetes audit writes not made by the ArgoCD service account (your out-of-band changes). "Everyone who could have deployed to production on date Y" needs policy.csv and IdP group membership as of that date, which is why both belong in Git.
Evidence source
What it proves
Default retention
Export effort
Gap it leaves
Git PR reviews
Who approved the change
GitHub Enterprise Cloud audit log: 180 days
Low: native streaming
Says nothing about what ran
Signed commits
Who authored the commit
Lives in Git history
None
Nothing about approval or deploy
ArgoCD sync events
Which SHA synced where, and who clicked sync
10 history entries; Events 1 hour
Medium: Notifications templates
No approver; auto-sync has no human
Kubernetes API audit logs
What changed in the cluster and by which identity
Off until enabled; then provider-specific
Medium to high, per cluster
ArgoCD writes show a service account
Cloud IAM logs (CloudTrail, Cloud Audit Logs, Activity Log)
Who obtained cluster credentials
Provider-specific
Low to medium
No link to a commit
Budget for it: weeks of platform work up front, then recurring work every time you add a cluster, a cloud provider, or an SSO group.
What are your options if you do not want to assemble this pipeline yourself?
You have three realistic paths: keep DIY ArgoCD plus a SIEM and own the correlation, buy a managed Argo control plane like Akuity or Codefresh, or use an internal developer platform like Qovery that runs on your own clusters and attaches the human actor to every deployment.
Managed Argo solves a real half of the problem. Akuity runs Argo CD with a hybrid agent model and advertises "audit by default", and Codefresh GitOps offers its own Argo-based control plane. You still own Git approval correlation and Kubernetes API audit logging.
Promotion gates are a separate layer. Kargo, from the Argo creators at Akuity, models each step between stages as a Promotion resource, which puts back the explicit, recorded step that plain ArgoCD auto-sync removes. Argo Rollouts adds progressive delivery with pauses inside a single stage. Flux CD has the same attribution gap as ArgoCD and pairs with policy tools like Kyverno or OPA Gatekeeper.
Qovery gets the outcome without a dedicated platform team building the pipeline above:
Your own cloud account (BYOC). Clusters run in your AWS, GCP, Azure, or Scaleway account, or you attach an existing Kubernetes cluster. CloudTrail, Cloud Audit Logs, the Azure Activity Log, and your Savings Plans still apply.
Per-environment RBAC.Permissions are set per project and per environment type (Development, Preview, Staging, Production), from No Access to Full Access. Shell and port-forward are separate permissions. SSO over SAML or OIDC maps IdP groups to roles.
Deployment history.Each deployment records the user who triggered it, the commit hash or image tag, the trigger origin (Git, Console, API, CLI, Terraform), and the status. Audit logs record who changed what and through which tool, with retention that depends on your plan.
Managed cluster upgrades with staged weekly maintenance, dev and staging first, production two days later.
Qovery is not a drop-in ArgoCD replacement if your platform contract is the Application CRD. It is not a SIEM, and it does not ingest your Kubernetes API audit logs. The common hybrid keeps ArgoCD for platform infrastructure and cluster add-ons, and uses a developer platform for application delivery.
Approach
Who-approved traceability
RBAC granularity
Multi-cluster setup effort
Data residency
Ongoing maintenance
DIY ArgoCD + SIEM (Datadog, Splunk)
Full, once you build the Git-ArgoCD-Kubernetes join
AppProject + policy.csv
High
Yours
High
Managed ArgoCD (Akuity, Codefresh)
Consolidated sync history; Git and Kubernetes audit still yours
Argo RBAC with hosted SSO
Medium
Control plane hosted by vendor, workloads in your clusters
Medium
Flux CD + Kyverno/OPA
Same gap as ArgoCD; policy blocks bad changes
Kubernetes RBAC per tenant
Medium to high
Yours
High
Kargo / Argo Rollouts on ArgoCD
Recorded promotion steps between stages
Kargo projects on Kubernetes RBAC
Medium, on top of ArgoCD
Yours
Medium
Qovery
User and commit on every deployment
Per project and environment type, SSO group mapping
Low
Your own cloud account or cluster
Low; managed cluster upgrades
What should you check this week? A ten-point audit-readiness checklist
Run these ten checks against every cluster. Most teams fail at least three, and the usual suspects are a local admin account still enabled, sync and override granted to the same group, and audit logging only ever turned on for the first cluster.
Is admin.enabled: "false" set, and is every ArgoCD login SSO-backed with group claims?
Does policy.csv grant applications, sync and applications, override to the same group?
Does any AppProject, including default, allow destination server '*' or an unrestricted sourceRepos?
Are API server audit logs enabled on every cluster, including the ones created after the first?
Are those logs shipped off-cluster to an append-only store that covers your audit period?
Can you name, right now, the reviewer of the commit running in production?
Do bot or CI tokens have direct push access to the GitOps repo?
Is exec or port-forward granted to anyone who does not need production shell access?
Is source integrity verification enforced on projects that deploy to production?
Is argocd-rbac-cm under code review and change alerting, or edited in place with kubectl?
How do I set this up with Qovery from Claude Code or Cursor?
Install the Qovery Agent Skills once and your coding agent can set up deployments, environments, and access on your own clusters. Add the MCP Server, read-only by default, when you want the agent to inspect what already exists, including deployment history.
curl -fsSL https://skill.qovery.com/install.sh | bash
# Or scope the install to the current project
curl -fsSL https://skill.qovery.com/install.sh | bash -s -- --project
# Add the Qovery MCP Server to Claude Code (OAuth, read-only by default)
claude mcp add --transport http qovery https://mcp.qovery.com/mcp --callback-port 4242
# Write access, only when you want it (also enable it in the Qovery Console)
claude mcp add --transport http qovery "https://mcp.qovery.com/mcp?read_write=true" --callback-port 4242
The skills are qovery-onboard, qovery-deploy, qovery-troubleshoot, qovery-optimize, qovery-speedup, qovery-preview, and qovery-terraform. Then ask: "deploy this service to my GCP cluster with Qovery and show me who deployed the last three releases." Actions the agent takes stay attributed to the authenticated human whose credentials it uses, which is the audit property you want now that agents deploy too.
How do you get full audit trails and RBAC across multiple Kubernetes clusters running ArgoCD so security can see who approved every deploy?
Join four sources on the commit SHA: Git PR reviews for the approver, ArgoCD sync events for what synced where, Kubernetes API audit logs for cluster-side writes, and cloud IAM logs for credential access. Enable API server auditing on every cluster, export ArgoCD events with Notifications, and land everything in an append-only store. For RBAC, disable the ArgoCD admin account, use SSO groups in policy.csv, and scope every role to a locked-down AppProject.
Does ArgoCD record which user triggered a sync?
Yes, for manual syncs. ArgoCD stores the SSO username in status.operationState.operation.initiatedBy.username. Automated syncs set initiatedBy.automated: true with no human, and the Kubernetes audit log attributes the write to ArgoCD's service account.
How do I prove to an auditor who approved a production deployment in a GitOps setup?
Show the pull request review on a protected branch for the commit SHA that ArgoCD synced, with stale-approval dismissal, code owner review, and no admin bypass enabled. Then show the ArgoCD sync event for that SHA and the matching Kubernetes audit entries. Stream the GitHub or GitLab audit log off-platform, because GitHub Enterprise Cloud keeps general audit events for 180 days and Git events for seven.
Is one central ArgoCD or one ArgoCD per cluster better for RBAC and auditing?
A central ArgoCD is easier to audit: one policy.csv, one SSO config, one event stream. Its compromise reaches every cluster, so AppProject destinations must be strict. ArgoCD per cluster limits blast radius but multiplies RBAC drift and log consolidation work by the number of clusters.
Are Kubernetes audit logs enabled by default on EKS, GKE, and AKS?
Mostly no. Amazon EKS control plane logs, including audit, are off by default and bill through CloudWatch Logs. GKE always writes Admin Activity audit logs, but Data Access logs are off by default. AKS collects no control plane logs until you create a diagnostic setting, and kube-audit-admin is the lower-cost category.
How long should Kubernetes audit logs be retained for SOC 2 or ISO 27001?
Long enough to cover your full audit observation period plus the time your auditor needs to sample evidence, which your auditor sets for you. Provider defaults are often shorter: the GKE _Default log bucket keeps logs for 30 days. Store exported logs in a write-once bucket such as S3 Object Lock in compliance mode.
Can I get per-environment RBAC and a deployment audit trail without running ArgoCD myself?
Yes. Qovery deploys into your own AWS, GCP, Azure, or Scaleway account or an existing Kubernetes cluster, with permissions per project and environment type, SSO group mapping, and a deployment history recording the user and commit for each deployment. Qovery is not a SIEM and does not ingest Kubernetes API audit logs, so you still enable cluster auditing yourself.
Romaric founded Qovery to make Kubernetes accessible to every engineering team. He writes about platform strategy, developer experience, and the future of cloud infrastructure.
Next step
Ship faster on infrastructure you control.
Qovery gives your team self-service deployments on your own AWS, GCP, Azure, or Scaleway account - or your existing Kubernetes cluster - with per-environment RBAC and a deployment audit trail out of the box. Start deploying in under 10 minutes.