| name | azure-ad-sso |
| description | Azure AD OAuth2/OIDC SSO integration for Kubernetes applications. Use when implementing Single Sign-On, configuring Azure AD App Registrations, restricting access by groups, or integrating tools (DefectDojo, Grafana, ArgoCD, Harbor, SonarQube) with Azure AD authentication. |
Azure AD SSO Integration Skill
Overview
This skill provides comprehensive guidance for implementing Azure AD (Entra ID) OAuth2/OIDC Single Sign-On for applications deployed on Kubernetes clusters, including access restriction by Azure AD groups.
Quick Reference
Supported Applications
| Application | Provider | Redirect URI Pattern | Group Sync |
|---|
| DefectDojo | azuread-tenant-oauth2 | /complete/azuread-tenant-oauth2/ | Yes |
| Grafana | azuread | /login/azuread | Yes |
| ArgoCD | microsoft (Dex) | /api/dex/callback | Yes |
| Harbor | oidc | /c/oidc/callback | Yes |
| SonarQube | saml or oidc | /oauth2/callback/saml | Yes |
| OAuth2 Proxy | azure | /oauth2/callback | Yes |
| Keycloak | oidc | /realms/{realm}/broker/azure/endpoint | Yes |
Authentication Flow Decision
┌─────────────────────────────────────────────────────────────────┐
│ Access Control Decision │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Q: Who should access this application? │
│ │
│ ├─ Everyone in tenant ──► appRoleAssignmentRequired=false │
│ │ │
│ └─ Specific groups ────► appRoleAssignmentRequired=true │
│ + Assign groups to Enterprise App │
│ │
└─────────────────────────────────────────────────────────────────┘
Implementation Workflow
Phase 1: Azure AD App Registration
APP_NAME="<application>-<environment>"
REDIRECT_URI="https://<app-domain>/complete/<provider>/"
APP_ID=$(az ad app create \
--display-name "$APP_NAME" \
--sign-in-audience "AzureADMyOrg" \
--web-redirect-uris "$REDIRECT_URI" \
--query appId -o tsv)
echo "Application (client) ID: $APP_ID"
TENANT_ID=$(az account show --query tenantId -o tsv)
echo "Directory (tenant) ID: $TENANT_ID"
SECRET=$(az ad app credential reset \
--id $APP_ID \
--append \
--years 1 \
--query password -o tsv)
echo "Client Secret: $SECRET"
Phase 2: Enable Group Claims
az ad app update --id $APP_ID --set groupMembershipClaims=SecurityGroup
az ad app permission add \
--id $APP_ID \
--api 00000003-0000-0000-c000-000000000000 \
--api-permissions 5f8c59db-677d-491f-a6b8-5f174b11ec1d=Scope
az ad app permission admin-consent --id $APP_ID
Phase 3: Restrict Access by Group (CRITICAL)
SP_ID=$(az ad sp list --filter "appId eq '$APP_ID'" --query "[0].id" -o tsv)
az ad sp update --id $SP_ID --set appRoleAssignmentRequired=true
GROUP_ID=$(az ad group show --group "G-Usuarios-<App>-Admin" --query id -o tsv)
az rest --method POST \
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/$SP_ID/appRoleAssignments" \
--headers "Content-Type=application/json" \
--body "{
\"principalId\": \"$GROUP_ID\",
\"principalType\": \"Group\",
\"appRoleId\": \"00000000-0000-0000-0000-000000000000\",
\"resourceId\": \"$SP_ID\"
}"
Phase 4: Store Secret in Key Vault
az keyvault secret set \
--vault-name "<keyvault-name>" \
--name "<app>-azuread-client-secret" \
--value "$SECRET"
Secret Management
SecretProviderClass Template
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: <app>-secrets
namespace: <namespace>
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "true"
userAssignedIdentityID: "<managed-identity-client-id>"
keyvaultName: "<keyvault-name>"
tenantId: "<azure-tenant-id>"
objects: |
array:
- |
objectName: <app>-azuread-client-secret
objectType: secret
objectAlias: AZURE_AD_CLIENT_SECRET
secretObjects:
- secretName: <app>-azure-ad
type: Opaque
data:
- objectName: AZURE_AD_CLIENT_SECRET
key: client-secret
Pod Volume Mount
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "<app>-secrets"
volumeMounts:
- name: secrets-store
mountPath: "/mnt/secrets-store"
readOnly: true
Application Configurations
DefectDojo
extraEnv:
- name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_ENABLED
value: "True"
- name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY
value: "<client-id>"
- name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID
value: "<tenant-id>"
- name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET
valueFrom:
secretKeyRef:
name: defectdojo
key: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET
- name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GET_GROUPS
value: "True"
- name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_CLEANUP_GROUPS
value: "True"
- name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GROUPS_FILTER
value: "^G-Usuarios-DefectDojo-.*"
- name: DD_SECURE_PROXY_SSL_HEADER
value: "True"
Grafana
grafana.ini:
auth.azuread:
enabled: true
name: Azure AD
allow_sign_up: true
client_id: "<client-id>"
client_secret: "${GF_AUTH_AZUREAD_CLIENT_SECRET}"
scopes: openid email profile
auth_url: https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize
token_url: https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
allowed_groups: "<admin-group-id> <viewer-group-id>"
role_attribute_path: contains(groups[*], '<admin-group-id>') && 'Admin' || 'Viewer'
ArgoCD (via Dex)
configs:
cm:
dex.config: |
connectors:
- type: microsoft
id: microsoft
name: Azure AD
config:
clientID: "<client-id>"
clientSecret: $dex.azure.clientSecret
tenant: "<tenant-id>"
redirectURI: https://<argocd-domain>/api/dex/callback
groups:
- <admin-group-id>
rbac:
policy.csv: |
g, <admin-group-id>, role:admin
Harbor
externalURL: https://harbor.<domain>
core:
oidc:
name: "azure"
endpoint: "https://login.microsoftonline.com/<tenant-id>/v2.0"
clientId: "<client-id>"
clientSecret: "<from-secret>"
scope: "openid,profile,email"
groupsClaim: "groups"
adminGroup: "<admin-group-id>"
autoOnboard: true
Troubleshooting
Error Reference
| Error Code | Description | Solution |
|---|
| AADSTS50011 | Reply URL mismatch | Verify exact redirect URI including trailing slash |
| AADSTS50105 | User not assigned | Add user/group to Enterprise App assignments |
| AADSTS700016 | App not found | Check client ID and tenant ID |
| AADSTS7000218 | Secret expired | Rotate secret in Key Vault, restart pods |
| AADSTS90102 | Invalid redirect_uri | Check DD_SECURE_PROXY_SSL_HEADER=True for reverse proxy |
| AADSTS65001 | Consent not granted | Run az ad app permission admin-consent |
Common Issues
Malformed redirect_uri (Django apps behind proxy)
Symptom: redirect_uri=https,%20https://...
Root cause: DD_SECURE_PROXY_SSL_HEADER set incorrectly
Fix:
- name: DD_SECURE_PROXY_SSL_HEADER
value: "True"
Groups not syncing
az ad app show --id <app-id> --query groupMembershipClaims
az ad app permission list --id <app-id>
az ad group member check --group "<group-name>" --member-id "<user-object-id>"
Secret not syncing from Key Vault
kubectl describe secretproviderclass <name> -n <namespace>
kubectl get pods -n kube-system | grep secrets-store
az keyvault show --name <vault> --query properties.accessPolicies
Diagnostic Commands
curl -sS -k -D - -o /dev/null "https://<app>/login/<provider>/" 2>&1 | grep -i location
kubectl exec -n <ns> deploy/<app> -c <container> -- env | grep -i azure
Security Best Practices
- Never hardcode secrets - Always use Key Vault + CSI Driver
- Use managed identities - Avoid service principal credentials
- Restrict access by group - Enable
appRoleAssignmentRequired=true
- Rotate secrets - Set calendar reminders before expiration
- Use HTTPS only - All redirect URIs must use HTTPS
- Single tenant - Never use multi-tenant for internal apps
- Audit logging - Enable Azure AD sign-in logs
Environment Reference
| Environment | Key Vault | Managed Identity | Tenant ID |
|---|
| example-app-dev | kv-example-dev-hlg | <WORKLOAD_IDENTITY_CLIENT_ID> | <TENANT_ID> |
| example-app-hub | kv-example-default | <WORKLOAD_IDENTITY_CLIENT_ID> | <TENANT_ID> |
| example-app-prd | kv-example-prd | <WORKLOAD_IDENTITY_CLIENT_ID> | <TENANT_ID> |
Detailed Reference
For complete implementation examples:
Gotchas
- Group claims default to objectId, not name — enable
optionalClaims: groups: SecurityGroup AND set token version 2 in the manifest, or apps that match by name see nothing.
- Token scopes:
https://graph.microsoft.com/.default scope grants admin-consented permissions only; User.Read.All on a delegated token still requires admin consent at first use.
- OIDC redirect URIs require EXACT match — trailing slash mismatch returns AADSTS50011 with a vague error that looks like a network issue.
- Group-claims overage at 200 groups: users in >200 groups get NO group claims (overage). Switch to Graph query for membership lookup.
- App-token lifetime policies are per-app — default 1 hour. Long batch jobs hitting hour-2 silently 401 unless you wrap every call with refresh-on-401.