Microsoft Entra ID
Configure Microsoft Entra ID for deployment
Overview
The Guardian Platform integrates with Microsoft Entra ID (formerly Azure Active Directory) for enterprise-grade authentication and authorization.
Required app registrations:
- Guardian Platform Backend API – Web application for backend API authentication and token validation
- Microsoft Graph API Access – App with
User.Read.Allfor user lookups via Microsoft Graph; the bootstrap chart reuses the backend app - Governance Worker – M2M confidential client for automated governance workflow execution
- Guardian SPA – Single Page Application for frontend authentication
The entra-bootstrap Helm chart automates most of this: it runs a one-time Kubernetes Job that creates the backend, frontend, and worker app registrations, the access_as_user scope, and the Microsoft Graph permissions. It uses the backend app for Graph access instead of a separate registration. Run it before deploying the platform chart.
This guide covers:
- Creating app registrations with the bootstrap chart, or by hand with the Azure CLI or Portal
- Configuring API permissions and scopes
- Setting up Microsoft Graph access
- Organization and admin user setup by the post-install hook
- Generating the token signing key
- Configuring the Helm chart for Entra ID
Prerequisites
- Microsoft Entra ID tenant (Azure AD tenant)
- Azure CLI installed and configured
- Permissions to create app registrations and grant admin consent in the tenant
- Tenant ID (found in Azure Portal > Microsoft Entra ID > Overview)
kubectlandhelmaccess to the deployment namespacejq1.6 or later, to read the bootstrap’s generated credentials- A clone of the deployment repository for the
entra-bootstrapchart
Helm Configuration
Add the following to the values.yaml and secrets.yaml files. The setup steps below fill in the placeholders.
values.yaml:
auth-service:
config:
idp:
provider: "entra"
issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0"
entra:
tenantId: "<tenant-id>"
tokenExchange:
enabled: true
keyId: "auth-service-001" # Identifier for the token signing key
governance-service:
config:
authProvider: "entra"
entraTenantId: "<tenant-id>"
guardian:
config:
authProvider: "entra"
entraTenantId: "<tenant-id>"
entraClientId: "<spa-client-id>" # Frontend (SPA) app
entraScopes: "openid profile email offline_access api://<backend-client-id>/access_as_user"secrets.yaml:
global:
secrets:
create: true
auth:
provider: "entra"
entra:
secretName: "platform-entra"
values:
clientId: "<backend-client-id>"
clientSecret: "<backend-client-secret>"
tenantId: "<tenant-id>"
graphClientId: "<graph-client-id>" # The backend app when using the bootstrap chart
graphClientSecret: "<graph-client-secret>"
tokenExchangePrivateKey: | # Generate with: openssl genrsa 2048
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
governanceWorker:
secretName: "platform-governance-worker"
values:
encryptionKey: "<worker-encryption-key>" # Generate with: openssl rand -base64 32
clientId: "<worker-client-id>"
clientSecret: "<worker-client-secret>"Entra deployments always use token exchange: the Auth Service signs the platform’s access tokens with tokenExchangePrivateKey, which the chart stores as the token-exchange-private-key key of the platform-entra secret. Every Auth Service replica reads that key, so they all sign with the same one. With global.secrets.create: true, rendering fails without it; if you create platform-entra yourself, add the token-exchange-private-key key. guardctl generates the key and sets tokenExchange.
Quick Start (Bootstrap Chart - Recommended)
Create a service principal for the bootstrap, run the entra-bootstrap chart, and copy the IDs and secrets it saves into the platform’s chart values, in six steps.
1. Create the Bootstrap Service Principal
The bootstrap job calls Microsoft Graph as its own service principal, which needs Application.ReadWrite.All and DelegatedPermissionGrant.ReadWrite.All (application permissions). Sign in to the tenant and create it:
az login --tenant <tenant-id>
BOOTSTRAP_APP_ID=$(az ad app create --display-name "Governance Bootstrap SP" --query appId -o tsv)
az ad sp create --id $BOOTSTRAP_APP_ID
# Application.ReadWrite.All and DelegatedPermissionGrant.ReadWrite.All
az ad app permission add --id $BOOTSTRAP_APP_ID \
--api 00000003-0000-0000-c000-000000000000 \
--api-permissions 1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9=Role 8e8e4742-1d2d-4f22-b4fa-e31d2a2e3798=Role
az ad app permission admin-consent --id $BOOTSTRAP_APP_ID
# Confirm both Graph permissions are listed
az ad app permission list --id $BOOTSTRAP_APP_ID -o table
az ad app credential reset --id $BOOTSTRAP_APP_ID --display-name "bootstrap" --years 2Save the password from the last command as the bootstrap client secret. Admin consent needs a Global Administrator (or Privileged Role Administrator); if admin-consent fails, have one grant it in Azure Portal > Microsoft Entra ID > App registrations > Governance Bootstrap SP > API permissions.
2. Create the Service Principal Secret
The job reads the service principal’s credentials from the entra-bootstrap-sp secret:
kubectl create secret generic entra-bootstrap-sp \
--from-literal=client-id=<bootstrap-app-id> \
--from-literal=client-secret=<bootstrap-client-secret> \
--namespace guardianThis secret is only for the bootstrap; the platform services use platform-entra.
3. Prepare the Bootstrap Values
From the deployment repository checkout, start from the example values file:
cp charts/entra-bootstrap/examples/values.yaml bootstrap-values.yamlEdit these keys in bootstrap-values.yaml:
entra:
tenantId: "<tenant-id>"
apps:
frontend:
redirectUris:
- "https://<domain>"
- "http://localhost:5173"The SPA redirect URIs come only from apps.frontend.redirectUris; the http://localhost:5173 entry is for local Guardian development and can be removed. If the tenant already has a Guardian Platform deployment, add a per-environment suffix to the displayName values under apps: the job reuses the app registration with exactly that display name and overwrites its redirect URIs, and fails if more than one app registration has the name.
4. Run the Bootstrap
helm upgrade --install entra-bootstrap ./charts/entra-bootstrap \
--namespace guardian \
--values bootstrap-values.yaml \
--wait \
--timeout 10m
kubectl logs job/entra-bootstrap-entra-bootstrap --namespace guardian
# Confirm the backend, frontend, and worker app registrations exist, beside Governance Bootstrap SP
az ad app list --filter "startswith(displayName,'Guardian Platform') or startswith(displayName,'Governance')" --query "[].{name:displayName, appId:appId}" -o tableThe log ends with an Entra ID Bootstrap Summary of the backend, frontend, and worker app IDs. The job saves the app IDs, the tenant ID, and new client secrets to the entra-bootstrap-credentials secret (<release>-credentials, or credentialsSecret.name), not to its logs. Read them with jq:
kubectl get secret entra-bootstrap-credentials --namespace guardian -o json \
| jq '.data | map_values(@base64d)'| Key | Value |
|---|---|
tenant-id | The directory (tenant) ID |
backend-client-id, backend-client-secret | The backend app’s credentials |
frontend-client-id | The frontend app ID |
worker-client-id, worker-client-secret | The worker app’s credentials |
Helm creates the secret empty and keeps it on uninstall, and the job can only read and update it.
Instead of these commands, the deployment repository’s helper script checks for the service principal secret named in the values file, runs the chart, prints the job logs and the command that reads the credentials, and then deletes the job: ./scripts/entra/bootstrap-entra.sh -f bootstrap-values.yaml -n guardian.
The job is idempotent: re-running it reuses app registrations with the same display name and keeps their client secrets. For an existing app with no client secret, it creates one and saves it. The job fails if it cannot create or save a secret.
5. Finish the App Registrations
The bootstrap’s own admin consent attempt fails without an administrator role, and it doesn’t set the worker’s Application ID URI. Signed in as a Global Administrator or Privileged Role Administrator:
# Graph User.Read.All, which the admin-user hook and the Auth Service need
az ad app permission admin-consent --id <backend-client-id>
# The SPA's delegated scopes, needed where user consent is turned off
az ad app permission admin-consent --id <frontend-client-id>
# The Auth Service requests worker tokens for api://<worker-client-id>
az ad app update --id <worker-client-id> \
--identifier-uris "api://<worker-client-id>"6. Update Chart Values
Fill in the Helm Configuration sections from the credentials secret:
<tenant-id>:tenant-idclientId/clientSecretandgraphClientId/graphClientSecret:backend-client-idandbackend-client-secretgovernanceWorker.values.clientId/clientSecret:worker-client-idandworker-client-secret, plus anencryptionKeyfromopenssl rand -base64 32entraClientId:frontend-client-id;entraScopes: the backend app ID inapi://<backend-client-id>/access_as_usertokenExchangePrivateKey: a token signing key fromopenssl genrsa 2048, including itsBEGINandENDlines
Add the organization and admin user as described in Create Organization and Admin User, then proceed to the Helm Deployment guide.
Alternative: Manual Setup via Azure CLI
Without the bootstrap chart, create the platform’s four app registrations with the Azure CLI and generate the token signing key, in six steps.
1. Authenticate to Azure
The tenant ID can be found in Azure Portal > Microsoft Entra ID > Overview. Login to Azure with the tenant:
az login --tenant <tenant-id>Verify the correct tenant:
az account showUpdate values.yaml with the tenant ID.
values.yaml -> auth-service.config.idp:
issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0"
entra:
tenantId: "<tenant-id>"values.yaml -> governance-service.config:
entraTenantId: "<tenant-id>"values.yaml -> guardian.config:
entraTenantId: "<tenant-id>"Update secrets.yaml with the tenant ID.
secrets.yaml -> global.secrets.auth.entra.values:
tenantId: "<tenant-id>"2. Create Backend API App Registration
Create the backend API app registration:
az ad app create \
--display-name "Guardian Platform Backend" \
--sign-in-audience AzureADMyOrgSave the appId and id from the output:
appIdis the Client IDidis the Object ID
Store both as variables for use in subsequent commands:
BACKEND_CLIENT_ID="<app-id-from-output>"
BACKEND_OBJECT_ID="<id-from-output>"Create a service principal and set the application ID URI:
az ad sp create --id $BACKEND_CLIENT_ID
az ad app update --id $BACKEND_CLIENT_ID \
--identifier-uris "api://$BACKEND_CLIENT_ID"Generate a unique ID for the API scope:
uuidgenSave this UUID — it will be used as SCOPE_ID in the next step.
Create a file named backend-scope.json with the following content. Replace SCOPE_ID with the UUID from above:
{
"api": {
"requestedAccessTokenVersion": 2,
"oauth2PermissionScopes": [
{
"id": "SCOPE_ID",
"adminConsentDescription": "Allow the application to access Guardian Platform on behalf of the signed-in user",
"adminConsentDisplayName": "Access Guardian Platform",
"isEnabled": true,
"type": "User",
"userConsentDescription": "Allow the application to access Guardian Platform on your behalf",
"userConsentDisplayName": "Access Guardian Platform",
"value": "access_as_user"
}
]
}
}Add the API scope to the app registration:
az rest --method PATCH \
--uri "https://graph.microsoft.com/v1.0/applications/$BACKEND_OBJECT_ID" \
--headers "Content-Type=application/json" \
--body @backend-scope.jsonCreate a client secret for the backend app:
az ad app credential reset \
--id $BACKEND_CLIENT_ID \
--append \
--query password \
--output tsvSave the client secret — it cannot be retrieved later.
Update secrets.yaml with the backend credentials.
secrets.yaml -> global.secrets.auth.entra.values:
clientId: "<backend-client-id>"
clientSecret: "<client-secret-from-output>"Update values.yaml with the backend client ID (used in the scopes string).
values.yaml -> guardian.config:
entraScopes: "openid profile email offline_access api://<backend-client-id>/access_as_user"3. Create Graph API Access App Registration
Create the Graph API access app registration:
az ad app create \
--display-name "Guardian Platform Graph API" \
--sign-in-audience AzureADMyOrgSave the appId from the output — this is the Graph API Client ID.
Store it as a variable for use in subsequent commands:
GRAPH_CLIENT_ID="<app-id-from-output>"Create a service principal for the app:
az ad sp create --id $GRAPH_CLIENT_IDAdd the Microsoft Graph API permission for reading users:
az ad app permission add \
--id $GRAPH_CLIENT_ID \
--api 00000003-0000-0000-c000-000000000000 \
--api-permissions df021288-bdef-4463-88db-98f22de89214=RoleThis adds User.Read.All (df021288-bdef-4463-88db-98f22de89214) – Read all users’ full profiles.
Grant admin consent for the API permissions (requires admin privileges):
az ad app permission admin-consent --id $GRAPH_CLIENT_IDCreate a client secret for the Graph API app:
az ad app credential reset \
--id $GRAPH_CLIENT_ID \
--append \
--query password \
--output tsvSave the client secret — it cannot be retrieved later.
Update secrets.yaml with the Graph API credentials.
secrets.yaml -> global.secrets.auth.entra.values:
graphClientId: "<graph-client-id>"
graphClientSecret: "<client-secret-from-output>"4. Create Governance Worker App Registration
The Governance Worker is a confidential M2M (machine-to-machine) client used for automated governance workflow execution.
Create the Governance Worker app registration:
az ad app create \
--display-name "EQTY Governance Worker" \
--sign-in-audience AzureADMyOrgSave the appId from the output — this is the Worker Client ID.
Store it as a variable for use in subsequent commands:
WORKER_CLIENT_ID="<app-id-from-output>"Create a service principal and set the application ID URI. The Auth Service requests worker tokens for api://<worker-client-id>, and Entra rejects that request if the URI is not set:
az ad sp create --id $WORKER_CLIENT_ID
az ad app update --id $WORKER_CLIENT_ID \
--identifier-uris "api://$WORKER_CLIENT_ID"Create a client secret for the Worker app:
az ad app credential reset \
--id $WORKER_CLIENT_ID \
--append \
--query password \
--output tsvSave the client secret — it cannot be retrieved later.
Generate an encryption key for the worker:
openssl rand -base64 32Update secrets.yaml with the worker credentials.
secrets.yaml -> global.secrets.governanceWorker.values:
encryptionKey: "<generated-key>"
clientId: "<worker-client-id>"
clientSecret: "<client-secret-from-output>"5. Create Frontend SPA App Registration
Create the SPA app registration:
az ad app create \
--display-name "Guardian Platform Frontend" \
--sign-in-audience AzureADMyOrgSave the appId and id from the output:
appIdis the SPA Client IDidis the SPA Object ID
Store both as variables:
SPA_CLIENT_ID="<app-id-from-output>"
SPA_OBJECT_ID="<id-from-output>"Configure SPA redirect URIs and set token version to v2.0. Replace <domain> with the Guardian Platform domain:
az rest --method PATCH \
--uri "https://graph.microsoft.com/v1.0/applications/$SPA_OBJECT_ID" \
--headers "Content-Type=application/json" \
--body "{\"spa\":{\"redirectUris\":[\"https://<domain>\"]},\"api\":{\"requestedAccessTokenVersion\":2}}"Create a service principal for the SPA app:
az ad sp create --id $SPA_CLIENT_IDGet the backend API scope ID:
az ad app show --id $BACKEND_CLIENT_ID --query 'api.oauth2PermissionScopes[0].id' -o tsvSave this scope ID — it will be used in the next command.
Grant the SPA access to the backend API. Replace <scope-id> with the value from above:
az ad app permission add \
--id $SPA_CLIENT_ID \
--api $BACKEND_CLIENT_ID \
--api-permissions <scope-id>=ScopeOptionally, grant admin consent for the API permission:
az ad app permission admin-consent --id $SPA_CLIENT_IDNote: Admin consent may require higher privileges. If this fails, grant consent later through the Azure Portal.
Update values.yaml with the SPA client ID.
values.yaml -> guardian.config:
entraClientId: "<spa-client-id>"6. Generate the Token Signing Key
Generate an RSA private key for the Auth Service to sign the platform’s access tokens:
openssl genrsa 2048Update secrets.yaml with the key, including its BEGIN and END lines.
secrets.yaml -> global.secrets.auth.entra.values:
tokenExchangePrivateKey: |
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----Alternative: Web UI Setup
Click to expand Azure Portal instructions
Using the Azure Portal
1. Create Backend API App Registration
- Navigate to Azure Portal > Microsoft Entra ID > App registrations
- Click “New registration”
- Name: “Guardian Platform Backend”
- Supported account types: “Accounts in this organizational directory only”
- Click “Register”
- Note the Application (client) ID and Directory (tenant) ID
- Navigate to “Manifest”
- Find “accessTokenAcceptedVersion” and set it to
2(for v2.0 tokens) - Click “Save”
- Navigate to “Expose an API”
- Set Application ID URI:
api://<client-id> - Add a scope:
- Scope name:
access_as_user - Admin consent display name: “Access Guardian Platform”
- Admin consent description: “Allow the application to access Guardian Platform on behalf of the signed-in user”
- State: Enabled
- Click “Add scope”
- Scope name:
- Navigate to “Certificates & secrets”
- Click “New client secret”
- Description: “Guardian Platform Secret”
- Expires: 24 months (or the organization’s policy)
- Click “Add”
- Copy the secret Value immediately — it won’t be shown again
Update values.yaml with the tenant ID and backend credentials.
values.yaml -> auth-service.config.idp:
issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0"
entra:
tenantId: "<tenant-id>"values.yaml -> governance-service.config:
entraTenantId: "<tenant-id>"values.yaml -> guardian.config:
entraTenantId: "<tenant-id>"
entraScopes: "openid profile email offline_access api://<backend-client-id>/access_as_user"Update secrets.yaml with the backend credentials.
secrets.yaml -> global.secrets.auth.entra.values:
clientId: "<backend-client-id>"
clientSecret: "<client-secret>"
tenantId: "<tenant-id>"2. Create Graph API Access App Registration
- Create another app registration: “Guardian Platform Graph API”
- Navigate to “API permissions”
- Click “Add a permission”
- Select “Microsoft Graph” > “Application permissions”
- Add:
User.Read.All - Click “Add permissions”
- Click “Grant admin consent” (requires admin role)
- Navigate to “Certificates & secrets”
- Create a client secret
- Save the secret value
Update secrets.yaml with the Graph API credentials.
secrets.yaml -> global.secrets.auth.entra.values:
graphClientId: "<graph-client-id>"
graphClientSecret: "<graph-client-secret>"3. Create Governance Worker App Registration
- Create app registration: “EQTY Governance Worker”
- Supported account types: “Accounts in this organizational directory only”
- No redirect URI needed (M2M client)
- Click “Register”
- Navigate to “Expose an API”
- Set Application ID URI:
api://<worker-client-id> - Navigate to “Certificates & secrets”
- Create a client secret
- Save the secret value
Generate an encryption key for the worker:
openssl rand -base64 32Update secrets.yaml with the worker credentials.
secrets.yaml -> global.secrets.governanceWorker.values:
encryptionKey: "<generated-key>"
clientId: "<worker-client-id>"
clientSecret: "<worker-client-secret>"4. Create SPA App Registration
- Create app registration: “Guardian Platform Frontend”
- Navigate to “Manifest”
- Find “accessTokenAcceptedVersion” and set it to
2(for v2.0 tokens) - Click “Save”
- Navigate to “Authentication”
- Click “Add a platform” > “Single-page application”
- Redirect URIs:
https://<domain> - Click “Configure”
- Navigate to “API permissions”
- Click “Add a permission”
- Select “My APIs”
- Select “Guardian Platform Backend”
- Check the
access_as_userscope - Click “Add permissions”
- Optional: Click “Grant admin consent”
Update values.yaml with the SPA client ID.
values.yaml -> guardian.config:
entraClientId: "<spa-client-id>"Generate the token signing key as in step 6 of the manual CLI setup.
Create Organization and Admin User
Before deploying the platform, add the organization and admin user to values.yaml. The platform chart’s post-install hook creates them during the install:
# Add to values.yaml
entra:
createOrganization: true
organizationName: "your-org-name"
displayName: "Organization Display Name"
createPlatformAdmin: true
platformAdminEmail: "admin@example.onmicrosoft.com" # Must exist in Entra
tenantId: "<tenant-id>" # Omit to read tenant-id from the Entra secretThis will:
- Create an organization in the governance database
- Look up the Entra user by email via Graph API, using the Graph credentials in the Entra secret
- Create a platform-admin user record linked to the Entra user
The hook fails if the Graph credentials are missing or the user cannot be found. It runs on every install and upgrade and never creates duplicate records. On an existing install it points the organization and the platform admin’s user record at Entra and makes the admin an active organization_owner again: other organization roles are kept, and project roles and project assignments in the organization are removed. It does not change an existing organization’s display name.
Verification
Verify the OIDC discovery endpoint:
curl "https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration"Test token issuance with backend API credentials:
curl -X POST "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=<backend-client-id>" \
-d "client_secret=<backend-client-secret>" \
-d "scope=api://<backend-client-id>/.default" \
-d "grant_type=client_credentials"Test Graph API access:
curl -X POST "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=<graph-client-id>" \
-d "client_secret=<graph-client-secret>" \
-d "scope=https://graph.microsoft.com/.default" \
-d "grant_type=client_credentials"If both commands succeed and return access tokens, the Entra configuration is correct.
Security Best Practices
- Use Certificate Authentication: For production, consider using certificates instead of client secrets
- Rotate Secrets: Regularly rotate client secrets (recommended: every 6-12 months), update the Kubernetes secrets, and restart Auth Service so it picks up the new worker secret
- Conditional Access: Implement Conditional Access policies for enhanced security
- Multi-Factor Authentication: Require MFA for all users
- Least Privilege: Only grant the minimum required Graph API permissions
- Monitor Sign-ins: Enable sign-in logs and monitor for suspicious activity
- App Consent Policies: Configure app consent policies to control which apps users can consent to
Troubleshooting
- Bootstrap job fails to authenticate — Solution: Check the
client-idandclient-secretkeys inentra-bootstrap-sp,entra.tenantId, and that admin consent was granted for the bootstrap service principal’s permissions - Bootstrap log says “Admin consent may need to be granted manually” — Expected: the job’s consent attempt failed; run the consent commands in step 5
- No client secret in the credentials secret — Solution: The app already existed with a client secret, which the job keeps and cannot read. Create a new secret with
az ad app credential reset --id <app-id> --append --query password --output tsv Found more than one app registration named ...— Solution: Delete or rename the duplicate app registrations, or set a uniquedisplayNameunderapps, and re-run- “AADSTS65001: The user or administrator has not consented to use the application” — Solution: Grant admin consent for API permissions in the Azure Portal
- “AADSTS700016: Application not found in the directory” — Solution: Verify the client ID is correct and the app registration exists in the tenant
- “Invalid scope” — Solution: Ensure the
entraScopesconfiguration includesapi://<backend-client-id>/access_as_user - Graph API returns 403 Forbidden — Solution: Verify admin consent was granted for Graph API permissions
Official Documentation
- Azure CLI - App Registrations
- Microsoft Entra ID Overview
- Microsoft Graph API
- OAuth 2.0 and OpenID Connect in Entra ID
- Single-page application: Sign-in and Sign-out
- Helm Chart Examples
Related
- Authentication — how Entra ID compares with the other supported providers
- Identity Providers — what Auth Service supports in Entra mode
- Token Exchange and Refresh Tokens — Auth Service-issued tokens and refresh-token rotation for Entra
- Azure Key Vault — the Azure key provider for DID signing keys
- Azure Storage Account — the Azure object storage the platform needs
- Helm Deployment — deploying the platform once the app registrations exist