Service Principals in Depth
Why this matters
A service principal is how a non-human thing — a script, a Terraform run, a CI job, an app on a server outside Azure — proves it is allowed to call Azure. It is the oldest and most portable machine-identity mechanism, and it is the one you reach for when managed identity (module 02) and workload identity federation (module 03) are not available. It is also the one with a sharp edge: authenticating a service principal with a client secret means storing a long-lived credential somewhere, and that stored secret is the exact thing tracks 10 and 11 spent effort getting out of your pipelines. This module makes you fluent in service principals and clear-eyed about why you should avoid the secret-based version wherever you can.
Concepts
What create-for-rbac actually does
You may have run az ad sp create-for-rbac before without seeing the moving
parts. It does three things in one command: it creates an app
registration, creates the matching service principal (the two linked
objects from module 00), and — because of the for-rbac part — creates a
role assignment granting that principal a role at a scope. The output hands
you an appId (the client ID), a password (the client secret), and the
tenant — the three values a client needs to log in as that principal:
{
"appId": "00000000-0000-0000-0000-000000000000",
"displayName": "sp-terraform-learn",
"password": "the-only-time-you-ever-see-this",
"tenant": "11111111-1111-1111-1111-111111111111"
}
That password is shown exactly once and is never retrievable again. Lose
it and you must reset it. This one-time-view is your first hint that a secret
is an awkward thing to manage.
Client secret vs. certificate authentication
A service principal can authenticate two ways:
- Client secret — a shared string (effectively a password) the client
presents. Simple, but it is a bearer credential: anyone who reads it
is the principal, it is easy to accidentally commit to Git or log, and it
is long-lived unless you actively rotate it. This is the
passwordfromcreate-for-rbac. - Certificate — the client holds a private key and proves possession of it without ever sending it over the wire; Entra ID stores only the public key. This is strictly stronger: the secret material never transits, it is harder to exfiltrate from a keystore than a string is from an env var, and it maps naturally onto hardware-backed key storage. The trade-off is operational complexity (you manage a cert and its expiry).
Neither removes the fundamental problem: the credential still lives somewhere you have to protect. That is the motivation for module 02 and 03.
Why long-lived secrets are a liability
This connects directly back to 10-cicd-and-gitops/07-pipeline-security-and-secrets, which pushed you toward OIDC federation instead of a stored service principal secret. The reasons, stated plainly:
- It can leak. A string in a CI variable, an env file, a Terraform state, or a log line is a string that can be copied. A leaked secret is a valid credential until someone notices and rotates it.
- It rarely rotates. In theory secrets have an expiry; in practice they get set to a year or two and forgotten, so a leak in month one is exploitable for the whole window.
- It is a standing target. A long-lived secret is exactly the kind of credential the threat models in 11-security-deep-dive flag: it grants access continuously, whether or not any legitimate workload is running right now.
The modern hierarchy, worst to best: stored client secret → certificate → managed identity (no credential you hold at all) → workload identity federation (no stored credential, short-lived tokens minted per-run). This module owns the first two; modules 02 and 03 own the last two.
Secret expiry, rotation, and reset
Because a secret is a liability, treat its expiry as a feature, not a
nuisance. create-for-rbac lets you set --years (or you can add a credential
with an explicit end date). When a secret expires, the principal's logins start
failing with an authentication error (invalid client secret) — importantly
not a 403 authorization error. Recognizing "auth suddenly broke on a
schedule" as "the secret expired" is a real diagnosis skill (revisited in
module 07). You rotate by adding a new credential, cutting clients over,
then removing the old one — never by deleting the principal.
Service principal vs. managed identity, previewed
Hold this comparison; module 02 develops it. A service principal with a secret is a credential you create, hold, and must protect. A managed identity is also a service principal under the hood — but Azure creates and rotates its credential for you and never exposes it, so there is nothing for you to store or leak. The rule of thumb you are building toward: if the workload runs inside Azure, prefer a managed identity; if it runs outside Azure (like GitHub Actions), prefer workload identity federation; reach for a secret-based service principal only when neither is possible.
Command reference
| Command | What it does | Example |
|---|---|---|
az ad sp create-for-rbac |
Creates an app registration + service principal + role assignment, returns credentials | az ad sp create-for-rbac --name sp-learn --role Reader --scopes /subscriptions/<sub-id>/resourceGroups/rg-id-learn |
az ad app create |
Creates only the app registration (no SP, no role) | az ad app create --display-name sp-learn-app |
az ad sp create |
Creates the service principal for an existing app registration | az ad sp create --id <app-id> |
az ad sp credential reset |
Resets/rotates a service principal's client secret | az ad sp credential reset --id <app-id> --years 1 |
az ad app credential reset |
Adds/rotates credentials on the app registration | az ad app credential reset --id <app-id> --years 1 |
az ad sp show |
Shows a service principal's details | az ad sp show --id <app-id> --query "{name:displayName, spId:id}" -o jsonc |
az login --service-principal |
Logs in as a service principal (secret or cert) | az login --service-principal -u <app-id> -p <secret> --tenant <tenant-id> |
az ad sp delete |
Deletes the service principal (and app, if using the app id) | az ad sp delete --id <app-id> |
Flag-by-flag breakdowns:
az ad sp create-for-rbac --name sp-terraform-learn --role Contributor --scopes /subscriptions/<sub-id>/resourceGroups/rg-id-learn --years 1
--name— display name for the app registration/SP (ahttp://-prefixed name is auto-generated if omitted; prefer an explicit, greppable name).--role— the RBAC role to grant the new principal. Do not default toContributorat subscription scope — that is the over-privileging module 04 and track 11 warn against.--scopes— one or more resource IDs the role is granted on. Scope to a resource group or single resource, not the whole subscription, unless you truly need it. Space-separated for multiple scopes.--years— secret lifetime. A shorter lifetime is safer but means more frequent rotation; this flag existing at all is the tell that the secret is a managed liability.
az login --service-principal -u <app-id> -p <secret-or-cert-path> --tenant <tenant-id>
--service-principal— log in as an SP, not interactively as a human.-u— the app (client) ID of the principal.-p— the client secret, or a path to a.pemcertificate file (with--passwordpointing at the cert for cert auth in some CLI versions). Passing a secret on the command line leaks it into shell history — prefer an env var.--tenant— the tenant the principal lives in.
az ad sp credential reset --id <app-id> --years 1
--id— the app (client) ID (or the app registration's object ID).--years— validity of the new secret it generates and returns. This is your rotation command; the old secret keeps working until it expires or you remove it, enabling a zero-downtime cutover.
Hands-on exercises
Create a resource group to scope against. Run
az group create --name rg-id-learn --location eastus. You will scope this module's service principal to this group only, practicing least privilege from the first command.Create a scoped service principal. Run
$subId = az account show --query id -o tsv az ad sp create-for-rbac --name sp-id-learn --role Reader ` --scopes /subscriptions/$subId/resourceGroups/rg-id-learn --years 1Copy the
appId,password, andtenantfrom the output now — the password is shown only once. Verify:az ad sp show --id <appId> --query displayName -o tsvreturnssp-id-learn.Log in as the service principal. In a separate shell (so you do not disturb your main login), run
az login --service-principal -u <appId> -p <password> --tenant <tenant>. Verify:az account show --query user -o jsoncshows atypeofservicePrincipaland thenameis theappId— you are now acting as the machine identity, not yourself.Confirm its permissions are exactly what you granted. Still logged in as the SP, run
az group show --name rg-id-learn(should succeed — it has Reader there) and thenaz group create --name rg-id-should-fail --location eastus(should be denied — Reader cannot create, and it has no rights outsiderg-id-learnanyway). Verify: the second command returns a403authorization error. This is least privilege working. Log back in as yourself (az login) afterward.Inspect and reset (rotate) the secret. As yourself, run
az ad sp credential reset --id <appId> --years 1. Verify: a newpasswordis returned, different from the first. Conceptually, the old secret is still valid until expiry unless removed — this is how you rotate without downtime.Prove the SP has no retrievable secret after the fact. Try to read the secret back — there is no
az ad spcommand that returns an existing secret's value, onlyresetwhich makes a new one. Verify: confirm for yourself that Entra ID stores secrets one-way; the only recovery from a lost secret is a reset. Write down why that makes a stored secret operationally fragile.Diagnose-and-fix: expired/invalid secret (authn failure). Simulate a bad secret: run
az login --service-principal -u <appId> -p "definitely-wrong-secret" --tenant <tenant>. Observe the error. Diagnose: this is an authentication failure (invalid client secret /AADSTS7000215-style), not a403— the principal never got a token, so authorization never even ran. Contrast this with exercise 4's403, which happened after a successful login. Fix: log in with the correct current secret (from your exercise 5 reset). The lesson — wrong/expired secret = authn error before any permission check; missing role = authz403after login — is the core diagnostic split you will use in module 07.Diagnose-and-fix: over-scoped by accident. Create a throwaway SP with a deliberately too-broad grant:
az ad sp create-for-rbac --name sp-toobroad --role Contributor --scopes /subscriptions/$subId. Diagnose: runaz role assignment list --assignee <its-appId> --all -o tableand observe the role isContributorat subscription scope — this SP can now modify everything in the subscription, exactly the over-privilege antipattern from track 11. Fix: delete the subscription-scoped assignment (az role assignment delete --assignee <appId> --role Contributor --scope /subscriptions/$subId) and, if the SP still needs access, re-create it at resource-group scope only. Then delete the throwaway SP entirely (az ad sp delete --id <appId>).Clean up. Delete the learning SP (
az ad sp delete --id <sp-id-learn-appId>) and the resource group (az group delete --name rg-id-learn --yes --no-wait). Verify:az ad sp list --show-mine -o tableno longer listssp-id-learn. (Deleting the SP also cleans up its role assignments; leftover assignments to a deleted principal show up as an orphaned GUID — module 04 covers cleaning those.)
Independent challenge
Using only what you know, provision a service principal that a hypothetical external build server (something outside Azure that cannot use managed identity) would use to deploy into a single resource group — and then make the case, in writing, for why you would not actually ship this design to production. Grant it the least role that would let it deploy the kinds of resources you built in 09-terraform-on-azure, scoped to exactly one resource group, with a short secret lifetime. Then write a short "why this is the wrong long-term answer" note that references the stored-secret risks from 10-cicd-and-gitops/07 and names what you would replace it with once the workload could run either inside Azure (module 02's mechanism) or in GitHub Actions (module 03's mechanism). The deliverable is the SP plus the argument against it — proving you can both wield the tool and know when not to.
Stuck? One hint
The role that lets a pipeline "deploy resources" in a resource group is
usually Contributor scoped to that one resource group — not at
subscription scope. Build the --scopes value as
/subscriptions/<sub-id>/resourceGroups/<rg> and confirm with az role assignment list --assignee <appId> --all -o table that nothing broader was
granted. For the write-up, the single sentence that matters is: the password
this command prints is a long-lived bearer credential you now have to store and
protect, which is the exact thing OIDC federation (module 03) removes.
Common mistakes & troubleshooting
- Not copying the password on creation.
create-for-rbacshows the secret once. If you miss it, you cannot retrieve it — you mustcredential reset, which invalidates plans that captured the old one. - Defaulting to
--role Contributor --scopes /subscriptions/<id>. The convenient default is dangerously broad. Scope to a resource group or single resource and pick the narrowest role that works. - Confusing an authn failure with an authz failure. A wrong/expired secret
fails at login (no token issued). A missing role fails after login with a
403. The remedies are completely different (rotate the secret vs. add a role assignment) — never treat one as the other. - Leaking the secret into shell history or Git. Passing
-p <secret>on the command line records it in history; committing it to a repo publishes it. Use environment variables and never commit credentials — the same discipline track 10 built for pipeline secrets. - Deleting the SP to "rotate" it. Deleting and recreating changes the
appId, breaking every role assignment and every client that referenced it. Rotate withcredential reset, which keeps the principal and its assignments intact. - Forgetting that a managed identity would avoid all of this. If the workload runs in Azure, you probably should not be creating a secret-based SP at all — that is the whole pivot into module 02.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
az ad sp create-for-rbacdoes three distinct things in one command. Name them.- Which three values from its output does a client need to authenticate as the service principal, and which of the three can you never retrieve again?
- Give two concrete reasons a client certificate is stronger than a client secret for an SP.
- A service principal that worked yesterday suddenly fails to log in today, with no changes to its role assignments. What is the most likely cause, and is it an authn or authz failure?
- Rank these four machine-auth options from least to most secure: managed identity, stored client secret, workload identity federation, certificate.
- Why is deleting-and-recreating a service principal the wrong way to rotate its secret?
- You want an SP that can deploy into
rg-appbut nothing else. What--roleand--scopesshape do you use, and why not subscription scope?
Show answers
- It creates an app registration, creates the matching service principal, and creates a role assignment granting that principal a role at the scope you specify.
- The
appId(client ID), thepassword(client secret), and thetenantID. Thepasswordcan never be retrieved again — it is shown once at creation. - (any two) The private key never transits the network (only the public key is stored in Entra ID); it is harder to exfiltrate from a keystore than a string from an env var; it maps onto hardware-backed key storage; possession is proven cryptographically rather than by presenting a copyable bearer string.
- The secret expired (or was reset elsewhere). It is an authentication
failure — the SP cannot get a token at all, which is different from a
403that would happen after a successful login. - Least to most secure: stored client secret → certificate → managed identity → workload identity federation.
- Deleting recreates the principal with a new
appId, which breaks every role assignment and every client that referenced the old one.credential resetrotates the secret while keeping the principal and its assignments. --role Contributor(or narrower) with--scopes /subscriptions/<sub-id>/resourceGroups/rg-app. Not subscription scope because that would let the SP modify every resource in the subscription — a blast radius far larger than the job requires (least privilege).
Next
02-managed-identity-in-depth — the first real fix for the stored-secret problem: an identity Azure creates and rotates for you, with no credential you ever hold, generalized well beyond the basic usage you saw on Container Apps and AKS.