Identities that are not people

A nightly job that exports invoices, a web application that reads from a database, a deployment pipeline, a monitoring agent, a chatbot that creates tickets: each of them signs in somewhere and needs permissions. A non-human identity (NHI, also called a workload or machine identity) is an identity used by software rather than a person. In most organizations they outnumber human accounts, often by a wide margin, and they grow with every new integration.

They are harder to govern than people. No HR record starts or ends them, so the identity lifecycle does not cover them by itself. They cannot do MFA, so their credential alone is the proof. They often hold broad permissions "because the integration needed it", and their sign-ins happen at all hours, so unusual use is hard to spot. In the attack on Microsoft that it disclosed in January 2024, the attacker (Midnight Blizzard) got in through a password spray on a legacy test account without MFA and then abused a legacy test OAuth application with elevated access to Microsoft's corporate environment, used it to create new apps with full mailbox access, and read corporate mailboxes. Forgotten applications with powerful permissions are exactly what attackers look for.

The kinds you will meet:

Kind Where How it proves itself
User account used as a service account AD, Entra ID A password (the pattern to move away from)
Group Managed Service Account (gMSA), delegated MSA AD A password Windows rotates itself (see Users, Groups and Computers)
Service principal Entra ID Client secret, certificate or federated credential
Managed identity Azure resources The platform; no credential to handle
IAM role, service account AWS, Google Cloud Temporary credentials from the platform
Kubernetes service account A Kubernetes cluster A short-lived token mounted into the pod
API key Many SaaS and internal APIs A static secret string

The general idea of a service account is covered in the Cloud topic. This page looks at the Microsoft Entra side in more detail and at the credentials themselves.

Service principals and app registrations

In Microsoft Entra, an application is described by an app registration (the global definition: its name, its redirect URIs, the permissions it asks for, its credentials; see Microsoft Entra ID). A service principal is the identity of that application inside one tenant: the object that signs in, gets role assignments and API permissions, and appears in the sign-in logs. One app registration in a vendor's tenant can have service principals in thousands of customer tenants; in the Entra admin center they appear under Enterprise applications.

An application signs in as itself with the OAuth 2.0 client credentials flow: it sends its client ID and a credential to the token endpoint and receives an access token for an API (see Single Sign-On and Federation). Two kinds of permission matter:

  • Delegated permissions: the app acts on behalf of a signed-in user and can never do more than that user.
  • Application permissions: the app acts as itself, with no user. Mail.Read as an application permission means "read every mailbox in the tenant". These need admin consent and deserve the same scrutiny as a Global Administrator assignment.

Listing which applications hold credentials is the first inventory step (Microsoft Graph PowerShell, non-run):

Connect-MgGraph -Scopes "Application.Read.All"
Get-MgApplication -All -Property DisplayName, PasswordCredentials, KeyCredentials |
    Select-Object DisplayName,
        @{ n = "Secrets"; e = { $_.PasswordCredentials.Count } },
        @{ n = "Certificates"; e = { $_.KeyCredentials.Count } }
DisplayName      Secrets Certificates
-----------      ------- ------------
billing-export         1            0
hr-sync                2            0
portal-api             0            1
deploy-pipeline        0            0

PasswordCredentials are client secrets, KeyCredentials are certificates. deploy-pipeline has neither — it uses a federated credential, described below.

Managed identities

Every credential an application stores is a credential that can leak: from a configuration file, a backup, a log line or a repository. A managed identity is an Entra identity for an Azure resource whose credentials the platform creates, stores and rotates itself. The code running on the resource asks the platform for a token; there is no secret in the code, the configuration or the pipeline. Under the hood a managed identity is a special kind of service principal.

System-assigned User-assigned
Created Turned on as a setting of one resource Created as a standalone Azure resource
Lifecycle Deleted together with the resource Lives until you delete it
Shared No, one resource only Can be attached to several resources
Good for One app with its own permissions A group of instances that need the same access; pre-assigning permissions before the resource exists; workload identity federation

Inside an Azure virtual machine, the token comes from the instance metadata service, a link-local address that is only reachable from inside the VM. Software development kits do this for you, but the request shows that no secret is involved (non-run):

curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://storage.azure.com/"
{"access_token":"eyJ0eXAiOiJKV1Qi...","resource":"https://storage.azure.com/","token_type":"Bearer"}

The Metadata: true header is required, which stops simple server-side request forgery tricks from fetching tokens through the application. The managed identity still needs a role assignment (for example Storage Blob Data Reader on one storage account) before the token is useful: no secret does not mean no least privilege.

Workload identity federation

Managed identities only work for code running on Azure. A deployment pipeline on GitHub Actions, a workload in another cloud or a Kubernetes cluster elsewhere would traditionally get a client secret. Workload identity federation removes that secret: the external platform issues its own signed token to the workload, and the identity provider trusts that token instead of a stored credential. In Microsoft Entra, a federated identity credential on an app registration or a user-assigned managed identity says which external tokens to trust. Supported sources include GitHub Actions, Kubernetes clusters, Google Cloud, AWS, SPIFFE/SPIRE and Azure Pipelines.

For GitHub Actions the external token is the job's OpenID Connect token (see OIDC: no stored cloud keys). The trust on the Entra side looks like this (non-run):

{
  "name": "github-deploy-production",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:example-org/customer-portal:environment:production",
  "audiences": ["api://AzureADTokenExchange"],
  "description": "Production deployments of the customer portal"
}

Issuer, subject and audience must match the incoming token exactly, and the comparison is case-sensitive. The subject is what makes it safe: only jobs in that repository that run in the production environment can exchange their token. A subject that allowed any branch would let anyone who can push a branch deploy to production.

Workload identity federation with GitHub Actions. One: the workflow job asks GitHub's OIDC provider for a token. Two: GitHub returns a signed token naming the repository and environment. Three: the job sends that token to Microsoft Entra ID. Entra checks issuer, subject and audience against the federated credential. Four: Entra returns a short-lived access token. Five: the job calls the Azure resource with the access token. No secret is stored anywhere. Workflow job GitHub runner GitHub OIDC token issuer Microsoft Entra ID checks iss, sub, aud Azure resource e.g. web app 1 request 2 3 GitHub token 4 5 access token
Two short-lived tokens replace one long-lived secret: GitHub vouches for the job, Entra exchanges that for an access token.

AWS offers the same pattern through roles that trust an OIDC identity provider (the role is assumed with a web identity token through the Security Token Service), and Google Cloud through workload identity pools. The principle is the same everywhere: trust a token that the workload's own platform signs, scoped as narrowly as possible, instead of a secret copied into it.

Client secrets, certificates and federated credentials

When an application signs in as itself, it proves its identity with one of three kinds of credential. A client secret is a password generated for an application: a random string the application sends to the token endpoint. It is the easiest to set up and the easiest to leak.

Client secret Certificate Federated credential
What is sent The secret itself A signed assertion; the private key never leaves the app A token from an external issuer
If intercepted or copied Usable by anyone until it expires The assertion is short-lived; the key must be stolen from where it is stored Short-lived, bound to one subject
Storage Configuration or a secrets vault Certificate store, Key Vault or a hardware security module Nothing to store
Rotation Manual or scripted, before expiry Renew before expiry; can be automated Not needed
Recommendation Avoid in production Use when federation is not possible Prefer where supported

Microsoft's own order of preference follows the table: managed identity first, then workload identity federation, then certificates, and client secrets only where nothing else works. The same thinking applies outside Entra: see PKI and Certificates for how certificates work.

API keys

Many services authenticate callers with an API key: a long random string sent with every request, usually in a header. An API key identifies the calling application, but it is a bearer secret — whoever holds it is the application. Keys are often created without an expiry, with full rights, by one developer, and shared by several systems. Treat each one like a password: one key per consumer, the narrowest scope the service allows, an expiry date, storage in a secrets manager, and a named owner. Where a service offers OAuth or federation instead, use that.

Secret rotation and expiry monitoring

Secret rotation is replacing a credential with a new one on a schedule or after a suspected leak, and retiring the old one. It limits how long a leaked secret is useful. Rotation without an outage needs overlap: an Entra app can hold two secrets or certificates at once, so you add the new credential, update every consumer, verify, and only then remove the old one.

Expiry works in both directions. A secret that never expires is a permanent risk; a secret that expires unnoticed is an outage — the classic Monday-morning incident where an integration stopped at midnight because a two-year-old client secret ran out and its creator has left. The cure is an inventory with owners and a check that runs every day:

# Scan an inventory of application credentials for expiry and long lifetimes.
from datetime import date

TODAY = date(2026, 10, 1)
WARN_DAYS = 30        # warn this many days before expiry
MAX_LIFETIME = 365    # longest lifetime we accept for a secret or key

CREDENTIALS = [
    # application, credential type, created, expires (None = never)
    ("billing-export", "client secret", date(2025, 10, 20), date(2026, 10, 20)),
    ("portal-api", "certificate", date(2026, 3, 1), date(2027, 3, 1)),
    ("hr-sync", "client secret", date(2024, 9, 1), date(2026, 9, 1)),
    ("meter-reader", "API key", date(2023, 5, 1), None),
    ("deploy-pipeline", "federated credential", date(2026, 6, 1), None),
    ("ticket-bot", "client secret", date(2026, 8, 1), date(2028, 8, 1)),
]

for app, kind, created, expires in CREDENTIALS:
    if kind == "federated credential":
        status = "ok (no secret stored)"
    elif expires is None:
        status = "NEVER EXPIRES: rotate and set an expiry"
    elif expires < TODAY:
        status = f"EXPIRED {(TODAY - expires).days} days ago"
    elif (expires - TODAY).days <= WARN_DAYS:
        status = f"expires in {(expires - TODAY).days} days: rotate now"
    elif (expires - created).days > MAX_LIFETIME:
        status = f"lifetime {(expires - created).days} days: too long"
    else:
        status = f"ok, expires {expires.isoformat()}"
    print(f"{app:16} {kind:21} {status}")
billing-export   client secret         expires in 19 days: rotate now
portal-api       certificate           ok, expires 2027-03-01
hr-sync          client secret         EXPIRED 30 days ago
meter-reader     API key               NEVER EXPIRES: rotate and set an expiry
deploy-pipeline  federated credential  ok (no secret stored)
ticket-bot       client secret         lifetime 731 days: too long

Subtracting two date objects gives a timedelta, whose .days is a whole number, so the result is the same on every machine. TODAY is fixed here to keep the output stable; a real check uses date.today() and reads the inventory from Microsoft Graph (the endDateTime of each password and key credential), a secrets vault or a configuration database. The order of the tests matters: an expired secret is reported as expired, not as "too long".

AI agents and Kubernetes workloads

AI agents

An AI agent calls tools and APIs on its own, which makes it a non-human identity with an unusual twist: what it does next is decided by a language model, and it can be steered by text it reads. The identity rules apply with extra force — its own identity rather than a borrowed user account, least privilege per tool, short-lived tokens, logging of every action, and a named human owner. As of 2026, Microsoft lists Microsoft Entra Agent ID in the Entra product family as an identity type for AI agents, so that agents can be registered, given permissions, governed and monitored like other workload identities. Products in this area change quickly; check the current documentation before you design around a specific feature.

Kubernetes service accounts

Inside a Kubernetes cluster, each pod runs as a Kubernetes service account, which identifies it to the cluster's own API. Current Kubernetes versions mount a short-lived, automatically refreshed token into the pod rather than a permanent secret. Managed Kubernetes services can federate that token with the cloud's identity system (in Azure, Microsoft Entra Workload ID; AWS and Google Cloud have their own equivalents), so a pod gets cloud permissions without any stored key. Give each application its own service account; the namespace's default service account should have no permissions.

Common mistakes

  • Shared service accounts: one account used by five applications and three administrators. Nobody can tell from the logs who did what, and rotating its password breaks everything at once. One identity per workload.
  • A user account as a service account: a password that never changes, excluded from MFA "because it is a service". Microsoft's mandatory MFA for Azure management does not apply to managed identities and service principals, and Microsoft advises moving such user accounts to workload identities.
  • Application permissions granted too broadly: Directory.ReadWrite.All where reading one group would do.
  • No owner and no review: applications nobody remembers, still holding valid credentials and permissions.
  • A federated credential with a loose subject: trusting every branch or every repository in an organization instead of one environment.