Back to Distributed Systems & Kubernetes Series

Argo CD Track Part 4: RBAC & Projects

June 6, 2026 Wasil Zafar 25 min read

A single Argo CD instance can serve an entire organization — but only if you can give each team access to their own Applications without letting them touch anyone else's. AppProjects are Argo CD's multi-tenancy mechanism. RBAC controls who can do what within each project. SSO eliminates password management entirely.

Table of Contents

  1. AppProjects
  2. RBAC
  3. SSO Integration
  4. Declarative User Management
  5. Exercises
  6. Key Takeaways & Next Steps

AppProjects

Project Anatomy

An AppProject defines a security boundary in Argo CD. Every Application belongs to a project (default if unspecified). Projects control three things: which Git repos are allowed as sources, which clusters/namespaces are allowed as destinations, and which Kubernetes resource types can be deployed:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: grade-api-project
  namespace: argocd
spec:
  description: "Project for the Grade API team"

  # ── SOURCE RESTRICTIONS ─────────────────────────────────────────
  sourceRepos:
    - "https://github.com/your-org/grade-api-gitops"
    - "https://github.com/your-org/grade-api"
    - "oci://ghcr.io/your-org/charts"
    # Use '*' to allow all repos (not recommended for production)

  # ── DESTINATION RESTRICTIONS ────────────────────────────────────
  destinations:
    - server: "https://kubernetes.default.svc"
      namespace: "grade-api-dev"
    - server: "https://kubernetes.default.svc"
      namespace: "grade-api-staging"
    - server: "https://prod-cluster.example.com"
      namespace: "production"
    # Wildcard namespaces allowed:
    - server: "https://kubernetes.default.svc"
      namespace: "grade-api-*"

  # ── RESOURCE ALLOW/DENY LISTS ───────────────────────────────────
  # If clusterResourceWhitelist is empty, no cluster-scoped resources allowed
  clusterResourceWhitelist:
    - group: ""
      kind: Namespace         # Allow creating Namespaces
    - group: "rbac.authorization.k8s.io"
      kind: ClusterRoleBinding  # Allow CRBs for service accounts

  # Namespace-scoped resources allowed (if empty, all allowed)
  namespaceResourceWhitelist:
    - group: "*"
      kind: "*"               # Allow all namespace-scoped resources

  # Or: block specific resource types
  namespaceResourceBlacklist:
    - group: ""
      kind: ResourceQuota     # Teams can't create/modify ResourceQuotas
    - group: ""
      kind: LimitRange

  # ── ROLES ────────────────────────────────────────────────────────
  roles:
    - name: developer
      description: "Grade API developers"
      policies:
        - p, proj:grade-api-project:developer, applications, get, grade-api-project/*, allow
        - p, proj:grade-api-project:developer, applications, sync, grade-api-project/*, allow
        - p, proj:grade-api-project:developer, applications, action/*, grade-api-project/*, allow
      groups:
        - "github.com:grade-api-team"  # GitHub team mapping

    - name: readonly
      description: "Read-only access for stakeholders"
      policies:
        - p, proj:grade-api-project:readonly, applications, get, grade-api-project/*, allow
      groups:
        - "github.com:grade-api-stakeholders"

  # ── SYNC WINDOWS (review from Part 2) ────────────────────────────
  syncWindows:
    - kind: allow
      schedule: "0 9 * * 1-5"
      duration: 8h
      applications:
        - "*"
      manualSync: true

  # ── ORPHANED RESOURCE MONITORING ─────────────────────────────────
  # Alert when namespaced resources exist that Argo CD doesn't manage
  orphanedResources:
    warn: true               # Warn (don't sync fail) for unmanaged resources
    ignore:
      - group: ""
        kind: ConfigMap
        name: kube-root-ca.crt  # Ignore auto-injected CMs

Source Repo Restrictions

# Examples of sourceRepos patterns

# Allow only from your organization:
sourceRepos:
  - "https://github.com/your-org/*"    # All repos in org (glob)

# Allow specific OCI charts:
sourceRepos:
  - "oci://ghcr.io/your-org/*"

# Block all except one repo (strict):
sourceRepos:
  - "https://github.com/your-org/grade-api-gitops"

# Helm chart repos:
sourceRepos:
  - "https://charts.bitnami.com/bitnami"
  - "https://prometheus-community.github.io/helm-charts"

RBAC

The RBAC Model

Argo CD RBAC uses a Casbin policy format. Policies are stored in the argocd-rbac-cm ConfigMap and define what subjects (users, groups, or roles) can do on which resources:

# RBAC policy syntax:
# p, <subject>, <resource>, <action>, <object>, <effect>

# Examples:
# Allow role 'developer' to sync any application in 'my-project':
p, role:developer, applications, sync, my-project/*, allow

# Allow GitHub user 'alice' to get all applications:
p, alice, applications, get, */*, allow

# Allow GitHub team to create applications in specific project:
p, role:team-lead, applications, create, grade-api-project/*, allow

# Deny sync to production for developers (deny takes precedence):
p, role:developer, applications, sync, */production-*, deny

Roles & Policies

# argocd-rbac-cm ConfigMap — global RBAC policies
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: argocd
data:
  # Default policy for unauthenticated/unmatched users
  policy.default: role:readonly

  # CSV-formatted policy rules
  policy.csv: |
    # ── Platform team: full admin ────────────────────────────────
    p, role:platform-admin, *, *, */*, allow

    # ── Developer role: can deploy but not delete cluster resources ─
    p, role:developer, applications, get, */*, allow
    p, role:developer, applications, sync, */*, allow
    p, role:developer, applications, action/*, */*, allow
    p, role:developer, logs, get, */*, allow
    p, role:developer, exec, create, */*, allow

    # ── Operator role: can sync and manage, but not create/delete apps ─
    p, role:operator, applications, get, */*, allow
    p, role:operator, applications, sync, */*, allow
    p, role:operator, applications, action/*, */*, allow

    # ── Readonly role: view only ──────────────────────────────────
    p, role:readonly, applications, get, */*, allow
    p, role:readonly, repositories, get, *, allow

    # ── Group to role bindings ────────────────────────────────────
    g, github.com:platform-team, role:platform-admin
    g, github.com:backend-developers, role:developer
    g, github.com:sre-team, role:operator

  # OIDC scopes to include in token (for group membership)
  scopes: "[groups, email]"

Built-in Roles

  • role:admin — Full access to everything. Only the Argo CD admin account has this by default.
  • role:readonly — Read-only access to all Applications and their status. Good default for stakeholders.
Security principle: Set policy.default: role:readonly to ensure unauthenticated users and unmatched authenticated users get read-only access, not no access (which might default to admin in some configurations). Never set policy.default: role:admin in production.

SSO Integration

GitHub SSO

# argocd-cm — configure Dex for GitHub OAuth
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  # URL where Argo CD is accessible
  url: https://argocd.company.com

  # Dex OIDC configuration
  dex.config: |
    connectors:
      - type: github
        id: github
        name: GitHub
        config:
          clientID: $github-client-id     # from Secret 'argocd-secret'
          clientSecret: $github-client-secret
          # Redirect users here after GitHub auth
          redirectURI: https://argocd.company.com/api/dex/callback
          # Restrict to specific GitHub org
          orgs:
            - name: your-org
              teams:
                - platform-team
                - backend-developers
                - sre-team
          # Load group membership into the token
          loadAllGroups: false    # only load teams listed above
          useLoginAsID: false
# Create GitHub OAuth App at: github.com/organizations/your-org/settings/applications
# Homepage URL: https://argocd.company.com
# Authorization callback URL: https://argocd.company.com/api/dex/callback

# Store credentials in argocd-secret
kubectl create secret generic argocd-github-sso \
  --from-literal=github-client-id=YOUR_CLIENT_ID \
  --from-literal=github-client-secret=YOUR_CLIENT_SECRET \
  -n argocd

# Patch argocd-secret to include the github credentials
# (Or manage via external-secrets-operator — see External Secrets deep-dive)

Group-to-Role Mapping

# After GitHub SSO setup, groups appear as:
# github.com:your-org:team-name

# In argocd-rbac-cm:
g, github.com:your-org:platform-team, role:platform-admin
g, github.com:your-org:backend-developers, role:developer
g, github.com:your-org:sre-team, role:operator

# Verify your user's groups after login:
argocd account get-user-info
# Name:   alice
# Email:  alice@company.com
# Groups: [github.com:your-org:backend-developers]

# Check if a user would be allowed an action:
argocd admin settings rbac can \
  alice applications sync \
  grade-api-project/grade-api-prod
# YES (allowed)

Declarative User Management

# argocd-cm — define local accounts (for CI/CD pipelines, not humans)
# Use SSO for human users; local accounts for service accounts
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  url: https://argocd.company.com

  # CI pipeline service account (can only sync specific projects)
  accounts.ci-pipeline: apiKey
  accounts.ci-pipeline.enabled: "true"

  # Read-only monitoring account for observability tools
  accounts.grafana-argocd: apiKey
  accounts.grafana-argocd.enabled: "true"

  dex.config: |
    # ... SSO config above ...
# argocd-rbac-cm — policies for local accounts
data:
  policy.csv: |
    # CI pipeline can sync but not create/delete apps
    p, ci-pipeline, applications, sync, */*, allow
    p, ci-pipeline, applications, get, */*, allow

    # Grafana can only read (for Argo CD metrics)
    p, grafana-argocd, applications, get, */*, allow
    p, grafana-argocd, projects, get, *, allow
# Generate API token for CI pipeline
argocd account generate-token \
  --account ci-pipeline \
  --expires-in 8760h    # 1 year

# Use token in CI (GitHub Actions secret, GitLab variable, etc.)
# ARGOCD_TOKEN: <token from above>

# CI pipeline login
argocd login argocd.company.com \
  --auth-token $ARGOCD_TOKEN \
  --grpc-web

# Sync from CI
argocd app sync grade-api-prod \
  --auth-token $ARGOCD_TOKEN \
  --timeout 120

Exercises

Exercise 1 — AppProject: Create an AppProject that restricts deployments to a single namespace (grade-api-dev). Assign your grade-api Application to this project. Attempt to change the Application's destination to a different namespace — verify Argo CD rejects the sync with a permission error.
Exercise 2 — Local Accounts & RBAC: Create a local account called readonly-user with only get permissions. Create an API token for it. Try to sync an Application using that token — verify you get a permission denied error. Try argocd app list — verify it succeeds.
Exercise 3 — Project Roles: Add a project-scoped role in the AppProject that allows developers to sync but not delete. Create a group binding. Use argocd admin settings rbac can to verify the expected permissions without needing real users.

Key Takeaways & Next Steps

Key Takeaways:
  • AppProjects enforce tenancy: restrict which repos, clusters, namespaces, and resource types each team can use
  • RBAC in argocd-rbac-cm uses Casbin CSV format: p, <subject>, <resource>, <action>, <object>, <effect>
  • Set policy.default: role:readonly — never role:admin
  • Use SSO (GitHub, Okta, Azure AD) for human users; local accounts only for CI pipelines and service accounts
  • Group mappings: g, github.com:org:team, role:my-role map GitHub teams to Argo CD roles
  • All RBAC config should be managed declaratively in ConfigMaps, never via the UI

Next in This Track

In Part 5: Notifications & Image Updater, we complete the GitOps loop — Argo CD notifications for Slack/Teams/email alerts on sync events, and the Argo CD Image Updater for automatic image tag promotion when new container images are published.