Creating an Azure Stack AD FS SPN for use with az CLI

28 Sep

Following on from my previous blog post on filling in the gaps for AD FS on Azure Stack integrated systems, here are some more complete instructions on creating a Service Principal on Azure Stack systems using AD FS as the identity provider.

Why do you need this? Well, check out the following scenarios as taken from https://docs.microsoft.com/en-us/azure/azure-stack/azure-stack-integrate-identity#spn-creation:

There are many scenarios that require the use of a service principal name (SPN) for authentication. The following are some examples:

  • CLI usage with AD FS deployment of Azure Stack
  • System Center Management Pack for Azure Stack when deployed with AD FS
  • Resource providers in Azure Stack when deployed with AD FS
  • Various third party applications
  • You require a non-interactive logon

I’ve highlighted the first point ‘CLI usage with AD FS deployment of Azure Stack’. This is significant as AD FS only supports interactive login. At this point in time, the AZ CLI does not support interactive mode, so you must use a service principal.

There are a few areas that weren’t clear to me at first, so I worked it all out and tried to simplify the process.

At a high level, these are the tasks:

  • Create an X509 certificate (or use an existing one) to use for authentication
  • Create a new Service Principal (Graph Application) on the internal Azure Stack domain via PEP PowerShell session
  • Return pertinent details, such as Client ID, cert thumbprint, Tenant ID and relevant external endpoints for the Azure Stack instance
  • Export the certificate as PFX (for use on clients using PowerShell) and PEM file including private certificate (for use with Azure CLI)
  • Give the Service Principal permissions to the subscription

Here’s the link to the official doc’s: https://docs.microsoft.com/en-gb/azure/azure-stack/azure-stack-create-service-principals#create-service-principal-for-ad-fs

I’ve automated the process by augmenting the script provided in the link above. It creates a self-signed cert, AD FS SPN and files required to connect. It needs to be run on a system that has access to the PEP and also has the Azure Stack PowerShell module installed.

The script includes the steps to export the PFX (so you can use it with PowerShell on other systems) and PEM files, plus output ALL the relevant info you will need to connect via AZ CLI/ PoSh

# Following code taken from https://github.com/mongodb/support-tools/blob/master/ssl-windows/Convert-PfxToPem.ps1

Add-Type @'
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Collections.Generic;
using System.Text;
public class Cert_Utils
{
  public const int Base64LineLength = 64;
  private static byte[] EncodeInteger(byte[] value)
  {
    var i = value;
    if (value.Length > 0 && value[0] > 0x7F)
    {
      i = new byte[value.Length + 1];
      i[0] = 0;
      Array.Copy(value, 0, i, 1, value.Length);
    }
    return EncodeData(0x02, i);
  }
  private static byte[] EncodeLength(int length)
  {
    if (length < 0x80)
    return new byte[1] { (byte)length };
    var temp = length;
    var bytesRequired = 0;
    while (temp > 0)
    {
      temp >>= 8;
      bytesRequired++;
    }
    var encodedLength = new byte[bytesRequired + 1];
    encodedLength[0] = (byte)(bytesRequired | 0x80);
    for (var i = bytesRequired - 1; i >= 0; i--)
    encodedLength[bytesRequired - i] = (byte)(length >> (8 * i) & 0xff);
    return encodedLength;
  }
  private static byte[] EncodeData(byte tag, byte[] data)
  {
    List<byte> result = new List<byte>();
    result.Add(tag);
    result.AddRange(EncodeLength(data.Length));
    result.AddRange(data);
    return result.ToArray();
  }

  public static string RsaPrivateKeyToPem(RSAParameters privateKey)
  {
    // Version: (INTEGER)0 - v1998
    var version = new byte[] { 0x02, 0x01, 0x00 };
    // OID: 1.2.840.113549.1.1.1 - with trailing null
    var encodedOID = new byte[] { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
    List<byte> privateKeySeq = new List<byte>();
    privateKeySeq.AddRange(version);
    privateKeySeq.AddRange(EncodeInteger(privateKey.Modulus));
    privateKeySeq.AddRange(EncodeInteger(privateKey.Exponent));
    privateKeySeq.AddRange(EncodeInteger(privateKey.D));
    privateKeySeq.AddRange(EncodeInteger(privateKey.P));
    privateKeySeq.AddRange(EncodeInteger(privateKey.Q));
    privateKeySeq.AddRange(EncodeInteger(privateKey.DP));
    privateKeySeq.AddRange(EncodeInteger(privateKey.DQ));
    privateKeySeq.AddRange(EncodeInteger(privateKey.InverseQ));
    List<byte> privateKeyInfo = new List<byte>();
    privateKeyInfo.AddRange(version);
    privateKeyInfo.AddRange(encodedOID);
    privateKeyInfo.AddRange(EncodeData(0x04, EncodeData(0x30, privateKeySeq.ToArray())));
    StringBuilder output = new StringBuilder();
    var encodedPrivateKey = EncodeData(0x30, privateKeyInfo.ToArray());
    var base64Encoded = Convert.ToBase64String(encodedPrivateKey, 0, (int)encodedPrivateKey.Length);
    output.AppendLine("-----BEGIN PRIVATE KEY-----");
    for (var i = 0; i < base64Encoded.Length; i += Base64LineLength)
    output.AppendLine(base64Encoded.Substring(i, Math.Min(Base64LineLength, base64Encoded.Length - i)));
    output.Append("-----END PRIVATE KEY-----");
    return output.ToString();
  }
  public static string PfxCertificateToPem(X509Certificate2 certificate)
  {
    var certBase64 = Convert.ToBase64String(certificate.Export(X509ContentType.Cert));
    var builder = new StringBuilder();
    builder.AppendLine("-----BEGIN CERTIFICATE-----");
    for (var i = 0; i < certBase64.Length; i += Cert_Utils.Base64LineLength)
    builder.AppendLine(certBase64.Substring(i, Math.Min(Cert_Utils.Base64LineLength, certBase64.Length - i)));
    builder.Append("-----END CERTIFICATE-----");
    return builder.ToString();
  }
}
'@

# Credential for accessing the ERCS PrivilegedEndpoint typically domain\cloudadmin
$creds = Get-Credential
$pepIP = "172.16.101.224"
$date = (get-date).ToString("yyMMddHHmm")
$appName = "appSPN"

$PEMFile = "c:\temp\$appName-$date.pem"
$PFXFile = "c:\temp\$appName-$date.pfx"
$detailfile = "c:\temp\$appName-$date-details.txt"

# Creating a PSSession to the ERCS PrivilegedEndpoint
$session = New-PSSession -ComputerName $pepIP -ConfigurationName PrivilegedEndpoint -Credential $creds

# This produces a self signed cert for testing purposes. It is preferred to use a managed certificate for this.
$cert = New-SelfSignedCertificate -CertStoreLocation "cert:\CurrentUser\My" -Subject "CN=$appName" -KeySpec KeyExchange
$ServicePrincipal = Invoke-Command -Session $session {New-GraphApplication -Name $args[0] -ClientCertificates $args[1]} -ArgumentList $appName,$cert
$AzureStackInfo = Invoke-Command -Session $session -ScriptBlock { get-azurestackstampinformation }
$session|remove-pssession

# For Azure Stack development kit, this value is set to https://management.local.azurestack.external. We will read this from the AzureStackStampInformation output of the ERCS VM.
$ArmEndpoint = $AzureStackInfo.TenantExternalEndpoints.TenantResourceManager
$AdminEndpoint = $AzureStackInfo.AdminExternalEndpoints.AdminResourceManager
# For Azure Stack development kit, this value is set to https://graph.local.azurestack.external/. We will read this from the AzureStackStampInformation output of the ERCS VM.
$GraphAudience = "https://graph." + $AzureStackInfo.ExternalDomainFQDN + "/"
# TenantID for the stamp. We will read this from the AzureStackStampInformation output of the ERCS VM.
$TenantID = $AzureStackInfo.AADTenantID
# Register an AzureRM environment that targets your Azure Stack instance
Add-AzureRMEnvironment ` -Name "azurestacktenant" ` -ArmEndpoint $ArmEndpoint
Add-AzureRMEnvironment ` -Name "azurestackadmin" ` -ArmEndpoint $AdminEndpoint

# Set the GraphEndpointResourceId value
Set-AzureRmEnvironment ` -Name "azurestacktenant" -GraphAudience $GraphAudience -EnableAdfsAuthentication:$true

Add-AzureRmAccount -EnvironmentName "azurestacktenant" `
-ServicePrincipal ` -CertificateThumbprint $ServicePrincipal.Thumbprint `
-ApplicationId $ServicePrincipal.ClientId `
-TenantId $TenantID

# Output details required to pass to PowrShell or AZ CLI
write-output "ApplicationID : $($ServicePrincipal.ClientId)"
write-output "Cert Thumbprint : $($ServicePrincipal.Thumbprint)"
write-output "Application Name : $($ServicePrincipal.ApplicationName)"
write-output "TenantID : $TenantID"
write-output "ARM EndPoint : $ArmEndpoint"
write-output "Admin Endpoint : $AdminEndpoint"
write-output "Graph Audience : $GraphAudience"
write-output "PEM Certificate : $PEMFile"
write-output "PFX Certificate : $PFXFile"

write-output "ApplicationID : $($ServicePrincipal.ClientId)" | out-file -FilePath $detailfile
write-output "Cert Thumbprint : $($ServicePrincipal.Thumbprint)" | out-file -FilePath $detailfile -Append
write-output "Application Name : $($ServicePrincipal.ApplicationName)" | out-file -FilePath $detailfile -Append
write-output "TenantID : $TenantID" | out-file -FilePath $detailfile -Append
write-output "ARM EndPoint : $ArmEndpoint" | out-file -FilePath $detailfile -Append
write-output "Admin Endpoint : $AdminEndpoint" | out-file -FilePath $detailfile -Append
write-output "Graph Audience : $GraphAudience" | out-file -FilePath $detailfile -Append
write-output "PEM Certificate : $PEMFile" | out-file -FilePath $detailfile -Append
write-output "PFX Certificate : $PFXFile" | out-file -FilePath $detailfile -Append

# Export the Cert to a pem file for user with Azure CLI
$result = [Cert_Utils]::PfxCertificateToPem($cert)

$parameters = ([Security.Cryptography.RSACryptoServiceProvider] $cert.PrivateKey).ExportParameters($true)
$result += "`r`n" + [Cert_Utils]::RsaPrivateKeyToPem($parameters);

$result | Out-File -Encoding ASCII -ErrorAction Stop $PEMFile

# Now Export the cert to PFX
$pw = ConvertTo-SecureString -String 'P@ssword1' -Force -AsPlainText
Export-PfxCertificate -cert $cert -FilePath $PFXFile -Password $pw

Here is an example of the output produced:

Next, connect to the Tenant Portal and give the Service Principal access to the subscription you want it to have access to:

Once you’ve done the above, here are the high-level steps to use the Service Principal account with Azure CLI:

  • Trust the Azure Stack CA Root Certificate (if using Enterprise CA / ASDK) within AZ CLI (Python). This is a one-time operation per system you’re running AZ CLI on.
  • Register Azure Stack environment (either tenant/user or admin)
  • Set the active cloud environment for CLI
  • Set the CLI to use Azure Stack compatible API version
  • Sign into the Azure Stack environment with service principal account

For reference, here are the official links with the information on how to do it. It works well, so just follow those:

https://docs.microsoft.com/en-us/azure/azure-stack/user/azure-stack-version-profiles-azurecli2


  az cloud register -n AzureStackUser --endpoint-resource-manager 'https://management.<your Azure Stack Region FQDN>' --suffix-storage-endpoint '<your Azure Stack Region FQDN>' --suffix-keyvault-dns '.vault.<your Azure Stack Region FQDN>'
  az cloud register -n AzureStackAdmin --endpoint-resource-manager 'https://adminmanagement.<your Azure Stack Region FQDN>' --suffix-storage-endpoint '<your Azure Stack Region FQDN>' --suffix-keyvault-dns '.vault.<your Azure Stack Region FQDN>'

  az cloud set -n AzureStackUser

  az cloud update --profile 2017-03-09-profile

  az login --tenant <TenantID GUID>  --service-principal  -u  <Application ID> -p <PEM file path>

 

Danny McDermott

Danny is a Cloud Architect within the Azure Cloud Enablement Team, based in the UK.

Twitter LinkedIn