Entra ID — App Registrations at Scale

Enterprise Identity Management for Integration Workloads


Introduction

Managing application identities at enterprise scale requires more than just creating app registrations in Entra ID (formerly Azure AD). You need a structured approach to service principals, managed identities, certificate lifecycle management, and role-based access control. When your integration landscape spans dozens of Functions, Logic Apps, and API connections, having identity management at scale becomes critical for security and operational efficiency.

This comprehensive guide covers:

  • Enterprise app registration strategy — Organizing at scale
  • Managed identities — Elimination of secrets
  • Service principal management — Automation and governance
  • Certificate lifecycle — Rotation and renewal
  • Access control patterns — RBAC for integration services

Enterprise Organization

Application Hierarchy

┌─────────────────────────────────────────────────────────────────────┐
│                  ENTRA ID ORGANIZATION                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   Tenant                                                            │
│   └── Enterprise Applications                                       │
│       │                                                             │
│       ├── Production                                                │
│       │   ├── Integration Services                                  │
│       │   │   ├── order-processing-api                              │
│       │   │   ├── customer-sync-service                             │
│       │   │   └── inventory-webhook                                 │
│       │   │                                                         │
│       │   ├── External Integrations                                 │
│       │   │   ├── sap-connection                                    │
│       │   │   ├── salesforce-integration                            │
│       │   │   └── partner-api-access                                │
│       │   │                                                         │
│       │   └── Internal Tools                                        │
│       │       ├── monitoring-dashboard                              │
│       │       └── admin-portal                                      │
│       │                                                             │
│       ├── Staging (mirror production structure)                     │
│       │                                                             │
│       └── Development (ephemeral, auto-cleanup)                     │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Naming Conventions

{
  "namingConventions": {
    "appRegistration": {
      "pattern": "{environment}-{service}-{purpose}",
      "examples": [
        "prod-functions-orders-api",
        "staging-logicapps-customer-sync",
        "dev-servicebus-sender"
      ]
    },
    "servicePrincipal": {
      "pattern": "sp-{app-registration-name}",
      "autoProvision": true
    },
    "managedIdentity": {
      "pattern": "{resource-type}-{environment}-{name}",
      "examples": [
        "func-prod-order-processor",
        "la-staging-data-migration",
        "vm-prod-integration-runner"
      ]
    },
    "secretCredential": {
      "pattern": "{app}-{purpose}-{rotation-date}",
      "description": "Include rotation date in description"
    }
  }
}

Managed Identities

Azure Function with Managed Identity

// Using Managed Identity to access Service Bus
public class ManagedIdentityFunction
{
    private readonly ServiceBusClient _serviceBusClient;

    public ManagedIdentityFunction(IConfiguration config)
    {
        // No connection string needed - uses managed identity
        _serviceBusClient = new ServiceBusClient(
            "mynamespace.servicebus.windows.net",
            new DefaultAzureCredential());
    }

    [Function("ProcessOrders")]
    public async Task Run(
        [ServiceBusTrigger("orders", Connection = "ServiceBusEndpoint")]
        ServiceBusReceivedMessage message)
    {
        // Process message - automatically uses managed identity
        var order = JsonSerializer.Deserialize<Order>(message.Body.ToString());
        await _orderService.ProcessAsync(order);
    }
}

// Configuration (no secrets needed in configuration!)
// appsettings.json
{
  "Values": {
    "ServiceBusEndpoint": "https://mynamespace.servicebus.windows.net"
  },
  "ConnectionStrings": {}
}

Managed Identity for Multiple Resources

public class MultiResourceManagedIdentity
{
    private readonly TokenCredential _credential;

    public MultiResourceManagedIdentity()
    {
        _credential = new DefaultAzureCredential(
            new DefaultAzureCredentialOptions
            {
                ManagedIdentityClientId = "optional-specific-mi-id"
            });
    }

    public ServiceBusClient CreateServiceBusClient(string namespace_)
    {
        return new ServiceBusClient($"{namespace_}.servicebus.windows.net", 
            _credential);
    }

    public CosmosClient CreateCosmosClient(string accountName)
    {
        return new CosmosClient(
            $"https://{accountName}.documents.azure.com",
            _credential);
    }

    public BlobClient CreateBlobClient(string accountName, string container, string blob)
    {
        return new BlobClient(
            new Uri($"https://{accountName}.blob.core.windows.net/{container}/{blob}"),
            _credential);
    }

    public KeyVaultClient CreateKeyVaultClient(string vaultUrl)
    {
        return new KeyVaultClient(
            new KeyVaultCredentialHandler(_credential));
    }
}

Enabling Managed Identity

# Enable managed identity on Function App
az functionapp identity assign \
  --name my-function-app \
  --resource-group my-rg \
  --system-assigned

# Enable on Logic Apps (via ARM)
az deployment group create \
  --resource-group my-rg \
  --template-file logic-app-with-mi.json

# Verify managed identity
az functionapp show \
  --name my-function-app \
  --resource-group my-rg \
  --query identity

# Grant access to resources
# Grant Service Bus Sender role
az role assignment create \
  --assignee <managed-identity-principal-id> \
  --role "Azure Service Bus Data Sender" \
  --scope /subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.ServiceBus/namespaces/mynamespace

# Grant Key Vault access
az keyvault set-policy \
  --name my-keyvault \
  --object-id <managed-identity-principal-id> \
  --secret-permissions get list

Service Principal Management

Automated Service Principal Lifecycle

public class ServicePrincipalManager
{
    public async Task<Application> CreateApplicationAsync(ApplicationRequest request)
    {
        var app = new Application
        {
            DisplayName = request.DisplayName,
            Description = request.Description,
            SignInAudience = request.IsInternal 
                ? "AzureADMultipleOrgs" 
                : "AzureADMyOrg",
            RequiredResourceAccess = request.RequiredPermissions
                .Select(p => new RequiredResourceAccess
                {
                    ResourceAppId = p.ResourceAppId,
                    ResourceAccess = p.Accesses
                }).ToList()
        };

        return await _graphClient.Applications
            .Request()
            .AddAsync(app);
    }

    public async Task CreateServicePrincipalAsync(string appId)
    {
        var sp = new ServicePrincipal
        {
            AppId = appId,
            AccountEnabled = true
        };

        await _graphClient.ServicePrincipals
            .Request()
            .AddAsync(sp);
    }

    public async Task RotateCredentialAsync(string appId, int validityDays = 365)
    {
        // Add new password credential
        var passwordCredential = new PasswordCredential
        {
            DisplayName = $"Rotated {DateTime.UtcNow:yyyy-MM-dd}",
            EndDateTime = DateTime.UtcNow.AddDays(validityDays)
        };

        await _graphClient.Applications[appId]
            .AddPassword(passwordCredential)
            .Request()
            .PostAsync();

        // Optionally: Remove old credentials older than X days
        // This would require tracking credential creation dates
    }
}

GitHub Actions for App Registration

name: Manage App Registration

on:
  workflow_dispatch:
    inputs:
      action:
        description: 'Action to perform'
        required: true
        type: choice
        options:
          - create
          - rotate-secret
          - disable

jobs:
  manage-app:
    runs-on: ubuntu-latest
    steps:
      - name: Azure Login
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Create App Registration
        if: github.event.inputs.action == 'create'
        run: |
          az ad app create \
            --display-name "${{ vars.APP_NAME }}" \
            --identifier-uris "api://${{ vars.APP_NAME }}" \
            --required-resource-access @manifest.json

      - name: Rotate Secret
        if: github.event.inputs.action == 'rotate-secret'
        run: |
          # Generate new client secret
          az ad app credential reset \
            --id "${{ vars.APP_ID }}" \
            --append
          
          # Update Key Vault with new secret
          az keyvault secret set \
            --vault-name "${{ vars.KEY_VAULT }}" \
            --name "${{ vars.SECRET_NAME }}" \
            --value "$AZURE_CLIENT_SECRET"

      - name: Disable App
        if: github.event.inputs.action == 'disable'
        run: |
          az ad app update \
            --id "${{ vars.APP_ID }}" \
            --set availableToOtherTenants=false

Certificate Management

Certificate Lifecycle

# Create certificate with Key Vault
az keyvault certificate create \
  --vault-name my-keyvault \
  --name integration-cert \
  --policy @policy.json

# policy.json
{
  "issuerParameters": {
    "certificateType": "Self-Signed",
    "name": "Self"
  },
  "keyProperties": {
    "exportable": true,
    "keySize": 2048,
    "keyType": "RSA",
    "reuseKey": false
  },
  "lifetimeActions": [
    {
      "trigger": {
        "daysBeforeExpiry": 30
      },
      "action": {
        "actionType": "Notify"
      }
    },
    {
      "trigger": {
        "daysBeforeExpiry": 7
      },
      "action": {
        "actionType": "AutoRenew"
      }
    }
  ],
  "secretProperties": {
    "contentType": "application/x-pkcs12"
  }
}

Certificate Rotation Automation

public class CertificateRotationService
{
    private readonly KeyVaultClient _keyVaultClient;

    public async Task RotateCertificateAsync(string appRegObjectId)
    {
        // Get current certificate from Key Vault
        var currentCert = await _keyVaultClient
            .GetSecretAsync("keyvault-url", "integration-cert");

        // Create new certificate
        var newCert = await _keyVaultClient
            .CreateCertificateAsync("keyvault-url", new CertificatePolicy
            {
                IssuerParameters = new IssuerParameters("Self"),
                KeyProperties = new KeyProperties(keySize: 2048),
                LifetimeActions = new[]
                {
                    new LifetimeAction
                    {
                        Action = CertificateActionType.AutoRenew,
                        Trigger = new Trigger
                        {
                            DaysBeforeExpiry = 30
                        }
                    }
                }
            });

        // Update Application manifest with new certificate
        var updatedCredential = new KeyCredential
        {
            KeyId = Guid.NewGuid().ToString(),
            DisplayName = $"Certificate {DateTime.UtcNow:yyyy-MM}",
            StartDateTime = DateTime.UtcNow,
            EndDateTime = DateTime.UtcNow.AddYears(1),
            Type = "AsymmetricX509Cert",
            Usage = "Verify"
        };

        await _graphClient.Applications[appRegObjectId]
            .AddKey(updatedCredential)
            .Request()
            .PostAsync();

        // Store new thumbprint for reference
        await _configStore.SetAsync(
            $"cert-thumbprint-{DateTime.UtcNow:yyyyMM}",
            newCert.Thumbprint);
    }
}

Access Control Patterns

Role-Based Access for Integration Services

{
  "rbacModel": {
    "integrationDeveloper": {
      "description": "Can deploy and manage integration resources",
      "assignableScopes": [
        "/subscriptions/xxx/resourceGroups/rg-integration"
      ],
      "permissions": [
        "Microsoft.Web/sites/read",
        "Microsoft.Web/sites/write",
        "Microsoft.Web/functions/read",
        "Microsoft.Web/functions/write",
        "Microsoft.ServiceBus/namespaces/queues/read",
        "Microsoft.ServiceBus/namespaces/queues/write"
      ]
    },
    "integrationOperator": {
      "description": "Can monitor and operate integration flows",
      "assignableScopes": [
        "/subscriptions/xxx/resourceGroups/rg-integration"
      ],
      "permissions": [
        "Microsoft.Web/sites/read",
        "Microsoft.ServiceBus/namespaces/queues/read",
        "Microsoft.ServiceBus/namespaces/topics/read",
        "Microsoft.Insights/metrics/read"
      ]
    },
    "integrationViewer": {
      "description": "Read-only access to integration resources",
      "assignableScopes": [
        "/subscriptions/xxx"
      ],
      "permissions": [
        "Microsoft.Web/sites/read",
        "Microsoft.ServiceBus/namespaces/read",
        "Microsoft.Insights/components/read"
      ]
    }
  }
}

Custom Role Definition

az role definition create \
  --role-definition '{
    "Name": "Integration Developer",
    "IsCustom": true,
    "Description": "Can develop and deploy integration solutions",
    "Actions": [
      "Microsoft.Web/sites/read",
      "Microsoft.Web/sites/write",
      "Microsoft.Web/sites/extensions/read",
      "Microsoft.Web/sites/extensions/write",
      "Microsoft.Web/hostingEnvironments/read",
      "Microsoft.ServiceBus/namespaces/read",
      "Microsoft.ServiceBus/namespaces/queues/*",
      "Microsoft.ServiceBus/namespaces/topics/*",
      "Microsoft.Storage/storageAccounts/blobServices/containers/read",
      "Microsoft.KeyVault/vaults/secrets/read",
      "Microsoft.KeyVault/vaults/secrets/write"
    ],
    "NotActions": [],
    "DataActions": [],
    "NotDataActions": [],
    "AssignableScopes": [
      "/subscriptions/xxx/resourceGroups/rg-integration"
    ]
  }'

Best Practices

Implementation Checklist

PracticeDescription
Use managed identitiesAvoid secrets where possible
Implement naming conventionsConsistent naming enables management
Automate credential rotationDon't rely on manual processes
Use groups for assignmentsAssign roles to groups, not individuals
Audit access regularlyQuarterly access reviews
Document purposeEvery app registration should have clear purpose

Security Considerations

┌─────────────────────────────────────────────────────────────────────┐
│                  IDENTITY SECURITY CHECKLIST                        │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   ✓ Disable legacy authentication (basic auth)                      │
│   ✓ Require MFA for all admin accounts                              │
│   ✓ Use conditional access policies                                 │
│   ✓ Rotate credentials automatically (max 1 year)                   │
│   ✓ Monitor sign-in logs for anomalies                              │
│   ✓ Use service principals for automation                           │
│   ✓ Grant least-privilege permissions                               │
│   ✓ Enable identity protection                                      │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Related Topics


Azure Integration Hub - Architect Level Security Architecture & Zero Trust