Azure Service Principals Explained: A Deep Dive
A service principal is the application-level identity your code uses to authenticate to Azure. When a CI/CD pipeline deploys infrastructure, when a background service reads from a storage account, or when a third-party tool manages Azure resources — each of these is authenticating as a service principal. This page is the deep dive: how service principals are structured, how to create and configure them, and how to move beyond client secrets to federated credentials.
What a service principal is
Microsoft Entra ID has two related concepts that are easy to confuse: the application registration and the service principal.
The application registration is the global definition of an application — it exists in your Entra ID tenant and defines the application’s identity, what credentials it uses, and what permissions it requests. Think of it as the template.
The service principal is the instance of that application within a specific tenant. When you use an application in your tenant, a service principal is automatically created. The service principal is what you assign roles to, what appears in sign-in logs, and what Azure evaluates when your application tries to access a resource.
When you run az ad sp create-for-rbac, the CLI creates both the application registration and the service principal in one step, which is why most people use the terms interchangeably.
The introductory overview at azure-service-principals covers the conceptual foundation. This page focuses on the operational details.
Creating a service principal
The quickest way to create a service principal with a role assignment in a single command:
az ad sp create-for-rbac \
--name "myapp-pipeline" \
--role "Contributor" \
--scopes "/subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/production-app" \
--output jsonThis outputs:
{
"appId": "00000000-1111-2222-3333-444444444444",
"displayName": "myapp-pipeline",
"password": "A1B2C3D4E5F6G7H8I9J0...",
"tenant": "aaaabbbb-cccc-dddd-eeee-ffffffffffff"
}Save this output immediately. The password field is the client secret and it is shown only once. You cannot retrieve it later — if you lose it, you must create a new secret.
The four values map to the authentication parameters your application needs:
- appId →
AZURE_CLIENT_ID(also called the app ID or application ID) - tenant →
AZURE_TENANT_ID - password →
AZURE_CLIENT_SECRET - The subscription ID is separate — get it from
az account show —query id —output tsv
Creating a service principal without an immediate role assignment
If you want to create the service principal first and assign roles separately (which gives more control):
# Create the app registration and service principal
az ad sp create-for-rbac \
--name "myapp-pipeline" \
--skip-assignment \
--output json
# Get the service principal's object ID for role assignments
SP_OBJECT_ID=$(az ad sp show \
--id "APP_ID_FROM_ABOVE" \
--query id \
--output tsv)
# Assign a specific role
az role assignment create \
--assignee-object-id "$SP_OBJECT_ID" \
--assignee-principal-type "ServicePrincipal" \
--role "Storage Blob Data Reader" \
--scope "/subscriptions/YOUR_SUB/resourceGroups/data-rg/providers/Microsoft.Storage/storageAccounts/mydata"Credential types: passwords vs certificates
Service principals support two types of credentials: passwords (client secrets) and certificates.
Client secrets (passwords)
Client secrets are the default and most common credential type. They are simple to create and use, but they have significant drawbacks:
- They must be stored somewhere and protected. If they appear in logs, git history, or environment variables, they can be compromised.
- They expire and must be rotated before expiry or authentication fails.
- Rotation requires updating every system that uses the secret — which can be many places if the secret is widely shared.
See why secrets and keys are dangerous for a detailed look at how client secrets get exposed and the consequences.
Certificate credentials
Certificate credentials are more secure than client secrets because the private key never leaves the system that holds it — only the certificate (public key) is registered with Entra ID. This eliminates many of the credential theft risks associated with secrets.
Create a self-signed certificate and register it with a service principal:
# Create a self-signed certificate (for development/testing)
openssl req -x509 -newkey rsa:4096 -keyout myapp-key.pem \
-out myapp-cert.pem -days 365 -nodes \
-subj "/CN=myapp-pipeline"
# Upload the certificate to the service principal
az ad sp credential reset \
--id "APP_ID" \
--cert @myapp-cert.pem \
--appendFor production, use certificates issued by your organization’s CA or stored in Azure Key Vault rather than self-signed certificates.
Federated credentials (OIDC) — the modern approach
Federated credentials allow an external system (like GitHub Actions or Azure DevOps) to authenticate as a service principal using its own identity token, without any stored secret or certificate. This is the recommended approach for CI/CD pipelines as of 2024 and is covered in detail in the next section.
Complete workflow: GitHub Actions CI/CD with federated OIDC credentials
Here is the full setup for a GitHub Actions pipeline that deploys to Azure without any stored secrets.
Why OIDC instead of client secrets
The traditional approach stores AZURE_CLIENT_SECRET as a GitHub repository secret. This works, but it means there is a long-lived credential that:
- Never expires unless you rotate it manually
- Is stored in GitHub’s secret system (not under your direct control)
- Could be printed in logs by a misconfigured step
- Could be accessed by anyone with repository admin access
With OIDC (federated credentials), there is no secret. GitHub issues a short-lived token to the workflow, and Azure accepts that token as proof of identity because you have established a trust relationship between your Azure service principal and your GitHub repository.
Step 1: Create the service principal
az ad sp create-for-rbac \
--name "github-actions-deploy" \
--skip-assignment \
--output jsonNote the appId and tenant values. You do not need the password — you will delete it in a moment.
Step 2: Add a federated credential for GitHub Actions
az ad app federated-credential create \
--id "APP_ID" \
--parameters '{"name": "github-actions-main", "issuer": "https://token.actions.githubusercontent.com", "subject": "repo:yourorg/yourrepo:ref:refs/heads/main", "description": "GitHub Actions deployment from main branch", "audiences": ["api://AzureADTokenExchange"]}'The subject field is the constraint that defines exactly which GitHub Actions context this credential applies to. Options include:
repo:org/repo:ref:refs/heads/main— only workflows triggered from the main branchrepo:org/repo:environment:production— only workflows running in the production environmentrepo:org/repo:pull_request— workflows triggered by pull requests
Using environment-scoped subjects (:environment:production) is the most restrictive and recommended for production deployments.
Step 3: Assign the minimum required roles
Identify exactly what the pipeline needs to do and assign only those roles. For a typical Terraform deployment pipeline:
SP_OBJECT_ID=$(az ad sp show \
--id "APP_ID" \
--query id \
--output tsv)
SUBSCRIPTION_ID=$(az account show --query id --output tsv)
# Contributor for creating and managing resources
az role assignment create \
--assignee-object-id "$SP_OBJECT_ID" \
--assignee-principal-type "ServicePrincipal" \
--role "Contributor" \
--scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/production-app" \
--description "GitHub Actions pipeline deployment access"
# Storage Blob Data Contributor for Terraform state
az role assignment create \
--assignee-object-id "$SP_OBJECT_ID" \
--assignee-principal-type "ServicePrincipal" \
--role "Storage Blob Data Contributor" \
--scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/terraform-state-rg/providers/Microsoft.Storage/storageAccounts/mytfstate" \
--description "GitHub Actions Terraform state access"Notice: Contributor is scoped to the specific resource group being deployed to, not the whole subscription. The Terraform state storage access is scoped to the exact storage account, not the resource group.
Step 4: Remove the client secret
If a client secret was created when you ran az ad sp create-for-rbac, remove it — you do not need it with federated credentials and leaving it around is unnecessary risk:
# List credentials on the app registration
az ad app credential list --id "APP_ID"
# Delete each password credential by its keyId
az ad app credential delete \
--id "APP_ID" \
--key-id "KEY_ID_FROM_ABOVE"Step 5: Configure GitHub Actions
Set these three values as GitHub repository secrets or variables (they are not sensitive — they are IDs, not secrets):
AZURE_CLIENT_ID— the app ID of the service principalAZURE_TENANT_ID— your Entra ID tenant IDAZURE_SUBSCRIPTION_ID— the subscription ID
In your GitHub Actions workflow:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy with Terraform
run: |
terraform init
terraform apply -auto-approveThe permissions: id-token: write is required — it tells GitHub to issue an OIDC token for this workflow. Without it, the Azure login step fails.
You do not need to store AZURE_CLIENT_ID, AZURE_TENANT_ID, or AZURE_SUBSCRIPTION_ID as GitHub secrets — they are not sensitive values. You can store them as regular GitHub repository variables (Settings → Variables → Actions variables), which are more visible and easier to manage. Reserve GitHub secrets for genuinely sensitive values.
Rotating client secrets
If you are still using client secrets (perhaps for a workload that cannot use managed identities or federated credentials), you need a rotation process. Azure does not automatically notify you when a secret is about to expire — you need to track this yourself.
Check when a secret expires
az ad app credential list \
--id "APP_ID" \
--query "[].{KeyId:keyId, DisplayName:displayName, EndDate:endDateTime}" \
--output tableRotate a secret (add new, then remove old)
The safe rotation process is to add a new secret before removing the old one, update the application to use the new secret, verify the application is working, then delete the old secret. Never delete the old secret first — that causes downtime.
# Add a new secret with a 1-year expiry
NEW_SECRET=$(az ad app credential reset \
--id "APP_ID" \
--append \
--years 1 \
--query password \
--output tsv)
# Update your application to use NEW_SECRET
# Verify the application still works
# Then delete the old secret:
az ad app credential delete \
--id "APP_ID" \
--key-id "OLD_KEY_ID"If you are using Azure Key Vault to store the client secret, you can automate rotation using Key Vault’s secret rotation feature with an event-driven function that updates dependent systems when a new secret version is created.
Common service principal mistakes
- Assigning Owner instead of the minimum required roles. Service principals used by pipelines frequently get Owner because it is simple and “solves everything.” See the scenario in the principle of least privilege page for what can go wrong. Audit every service principal’s roles and downscope any that have more than necessary.
- Storing client secrets in environment variables in containers. Environment variables in containers can be read by any process inside the container and are sometimes logged. Use managed identities for container workloads in Azure, or at minimum store secrets in Key Vault and fetch them at startup rather than baking them into the environment at deploy time.
- Using the same service principal across multiple applications or pipelines. One service principal per application or pipeline. When you share a service principal across multiple uses, rotating a secret requires updating all consumers simultaneously, and a compromise affects all applications. Separate identities limit blast radius.
- Not cleaning up expired or unused service principals. Old service principals from retired projects accumulate with standing role assignments. Quarterly audits should include reviewing service principal last sign-in dates and removing any that have not authenticated recently.
Summary
- A service principal is the instance of an app registration in your Entra ID tenant — it is what gets role assignments and appears in logs.
- Client secrets are the simplest credential type but carry real risks: they must be stored, protected, and rotated.
- Federated OIDC credentials are the recommended approach for CI/CD pipelines — no secret is stored anywhere, and GitHub issues a short-lived token that Azure accepts directly.
- The OIDC setup requires: creating the service principal, adding a federated credential with the correct subject constraint, assigning minimum required roles, and configuring the workflow with
permissions: id-token: write. - Follow least-privilege for service principal roles — scope to resource groups, not subscriptions, and use service-specific roles rather than Contributor when possible.
- If using client secrets, rotate them by adding a new secret first, updating the application, verifying it works, then deleting the old secret.
Frequently asked questions
What is the difference between a service principal and a managed identity?
A service principal is an application identity you create and manage credentials for — you are responsible for creating, rotating, and protecting client secrets or certificates. A managed identity is an identity Azure manages on your behalf, with no credentials you need to handle. For workloads running inside Azure, managed identities are almost always preferable. Service principals are necessary for workloads running outside Azure (like CI/CD pipelines on GitHub Actions) unless you use federated credentials.
What is the client ID, tenant ID, and client secret in a service principal?
The client ID (also called app ID) is the unique identifier for the application registration in Microsoft Entra ID. The tenant ID identifies which Entra ID tenant the service principal belongs to. The client secret is a password-like credential the service principal uses to authenticate. All three are needed for a service to authenticate as the service principal using the client credentials flow.
How long do Azure service principal client secrets last?
By default, client secrets can be set to expire between 1 day and 2 years. You can set a custom expiry or, in some configurations, create secrets with no expiry (though Microsoft recommends against secrets with no expiry date). Secrets should be rotated regularly. If you are using service principals for CI/CD, switching to federated OIDC credentials eliminates the secret entirely and is strongly recommended.