Locking Down Service Accounts with Azure AD Conditional Access and Managed Identities

Locking Down Service Accounts with Azure AD Conditional Access and Managed Identities

Service accounts are the backbone of most cloud workloads. They give apps the permissions to read secrets, write logs, or call APIs. When an attacker breaks into one of those accounts the fallout can be huge. Azure AD Conditional Access (CA) and Managed Identities (MI) let you shrink that attack surface without breaking the automation you depend on. Below is a practical walk‑through of the most common patterns, the trade‑offs, and the troubleshooting steps you’ll hit in production.


Why Service Accounts Still Need Guardrails

Even though MIs remove the need to hard‑code secrets, a lot of legacy workloads still live on traditional service principals (SPs) or Windows service accounts mapped to Azure AD. Those SPs usually have a “one‑size‑fits‑all” scope – e.g. Directory.Read.All, User.ReadWrite.All – and never get MFA or device compliance checks. The result? A permission model that’s hard to audit and easy to abuse.

CA lets you add context to every sign‑in: device compliance, location, risk level. Combine that with MIs and you can enforce:

  • Least‑privilege – only the permissions the workload really needs.
  • Contextual access – device compliance, location, risk.
  • Auditability – every token request and policy decision is logged.

Setting the Stage: Prerequisites

Item Minimum Why
Azure AD tenant 2.0 CA needs Premium P1/P2
Azure CLI 2.55+ Quick scripting
PowerShell Az module 10.0+ Policy creation
Microsoft Graph Latest Query SPs

Tip: If you’re still on the old Azure AD Graph API, migrate to Microsoft Graph first – it’s the only supported API for new features.


1. Identify Service Accounts That Need Protection

Start by inventorying all SPs with elevated permissions. A quick Graph query will pull the ones with Directory.Read.All:

az ad sp list \
  --query "[?appOwnerOrganizationId=='<tenant-id>' && appRoles[?value=='Directory.Read.All']].{appId:appId,displayName:displayName}" \
  -o table

Filter by the roles that matter to you – Application.ReadWrite.All, User.ReadWrite.All, etc. Export the output to CSV for further analysis.

Trade‑off

Pros Cons
Clear view of attack vectors Query can be slow in very large tenants; consider paging or PowerShell

2. Convert Legacy SPs to Managed Identities (When Possible)

If you can refactor, replace the SP with a system‑assigned or user‑assigned MI. For an Azure Function that pulls secrets from Key Vault:

az functionapp identity assign --name myfunc --resource-group rg1

The function can now use the default credential without any secrets in code:

var client = new SecretClient(
    new Uri("https://myvault.vault.azure.net/"),
    new DefaultAzureCredential());

Caveat: MIs can’t be used for on‑premises workloads or services that require a client secret (e.g. some legacy APIs). In those cases stick with an SP but apply CA.


3. Create a Conditional Access Policy for Service Accounts

3.1 Define the Scope

A CA policy for a service account targets:

  • Cloud apps – the Azure AD application that represents the SP.
  • Conditions – sign‑in risk, device platform, location.

If you have several SPs that share the same policy, group them into an Azure AD group and target that group.

3.2 Policy Example: Require Device Compliance

$policyName = "Require compliant device for Service Accounts"
$cloudAppIds = @(
    "00000000-0000-0000-0000-000000000001",
    "00000000-0000-0000-0000-000000000002"
)

$policy = New-AzADConditionalAccessPolicy -DisplayName $policyName `
    -State Enabled `
    -GrantControls @{BuiltInControls = @("RequireDeviceCompliant")} `
    -Conditions @{
        SignInRiskLevels = @("low","medium","high")
        ClientAppTypes = @("all")
        Locations = @{IncludeLocations = @("All")}
        DevicePlatforms = @{IncludePlatforms = @("All")}
    }

What this does: every time the service account requests a token, CA checks the device. If the device isn’t compliant (not Intune‑managed, for example) the token is denied.

3.3 Policy Example: Require MFA for High‑Risk Sign‑Ins

$policyName = "MFA for high‑risk service account sign‑ins"

New-AzADConditionalAccessPolicy -DisplayName $policyName `
    -State Enabled `
    -GrantControls @{BuiltInControls = @("RequireMultiFactorAuth")} `
    -Conditions @{
        SignInRiskLevels = @("high")
        ClientAppTypes = @("all")
        Locations = @{IncludeLocations = @("All")}
    }

Caveat: Service accounts normally can’t do MFA because they lack a UI. This policy is useful when the service account is accessed via a privileged user account that must be protected. In pure service‑to‑service scenarios skip MFA and rely on device compliance.


4. Enforce Least‑Privilege with Azure AD App Permissions

Even with CA, the permissions you grant matter. Follow the principle of least privilege:

  1. Scope to the minimal API – e.g. if you only need to read Key Vault secrets, grant https://vault.azure.net/.default with SecretGet.

  2. Add incremental consent – for MIs the default scope is /.default. Add any extra scopes via the portal or CLI:

    az ad app permission add --id <app-id> --api 00000000-0000-0000-0000-000000000000 --api-permissions 12345678-1234-1234-1234-1234567890=Scope
    
  3. Revoke unused permissions – run a script to clean up:

    Get-AzADAppPermission -ApplicationId <app-id> |
      Where-Object {$_.Value -eq 'NotUsed'} |
      Remove-AzADAppPermission
    

Trade‑off

Pros Cons
Reduces blast radius Requires careful testing – a missing permission can break a deployment

5. Monitoring and Alerting

Azure AD Sign‑In logs capture every token request. Use Azure Monitor or Sentinel for alerts. A simple Kusto query:

SigninLogs
| where AppId in ("<app-id-1>", "<app-id-2>")
| where ResultType == "0" // Success
| summarize Count = count() by bin(TimeGenerated, 1h), AppId

Set an alert if the count drops to zero for a period – that could mean a policy block or a credential compromise.


6. Common Pitfalls and Troubleshooting

Symptom Likely Cause Fix
Service account can’t acquire token after rollout Policy applied to wrong cloud app Verify the AppId in the policy matches the SP’s appId.
Token acquisition succeeds but API calls fail Permissions revoked by CA but not updated in app Re‑grant the required scopes in Azure AD.
MFA prompt appears for a service account Policy targeting a user account instead of the SP Ensure the policy’s Users field is empty or excludes the SP.
Device compliance check fails on a VM VM not enrolled in Intune Enroll the VM or exempt it in the policy.

7. Real‑World Scenario: Protecting a Data‑Lake Pipeline

A Databricks cluster writes logs to Azure Monitor and pulls config from Key Vault using a service principal. Here’s how to secure it:

  1. Convert the SP to a user‑assigned MI:

    az identity create --name databricks-mi --resource-group rg1
    
  2. Assign the MI to the cluster:

    az databricks workspace update --name dbws --resource-group rg1 --assign-identity <mi-id>
    
  3. Create a CA policy that requires the MI to be on a compliant device (the cluster’s VM must be Intune‑managed).

  4. Grant only SecretGet on Key Vault and LogsWrite on Azure Monitor.

  5. Set up an alert that fires if the MI fails to acquire a token for more than 5 minutes.

Result: The pipeline runs smoothly, but any attempt to hijack the MI from an untrusted VM is blocked by CA.


8. When Managed Identities Are Not an Option

Legacy workloads that run on‑premises or use third‑party APIs that need a client secret still rely on SPs. In those cases:

  • Rotate secrets regularly – store the secret in Key Vault and rotate it automatically.
  • Apply CA to the SP’s cloud app – enforce device compliance or risk‑based policies even without a UI.
  • Use a “just‑in‑time” approach – generate a short‑lived credential that the workload fetches from Key Vault at startup.

9. Future‑Proofing

Azure AD is moving toward Identity Governance. In 2026 you can schedule automatic reviews for service accounts that have been inactive for 90 days. Combine that with CA to automatically revoke access when a policy is no longer needed.


See also