Serverless LAPS powered by Microsoft Intune, Azure Functions and Azure Key Vault!

Serverless LAPS powered by Microsoft Intune, Azure Functions and Azure Key Vault!

I’m excited to introduce a Serverless Local Administrator Password Solution (SLAPS ūüėČ) for Windows 10 Intune Managed devices, powered by¬†Microsoft Intune PowerShell scripts,¬†Azure Functions and¬†Azure Key Vault.

Building this solution has been quite a challenge, as there were many obstacles to overcome. If you’ve read the article of Oliver Kieselbach: “Deep dive Microsoft Intune Management Extension – PowerShell Scripts“, then you know that all script contents are logged in plain text (including passwords!)¬†to the IntuneManagementExtension.log file. So I had to come up with a solution that would not expose passwords in plain text at any time.

That’s where¬†Azure Functions come in! I managed to create an HTTP-based API, using the¬†experimental PowerShell language support in a Function App, that contains all of the logics for returning random passwords on demand and simultaneously storing them securely into an Azure Key Vault. Passwords are contained in a variable and never exposed in plain text.

From the Intune PowerShell script, the Azure Function is queried over a TLS 1.2 encrypted connection, using the Invoke-RestMethod cmdlet. The hostname of the initiating device, and the username defined in the PowerShell Script are used to create the Local Administrator, and are also passed with the POST method to the Azure Function within the request body of the Invoke-RestMethod cmdlet.

When the Azure Function is triggered, it will return a random generated password to the PowerShell script, and also creates or updates a secret in your Azure Key Vault. The name of the secret is based on the hostname of the device. The value will be the randomly generated password, and the username is added as a tag.

Sounds nice, right? But there’s one more challenge… I don’t want to expose the url containing the authorization code to trigger the¬†Azure Function in plain text within the log file. I want to be able to rely on the contents of Key vault. Exposing this information would allow anyone to read the log file and put false information in the Key vault, by executing the same cmdlets with modified data. I figured out I could manipulate the log file during the runtime of the PowerShell script using the code below:

This would remove each line from the log file that matches the “azurewebsites.net” string.

Note:
Please note that this is just a workaround to hide information from the log file. It is absolutely not¬†waterproof. An ‘attacker’ will still be able to retrieve the contents of the PowerShell script. The¬†Intune Management Extension will download scripts just before they are executed to “C:\Program Files (x86)\Microsoft Intune Management Extension\Policies\Scripts”, where it will be removed after execution.

Your Local Administrator credentials are safe, because they are never exposed, however your Key vault may get manipulated if the Azure Function is abused. If you require a better solution, please vote for Secure authentication within PowerShell scripts for Intune MDM on the Microsoft Intune UserVoice forums

 


Prerequisites

Before being able to configure the Local Administrator Password Solution using the instructions below, the following prerequisites must be met:

 

Note:
The Azure Function is using the experimental PowerShell language support. I’d be happy to share a C# example if anyone is up for the challenge ūüôā

 


Configuration

Considering that a Function App and Key vault have already been deployed, I’ve divided the configuration of the Serverless Local Administrator Password Solution into six parts:

  1. Register a Managed Service Identity with Azure Active Directory;
  2. Grant the Managed Service Identity access to the Key vault;
  3. Create the Azure Function;
  4. Test the Azure Function;
  5. Deploy the PowerShell script with Microsoft Intune;
  6. Validate the deployment of the PowerShell script;

 

1. Register a Managed Service Identity with Azure Active Directory

A Managed Service Identity needs to be registered with Azure Active Directory first, that will be used to authenticate with the Azure Key Vault.

  • Open the¬†Azure Portal;
  • Navigate to¬†Function Apps;
  • In the¬†Functions Apps blade, select the Function App you wish to configure;
  • Navigate to the Platform features tab;
  • Under¬†Networking, click¬†Managed service identity;
  • Set¬†Register with Azure Active Directory to¬†On and click¬†Save.

2. Grant the Managed Service Identity access to the Key vault

The next step is to add the Managed Service Identity to an Access policy in the Key vault.

  • Navigate to¬†Key vaults;
  • On the¬†Key vaults blade, select the¬†Key vault you wish to configure;
  • In the¬†Key vault¬†blade that displays, click¬†Access policies;
  • In the¬†Access policies blade that displays, click¬†Add new;
  • In the¬†Add access policy blade that displays, click¬†Select principal;
  • Enter the name of your¬†Function App and click¬†Select;
  • In the¬†Secret permissions, select¬†Secret Management Operations¬†>¬†Set;
  • Click¬†OK and click Save.

 

3. Create the Azure Function

Now that the¬†Managed Service Identity¬†of the¬†Function App has been granted access to¬†Set secrets in the¬†Key vault. It’s time to create the Azure Function.

  • Navigate to¬†Function Apps;
  • In the¬†Functions Apps blade, select the Function App you wish to configure;
  • Select¬†Functions and click¬†New function;
  • Enable the¬†Experimental Language Support functions;
  • Click¬†HTTP trigger >¬†PowerShell;
  • Configure the¬†new function:
    • Language: PowerShell
    • Name: Set-KeyVaultSecret
    • Authorization level:¬†Function
  • Replace the content of the Set-KeyVaultSecret function with the PowerShell code of the¬†Set-KeyVaultSecret.ps1 file downloaded from the TechNet Gallery;
  • Set the name of your Key vault in the $keyVaultName variable;
  • Click¬†Save;

Note:
Take note of the Function URL (</> Get Function URL), which needs to be set in the New-LocalAdmin.ps1 script later.

By default the Function App requires a TLS connection with a minimum version of 1.2.

 

4. Test the Azure Function

Before continuing to configure the PowerShell script in Intune that creates the¬†Local Administrator accounts, it’s recommended to test if the Azure Function works as expected.

  • Grab the request body from the¬†New-LocalAdmin.ps1 script;
  • Replace the variables with values, for example:
  • Run the Azure Function and review the output;
    If the Function runs successfully, a random password is returned the Output pane.
  • Validate the creation of the secret in the¬†Key vault as defined in the¬†keyName.

 

5. Deploy the PowerShell script with Microsoft Intune

  • Modify the configuration¬†section of the¬†New-LocalAdmin.ps1 script;
    • Choose a username for the Local Administrator accounts;
    • Enter the Function URL;

      The New-LocalAdmin.ps1 script will restart itself in a 64-bit process, because cmdlets like New-LocalUser are not available in a 32-bit process, which is the architecture of the Intune Management Extension agent.

  • In the Azure Portal, navigate to¬†Intune >¬†Device Configuration¬†>¬†PowerShell scripts;
  • In the¬†PowerShell scripts¬†blade, click¬†+ Add;
  • On the¬†Add PowerShell script blade, enter the following information and click Create;
    • Name: New-LocalAdmin
    • Script Location:¬†Select the¬†New-LocalAdmin.ps1 file, previously downloaded from the TechNet Gallery
  • In the¬†New-LocalAdmin¬†blade that displays, click Assignments;
  • Select a group you wish to deploy the solution to;
  • Save¬†the assignments;

 

Note:
PowerShell scripts can only be deployed to users. On shared devices, the provided PowerShell script will change the password of the Local Administrator user every time a new user logs on to a device.

 

6. Validate the deployment of the PowerShell script

PowerShell scripts are executed by the Intune Management Extension. The agent will check every 60 minutes if a script is ready to be deployed. You can force a sync by restarting the Microsoft Intune Management Extension Service on the device.

The status of the PowerShell script deployment is returned to the Monitor > Device/User status overview in Intune.

When the PowerShell script succeeds, validate the creation of a Local Administrator on the device, and that the credentials were stored in the Key vault.

 


More information

Need to troubleshoot further? Check the log files on a device at C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log, which contains the logging for PowerShell Scripts.

If you have any questions or feedback, feel free to leave a comment!

 

17 thoughts on “Serverless LAPS powered by Microsoft Intune, Azure Functions and Azure Key Vault!

  1. Pretty nice man… thanks for this! Have you made any changes since posting? Any better ways to control access to being able to invoke the Azure function, besides PKI?
    I went ahead and upvoted the uservoice on secure authentication through PowerShell.

    Thanks again!

  2. So I am failing on this somehow, interface changed and the “Managed Service Identity” has changed to “Identity” and the object seems to never exist when I create it. As such, when I run the script, I get this in out Output:

    {
    “id”: “f7a61fa2-6aec-4672-b547-77154614306d”,
    “requestId”: “abeff90e-055f-4519-902d-6cfbcc64f389”,
    “statusCode”: 500,
    “errorCode”: 0,
    “message”: “An error has occurred. For more information, please check the logs for error ID f7a61fa2-6aec-4672-b547-77154614306d”
    }

    and in the log window i receive:

    2019-03-19T16:37:16.952 [Info] Function started (Id=8459ee2d-f154-4355-aa5d-db8d0242faf8)
    2019-03-19T16:37:17.311 [Error] Invoke-RestMethod : {“error”:{“code”:”Forbidden”,”message”:”Access denied”,”innererror”:{“code”:”AccessDenied”}}}
    at run.ps1: line 34
    + Invoke-RestMethod
    + _________________
    + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand
    2019-03-19T16:37:17.342 [Error] Exception while executing function: Functions.Set-KeyVaultSecret. Microsoft.Azure.WebJobs.Script: PowerShell script error. Microsoft.PowerShell.Commands.Utility: The remote server returned an error: (403) Forbidden.
    2019-03-19T16:37:17.373 [Error] Function completed (Failure, Id=8459ee2d-f154-4355-aa5d-db8d0242faf8, Duration=416ms)

    the permissions are set properly per this article so I am unsure why this is occurring.

    1. Hi Corey,

      You are getting an authentication error on setting the secret to Azure Key Vault. It seems that the Managed Service Identity (Service Principal) of the Function App itself does not have the “Secret permissions” set to “Set” on the Azure Key Vault, hence you get a 403 forbidden error.

      Can you double check that the “Access policy” on the Azure Key Vault, that it contains the “Application” (Function App) with “Secret permissions” set to “Set”? When I remove my Access Policy, I get the same error as you do.

  3. Admiring the time and energy you put into your blog and in depth information you provide.
    It’s awesome to come across a blog every once
    in a while that isn’t the same unwanted rehashed material.
    Fantastic read! I’ve saved your site and I’m adding
    your RSS feeds to my Google account.

  4. Hi John, I getting this error. Please let me know where I went wrong?

    2019-04-25T04:39:49 Welcome, you are now connected to log-streaming service.
    2019-04-25T04:40:49 No new trace in the past 1 min(s).
    2019-04-25T04:41:49 No new trace in the past 2 min(s).
    2019-04-25T04:42:27.065 [Information] Executing ‘Functions.Set-KeyVaultSecret’ (Reason=’This function was programmatically called via the host APIs.’, Id=9907eb8d-80af-426c-b3a9-a5ae9eeece66)
    2019-04-25T04:42:27.113 [Error] Executed ‘Functions.Set-KeyVaultSecret’ (Failed, Id=9907eb8d-80af-426c-b3a9-a5ae9eeece66)
    Result: Failure
    Exception: No parameter defined in the script or function for the input binding ‘Request’.

    Stack: at Microsoft.Azure.Functions.PowerShellWorker.AzFunctionInfo..ctor(RpcFunctionMetadata metadata) in C:\projects\azure-functions-powershell-worker\src\FunctionInfo.cs:line 120
    at Microsoft.Azure.Functions.PowerShellWorker.FunctionLoader.LoadFunction(FunctionLoadRequest request) in C:\projects\azure-functions-powershell-worker\src\FunctionLoader.cs:line 45
    at Microsoft.Azure.Functions.PowerShellWorker.RequestProcessor.ProcessFunctionLoadRequest(StreamingMessage request) in C:\projects\azure-functions-powershell-worker\src\RequestProcessor.cs:line 187
    2019-04-25T04:43:49 No new trace in the past 1 min(s).
    2019-04-25T04:44:49 No new trace in the past 2 min(s).
    2019-04-25T04:45:49 No new trace in the past 3 min(s).
    2019-04-25T04:46:44.174 [Information] Executing ‘Functions.Set-KeyVaultSecret’ (Reason=’This function was programmatically called via the host APIs.’, Id=b62dc2e5-9ae0-4315-9a6c-200a7be2a4bc)
    2019-04-25T04:46:44.215 [Error] Executed ‘Functions.Set-KeyVaultSecret’ (Failed, Id=b62dc2e5-9ae0-4315-9a6c-200a7be2a4bc)
    Result: Failure
    Exception: No parameter defined in the script or function for the input binding ‘Request’.

    Stack: at Microsoft.Azure.Functions.PowerShellWorker.AzFunctionInfo..ctor(RpcFunctionMetadata metadata) in C:\projects\azure-functions-powershell-worker\src\FunctionInfo.cs:line 120
    at Microsoft.Azure.Functions.PowerShellWorker.FunctionLoader.LoadFunction(FunctionLoadRequest request) in C:\projects\azure-functions-powershell-worker\src\FunctionLoader.cs:line 45
    at Microsoft.Azure.Functions.PowerShellWorker.RequestProcessor.ProcessFunctionLoadRequest(StreamingMessage request) in C:\projects\azure-functions-powershell-worker\src\RequestProcessor.cs:line 187
    2019-04-25T04:47:49 No new trace in the past 1 min(s).
    2019-04-25T04:48:49 No new trace in the past 2 min(s).
    2019-04-25T04:49:49 No new trace in the past 3 min(s).
    2019-04-25T04:50:49 No new trace in the past 4 min(s).
    2019-04-25T04:51:49 No new trace in the past 5 min(s).
    2019-04-25T04:52:49 No new trace in the past 6 min(s).
    2019-04-25T04:53:49 No new trace in the past 7 min(s).
    2019-04-25T04:54:49 No new trace in the past 8 min(s).
    2019-04-25T04:55:49 No new trace in the past 9 min(s).
    2019-04-25T04:56:49 No new trace in the past 10 min(s).
    2019-04-25T04:57:49 No new trace in the past 11 min(s).
    2019-04-25T04:58:49 No new trace in the past 12 min(s).

    2019-04-25T04:59:34 The application was terminated.

    1. Hi Nkoro,

      Please note that Azure Functions have had a big overhaul since writen this post. I used the “Experimental” PowerShell support in Azure Functions v1. Nowadays there is Azure Functions v2 and recently released with support for PowerShell Core in Public Preview. I’m expecting it works differently.

      Regards,

      John

  5. Awesome blog and thanks very much for all your help John.

    Got this working with v2 using the code below.

    Had to generate a password using the function as it’s not available in core. Removed some characters I don’t like ūüôā

    ###Azure Function v2####

    using namespace System.Net

    param($Request)

    $keyVaultName = “YOUR KEY VAULT”
    $computer = $request.body.keyname
    write-host “Attempting to connect to Key Vault using MSI”
    # Get Azure Key Vault Access Token using the Function’s Managed Service Identity

    # Azure Key Vault resource to obtain access token
    $vaultTokenUri = ‘https://vault.azure.net’
    $apiVersion = ‘2017-09-01’

    # Get Azure Key Vault Access Token using the Function’s Managed Service Identity
    $authToken = Invoke-RestMethod -Method Get -Headers @{ ‘Secret’ = $env:MSI_SECRET } -Uri “$($env:MSI_ENDPOINT)?resource=$vaultTokenUri&api-version=$apiVersion”

    # Use Azure Key Vault Access Token to create Authentication Header
    $authHeader = @{ Authorization = “Bearer $($authToken.access_token)” }

    write-host “Setting Random Password”

    function new-password{

    $Alphabets = ‘a,b,c,d,e,f,g,h,i,j,k,m,n,p,q,r,t,u,v,w,x,y,z’
    $numbers = 2..9
    $specialCharacters = ‘!,@,#,$,%,&,*,?,+’
    $array = @()
    $array += $Alphabets.Split(‘,’) | Get-Random -Count 6
    $array[0] = $array[0].ToUpper()
    $array[-1] = $array[-1].ToUpper()
    $array += $numbers | Get-Random -Count 3
    $array += $specialCharacters.Split(‘,’) | Get-Random -Count 3
    ($array | Get-Random -Count $array.Count) -join “”
    }

    $password = new-password

    # Generate a new body to set a secret in the Azure Key Vault
    $body = $request.body | Select-Object -Property * -ExcludeProperty keyName

    # Append the random password to the new body
    $body | Add-Member -NotePropertyName value -NotePropertyValue “$password”

    # Convert the body to JSON
    $body = $body | ConvertTo-Json

    # Azure Key Vault Uri to set a secret
    Write-host “Creating string to key vault”
    $vaultSecretUri = “https://$keyVaultName.vault.azure.net/secrets/$($request.Body.keyName)/?api-version=2016-10-01”

    # Set the secret in Azure Key Vault
    try{
    write-host “Setting secret in Azure Key Vault”
    $null = Invoke-RestMethod -Method PUT -Body $body -Uri $vaultSecretUri -ContentType ‘application/json’ -Headers $authHeader

    # Associate values to output bindings by calling ‘Push-OutputBinding’.
    Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    Body = $password })
    }
    catch{$error[0].errordetails.message}

    1. Hi Bally Singh,

      Thank you for your comment and sharing your code. Great that you’ve got it working with v2. I will try it out some time and update the post accordingly.

      Regards,

      John

  6. Do you have this on Github by chance? I think theres a large option for the community to help expand this and bug fix. Such as the ability to add a scheduled task creation to run this at X date.

Leave a Reply

Your email address will not be published. Required fields are marked *