When automating tasks in Azure, you'll often need a service principal.
Setting these up using the UI feels like driving my car to the mailbox.
Let's automate this.
The final product when we finish automating everything. |
Terms Bingo
Before getting into the article let's get a couple basic terms out of the way first:
- Active Directory (AD): Microsoft's on-premises solution for managing users, computers, etc. introduced in Windows 2000.
- Azure
Active Directory (AAD): Microsoft's cloud solution for
managing users, applications, and more. Hosted on Microsoft's Azure
platform, this can integrate with on-premises Active Directory if
desired, but is not required.
- (AAD) Tenant: An instance of Azure Active Directory for a customer is called a "tenant". Most customers will have one tenant, but larger organizations may have multiple for varying reasons.
A Service Principal?
In the Active Directory world, automated tasks are performed by service accounts, be they traditional dedicated accounts or (group) managed service accounts. The key to securely performing these tasks is ensuring that unique security principals are used for each task, ensuring they have only the amount of access they need to perform the task in question, and that they're monitored proactively.
Azure Active Directory does not have the concept of service accounts, but there is a functional equivalent: Service Principals. Service principals are comprised of:
- Azure Active Directory Registered Application: Registering an application in AAD is a way to then grant permissions (using a Service Principal) to that application within Azure and/or Azure Active Directory. This can be an application hosted in Azure, externally, or in our case, an automation task of another nature. An AAD registered application can also be used by other tenants (with a Service Principal on their side), but as we're talking about automating our own tasks that is outside the scope of this article.
- The Service Principal itself: The service principal is an association with an AAD Application that allows for granting of permissions within that tenant. In our case we'll be discussing AAD registered apps and service principals in a 1:1 ratio.
Azure Entitlement Domains
To facilitate the principle of least privilege, we need to understand the levels of granularity by which permissions are assigned in Azure. There are three levels rights can be assigned at:
- Subscription: One can grant a service principal, user, or other object access at an entire subscription level. There are very few cases where doing so would adhere to the principle of least privilege, so don't do this unless you have no option.
- Resource Group: A resource group is a logical grouping of Azure resources. Depending on how you split your resource groups, this is likely a common place to assign privileges.
- Object: Privileges can be assigned on a per-object basis as well, but managing security on a per-object basis is very complex and usually only done when absolutely necessary from a security perspective.
Sorry son, your RBAC doesn't give you access to the keyvault. |
Usage Examples
So when would we use a service principal? Here are a couple examples:
- Webjobs that interact with other resources: If you
have webjobs
that interact with other resources in your subscription you may choose
to use a service principal to access those resources. An excellent
example of this is the Let's
Encrypt! web app extension.
- Automated Tasks: Azure automation and/or external jobs running against Azure can leverage service principals to authenticate and perform their work. Code deployment platforms such as OctopusDeploy and VSTS are perfect examples.
Let Us Do This
And by this I mean the point of the article.
To make quick work of this from an automation perspective, we'll make a quick PowerShell function we can re-use in other scripts. The main PowerShell cmdlets we'll be leveraging are:
Just running these two commands is easy enough, but that's not all that useful from an automation perspective where we want to automate multiple operations that rely on the creation or existence of a service principal. To that end, we'll make a function that can accept a desired service principal name, check to see if it exists & create it if not. This function will output an object with all the necessary information for further use. We'll also generate a password if one isn't specified and report status regarding if the service principal already existed or not.We'll return everything in an object so our scripts can take appropriate actions for all possible scenarios.
Critical Note: The code below contains reference to a function that is not included, so before copying/pasting please read at least the "Note" sections in the code discussion below.
Update 1/2018: AzureRM 5.0 cmdlets require a securestring for New-AzureRmADApplication whereas it was not supported previously.################################################################################
# Register-AzureServicePrincipal
# Given the correct input, does one of the following:
# 1> checks for existence of application registration
# 2> checks to see if the app is registered as a service principal
# 3> if neither of those is true, creates the app and service principal
# > outputs an object with all details possible. If the app already exists the
# password will be null because we can't look it up.
# INPUT: servicePrincipalName, the displayname of the desired App/ServicePrincipal and the desired password.
# The password field is optional and if omitted a 30 character random password will be generated and returned.
# OUTPUT: an object containing the following NoteProperties
# > ClientID: the GUID representing the application ID
# > ServicePrincipalID: the GUID representing the Service Principal association
# > SPNNames: The service principal names of the SP
# > ServicePrincipalPassword: A securestring of the Application Password. NOTE: This will be NULL if the app is already registered in AD as we cannot retrieve it.
# > ServicePrincipalAlreadyExists: boolean to indicate if the sp already existed or not
# USAGE NOTES: Assumes already logged into Azure with proper permissions and that the desired subscription is selected.
Function Register-AzureServicePrincipal{
param(
# The name for the service principal. We won't make this mandatory to allow for manual entry mode with guidance. Obviously it needs to be specified for automation.
[string]$servicePrincipalName,
# the password if you choose to specify it, otherwise the script will generate one for you.
[string]$servicePrincipalPassword
)
# Set the regex for the input validation on the SPN
$SPNNamingStandard='^[--z]{5,40}$'
Write-Host "Provisioning AzureAD App/Service Principal"
Write-Warning "The account operating this script MUST have the role Subscription Admin or Owner in the desired subscription"
$ErrorActionPreference = "Stop" # Error handing is not yet sufficient; try/catch the stuff below!
if (!$servicePrincipalName){
do {
Write-Host "SPN naming standard is (in RegEx): $SPNNamingStandard"
$servicePrincipalName=Read-Host "Service Principal Name not specified on startup; Please enter desired name or type GUID and press enter for a guid based random name"
if ($servicePrincipalName -eq "GUID"){
$guid=([guid]::NewGuid()).toString()
$servicePrincipalName="SPN-$guid"
}
} until ($servicePrincipalName -match $SPNNamingStandard)
}
# handle command line specification of GUID
if ($servicePrincipalName -eq "GUID"){
$guid=([guid]::NewGuid()).toString()
$servicePrincipalName="SPN-$guid"
}
# set URL and IdentifierUris
$homePage = "http://" + $servicePrincipalName
$identifierUri = $homePage
Write-Host "Desired Service Principal Name is $servicePrincipalName `n"
# Now we need to determine if 1> the Application exists and 2> if it has been registered as a service principal. This will guide our execution through the end of the function.
$appExists=Get-AzureRmADApplication -DisplayNameStartWith $servicePrincipalName -ErrorAction SilentlyContinue
# check for SPN only if app exists. SPN can't exist without app so no reason to check if not.
if ($appExists){$spnExists=Get-AzureRmADServicePrincipal | Where-Object {$_.ApplicationId -eq $appExists.ApplicationId} -ErrorAction SilentlyContinue}
# we only need a password if the app hasn't been created yet.
if (!$appExists){
# Generate a password if needed
if (!$servicePrincipalPassword){
$servicePrincipalPassword=New-RandomPassword -passwordLength 40
}
# NOTE! We had a convertto-securestring here but as it turns out new-azurermadapplication doesn't take a securestring, only a string
# NOTEUPDATE! AzureRM 5.0 and higher requires a securestring (yay!) This has been updated but notes left here for reference.
$servicePrincipalPassword=ConvertTo-SecureString $servicePrincipalPassword -AsPlainText -Force
}
# we set this to NULL as a "valid" return as the appID already exists and we can't lookup the password from here
else {$servicePrincipalPassword=$null}
# Create the App if it wasn't already
if (!$appExists){
$azureADApplication=New-AzureRmADApplication -DisplayName $servicePrincipalName -HomePage $homePage -IdentifierUris $identifierUri -Password $servicePrincipalPassword
Write-Host "Azure AAD Application creation completed successfully"
}
# if it already exists we'll just redirect the variable
else{$azureADApplication=$appExists}
$appID=$azureADApplication.ApplicationId
# Create new SPN if needed
if (!$spnExists){
$spn=New-AzureRmADServicePrincipal -ApplicationId $appId
Write-Host "SPN creation completed successfully"
}
else{$spn=$spnExists}
$spnNames=$spn.ServicePrincipalNames
# Create object to store information.
$outputObject=New-Object -TypeName PSObject
$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalName -Value $servicePrincipalName
$outputObject | Add-Member -MemberType NoteProperty -Name ClientID -Value $appID
$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalID -Value $spn.Id
$outputObject | Add-Member -MemberType NoteProperty -Name SPNNames -Value $spnNames
$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalPassword -Value $servicePrincipalPassword
if ($appExists -and $spnexists){$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalAlreadyExists -Value $true}
else {$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalAlreadyExists -Value $false}
return $outputObject
}
################################################################################
Discussion
param(
# The name for the service principal.
[string]$servicePrincipalName,
# the password if you choose to specify it, otherwise the script will generate one for you.
[string]$servicePrincipalPassword
)
This is where the input to the function is defined; as you'll see below the password is optional, but the servicePrincipalName is mandatory. I don't mark the parameter as mandatory in the parameter definition to facilitate interactive use of this script. Feel free to change add mandatory=$true if desired.
While it would be logical to use [securestring] for the servicePrincipalPassword, the PowerShell cmdlet we're going to use downstream only supports regular strings at this time.
A quick note on interactive vs. non-interactive scripts
While the goal of automation should be running tasks headless, thus fully non-interactive, there are some scenarios where facilitating both non-interactive and interactive running can be very useful. In some cases when acting as a consultant I will allow for full non-interactive running of automation scripts so the customer can walk through each option guided once to understand the context of the options available to them. At the end of script execution, I echo out what the equivalent command line would be to the console if it were run entirely headless. This scenario does require a bit more control flow logic, mainly using an additional parameter to specify we're in a headless scenario and error out quickly prior to execution when running in those scenarios. This also allows for use of code snippets (mainly functions) on a day-to-day basis as well as in dedicated automation framework.
# Set the regex for the input validation on the SPN
$SPNNamingStandard='^[--z]{5,40}$'
Write-Host "Provisioning AzureAD App/Service Principal"
Write-Warning "The account operating this script MUST have the role Subscription Admin or Owner in the desired subscription"
$ErrorActionPreference = "Stop" # Error handing is not yet sufficient, try/catch the stuff below!
if (!$servicePrincipalName){
do {
Write-Host ""
Write-Host "SPN naming standard is (in RegEx): $SPNNamingStandard"
$servicePrincipalName=Read-Host "Service Principal Name not specified on startup; Please enter desired name or type GUID and press enter for a guid based random name"
if ($servicePrincipalName -eq "GUID"){
$guid=([guid]::NewGuid()).toString()
$servicePrincipalName="SPN-$guid"
}
} until ($servicePrincipalName -match $SPNNamingStandard)
}
$SPNNamingStandard and the following logic is if you want to use a
RegEx to ensure interactive input is validated. Change this expression to
meet your needs or return the entire section if you don't want to facilitate
interactive running. Limited time bonus offer, here's
a link to my favorite regular expression evaluation site!Note: As the script warns, the credential used to create the Application ID/Service Principal must have the role Subscription Admin or Owner to perform the provisioning actions.
# handle command line specification of GUID
if ($servicePrincipalName -eq "GUID"){
$guid=([guid]::NewGuid()).toString()
$servicePrincipalName="SPN-$guid"
}
This little trick allows the specification of "GUID" (i.e. Register-AzureServicePrincipal
-$servicePrincipalName GUID) to tell our function to generate a GUID
for the name. At the end you see I'm pre-pending a "SPN-" to the GUID to
ensure the programmatically generated ApplicationIDs/SPNs stand out. # set URL and IdentifierUris
$homePage = "http://" + $servicePrincipalName
$identifierUri = $homePage
Write-Host "Desired Service Principal Name is $servicePrincipalName `n"
Homepage and IdentifierURI settings for a service principal that we're
using in the aforementioned capacity don't matter, but they do need to be
set, so we base them on the SP name itself an move on.
# Now we need to determine if 1> the Application exisists and 2> if it has been registered as a service principal. This will guide our execution through the end of the function.
$appExists=Get-AzureRmADApplication -DisplayNameStartWith $servicePrincipalName -ErrorAction SilentlyContinue
# check for SPN only if app exists. SPN can't exist without app so no reason to check if not.
if ($appExists){$spnExists=Get-AzureRmADServicePrincipal | Where-Object {$_.ApplicationId -eq $appExists.ApplicationId} -ErrorAction SilentlyContinue}
# we only need a password if the app hasn't been created yet.
if (!$appExists){
# Generate a password if needed
if (!$servicePrincipalPassword){
$servicePrincipalPassword=New-RandomPassword -passwordLength 40
}
# NOTE! We had a convertto-securestring here but as it turns out new-azurermadapplication doesn't take a securestring, only a string
# NOTEUPDATE! AzureRM 5.0 and higher requires a securestring (yay!) This has been updated but notes left here for reference.
$servicePrincipalPassword=ConvertTo-SecureString $servicePrincipalPassword -AsPlainText -Force
}
# we set this to NULL as a "valid" return as the appID already exists and we can't lookup the password from here
else {$servicePrincipalPassword=$null}
This code allows us to insert this function into a workstream
regardless of if the Application ID and service principal already exist. If
they do, we get all the information we can but set the password to $null
since we can't look it up. As you'll see below we also add another
noteproperty to inform the caller explicitly that the AppID/service
principal already existed. Note: This script block contains reference to another function that I have not provided, New-RandomPassword. You'll need to provide your own function that generates a password and call it here or specify the desired password when calling the function explicitly. Perhaps I'll write another post in the future to cover generating a random password in PowerShell.
# Create the App if it wasn't already
if (!$appExists){
$azureADApplication=New-AzureRmADApplication -DisplayName $servicePrincipalName -HomePage $homePage -IdentifierUris $identifierUri -Password $servicePrincipalPassword
Write-Host "Azure AAD Application creation completed successfully"
}
# if it already exists we'll just redirect the variable
else{$azureADApplication=$appExists}
$appID=$azureADApplication.ApplicationId
# Create new SPN if needed
if (!$spnExists){
$spn=New-AzureRmADServicePrincipal -ApplicationId $appId
Write-Host "SPN creation completed successfully"
}
else{$spn=$spnExists}
$spnNames=$spn.ServicePrincipalNames
Now we do the actual creation of the Application ID and service
principal if necessary. Notice that if they already existed we set the
downstream variables to relay to the user. This section could be improved
with additional error handling if desired. # Create object to store information.
$outputObject=New-Object -TypeName PSObject
$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalName -Value $servicePrincipalName
$outputObject | Add-Member -MemberType NoteProperty -Name ClientID -Value $appID
$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalID -Value $spn.Id
$outputObject | Add-Member -MemberType NoteProperty -Name SPNNames -Value $spnNames
$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalPassword -Value $servicePrincipalPassword
if ($appExists -and $spnexists){$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalAlreadyExists -Value $true}
else {$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalAlreadyExists -Value $false}
return $outputObject
Now we'll create our output object. This gives us everything we
might want to know (and probably more) for our consuming processes. Here's
what our object looks like: - $output.ServicePrincipalName=(Suprise!!!) The service principal name
- $ouput.ClientID=The client ID (appID) is one of the critical pieces of info for downstream applications. This is what you'll specify when authenticating later (think of it as your user ID).
- $output.ServicePrincipalID=The SP ID, though not used in any capacity directly that I've seen yet short of programmatically referencing it when deleting, etc.
- $output.SPNNames=The reference names of the service principal. These would be used by third party apps, but in most cases I'm addressing with this article they'll go unused.
- $output.ServicePrincipalPassword=Keep it secret! Keep it safe! This is a clear text copy of the password associated with this service principal. Obviously this is the other key piece of information you'll need as a takeaway. The prudent next step would be to check this into a Azure Keyvault or something similar, but that's for another article...
- $output.ServicePrincipalAlreadyExists=$true or $false, this is also critical for downstream processing. If $true, you'll know that this is newly created and the password is contained in the object meaning you'll need to scrape and store/use that accordingly. If $false it means you can look up the ID by the name if needed, but you better have the password stored somewhere else as we can't look it up now. Either way you have two clear courses of action. While we could have relied on the password being $null, I added this property to definitively set it one way or the other to account for any unknown circumstances due to upstream changes down the road.
Note: Make sure you both store the password for the newly created service principal as you won't be able to retrieve it in the future. Also, make sure your session or variables are cleared after running as the password exists in clear text in memory.
Update/Note 2: By default, this password is only good for one year, and will expire after that time making it impossible to use the SPN. To manage the password on an existing object, you'll need to use the Get/Remove/New-AzureAdApplicationPasswordCredential cmdlets in the AzureAD module (not AzureRM).
Bringing it Home
Now that we've created the function, let's use it to create a principal and give it access to a resource group:
$spnOutput=Register-AzureServicePrincipal -servicePrincipalName <myPrincipalName> -servicePrincipalPassword <myPassword>
New-AzureRmRoleAssignment -ObjectId ($spnOutput.ServicePrincipalID.toString()) -RoleDefinitionName Contributor -ResourceGroupName <myResourceGroup>
This will use our function to create a service principal and give
it contributor level access to <myResourceGroup>. If you have multiple
environments in your subscription you should create a principal for each and
restrict access to resource group(s) associated with each environment. I
also recommend splitting production into a separate subscription; the entire
reasons behind that are a story for another article...Did it Work?
If you would like to manually check your work, you can find it in the Azure portal by navigating to the hamburger menu -> "More Services" -> "Azure Active Directory" -> "App Registrations".
There you should see your newly created app.
You can also check the role based access controls on your resource group or whatever object you applied the permissions to.
Only the chosen shall access Nachos Deathstar. |
In Conclusion
The Azure model of auth/auth management is sound, but adherence to long standing security design principles requires a bit of effort. Hopefully this article will assist you in doing so. Please leave any comments/criticism/coffee donations below.