Introduction

An important part of Azure Landing Zones is the ability to create Azure subscriptions (landing zones) through automation.

Any scripts and code used in this post can be found at: https://github.com/SebastianClaesson/SubscriptionVendingExample

Enterprise Agreement Role Assignment for workload identities

There's an article on Microsoft Learn that goes through how to assign a workload identity permissions to an Enterprise Agreement.

If you are interested in following the least-privileged access model, then we must follow the article above to grant our workload identity access as "SubscriptionCreator" over our enrollment account. The process of subscription vending must not be bound to a employees account or permissions. However, not everyone is comfortable following the guide and take short-cuts such as assigning Enterprise Administrator over the billing account using the IAM controls in Azure.

To assist with the creation of the EA Role assignment, I've created the following script New-EnterpriseAgreementRoleAssignment

Once the assignment has been done, we need to build our subscription vending automation somewhere. This could be an Azure Function, GitHub/Azure DevOps Pipeline, Custom container or part of your self-service portal.

Create your first Azure Subscription using PowerShell

The Az.Subscription PowerShell module contains the function "New-AzSubscriptionAlias" to provision a new Azure Subscription.

We can simply write a function to provision the subscription using our workload identity. The function will run in the current logged in users context.

[CmdletBinding()]
param (
    # BillingScope
    [Parameter(Mandatory)]
    [string]
    $BillingScope,

    # Workload
    [Parameter(Mandatory)]
    [string]
    $Workload,

    # ManagementGroupId
    [Parameter(Mandatory)]
    [string]
    $ManagementGroupId,

    # Identifier
    [Parameter(Mandatory)]
    [string]
    $Identifier,

    # Environment
    [Parameter(Mandatory)]
    [string]
    $EnvironmentShortName,

    # DisplayName
    [Parameter(Mandatory)]
    [string]
    $DisplayName
)

# The script requires the Az PowerShell Module
if (! (Get-Module 'Az.Subscription' -ListAvailable)) {
    Throw 'Please install the Az PowerShell Module'
}

Import-Module .\Az.Subscription

$params = @{
    AliasName = "$Identifier-$EnvironmentShortName".toLower()
    SubscriptionName = "$DisplayName-$EnvironmentShortName".toLower()
    BillingScope = $BillingScope
    Workload = $Workload
    ManagementGroupId = $ManagementGroupId
}

Write-Verbose "Attempting to list any Azure Subscription" -Verbose
$SubAliases = Get-AzSubscriptionAlias

if ($SubAliases.AliasName -Contains "$($params.AliasName)") {
    $SubscriptionInfo = $SubAliases | Where-Object {$_.AliasName -eq "$($params.AliasName)"}
    Write-Verbose "The subscription already exists, Skipping creation." -Verbose
} else {
    try {
        $SubscriptionInfo = New-AzSubscriptionAlias @params
        Write-verbose "Successfully created '$($SubscriptionInfo.AliasName)'" -Verbose
    } catch {
        throw
    }
}

Azure DevOps pipeline example

Configure Service Connection with Federated Credentials

To utilize Azure DevOps for our Workload identity, we can utilize different types of authentication. I strongly recommend that federated credentials is configured to authenticate our workload identity.

This means we do not have to manage a client secret, instead we trust the Azure DevOps directory to manage the credentials to our workload identity.

You must register the resource providers for Management Groups and Subscriptions to successfully run the automation in your initial subscription.

## Requires the module ADOPS
function New-ADOServiceConnection {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)] [string] $SubscriptionId,
        [Parameter(Mandatory)] [string] $SubscriptionName,
        [Parameter(Mandatory)] [string] $Environment,
        [Parameter(Mandatory)] [string] $Identifier,
        [Parameter(Mandatory)] [string] $ADOOrganization,
        [Parameter(Mandatory)] [string] $ADOProjectName,
        [Parameter(Mandatory)] [string] $RoleDefinitionName
    )
}

# Connects to Azure DevOps
Connect-ADOPS -Organization $Organization -OAuthToken $AccessToken

# Gets the Azure DevOps Service Connection
$AdopsSC = Get-ADOPSServiceConnection -Name $ServiceConnectionName -Project $ProjectName

# Creates Federated Credentials for authentication
$FederatedCredentialsParams = @{
    ApplicationObjectId = $AppDetails.Id
    Issuer = $AdopsSC.authorization.parameters.workloadIdentityFederationIssuer
    Subject = $AdopsSC.authorization.parameters.workloadIdentityFederationSubject
    Name = 'AzureDevOpsAuthentication'
    Audience = 'api://AzureADTokenExchange'
}
New-AzADAppFederatedCredential @FederatedCredentialsParams

Do not forget to limit access to the service principal. If you intend to let non-platform engineers provision and create landing zones then perhaps a good option is to configure approval for the use of the service connection.

Azure DevOps .yml example

trigger: none

variables:
- name: subscriptionCreationServiceConnection
  value: 'SERVICECONNECTIONNAME'

parameters:
- name: Identifier
  displayName: What is the identifying name of the Azure Landing Zone?
  type: string
- name: DisplayName
  displayName: What is the display name of the Azure Landing Zone?
  type: string
- name: ManagementGroupName
  displayName: Azure Management Group Name
  type: string
- name: Workload
  displayName: Azure Subscription Workload offer
  type: string
  values:
  - Production
  - DevTest
- name: Environment
  displayName: Environment?
  type: string
  default: Sandbox
  values:
  - sbx
  - dev
  - acc
  - prod

stages:
  - stage: provision
    displayName: 'Subscription Vending'
    jobs:
    - job: subscriptionCreate
      displayName: 'Create Azure Subscription'
      steps:
      - task: AzurePowerShell@5
        displayName: 'Create Azure Subscription'
        inputs:
          azureSubscription: $(subscriptionCreationServiceConnection)
          ScriptType: 'FilePath'
          azurePowerShellVersion: LatestVersion
          pwsh: true
          ScriptPath: 'New-AzureSubscription.ps1'

After importing and running the Azure DevOps pipeline, the output should simply look like this:

Azure DevOps subscription vending pipeline output

Conclusion

We have now established a workload identity with federated credentials to provision our Azure Subscriptions. We have also created the necessary automation & Azure DevOps pipeline to provide a basic self-service feature for our colleagues.

We can continue to add steps to our subscription vending, for example incorporating the orchestration of Entra Id security groups, privileged access management, entitlement management, IP address management, critical infrastructure resources such as peering to hub network, budgets, service health alerts and so on.

References: