---
cuppy_hub: azure
cuppy_path: guides/entra/create-service-principal.md
cuppy_section: docs
cuppy_doc: VJ76-MSHD
cuppy_doc_uuid: 8f6345e8-b2ce-440f-b249-2f99ec286054
cuppy_permalink: https://cuppy.io/azure/d/VJ76-MSHD
cuppy_version: 1
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/azure/docs/guides/entra/create-service-principal
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: a14e20f61465ae694f22baf70103790192a512c98b5cd395c1f5e2b35e2c4556
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# 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 `--role` and `--scopes`.** Without them, the principal exists but can do nothing.
- `--create-cert` makes 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** (`--years` to 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-auth` is 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`:

```ts
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:

1. Add the new credential with `--append`.
2. Deploy it everywhere.
3. Confirm sign-ins use it.
4. Delete the old one: `az ad app credential delete --id <appId> --key-id <old-keyId>`. Find it with `az ad app credential list --id <appId>`, which shows each credential's `keyId` and `endDateTime`.

Put a reminder before `endDateTime`; an expired secret is `AADSTS7000222`.

## Next

Token errors: `reference/entra/errors.md`. SDK errors: `reference/azure-identity/errors.md`.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 107 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/azure/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
