Webinar · Oct 20: The migration takes 2 weeks. Deciding to do it takes 6 months.

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.

Romaric Philogene
CEO & Co-founder
OCT 26, 2026 · 12 MIN
Who Approved That Deploy? Audit Trails and RBAC Across Multi-Cluster ArgoCD

Key Points:

  • 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.

Qovery · Agentic Infrastructure Platform
A control plane for platform teams and their coding agents
Learn more

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.

The fields with audit value:

Application fieldWhat it tells you
status.sync.revisionThe commit SHA ArgoCD compared and applied
status.operationState.operation.initiatedBy.usernameThe SSO user who started a manual sync
status.operationState.operation.initiatedBy.automatedtrue if the application controller started it
status.operationState.startedAt / finishedAtWhen the sync ran
status.operationState.syncResult.resourcesPer-resource result of the sync
status.historyPast 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.

The GitHub settings that make an approval mean something (about protected branches):

  • 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>.

CSV|argocd-rbac-cm -> policy.csv
p, role:payments-deployer, applications, get, payments/*, allow
p, role:payments-deployer, applications, sync, payments/*, allow
p, role:payments-deployer, logs, get, payments/*, allow
p, role:platform-admin, applications, override, */*, allow
p, role:platform-admin, applications, delete, */*, allow
g, my-org:payments-devs, role:payments-deployer
g, my-org:platform, role:platform-admin
  • 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:

TopologyBlast radiusRBAC complexityAudit consolidation effortSSO / group managementUpgrade overhead
Single central ArgoCDOne compromise reaches every clusterOne policy.csv, many AppProjectsLow: one event streamOne Dex/OIDC configOne instance
ArgoCD per clusterLimited to one clusterSame policy duplicated N times, driftsHigh: N event streams to mergeN OIDC clientsN instances
Hub-and-spoke with ApplicationSetsHub compromise reaches all spokesCentral policy, cluster generatorsMedium: hub stream plus per-cluster audit logsOne per hubHub 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 types api, 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.

FieldSource
Actor and approverGit provider audit stream (author, reviewers, merger)
Action and revisionArgoCD Notifications (initiatedBy, sync.revision)
TargetArgoCD (spec.destination.server, namespace, app name)
Cluster-side writeKubernetes API audit log
Privilege pathCloudTrail, Cloud Audit Logs, Azure Activity Log

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:

YAML
service.webhook.audit-sink: |
  url: https://audit.example.com/argocd
  headers:
  - name: Content-Type
    value: application/json
template.audit-event: |
  webhook:
    audit-sink:
      method: POST
      body: |
        {
          "app": "{{.app.metadata.name}}",
          "cluster": "{{.app.spec.destination.server}}",
          "namespace": "{{.app.spec.destination.namespace}}",
          "revision": "{{.app.status.sync.revision}}",
          "initiatedBy": "{{.app.status.operationState.operation.initiatedBy.username}}",
          "automated": "{{.app.status.operationState.operation.initiatedBy.automated}}",
          "phase": "{{.app.status.operationState.phase}}",
          "finishedAt": "{{.app.status.operationState.finishedAt}}"
        }
trigger.on-sync-audit: |
  - when: app.status.operationState.phase in ['Succeeded', 'Failed', 'Error']
    send: [audit-event]

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 sourceWhat it provesDefault retentionExport effortGap it leaves
Git PR reviewsWho approved the changeGitHub Enterprise Cloud audit log: 180 daysLow: native streamingSays nothing about what ran
Signed commitsWho authored the commitLives in Git historyNoneNothing about approval or deploy
ArgoCD sync eventsWhich SHA synced where, and who clicked sync10 history entries; Events 1 hourMedium: Notifications templatesNo approver; auto-sync has no human
Kubernetes API audit logsWhat changed in the cluster and by which identityOff until enabled; then provider-specificMedium to high, per clusterArgoCD writes show a service account
Cloud IAM logs (CloudTrail, Cloud Audit Logs, Activity Log)Who obtained cluster credentialsProvider-specificLow to mediumNo 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.
  • Preview environments per pull request and environment auto-stop for non-production.

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.

ApproachWho-approved traceabilityRBAC granularityMulti-cluster setup effortData residencyOngoing maintenance
DIY ArgoCD + SIEM (Datadog, Splunk)Full, once you build the Git-ArgoCD-Kubernetes joinAppProject + policy.csvHighYoursHigh
Managed ArgoCD (Akuity, Codefresh)Consolidated sync history; Git and Kubernetes audit still yoursArgo RBAC with hosted SSOMediumControl plane hosted by vendor, workloads in your clustersMedium
Flux CD + Kyverno/OPASame gap as ArgoCD; policy blocks bad changesKubernetes RBAC per tenantMedium to highYoursHigh
Kargo / Argo Rollouts on ArgoCDRecorded promotion steps between stagesKargo projects on Kubernetes RBACMedium, on top of ArgoCDYoursMedium
QoveryUser and commit on every deploymentPer project and environment type, SSO group mappingLowYour own cloud account or clusterLow; 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.

  1. Is admin.enabled: "false" set, and is every ArgoCD login SSO-backed with group claims?
  2. Does policy.csv grant applications, sync and applications, override to the same group?
  3. Does any AppProject, including default, allow destination server '*' or an unrestricted sourceRepos?
  4. Are API server audit logs enabled on every cluster, including the ones created after the first?
  5. Are those logs shipped off-cluster to an append-only store that covers your audit period?
  6. Can you name, right now, the reviewer of the commit running in production?
  7. Do bot or CI tokens have direct push access to the GitOps repo?
  8. Is exec or port-forward granted to anyone who does not need production shell access?
  9. Is source integrity verification enforced on projects that deploy to production?
  10. 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.

BASH|Install Qovery Agent Skills globally (Claude Code, Cursor, Codex, OpenCode, Gemini CLI)
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.

Details: AI agent quickstart and Claude Code integration.

Frequently asked questions
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 Philogene
About the author
Romaric Philogene

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.