Skip to main content
RunBook Academy

Git, CI/CD & GitOpsLXXXIII · GitOps RBACArgoCDRBAC

Argo CD RBAC in detail — projects, policy rows, and the Casbin model

Advanced⏱ ~26 mingitargocdkubectl

What you'll learn

  • Read a policy row and name each of its six fields and the value space of each
  • Explain how Casbin evaluates default policy, user policies, and group policies to one effect
  • Distinguish global RBAC in argocd-rbac-cm from project-scoped roles in an AppProject
  • Validate and test a policy change before it reaches a running Argo CD instance

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

Argo CD has no user database. It has one built-in superuser, an identity provider integration, and a policy engine that turns claims from that provider into decisions about resources. The policy is a CSV, the engine is Casbin, and the whole authorisation surface of the platform is a few dozen lines of comma-separated text that most teams write once and never read again. Reading it properly is the difference between an RBAC configuration and the appearance of one.

The two row types

Everything is expressed with two shapes. A group row binds a subject to a role; a policy row grants or denies something to a subject.

g, platform-team, role:platform-admin
p, role:platform-admin, applications, sync, platform-prod/*, allow
p, role:platform-admin, exec, create, platform-prod/*, deny

The policy row has six fields, in order: the literal p, the subject, the resource, the action, the object, and the effect. The resource is drawn from a fixed set — applications, applicationsets, clusters, projects, repositories, accounts, certificates, gpgkeys, logs, exec, and extensions — and each resource accepts only some actions. Applications accept get, create, update, delete, sync, rollback, action, and override; exec accepts only create; logs accepts only get.

The object field is where most mistakes live. For application-specific resources it is project/application, so a row whose object is a bare application name matches nothing. Fine-grained forms extend the action instead of the object: an action of delete/*/Pod/*/* grants deletion of Pods inside an application without granting deletion of the Application itself.

flowchart TB
    A["Request from user or group"] --> B["Evaluate policy.default"]
    B -->|"allow or deny decided"| C["Effect returned immediately"]
    B -->|"undefined"| D["Evaluate rows for the user"]
    D --> E["Evaluate rows for each group"]
    E --> F{"any deny matched?"}
    F -->|"yes"| G["Denied"]
    F -->|"no, at least one allow"| H["Allowed"]
    F -->|"no rows matched"| I["Denied by absence"]

Where the policy lives

Two places, with different scopes and different reviewers:

  • argocd-rbac-cm in the Argo CD namespace holds the global policy under policy.csv, the fallback role under policy.default, and the OIDC claims to inspect under scopes. Additional keys matching policy.NAME.csv are concatenated after the main policy, which is how overlays add team rules without editing a shared file.
  • AppProject.spec.roles holds project-scoped roles. Their subjects are written as proj:PROJECT:ROLE, their groups list maps identity-provider claims directly onto the role, and their reach is limited to that project.
argocd proj create platform-prod \
  -d https://kubernetes.default.svc,platform-prod \
  -s https://github.com/example/platform-gitops
kubectl apply -f rbac.yaml

The practical split: put roles that cross projects in the ConfigMap and roles that belong to one team in that team AppProject, so the team can own its own reviewer list without holding write access to the global policy.

How the effect is decided

Casbin evaluates in a fixed order and the row order in the file is irrelevant. The default policy is checked first, and if it produces an effect that effect stands. Otherwise the rows for the user are evaluated, then the rows for each group the user belongs to. A matched deny wins over any number of matching allow rows, regardless of specificity. With no match at all, access is refused.

Matching is glob-based by default, and the glob does not treat / as a separator. A pattern of platform-* therefore matches platform-prod/api as readily as platform-prod, which is why object patterns should always be written with every segment present.

Testing before shipping

The policy is text, so it can be checked without a running instance. argocd admin settings rbac validate parses a policy file and reports syntax the engine will not understand; argocd admin settings rbac can answers a concrete question about a subject, action, resource, and object against either a local file or the live ConfigMap. Running both in CI on any change to the policy turns an RBAC review into a diff plus a set of assertions, which is the only form of review that survives a busy week.

Production discipline

  1. policy.default names a minimal role, never readonly, on a multi-tenant instance. Verify anonymous access is disabled at the same time.
  2. Object patterns are fully qualified. Every segment present, no bare names, no leading wildcards on the project segment.
  3. Every policy change carries a test. Add the assertions to CI in the same pull request that adds the rows.
  4. Project roles for teams, global policy for the platform. A team that needs a global row is a signal that the project boundary is drawn in the wrong place.

Cross-course references

  • Kubernetes for Production Sysadmins - Part LVII (Authentication) and Part LVIII (RBAC) cover the identity and authorisation models Argo CD sits alongside rather than replaces.
  • Git, CI/CD & GitOps for Infrastructure Engineers - Part LXXVII (ArgoCD) introduced AppProjects, and Part CXVI (Governance) covers the review process that keeps a policy file honest over years.
  • Observability for Production Sysadmins - Part LXXIX (SecureGrafana) covers the same class of problem in a tool where the default role is also more generous than teams expect.

Quiz

Knowledge check · 4 questions

  1. Q1. An instance ships with policy.default set to role:readonly and a deny row intended to hide one tenant project from everyone else. What actually happens?

  2. Q2. The order in which policy rows appear in the CSV changes which effect is returned.

  3. Q3. Name the six fields of an Argo CD policy row in order, and give the object format used for application-specific resources.

  4. Q4. Explain how a group with no intended production access ended up syncing a production application.

    A policy contains the row p, role:tooling, applications, *, tool-*/*, allow, bound to an identity-provider group that any engineer can self-join. A project named tool-prod-bridge was created to host a shared ingress controller and reconciles into the production cluster. An engineer in the tooling group synced a change to it out of hours; the sync succeeded and the change reached production.

Passing score: 75%. Answers are checked in this browser.