Git, CI/CD & GitOpsLXXXIII · GitOps RBACArgoCDRBAC
Argo CD RBAC in detail — projects, policy rows, and the Casbin model
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
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-cmin the Argo CD namespace holds the global policy underpolicy.csv, the fallback role underpolicy.default, and the OIDC claims to inspect underscopes. Additional keys matchingpolicy.NAME.csvare concatenated after the main policy, which is how overlays add team rules without editing a shared file.AppProject.spec.rolesholds project-scoped roles. Their subjects are written asproj: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
policy.defaultnames a minimal role, never readonly, on a multi-tenant instance. Verify anonymous access is disabled at the same time.- Object patterns are fully qualified. Every segment present, no bare names, no leading wildcards on the project segment.
- Every policy change carries a test. Add the assertions to CI in the same pull request that adds the rows.
- 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
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?
Q2. The order in which policy rows appear in the CSV changes which effect is returned.
Q3. Name the six fields of an Argo CD policy row in order, and give the object format used for application-specific resources.
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.