Back to Blog
6 min read

Azure Lighthouse for Multi-Tenant Management

Introduction

Azure Lighthouse enables service providers and enterprises to manage multiple Azure tenants with enhanced visibility, automation, and governance. Through Azure delegated resource management, you can access customer resources directly from your tenant while maintaining security and compliance. This guide covers practical implementation patterns for multi-tenant scenarios.

Understanding Azure Lighthouse

Key Concepts

**Managing Tenant**: Your organization's Azure AD tenant
**Customer Tenant**: The tenant you're managing (customer or subsidiary)
**Delegation**: Permission grant from customer to managing tenant
**Authorization**: Specific RBAC roles granted to managing tenant users/groups

Benefits:
- Single pane of glass for multiple tenants
- No context switching between directories
- Automated at-scale operations
- Audit trail across tenants

Onboarding Customers

ARM Template for Delegation

{
  "$schema": "https://schema.management.azure.com/schemas/2019-08-01/subscriptionDeploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "mspOfferName": {
      "type": "string",
      "metadata": {
        "description": "Managed Service Provider offer name"
      },
      "defaultValue": "Contoso Managed Services"
    },
    "mspOfferDescription": {
      "type": "string",
      "metadata": {
        "description": "Description of the MSP offer"
      },
      "defaultValue": "Provides monitoring, security, and management services"
    },
    "managedByTenantId": {
      "type": "string",
      "metadata": {
        "description": "Managing tenant ID"
      }
    },
    "authorizations": {
      "type": "array",
      "metadata": {
        "description": "Array of authorization objects"
      }
    }
  },
  "variables": {
    "mspRegistrationName": "[guid(parameters('mspOfferName'))]",
    "mspAssignmentName": "[guid(parameters('mspOfferName'))]"
  },
  "resources": [
    {
      "type": "Microsoft.ManagedServices/registrationDefinitions",
      "apiVersion": "2020-02-01-preview",
      "name": "[variables('mspRegistrationName')]",
      "properties": {
        "registrationDefinitionName": "[parameters('mspOfferName')]",
        "description": "[parameters('mspOfferDescription')]",
        "managedByTenantId": "[parameters('managedByTenantId')]",
        "authorizations": "[parameters('authorizations')]"
      }
    },
    {
      "type": "Microsoft.ManagedServices/registrationAssignments",
      "apiVersion": "2020-02-01-preview",
      "name": "[variables('mspAssignmentName')]",
      "dependsOn": [
        "[resourceId('Microsoft.ManagedServices/registrationDefinitions', variables('mspRegistrationName'))]"
      ],
      "properties": {
        "registrationDefinitionId": "[resourceId('Microsoft.ManagedServices/registrationDefinitions', variables('mspRegistrationName'))]"
      }
    }
  ],
  "outputs": {
    "registrationDefinitionId": {
      "type": "string",
      "value": "[resourceId('Microsoft.ManagedServices/registrationDefinitions', variables('mspRegistrationName'))]"
    }
  }
}

Parameters File

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "mspOfferName": {
      "value": "Contoso Managed Services"
    },
    "mspOfferDescription": {
      "value": "Infrastructure monitoring and management services"
    },
    "managedByTenantId": {
      "value": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    },
    "authorizations": {
      "value": [
        {
          "principalId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
          "roleDefinitionId": "acdd72a7-3385-48ef-bd42-f606fba81ae7",
          "principalIdDisplayName": "MSP Operators Group"
        },
        {
          "principalId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
          "roleDefinitionId": "b24988ac-6180-42a0-ab88-20f7382dd24c",
          "principalIdDisplayName": "MSP Administrators Group"
        },
        {
          "principalId": "cccccccc-cccc-cccc-cccc-cccccccccccc",
          "roleDefinitionId": "91c1777a-f3dc-4fae-b103-61d183457e46",
          "principalIdDisplayName": "MSP Security Team",
          "delegatedRoleDefinitionIds": [
            "a]7ffa36-a066-4b39-a5ea-ed1dbe8e3e6c"
          ]
        }
      ]
    }
  }
}

Deploying Delegation

PowerShell Deployment

# Deploy to customer subscription
$customerSubscriptionId = "customer-subscription-id"

# Connect to customer tenant
Connect-AzAccount -Tenant "customer-tenant-id" -Subscription $customerSubscriptionId

# Deploy the delegation template
New-AzSubscriptionDeployment `
  -Location "australiaeast" `
  -TemplateFile "./lighthouse-delegation.json" `
  -TemplateParameterFile "./lighthouse-params.json" `
  -Name "ContosoDelegation"

Azure CLI Deployment

# Login to customer context
az login --tenant customer-tenant-id

# Set subscription
az account set --subscription customer-subscription-id

# Deploy delegation
az deployment sub create \
  --location australiaeast \
  --template-file lighthouse-delegation.json \
  --parameters @lighthouse-params.json \
  --name ContosoDelegation

Managing Delegated Resources

Listing Delegated Subscriptions

# From managing tenant
Connect-AzAccount -Tenant "managing-tenant-id"

# List all delegated subscriptions
Get-AzSubscription | ForEach-Object {
    $sub = $_
    $delegation = Get-AzManagedServicesAssignment -Scope "/subscriptions/$($sub.Id)" -ErrorAction SilentlyContinue
    if ($delegation) {
        [PSCustomObject]@{
            SubscriptionName = $sub.Name
            SubscriptionId = $sub.Id
            TenantId = $sub.TenantId
            OfferName = $delegation.Properties.RegistrationDefinitionName
        }
    }
}

Cross-Tenant Azure CLI Commands

# List VMs across all delegated subscriptions
az vm list --query "[].{Name:name, ResourceGroup:resourceGroup, Location:location}" --output table

# Query with Resource Graph across delegated subscriptions
az graph query -q "
Resources
| where type =~ 'microsoft.compute/virtualmachines'
| project name, resourceGroup, subscriptionId, location
| limit 100"

Automation with Azure Lighthouse

Azure Automation Runbook

# Runbook: Start-DelegatedVMs.ps1
param(
    [Parameter(Mandatory=$false)]
    [string]$TagName = "AutoStart",
    [Parameter(Mandatory=$false)]
    [string]$TagValue = "True"
)

# Authenticate using Managed Identity
Connect-AzAccount -Identity

# Get all delegated subscriptions
$subscriptions = Get-AzSubscription

foreach ($sub in $subscriptions) {
    Set-AzContext -SubscriptionId $sub.Id

    # Find VMs with specific tag
    $vms = Get-AzVM | Where-Object {
        $_.Tags[$TagName] -eq $TagValue
    }

    foreach ($vm in $vms) {
        Write-Output "Starting VM: $($vm.Name) in subscription $($sub.Name)"

        Start-AzVM -ResourceGroupName $vm.ResourceGroupName `
                   -Name $vm.Name -NoWait
    }
}

Azure Policy at Scale

# Apply policy to all delegated subscriptions
$policyDefinitionId = "/providers/Microsoft.Authorization/policyDefinitions/0015ea4d-51ff-4ce3-8d8c-f3f8f0179a56"

$subscriptions = Get-AzSubscription

foreach ($sub in $subscriptions) {
    Set-AzContext -SubscriptionId $sub.Id

    $existingAssignment = Get-AzPolicyAssignment -Name "DiagnosticSettings" -Scope "/subscriptions/$($sub.Id)" -ErrorAction SilentlyContinue

    if (-not $existingAssignment) {
        New-AzPolicyAssignment `
            -Name "DiagnosticSettings" `
            -DisplayName "Enable Diagnostic Settings" `
            -PolicyDefinition (Get-AzPolicyDefinition -Id $policyDefinitionId) `
            -Scope "/subscriptions/$($sub.Id)"

        Write-Output "Policy assigned to: $($sub.Name)"
    }
}

Azure Functions for Multi-Tenant Operations

using Azure.Identity;
using Azure.ResourceManager;
using Azure.ResourceManager.Compute;

public class CrossTenantVmService
{
    private readonly ArmClient _armClient;

    public CrossTenantVmService()
    {
        // Uses Managed Identity - works across delegated subscriptions
        _armClient = new ArmClient(new DefaultAzureCredential());
    }

    public async Task<IList<VmInfo>> GetAllVmsAsync()
    {
        var vms = new List<VmInfo>();

        await foreach (var subscription in _armClient.GetSubscriptions())
        {
            await foreach (var vm in subscription.GetVirtualMachinesAsync())
            {
                vms.Add(new VmInfo
                {
                    Name = vm.Data.Name,
                    SubscriptionId = subscription.Id,
                    ResourceGroup = vm.Id.ResourceGroupName,
                    Location = vm.Data.Location,
                    PowerState = vm.Data.InstanceView?.Statuses?
                        .FirstOrDefault(s => s.Code?.StartsWith("PowerState/") == true)?.Code
                });
            }
        }

        return vms;
    }

    public async Task StartVmAsync(string subscriptionId, string resourceGroup, string vmName)
    {
        var subscription = await _armClient.GetSubscriptions()
            .GetAsync(subscriptionId);

        var vm = await subscription.Value.GetResourceGroups()
            .GetAsync(resourceGroup)
            .Result.Value.GetVirtualMachines()
            .GetAsync(vmName);

        await vm.Value.PowerOnAsync(Azure.WaitUntil.Started);
    }
}

public record VmInfo
{
    public string Name { get; init; }
    public string SubscriptionId { get; init; }
    public string ResourceGroup { get; init; }
    public string Location { get; init; }
    public string PowerState { get; init; }
}

Monitoring Delegated Resources

Log Analytics Workspace Query

// Query across all delegated workspaces
union withsource=SubscriptionId *
| where TimeGenerated > ago(24h)
| where Category == "Administrative"
| summarize EventCount = count() by SubscriptionId, OperationName
| order by EventCount desc

Azure Monitor Alerts

{
  "type": "Microsoft.Insights/scheduledQueryRules",
  "apiVersion": "2021-08-01",
  "name": "CrossTenantAlert",
  "location": "australiaeast",
  "properties": {
    "displayName": "Critical Events Across Tenants",
    "severity": 1,
    "enabled": true,
    "evaluationFrequency": "PT5M",
    "windowSize": "PT5M",
    "scopes": [
      "/subscriptions/{subscription-id}/resourceGroups/{rg}/providers/Microsoft.OperationalInsights/workspaces/{workspace}"
    ],
    "criteria": {
      "allOf": [
        {
          "query": "AzureActivity | where Level == 'Critical' | summarize count() by SubscriptionId",
          "timeAggregation": "Count",
          "operator": "GreaterThan",
          "threshold": 0
        }
      ]
    },
    "actions": {
      "actionGroups": ["/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Insights/actionGroups/ops-team"]
    }
  }
}

Best Practices

1. **Use Azure AD Groups** - Assign roles to groups, not individuals
2. **Principle of Least Privilege** - Grant minimum required permissions
3. **Separate Environments** - Different delegations for prod/non-prod
4. **Audit Regularly** - Review delegation assignments
5. **Document Authorizations** - Maintain clear records
6. **Use Eligible Roles** - Enable PIM for sensitive operations
7. **Automate Onboarding** - Use templates for consistency

Conclusion

Azure Lighthouse transforms multi-tenant management from a fragmented experience to a unified operational model. Whether you’re a managed service provider or an enterprise with multiple subsidiaries, Lighthouse enables efficient, secure, and auditable cross-tenant operations. Combined with Azure Policy and automation, it provides the foundation for scalable governance.

References

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.