ArgoCD SSO and RBAC Deep Dive
Wiring up Dex or direct OIDC for single sign-on, and writing fine-grained RBAC policy - roles, groups, and per-project scoping
Introduction
Out of the box, ArgoCD’s only login is a local admin account with a bootstrap password - fine for a first argocd login, not something you want to be the only access path once real teams are using the cluster. Getting this right is two separate pieces: authentication (who can log in - SSO via Dex or a direct OIDC provider) and authorization (what they can do once logged in - RBAC policy mapping identities to permissions). Getting only one right is a common, incomplete setup: SSO with everyone landing on the built-in readonly role, or RBAC groups referencing an SSO integration that was never actually finished.
Authentication: Dex vs Direct OIDC
ArgoCD ships with Dex, an OIDC provider that itself proxies to upstream identity providers (GitHub, GitLab, Google, SAML, LDAP, Microsoft) - it’s the default path because it normalizes all of those into one OIDC flow ArgoCD’s core understands natively:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
url: https://argocd.example.com
dex.config: |
connectors:
- type: github
id: github
name: GitHub
config:
clientID: $dex.github.clientID
clientSecret: $dex.github.clientSecret
orgs:
- name: example-org
teams:
- platform-team
- sre-team
apiVersion: v1
kind: Secret
metadata:
name: argocd-secret
namespace: argocd
stringData:
dex.github.clientID: your-github-oauth-app-client-id
dex.github.clientSecret: your-github-oauth-app-client-secret
If your identity provider already speaks OIDC natively (Okta, Azure AD/Entra ID, Google Workspace), you can skip Dex entirely and configure ArgoCD’s oidc.config directly in argocd-cm instead - one fewer moving part, at the cost of losing Dex’s ability to normalize non-OIDC providers (SAML, LDAP) into the same flow:
data:
oidc.config: |
name: Okta
issuer: https://example.okta.com
clientID: your-okta-client-id
clientSecret: $oidc.okta.clientSecret
requestedScopes: ["openid", "profile", "email", "groups"]
requestedScopes must include groups (or whatever your provider calls group claims) if RBAC policy is going to map on group membership - without it, ArgoCD only knows the user’s identity, not which teams they belong to, and every group-based g, policy line below silently matches nobody.
Authorization: RBAC Policy
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
data:
policy.default: role:readonly
policy.csv: |
p, role:platform-admin, applications, *, */*, allow
p, role:platform-admin, clusters, *, *, allow
p, role:platform-admin, repositories, *, *, allow
p, role:app-developer, applications, get, team-a/*, allow
p, role:app-developer, applications, sync, team-a/*, allow
p, role:app-developer, applications, action/*, team-a/*, allow
g, example-org:platform-team, role:platform-admin
g, example-org:team-a-devs, role:app-developer
scopes: '[groups]'
policy.default is the fallback role for anyone authenticated but not matched by any g, line - role:readonly (a role ArgoCD provides built in, alongside role:admin) is the safe default, so a new SSO user who isn’t yet mapped to a team gets read access, not accidental admin. p, lines define permissions (subject, resource, action, object, effect); g, lines map an identity or group to a role. example-org:platform-team is Dex’s GitHub-connector format for a team (org:team) - the exact group-string format is provider-specific (Azure AD groups are object IDs by default, for instance), so it’s worth confirming with argocd account get-user-info after login rather than assuming the format.
applications, sync, team-a/* scopes the permission to a specific AppProject (team-a) rather than every Application cluster-wide - the object field’s format is <project>/<application-name>, so team-a/* grants it across every app in that project without touching any other team’s.
Scoping with AppProjects
RBAC alone can restrict what actions a role can take, but pairing it with AppProject restricts what an Application in that project is even allowed to deploy - the two work together:
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: team-a
namespace: argocd
spec:
sourceRepos:
- https://github.com/example/team-a-*
destinations:
- namespace: team-a-*
server: https://kubernetes.default.svc
clusterResourceWhitelist: [] # no cluster-scoped resources allowed
namespaceResourceBlacklist:
- group: ""
kind: ResourceQuota
Even a user with role:app-developer’s full sync permission on team-a/* can only ever sync Applications whose AppProject is team-a - and that project itself can’t deploy to another team’s namespace, source from a repo outside the allowed pattern, or (via the empty clusterResourceWhitelist) create any cluster-scoped resource at all. This is the actual multi-tenancy boundary; RBAC policy without project-level restrictions still lets an over-permissioned Application deploy anything the ArgoCD service account itself can reach.
Local Users for Break-Glass Access
argocd account update-password --account admin
argocd account generate-token --account ci-bot
SSO should be the normal login path, but keeping the admin local account (with its password rotated and MFA-equivalent protections in place operationally) as a break-glass fallback is standard practice - an SSO outage shouldn’t also mean nobody can operate ArgoCD. Local accounts also cover service-to-service auth (ci-bot here) where there’s no human to authenticate via SSO at all.
Best Practices
- Set
policy.default: role:readonly, neverrole:admin- an unmapped SSO login should never default to full access; make every elevated permission an explicitg,mapping. - Pair RBAC roles with
AppProjectrestrictions, not RBAC alone -sourceRepos/destinations/clusterResourceWhitelistare the actual tenancy boundary; RBAC policy without them is trusting the Application’s declared scope rather than enforcing it. - Confirm the SSO provider’s group-claim format before writing
g,lines -argocd account get-user-infoafter a real login shows exactly what group strings ArgoCD actually received, rather than guessing at the provider’s documented format. - Rotate and restrict local account credentials even though SSO is the primary path -
adminand any service-account tokens are still full-access local credentials and deserve the same operational hygiene as any other credential.
Conclusion
SSO answers “who is this,” RBAC answers “what can they do,” and AppProject answers “what can any Application in their scope ever touch, regardless of who’s operating it” - all three together are what “multi-team ArgoCD instance” actually requires; any one alone leaves a real gap.