azure-ad-sso
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAzure AD SSO Integration Skill
Azure AD SSO集成技能
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.
本技能为部署在Kubernetes集群上的应用提供实现Azure AD(Entra ID)OAuth2/OIDC单点登录的全面指导,包括通过Azure AD组限制访问的配置方法。
Quick Reference
快速参考
Supported Applications
支持的应用程序
| Application | Provider | Redirect URI Pattern | Group Sync |
|---|---|---|---|
| DefectDojo | | | Yes |
| Grafana | | | Yes |
| ArgoCD | | | Yes |
| Harbor | | | Yes |
| SonarQube | | | Yes |
| OAuth2 Proxy | | | Yes |
| Keycloak | | | Yes |
| 应用程序 | 提供者 | 重定向URI模式 | 组同步 |
|---|---|---|---|
| DefectDojo | | | 是 |
| Grafana | | | 是 |
| ArgoCD | | | 是 |
| Harbor | | | 是 |
| SonarQube | | | 是 |
| OAuth2 Proxy | | | 是 |
| Keycloak | | | 是 |
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 │
│ │
└─────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────┐
│ 访问控制决策 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Q: 哪些用户可以访问此应用? │
│ │
│ ├─ 租户内所有用户 ──► appRoleAssignmentRequired=false │
│ │ │
│ └─ 特定组 ────► appRoleAssignmentRequired=true │
│ + 将组分配到企业应用 │
│ │
└─────────────────────────────────────────────────────────────────┘Implementation Workflow
实施工作流
Phase 1: Azure AD App Registration
阶段1:Azure AD应用注册
bash
undefinedbash
undefined1. Create App Registration
1. 创建应用注册
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)
--display-name "$APP_NAME"
--sign-in-audience "AzureADMyOrg"
--web-redirect-uris "$REDIRECT_URI"
--query appId -o tsv)
echo "Application (client) ID: $APP_ID"
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)
--display-name "$APP_NAME"
--sign-in-audience "AzureADMyOrg"
--web-redirect-uris "$REDIRECT_URI"
--query appId -o tsv)
echo "应用(客户端)ID: $APP_ID"
2. Get Tenant ID
2. 获取租户ID
TENANT_ID=$(az account show --query tenantId -o tsv)
echo "Directory (tenant) ID: $TENANT_ID"
TENANT_ID=$(az account show --query tenantId -o tsv)
echo "目录(租户)ID: $TENANT_ID"
3. Create Client Secret
3. 创建客户端密钥
SECRET=$(az ad app credential reset
--id $APP_ID
--append
--years 1
--query password -o tsv)
--id $APP_ID
--append
--years 1
--query password -o tsv)
echo "Client Secret: $SECRET" # Save immediately!
undefinedSECRET=$(az ad app credential reset
--id $APP_ID
--append
--years 1
--query password -o tsv)
--id $APP_ID
--append
--years 1
--query password -o tsv)
echo "客户端密钥: $SECRET" # 请立即保存!
undefinedPhase 2: Enable Group Claims
阶段2:启用组声明
bash
undefinedbash
undefinedEnable security group claims in tokens
在令牌中启用安全组声明
az ad app update --id $APP_ID --set groupMembershipClaims=SecurityGroup
az ad app update --id $APP_ID --set groupMembershipClaims=SecurityGroup
Add Group.Read.All permission (delegated)
添加Group.Read.All权限(委托型)
az ad app permission add
--id $APP_ID
--api 00000003-0000-0000-c000-000000000000
--api-permissions 5f8c59db-677d-491f-a6b8-5f174b11ec1d=Scope
--id $APP_ID
--api 00000003-0000-0000-c000-000000000000
--api-permissions 5f8c59db-677d-491f-a6b8-5f174b11ec1d=Scope
az ad app permission add
--id $APP_ID
--api 00000003-0000-0000-c000-000000000000
--api-permissions 5f8c59db-677d-491f-a6b8-5f174b11ec1d=Scope
--id $APP_ID
--api 00000003-0000-0000-c000-000000000000
--api-permissions 5f8c59db-677d-491f-a6b8-5f174b11ec1d=Scope
Grant admin consent
授予管理员同意
az ad app permission admin-consent --id $APP_ID
undefinedaz ad app permission admin-consent --id $APP_ID
undefinedPhase 3: Restrict Access by Group (CRITICAL)
阶段3:按组限制访问(关键步骤)
bash
undefinedbash
undefinedGet Service Principal object ID
获取服务主体对象ID
SP_ID=$(az ad sp list --filter "appId eq '$APP_ID'" --query "[0].id" -o tsv)
SP_ID=$(az ad sp list --filter "appId eq '$APP_ID'" --query "[0].id" -o tsv)
Enable user assignment requirement
启用用户分配要求
az ad sp update --id $SP_ID --set appRoleAssignmentRequired=true
az ad sp update --id $SP_ID --set appRoleAssignmentRequired=true
Get the group ID to restrict access
获取用于限制访问的组ID
GROUP_ID=$(az ad group show --group "G-Usuarios-<App>-Admin" --query id -o tsv)
GROUP_ID=$(az ad group show --group "G-Usuarios-<App>-Admin" --query id -o tsv)
Assign group to the application (only these users can login)
将组分配到应用(仅该组内的用户可登录)
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" }"
--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" }"
undefinedaz 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" }"
--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" }"
undefinedPhase 4: Store Secret in Key Vault
阶段4:在Key Vault中存储密钥
bash
az keyvault secret set \
--vault-name "<keyvault-name>" \
--name "<app>-azuread-client-secret" \
--value "$SECRET"bash
az keyvault secret set \
--vault-name "<keyvault-name>" \
--name "<app>-azuread-client-secret" \
--value "$SECRET"Secret Management
密钥管理
SecretProviderClass Template
SecretProviderClass模板
yaml
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-secretyaml
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-secretPod Volume Mount
Pod卷挂载
yaml
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: trueyaml
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: trueApplication Configurations
应用程序配置
DefectDojo
DefectDojo
yaml
undefinedyaml
undefinedEnable SSO
启用SSO
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
Group sync
- 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-.*"
CRITICAL: For apps behind reverse proxy
- name: DD_SECURE_PROXY_SSL_HEADER value: "True"
undefinedextraEnv:
- 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"
undefinedGrafana
Grafana
yaml
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'yaml
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)
ArgoCD(通过Dex)
yaml
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:adminyaml
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:adminHarbor
Harbor
yaml
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: trueyaml
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: trueTroubleshooting
故障排查
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 |
| AADSTS65001 | Consent not granted | Run |
| 错误代码 | 描述 | 解决方案 |
|---|---|---|
| AADSTS50011 | 回复URL不匹配 | 验证重定向URI是否完全一致,包括末尾斜杠 |
| AADSTS50105 | 用户未被分配权限 | 将用户/组添加到企业应用的分配列表中 |
| AADSTS700016 | 应用未找到 | 检查客户端ID和租户ID是否正确 |
| AADSTS7000218 | 密钥已过期 | 在Key Vault中轮换密钥,重启Pod |
| AADSTS90102 | 无效的redirect_uri | 对于反向代理后的应用,检查是否设置了 |
| AADSTS65001 | 未授予同意权限 | 执行 |
Common Issues
常见问题
Malformed redirect_uri (Django apps behind proxy)
格式错误的redirect_uri(反向代理后的Django应用)
Symptom:
redirect_uri=https,%20https://...Root cause: set incorrectly
DD_SECURE_PROXY_SSL_HEADERFix:
yaml
- name: DD_SECURE_PROXY_SSL_HEADER
value: "True" # NOT "HTTP_X_FORWARDED_PROTO,https"症状:
redirect_uri=https,%20https://...根因: 配置错误
DD_SECURE_PROXY_SSL_HEADER修复方案:
yaml
- name: DD_SECURE_PROXY_SSL_HEADER
value: "True" # 不要设置为"HTTP_X_FORWARDED_PROTO,https"Groups not syncing
组未同步
bash
undefinedbash
undefinedVerify group claims enabled
验证组声明是否启用
az ad app show --id <app-id> --query groupMembershipClaims
az ad app show --id <app-id> --query groupMembershipClaims
Check API permissions
检查API权限
az ad app permission list --id <app-id>
az ad app permission list --id <app-id>
Verify group exists and user is member
验证组是否存在且用户为组成员
az ad group member check --group "<group-name>" --member-id "<user-object-id>"
undefinedaz ad group member check --group "<group-name>" --member-id "<user-object-id>"
undefinedSecret not syncing from Key Vault
密钥未从Key Vault同步
bash
undefinedbash
undefinedCheck SecretProviderClass
检查SecretProviderClass配置
kubectl describe secretproviderclass <name> -n <namespace>
kubectl describe secretproviderclass <name> -n <namespace>
Check CSI driver pods
检查CSI驱动Pod状态
kubectl get pods -n kube-system | grep secrets-store
kubectl get pods -n kube-system | grep secrets-store
Check managed identity access
验证托管身份的访问权限
az keyvault show --name <vault> --query properties.accessPolicies
undefinedaz keyvault show --name <vault> --query properties.accessPolicies
undefinedDiagnostic Commands
诊断命令
bash
undefinedbash
undefinedTest OAuth redirect
测试OAuth重定向
curl -sS -k -D - -o /dev/null "https://<app>/login/<provider>/" 2>&1 | grep -i location
curl -sS -k -D - -o /dev/null "https://<app>/login/<provider>/" 2>&1 | grep -i location
Check environment variables in pod
检查Pod中的环境变量
kubectl exec -n <ns> deploy/<app> -c <container> -- env | grep -i azure
kubectl exec -n <ns> deploy/<app> -c <container> -- env | grep -i azure
Decode JWT token (after login, from browser dev tools)
解码JWT令牌(登录后从浏览器开发者工具获取)
Use https://jwt.io to decode and verify claims
undefinedundefinedSecurity 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
- 切勿硬编码密钥 - 始终使用Key Vault + CSI驱动
- 使用托管身份 - 避免使用服务主体凭据
- 按组限制访问 - 启用
appRoleAssignmentRequired=true - 定期轮换密钥 - 在密钥过期前设置日历提醒
- 仅使用HTTPS - 所有重定向URI必须使用HTTPS
- 单租户模式 - 内部应用切勿使用多租户模式
- 启用审计日志 - 开启Azure AD登录日志
Environment Reference
环境参考
| Environment | Key Vault | Managed Identity | Tenant ID |
|---|---|---|---|
| cafehyna-dev | | | |
| cafehyna-hub | | | |
| cafehyna-prd | | | |
| 环境 | Key Vault | 托管身份 | 租户ID |
|---|---|---|---|
| cafehyna-dev | | | |
| cafehyna-hub | | | |
| cafehyna-prd | | | |
Detailed Reference
详细参考
For complete implementation examples:
- references/azure-ad-sso-guide.md - Full guide with manifests
- references/app-configs.md - Application-specific configurations
- references/troubleshooting.md - Extended troubleshooting guide
完整的实施示例请查看:
- references/azure-ad-sso-guide.md - 包含清单文件的完整指南
- references/app-configs.md - 应用程序专属配置
- references/troubleshooting.md - 扩展故障排查指南