Create a service principal for automation — and rotate its credential safely
When no managed identity or federated credential fits, create a service principal that works on the first token request — app plus service principal (az ad sp create-for-rbac or az ad app create + az ad sp create), a certificate instead of a secret, a role at the narrowest scope, application permissions with admin consent for Microsoft Graph, and credential rotation with --append so the running workload never loses its credential.
markdownguidev1VJ76-MSHDstage: buildtrust: officialservice: entrainterface: cliinterface: sdkcreate service principalaz ad sp create-for-rbacaz ad app createaz ad sp createclient secretcertificate credentialaz ad app credential reset --appendsecret rotationcredential overwrittenAZURE_CLIENT_ID AZURE_TENANT_ID AZURE_CLIENT_SECRETClientCertificateCredentialapplication permissions admin consentaz ad app permission addaz ad app permission admin-consent--json-auth deprecated--sdk-auth deprecated
Create a service principal for automation
Use this only when the workload can't have a managed identity or a federated credential (reference/overview/services-and-identities.md). A service principal has a credential that can leak and expires; everything below keeps those two risks small.
1. Create the app and its service principal
The one-command way:
az ad sp create-for-rbac --name sp-nightly-report \
--role Reader --scopes /subscriptions/<sub-id>/resourceGroups/rg-reports \
--create-cert
- No role is assigned unless you pass
--roleand--scopes. Without them, the principal exists but can do nothing. --create-certmakes a self-signed certificate readable only by your OS user (add--keyvault <vault> --cert <name>to create it in Key Vault instead). Without a certificate option, the command creates a password (secret) valid for one year (--yearsto change it).- The output includes the credential once. Store it in a secret store at once; it can't be read back.
--json-auth/--sdk-authis deprecated.
The two-step way, when you need control over the app:
az ad app create --display-name sp-nightly-report # app object only
az ad sp create --id <appId> # service principal: required
Microsoft Graph, which the CLI uses, doesn't create the service principal for you; registering in the portal does. An app without one fails sign-in with AADSTS700016.
2. Prefer a certificate to a secret
Microsoft recommends a certificate from a trusted CA. With @azure/identity:
import { ClientCertificateCredential } from "@azure/identity";
// PEM file containing the certificate AND its private key. PFX isn't supported by the JS library.
export const credential = new ClientCertificateCredential(
process.env.AZURE_TENANT_ID ?? "",
process.env.AZURE_CLIENT_ID ?? "",
{ certificatePath: process.env.AZURE_CLIENT_CERTIFICATE_PATH ?? "" },
);
Through DefaultAzureCredential, set AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_CERTIFICATE_PATH. If AZURE_CLIENT_SECRET is also set, the secret wins.
3. Give it only what it needs
- Azure resources: a role at the narrowest scope (
guides/rbac/assign-roles.md). Data needs data roles; Contributor reads no secrets or blobs. - Microsoft Graph or another API, no user involved: add application permissions, then get admin consent. Without consent, Graph returns 403
Authorization_RequestDenied/Insufficient privileges to complete the operation.
az ad app permission add --id <appId> --api 00000003-0000-0000-c000-000000000000 --api-permissions <permission-id>=Role
az ad app permission admin-consent --id <appId>
Find <permission-id> with az ad sp show --id 00000003-0000-0000-c000-000000000000 --query "appRoles[?value=='User.Read.All'].id" -o tsv (application permissions are the API's appRoles). 00000003-0000-0000-c000-000000000000 is Microsoft Graph's app ID; =Role marks an application permission (=Scope is delegated). The token then carries the permission in its roles claim, and requests use the scope https://graph.microsoft.com/.default.
4. Rotate without an outage
az ad app credential reset --id <appId> --append --display-name "2026-10 rotation" --years 1
Without --append, credential reset replaces the existing credentials: every running copy of the workload fails at its next token request with AADSTS7000215. The safe order is:
- Add the new credential with
--append. - Deploy it everywhere.
- Confirm sign-ins use it.
- Delete the old one:
az ad app credential delete --id <appId> --key-id <old-keyId>. Find it withaz ad app credential list --id <appId>, which shows each credential'skeyIdandendDateTime.
Put a reminder before endDateTime; an expired secret is AADSTS7000222.
Next
Token errors: reference/entra/errors.md. SDK errors: reference/azure-identity/errors.md.