SECURITY
Adding Entra ID single sign-on to Argo CD
Configuring Entra ID OIDC through the argo-cd Helm chart, mapping groups to roles, and the group overage claim that breaks it in larger tenants.
Argo CD ships with a single local admin account. That is fine for a proof of concept and a liability everywhere else: the credential is shared, it does not expire with an employee, and nothing in the audit trail tells you who performed an action. Wiring it to Entra ID replaces that with real identities and group-based access. Argo CD speaks OIDC natively, so Dex is not required, and the official argo-cd Helm chart exposes every setting involved.
Register the application in Entra ID
Create an app registration in the tenant. Four settings matter, and getting the redirect URIs wrong accounts for most failed first attempts.
- A Web redirect URI of
https://argocd.example.com/auth/callback. Note that this differs from the Dex path,/api/dex/callback, which does not apply here. - A second redirect URI of
http://localhost:8085/auth/callbackif engineers will runargocd login --ssofrom the CLI. That is the port the CLI listens on by default. - A client secret. Copy the value at creation time, because Entra will not show it again.
- A groups claim under Token configuration, otherwise the token arrives with no group membership and every user falls through to the default policy.
Put the configuration in the chart values
The argo-cd chart exposes the server ConfigMap under configs.cm and the RBAC ConfigMap under configs.rbac, so the whole integration is a values file rather than a set of hand-applied manifests. Set global.domain as well: the chart derives the external URL from it, and Argo CD builds the OIDC redirect from that URL. Leaving it unset produces a callback to the wrong host.
global:
domain: argocd.example.com
configs:
cm:
admin.enabled: true
oidc.config: |
name: Entra ID
issuer: https://login.microsoftonline.com/<tenant-id>/v2.0
clientID: <application-client-id>
clientSecret: $argocd-entra:clientSecret
requestedScopes:
- openid
- profile
- email
requestedIDTokenClaims:
groups:
essential: true
rbac:
policy.default: ""
policy.csv: |
g, 7f9c2b41-0d3e-4a86-9a1f-2c5d8e6b4a30, role:admin
g, c14a8d52-6b7f-4e91-8d20-3af5c9e17b64, role:readonlyEntra emits group object IDs rather than display names, so the RBAC rules reference GUIDs. This is unreadable but stable, which is the correct trade-off: renaming a group in Entra will not silently revoke access. Leaving policy.default empty means an authenticated user with no matching group gets nothing. Set it deliberately, because a default of role:readonly grants every person in the tenant visibility of every application.
Keep the client secret out of the values file
The dollar prefix on clientSecret is an Argo CD secret reference, not a shell variable. The chart will write the value for you through configs.secret.extra, but that places a live credential in values.yaml and therefore in Git. Reference a separately managed Secret instead. Argo CD resolves the $name:key form against any Secret in its namespace carrying the argocd part-of label.
apiVersion: v1
kind: Secret
metadata:
name: argocd-entra
namespace: argocd
labels:
app.kubernetes.io/part-of: argocd
type: Opaque
stringData:
clientSecret: <client-secret-value>In practice this Secret comes from External Secrets or SOPS rather than a literal manifest. The label is the part most often missed: without it Argo CD will not read the Secret at all, and the server logs a failure to resolve the reference rather than anything that mentions Entra.
Apply the release
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
helm upgrade --install argocd argo/argo-cd \
--namespace argocd --create-namespace \
--values values.yamlIf Argo CD manages itself, the same values belong in the Application that points at the chart rather than in a manual helm upgrade. Either way the server picks up ConfigMap changes without a restart, so a failed login is a configuration problem rather than a rollout that has not landed yet.
The group overage claim
This is the failure that appears once you roll out beyond a pilot group. If a user belongs to more than roughly 200 groups, Entra stops emitting the groups array and substitutes a pair of claims, _claim_names and _claim_sources, pointing at a Microsoft Graph endpoint. Argo CD does not follow that reference. The user authenticates successfully, arrives with no groups, and matches no RBAC rule.
- Set the groups claim to groups assigned to the application rather than all security groups. This is the fix in most tenants.
- Alternatively define app roles on the registration, assign groups to those roles, and map the
rolesclaim instead ofgroups. - Confirm the outcome by decoding the
id_tokenand checking that a groups array is actually present.
A user who signs in successfully but sees no applications is almost always a claims problem, not an RBAC problem. Decode the token before editing policy.csv.
Verify, then close the back door
- The login page offers a Log in via Entra ID button and completes the round trip without an error.
argocd login argocd.example.com --ssoopens a browser and returns a working token.- The
argocd-serverlogs show no claim or issuer mismatch during the callback. - Only once all three pass, set
configs.cm.admin.enabledto false and upgrade the release again.
Disabling the local admin before SSO is confirmed working will lock you out of the UI. Recovery means flipping the value back and running helm upgrade, or patching the ConfigMap directly with kubectl, so it is not fatal. It is still an avoidable ten minutes.
Summary
Direct OIDC against Entra is a values file and one Secret. The configuration itself is short. The parts worth attention are the redirect URIs, the part-of label on the Secret, an explicit policy.default, and the group overage claim, which fails quietly and looks like a permissions bug rather than a token one.