├── Add-MSIAppRoleAssignment.ps1 ├── Azure Functions ├── .funcignore ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── SendClientEvent │ ├── SendClientEvent.ps1 │ └── function.json ├── SetSecret │ ├── SetSecret.ps1 │ └── function.json ├── host.json ├── modules │ └── AADDeviceTrust │ │ ├── AADDeviceTrust.psd1 │ │ ├── AADDeviceTrust.psm1 │ │ └── Public │ │ ├── Get-AzureADDeviceAlternativeSecurityIds.ps1 │ │ ├── Get-AzureADDeviceRecord.ps1 │ │ ├── New-HashString.ps1 │ │ ├── Test-AzureADDeviceAlternativeSecurityIds.ps1 │ │ └── Test-Encryption.ps1 ├── profile.ps1 └── requirements.psd1 ├── Deploy ├── Update │ ├── dev-update-function.bicep │ ├── dev-update-function.json │ ├── prd-update-function.bicep │ └── prd-update-function.json ├── cloudlaps.bicep ├── cloudlaps.json ├── dev-cloudlaps.bicep └── dev-cloudlaps.json ├── LICENSE ├── Packages ├── Dev │ └── CloudLAPS-FunctionApp1.1.0.zip └── Prod │ ├── CloudLAPS-FunctionApp1.0.0.zip │ └── CloudLAPS-Portal1.1.0.zip ├── Proactive Remediation ├── Detection.ps1 └── Remediate.ps1 ├── README.md └── Workbooks ├── CloudLAPS-AdminDetails-Template.json └── CloudLAPS.json /Add-MSIAppRoleAssignment.ps1: -------------------------------------------------------------------------------- 1 | # Assign static variables 2 | $TenantID = "" 3 | $MSIObjectID = "" 4 | 5 | # Authenticate against Azure AD, as Global Administrator 6 | Connect-AzureAD -TenantId $TenantID 7 | 8 | $MSGraphAppId = "00000003-0000-0000-c000-000000000000" # Microsoft Graph (graph.microsoft.com) application ID 9 | $MSGraphServicePrincipal = Get-AzureADServicePrincipal -Filter "appId eq '$($MSGraphAppId)'" 10 | $RoleNames = @("Device.Read.All") 11 | 12 | # Assign each roles to Managed System Identity, first validate they exist 13 | foreach ($RoleName in $RoleNames) { 14 | $AppRole = $MSGraphServicePrincipal.AppRoles | Where-Object { $PSItem.Value -eq $RoleName -and $PSItem.AllowedMemberTypes -contains "Application" } 15 | if ($AppRole -ne $null) { 16 | New-AzureAdServiceAppRoleAssignment -ObjectId $MSIObjectID -PrincipalId $MSIObjectID -ResourceId $MSGraphServicePrincipal.ObjectId -Id $AppRole.Id 17 | } 18 | } -------------------------------------------------------------------------------- /Azure Functions/.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | local.settings.json 4 | test -------------------------------------------------------------------------------- /Azure Functions/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Azure Functions artifacts 3 | bin 4 | obj 5 | appsettings.json 6 | local.settings.json -------------------------------------------------------------------------------- /Azure Functions/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-vscode.PowerShell" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /Azure Functions/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to PowerShell Functions", 6 | "type": "PowerShell", 7 | "request": "attach", 8 | "customPipeName": "AzureFunctionsPSWorker", 9 | "runspaceId": 1, 10 | "preLaunchTask": "func: host start" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /Azure Functions/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": ".", 3 | "azureFunctions.projectLanguage": "PowerShell", 4 | "azureFunctions.projectRuntime": "~3", 5 | "debug.internalConsoleOptions": "neverOpen" 6 | } 7 | -------------------------------------------------------------------------------- /Azure Functions/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "command": "host start", 7 | "problemMatcher": "$func-powershell-watch", 8 | "isBackground": true 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /Azure Functions/SendClientEvent/SendClientEvent.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Net 2 | 3 | # Input bindings are passed in via param block. 4 | param( 5 | [Parameter(Mandatory = $true)] 6 | $Request, 7 | 8 | [Parameter(Mandatory = $false)] 9 | $TriggerMetadata 10 | ) 11 | 12 | # Functions 13 | function Get-AuthToken { 14 | <# 15 | .SYNOPSIS 16 | Retrieve an access token for the Managed System Identity. 17 | 18 | .DESCRIPTION 19 | Retrieve an access token for the Managed System Identity. 20 | 21 | .NOTES 22 | Author: Nickolaj Andersen 23 | Contact: @NickolajA 24 | Created: 2021-06-07 25 | Updated: 2021-06-07 26 | 27 | Version history: 28 | 1.0.0 - (2021-06-07) Function created 29 | #> 30 | Process { 31 | # Get Managed Service Identity details from the Azure Functions application settings 32 | $MSIEndpoint = $env:MSI_ENDPOINT 33 | $MSISecret = $env:MSI_SECRET 34 | 35 | # Define the required URI and token request params 36 | $APIVersion = "2017-09-01" 37 | $ResourceURI = "https://graph.microsoft.com" 38 | $AuthURI = $MSIEndpoint + "?resource=$($ResourceURI)&api-version=$($APIVersion)" 39 | 40 | # Call resource URI to retrieve access token as Managed Service Identity 41 | $Response = Invoke-RestMethod -Uri $AuthURI -Method "Get" -Headers @{ "Secret" = "$($MSISecret)" } 42 | 43 | # Construct authentication header to be returned from function 44 | $AuthenticationHeader = @{ 45 | "Authorization" = "Bearer $($Response.access_token)" 46 | "ExpiresOn" = $Response.expires_on 47 | } 48 | 49 | # Handle return value 50 | return $AuthenticationHeader 51 | } 52 | } 53 | 54 | function Send-LogAnalyticsPayload { 55 | <# 56 | .SYNOPSIS 57 | Send data to Log Analytics Collector API through a web request. 58 | 59 | .DESCRIPTION 60 | Send data to Log Analytics Collector API through a web request. 61 | 62 | .PARAMETER WorkspaceID 63 | Specify the Log Analytics workspace ID. 64 | 65 | .PARAMETER SharedKey 66 | Specify either the Primary or Secondary Key for the Log Analytics workspace. 67 | 68 | .PARAMETER Body 69 | Specify a JSON representation of the data objects. 70 | 71 | .PARAMETER LogType 72 | Specify the name of the custom log in the Log Analytics workspace. 73 | 74 | .PARAMETER TimeGenerated 75 | Specify a custom date time string to be used as TimeGenerated value instead of the default. 76 | 77 | .NOTES 78 | Author: Nickolaj Andersen 79 | Contact: @NickolajA 80 | Created: 2021-04-20 81 | Updated: 2021-04-20 82 | 83 | Version history: 84 | 1.0.0 - (2021-04-20) Function created 85 | #> 86 | param( 87 | [parameter(Mandatory = $true, HelpMessage = "Specify the Log Analytics workspace ID.")] 88 | [ValidateNotNullOrEmpty()] 89 | [string]$WorkspaceID, 90 | 91 | [parameter(Mandatory = $true, HelpMessage = "Specify either the Primary or Secondary Key for the Log Analytics workspace.")] 92 | [ValidateNotNullOrEmpty()] 93 | [string]$SharedKey, 94 | 95 | [parameter(Mandatory = $true, HelpMessage = "Specify a JSON representation of the data objects.")] 96 | [ValidateNotNullOrEmpty()] 97 | [string]$Body, 98 | 99 | [parameter(Mandatory = $true, HelpMessage = "Specify the name of the custom log in the Log Analytics workspace.")] 100 | [ValidateNotNullOrEmpty()] 101 | [string]$LogType, 102 | 103 | [parameter(Mandatory = $false, HelpMessage = "Specify a custom date time string to be used as TimeGenerated value instead of the default.")] 104 | [ValidateNotNullOrEmpty()] 105 | [string]$TimeGenerated = [string]::Empty 106 | ) 107 | Process { 108 | # Construct header string with RFC1123 date format for authorization 109 | $RFC1123Date = [DateTime]::UtcNow.ToString("r") 110 | $Header = -join@("x-ms-date:", $RFC1123Date) 111 | 112 | # Convert authorization string to bytes 113 | $ComputeHashBytes = [Text.Encoding]::UTF8.GetBytes(-join@("POST", "`n", $Body.Length, "`n", "application/json", "`n", $Header, "`n", "/api/logs")) 114 | 115 | # Construct cryptographic SHA256 object 116 | $SHA256 = New-Object -TypeName "System.Security.Cryptography.HMACSHA256" 117 | $SHA256.Key = [System.Convert]::FromBase64String($SharedKey) 118 | 119 | # Get encoded hash by calculated hash from bytes 120 | $EncodedHash = [System.Convert]::ToBase64String($SHA256.ComputeHash($ComputeHashBytes)) 121 | 122 | # Construct authorization string 123 | $Authorization = 'SharedKey {0}:{1}' -f $WorkspaceID, $EncodedHash 124 | 125 | # Construct Uri for API call 126 | $Uri = -join@("https://", $WorkspaceID, ".ods.opinsights.azure.com/", "api/logs", "?api-version=2016-04-01") 127 | 128 | # Construct headers table 129 | $HeaderTable = @{ 130 | "Authorization" = $Authorization 131 | "Log-Type" = $LogType 132 | "x-ms-date" = $RFC1123Date 133 | "time-generated-field" = $TimeGenerated 134 | } 135 | 136 | # Invoke web request 137 | $WebResponse = Invoke-WebRequest -Uri $Uri -Method "POST" -ContentType "application/json" -Headers $HeaderTable -Body $Body -UseBasicParsing 138 | 139 | $ReturnValue = [PSCustomObject]@{ 140 | StatusCode = $WebResponse.StatusCode 141 | PayloadSizeKB = ($Body.Length/1024).ToString("#.#") 142 | } 143 | 144 | # Handle return value 145 | return $ReturnValue 146 | } 147 | } 148 | 149 | Write-Output -InputObject "Inbound request from IP: $($TriggerMetadata.'$Request'.headers.'x-forwarded-for'.Split(":")[0])" 150 | 151 | # Read application settings for internal variables 152 | $WorkspaceID = if (-not([string]::IsNullOrEmpty($env:LogAnalyticsWorkspaceId))) { $env:LogAnalyticsWorkspaceId } else { "InvalidWorkspace" } 153 | $SharedKey = if (-not([string]::IsNullOrEmpty($env:LogAnalyticsWorkspaceSharedKey))) { $env:LogAnalyticsWorkspaceSharedKey } else { "InvalidSharedKey" } 154 | $LogType = if (-not([string]::IsNullOrEmpty($env:LogTypeClient))) { $env:LogTypeClient } else { "CloudLAPSClient" } 155 | $DebugLogging = if (-not([string]::IsNullOrEmpty($env:DebugLogging))) { $env:DebugLogging } else { $false } 156 | 157 | # Retrieve authentication token 158 | $AuthToken = Get-AuthToken 159 | 160 | # Initate variables 161 | $StatusCode = [HttpStatusCode]::OK 162 | $Body = [string]::Empty 163 | $HeaderValidation = $true 164 | 165 | # Assign incoming request properties to variables 166 | $DeviceName = $Request.Body.DeviceName 167 | $DeviceID = $Request.Body.DeviceID 168 | $SerialNumber = $Request.Body.SerialNumber 169 | $Signature = $Request.Body.Signature 170 | $Thumbprint = $Request.Body.Thumbprint 171 | $PublicKey = $Request.Body.PublicKey 172 | $PasswordRotationResult = $Request.Body.PasswordRotationResult 173 | $DateTimeUtc = $Request.Body.DateTimeUtc 174 | $ClientEventMessage = $Request.Body.ClientEventMessage 175 | 176 | # Construct body for Log Analytics custom log content 177 | $WorkspaceBody = @{ 178 | SerialNumber = $SerialNumber 179 | AzureADDeviceId = $DeviceID 180 | PasswordRotationResult = $PasswordRotationResult 181 | DateTimeUtc = $DateTimeUtc 182 | Message = $ClientEventMessage 183 | } 184 | 185 | # Validate request header values 186 | $HeaderValidationList = @(@{ "DeviceName" = $DeviceName }, @{ "DeviceID" = $DeviceID }, @{ "SerialNumber" = $SerialNumber }, @{ "Signature" = $Signature }, @{ "Thumbprint" = $Thumbprint }, @{ "PublicKey" = $PublicKey }, @{ "PasswordRotationResult" = $PasswordRotationResult }, @{ "DateTimeUtc" = $DateTimeUtc }, @{ "ClientEventMessage" = $ClientEventMessage }) 187 | foreach ($HeaderValidationItem in $HeaderValidationList) { 188 | foreach ($HeaderItem in $HeaderValidationItem.Keys) { 189 | if ([string]::IsNullOrEmpty($HeaderValidationItem[$HeaderItem])) { 190 | Write-Warning -Message "Header validation for '$($HeaderItem)' failed, request will not be handled" 191 | $StatusCode = [HttpStatusCode]::BadRequest 192 | $HeaderValidation = $false 193 | $Body = "Header validation failed" 194 | } 195 | else { 196 | if ($HeaderItem -in @("Signature", "PublicKey")) { 197 | if ($DebugLogging -eq $true) { 198 | Write-Output -InputObject "Header validation succeeded for '$($HeaderItem)' with value: $($HeaderValidationItem[$HeaderItem])" 199 | } 200 | else { 201 | Write-Output -InputObject "Header validation succeeded for '$($HeaderItem)' with value: " 202 | } 203 | } 204 | else { 205 | Write-Output -InputObject "Header validation succeeded for '$($HeaderItem)' with value: $($HeaderValidationItem[$HeaderItem])" 206 | } 207 | } 208 | } 209 | } 210 | 211 | if ($HeaderValidation -eq $true) { 212 | # Initiate request handling 213 | Write-Output -InputObject "Initiating request handling for device named as '$($DeviceName)' with identifier: $($DeviceID)" 214 | 215 | $AzureADDeviceRecord = Get-AzureADDeviceRecord -DeviceID $DeviceID -AuthToken $AuthToken 216 | if ($AzureADDeviceRecord -ne $null) { 217 | Write-Output -InputObject "Found trusted Azure AD device record with object identifier: $($AzureADDeviceRecord.id)" 218 | 219 | # Get required validation data for debug logging when enabled 220 | if ($DebugLogging -eq $true) { 221 | $AzureADDeviceAlternativeSecurityIds = Get-AzureADDeviceAlternativeSecurityIds -Key $AzureADDeviceRecord.alternativeSecurityIds.key 222 | } 223 | 224 | # Validate thumbprint from input request with Azure AD device record's alternativeSecurityIds details 225 | if ($DebugLogging -eq $true) { 226 | Write-Output -InputObject "ValidatePublicKeyThumbprint: Value from param 'Thumbprint': $($Thumbprint)" 227 | Write-Output -InputObject "ValidatePublicKeyThumbprint: Value from AAD device record: $($AzureADDeviceAlternativeSecurityIds.Thumbprint)" 228 | } 229 | if (Test-AzureADDeviceAlternativeSecurityIds -AlternativeSecurityIdKey $AzureADDeviceRecord.alternativeSecurityIds.key -Type "Thumbprint" -Value $Thumbprint) { 230 | Write-Output -InputObject "Successfully validated certificate thumbprint from inbound request" 231 | 232 | # Validate public key hash from input request with Azure AD device record's alternativeSecurityIds details 233 | if ($DebugLogging -eq $true) { 234 | $ComputedHashString = New-HashString -Value $PublicKey 235 | Write-Output -InputObject "ValidatePublicKeyHash: Encoded hash from param 'PublicKey': $($ComputedHashString)" 236 | Write-Output -InputObject "ValidatePublicKeyHash: Encoded hash from AAD device record: $($AzureADDeviceAlternativeSecurityIds.PublicKeyHash)" 237 | } 238 | if (Test-AzureADDeviceAlternativeSecurityIds -AlternativeSecurityIdKey $AzureADDeviceRecord.alternativeSecurityIds.key -Type "Hash" -Value $PublicKey) { 239 | Write-Output -InputObject "Successfully validated certificate SHA256 hash value from inbound request" 240 | 241 | $EncryptionVerification = Test-Encryption -PublicKeyEncoded $PublicKey -Signature $Signature -Content $AzureADDeviceRecord.deviceId 242 | if ($EncryptionVerification -eq $true) { 243 | Write-Output -InputObject "Successfully validated inbound request came from a trusted Azure AD device record" 244 | 245 | # Validate that the inbound request came from a trusted device that's not disabled 246 | if ($AzureADDeviceRecord.accountEnabled -eq $true) { 247 | Write-Output -InputObject "Azure AD device record was validated as enabled" 248 | 249 | # Send client event message details to Log Analytics workspace 250 | $LogAnalyticsAPIResponse = Send-LogAnalyticsPayload -WorkspaceID $WorkspaceID -SharedKey $SharedKey -Body ($WorkspaceBody | ConvertTo-Json) -LogType $LogType 251 | if ($LogAnalyticsAPIResponse.StatusCode -like "200") { 252 | Write-Output -InputObject "Successfully sent client message to workspace" 253 | } 254 | else { 255 | Write-Warning -Message "Failed to send client message to workspace" 256 | } 257 | } 258 | else { 259 | Write-Output -InputObject "Trusted Azure AD device record validation for inbound request failed, record with deviceId '$($DeviceID)' is disabled" 260 | $StatusCode = [HttpStatusCode]::Forbidden 261 | $Body = "Disabled device record" 262 | } 263 | } 264 | else { 265 | Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not validate signed content from client" 266 | $StatusCode = [HttpStatusCode]::Forbidden 267 | $Body = "Untrusted request" 268 | } 269 | } 270 | else { 271 | Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not validate certificate SHA256 hash value" 272 | $StatusCode = [HttpStatusCode]::Forbidden 273 | $Body = "Untrusted request" 274 | } 275 | } 276 | else { 277 | Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not validate certificate thumbprint" 278 | $StatusCode = [HttpStatusCode]::Forbidden 279 | $Body = "Untrusted request" 280 | } 281 | } 282 | else { 283 | Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not find device with deviceId: $($DeviceID)" 284 | $StatusCode = [HttpStatusCode]::Forbidden 285 | $Body = "Untrusted request" 286 | } 287 | } 288 | 289 | # Associate values to output bindings by calling 'Push-OutputBinding'. 290 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 291 | StatusCode = $StatusCode 292 | Body = $Body 293 | }) -------------------------------------------------------------------------------- /Azure Functions/SendClientEvent/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "Request", 8 | "methods": [ 9 | "post" 10 | ] 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "Response" 16 | } 17 | ], 18 | "scriptFile": "SendClientEvent.ps1" 19 | } 20 | -------------------------------------------------------------------------------- /Azure Functions/SetSecret/SetSecret.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Net 2 | 3 | # Input bindings are passed in via param block. 4 | param( 5 | [Parameter(Mandatory = $true)] 6 | $Request, 7 | 8 | [Parameter(Mandatory = $false)] 9 | $TriggerMetadata 10 | ) 11 | 12 | # Functions 13 | function Get-AuthToken { 14 | <# 15 | .SYNOPSIS 16 | Retrieve an access token for the Managed System Identity. 17 | 18 | .DESCRIPTION 19 | Retrieve an access token for the Managed System Identity. 20 | 21 | .NOTES 22 | Author: Nickolaj Andersen 23 | Contact: @NickolajA 24 | Created: 2021-06-07 25 | Updated: 2021-06-07 26 | 27 | Version history: 28 | 1.0.0 - (2021-06-07) Function created 29 | #> 30 | Process { 31 | # Get Managed Service Identity details from the Azure Functions application settings 32 | $MSIEndpoint = $env:MSI_ENDPOINT 33 | $MSISecret = $env:MSI_SECRET 34 | 35 | # Define the required URI and token request params 36 | $APIVersion = "2017-09-01" 37 | $ResourceURI = "https://graph.microsoft.com" 38 | $AuthURI = $MSIEndpoint + "?resource=$($ResourceURI)&api-version=$($APIVersion)" 39 | 40 | # Call resource URI to retrieve access token as Managed Service Identity 41 | $Response = Invoke-RestMethod -Uri $AuthURI -Method "Get" -Headers @{ "Secret" = "$($MSISecret)" } 42 | 43 | # Construct authentication header to be returned from function 44 | $AuthenticationHeader = @{ 45 | "Authorization" = "Bearer $($Response.access_token)" 46 | "ExpiresOn" = $Response.expires_on 47 | } 48 | 49 | # Handle return value 50 | return $AuthenticationHeader 51 | } 52 | } 53 | 54 | Write-Output -InputObject "Inbound request from IP: $($TriggerMetadata.'$Request'.headers.'x-forwarded-for'.Split(":")[0])" 55 | 56 | # Read application settings for Key Vault values 57 | $KeyVaultName = $env:KeyVaultName 58 | $KeyVaultUpdateFrequencyDays = if (-not([string]::IsNullOrEmpty($env:UpdateFrequencyDays))) { $env:UpdateFrequencyDays } else { 3 } 59 | $PasswordLength = if (-not([string]::IsNullOrEmpty($env:PasswordLength))) { $env:PasswordLength } else { 16 } 60 | $PasswordAllowedCharacters = if (-not([string]::IsNullOrEmpty($env:PasswordAllowedCharacters))) { $env:PasswordAllowedCharacters } else { "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.:;,-_!?$%*=+&<>@#()23456789" } 61 | $DebugLogging = if (-not([string]::IsNullOrEmpty($env:DebugLogging))) { $env:DebugLogging } else { $false } 62 | 63 | # Retrieve authentication token 64 | $AuthToken = Get-AuthToken 65 | 66 | # Initate variables 67 | $StatusCode = [HttpStatusCode]::OK 68 | $Body = [string]::Empty 69 | $HeaderValidation = $true 70 | 71 | # Assign incoming request properties to variables 72 | $DeviceName = $Request.Body.DeviceName 73 | $DeviceID = $Request.Body.DeviceID 74 | $SerialNumber = $Request.Body.SerialNumber 75 | $Type = $Request.Body.Type 76 | $Signature = $Request.Body.Signature 77 | $Thumbprint = $Request.Body.Thumbprint 78 | $PublicKey = $Request.Body.PublicKey 79 | $ContentType = $Request.Body.ContentType 80 | $UserName = $Request.Body.UserName 81 | $SecretUpdateOverride = if ([string]::IsNullOrEmpty($Request.Body.SecretUpdateOverride)) { $false } else { $Request.Body.SecretUpdateOverride } 82 | 83 | # Validate request header values 84 | $HeaderValidationList = @(@{ "DeviceName" = $DeviceName }, @{ "DeviceID" = $DeviceID }, @{ "SerialNumber" = $SerialNumber }, @{ "Type" = $Type }, @{ "Signature" = $Signature }, @{ "Thumbprint" = $Thumbprint }, @{ "PublicKey" = $PublicKey }, @{ "ContentType" = $ContentType }, @{ "UserName" = $UserName }) 85 | foreach ($HeaderValidationItem in $HeaderValidationList) { 86 | foreach ($HeaderItem in $HeaderValidationItem.Keys) { 87 | if ([string]::IsNullOrEmpty($HeaderValidationItem[$HeaderItem])) { 88 | Write-Warning -Message "Header validation for '$($HeaderItem)' failed, request will not be handled" 89 | $StatusCode = [HttpStatusCode]::BadRequest 90 | $HeaderValidation = $false 91 | $Body = "Header validation failed" 92 | } 93 | else { 94 | if ($HeaderItem -in @("Signature", "PublicKey")) { 95 | if ($DebugLogging -eq $true) { 96 | Write-Output -InputObject "Header validation succeeded for '$($HeaderItem)' with value: $($HeaderValidationItem[$HeaderItem])" 97 | } 98 | else { 99 | Write-Output -InputObject "Header validation succeeded for '$($HeaderItem)' with value: " 100 | } 101 | } 102 | else { 103 | Write-Output -InputObject "Header validation succeeded for '$($HeaderItem)' with value: $($HeaderValidationItem[$HeaderItem])" 104 | } 105 | } 106 | } 107 | } 108 | 109 | if ($HeaderValidation -eq $true) { 110 | # Initiate request handling 111 | Write-Output -InputObject "Initiating request handling for device named as '$($DeviceName)' with identifier: $($DeviceID)" 112 | 113 | $AzureADDeviceRecord = Get-AzureADDeviceRecord -DeviceID $DeviceID -AuthToken $AuthToken 114 | if ($AzureADDeviceRecord -ne $null) { 115 | Write-Output -InputObject "Found trusted Azure AD device record with object identifier: $($AzureADDeviceRecord.id)" 116 | 117 | # Get required validation data for debug logging when enabled 118 | if ($DebugLogging -eq $true) { 119 | $AzureADDeviceAlternativeSecurityIds = Get-AzureADDeviceAlternativeSecurityIds -Key $AzureADDeviceRecord.alternativeSecurityIds.key 120 | } 121 | 122 | # Validate thumbprint from input request with Azure AD device record's alternativeSecurityIds details 123 | if ($DebugLogging -eq $true) { 124 | Write-Output -InputObject "ValidatePublicKeyThumbprint: Value from param 'Thumbprint': $($Thumbprint)" 125 | Write-Output -InputObject "ValidatePublicKeyThumbprint: Value from AAD device record: $($AzureADDeviceAlternativeSecurityIds.Thumbprint)" 126 | } 127 | if (Test-AzureADDeviceAlternativeSecurityIds -AlternativeSecurityIdKey $AzureADDeviceRecord.alternativeSecurityIds.key -Type "Thumbprint" -Value $Thumbprint) { 128 | Write-Output -InputObject "Successfully validated certificate thumbprint from inbound request" 129 | 130 | # Validate public key hash from input request with Azure AD device record's alternativeSecurityIds details 131 | if ($DebugLogging -eq $true) { 132 | $ComputedHashString = New-HashString -Value $PublicKey 133 | Write-Output -InputObject "ValidatePublicKeyHash: Encoded hash from param 'PublicKey': $($ComputedHashString)" 134 | Write-Output -InputObject "ValidatePublicKeyHash: Encoded hash from AAD device record: $($AzureADDeviceAlternativeSecurityIds.PublicKeyHash)" 135 | } 136 | if (Test-AzureADDeviceAlternativeSecurityIds -AlternativeSecurityIdKey $AzureADDeviceRecord.alternativeSecurityIds.key -Type "Hash" -Value $PublicKey) { 137 | Write-Output -InputObject "Successfully validated certificate SHA256 hash value from inbound request" 138 | 139 | $EncryptionVerification = Test-Encryption -PublicKeyEncoded $PublicKey -Signature $Signature -Content $AzureADDeviceRecord.deviceId 140 | if ($EncryptionVerification -eq $true) { 141 | Write-Output -InputObject "Successfully validated inbound request came from a trusted Azure AD device record" 142 | 143 | # Validate that the inbound request came from a trusted device that's not disabled 144 | if ($AzureADDeviceRecord.accountEnabled -eq $true) { 145 | Write-Output -InputObject "Azure AD device record was validated as enabled" 146 | 147 | # Determine parameter input variable to use for secret name 148 | switch ($Type) { 149 | "NonVM" { 150 | $SecretName = $SerialNumber 151 | } 152 | "VM" { 153 | $SecretName = $DeviceName 154 | } 155 | } 156 | 157 | # Validate that request to set or update key vault secret for provided secret name hasn't already been updated within the amount of days set in UpdateFrequencyDays application setting 158 | Write-Output -InputObject "Attempting to retrieve secret from vault with name: $($SecretName)" 159 | $KeyVaultSecretUpdateAllowed = $false 160 | $KeyVaultSecret = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name $SecretName -ErrorAction SilentlyContinue 161 | if ($KeyVaultSecret -ne $null) { 162 | Write-Output -InputObject "Existing secret was last updated on (UTC): $(($KeyVaultSecret.Updated).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss"))" 163 | if ($SecretUpdateOverride -eq $true) { 164 | $KeyVaultSecretUpdateAllowed = $true 165 | } 166 | else { 167 | if ((Get-Date).ToUniversalTime() -ge ($KeyVaultSecret.Updated).ToUniversalTime().AddDays($KeyVaultUpdateFrequencyDays)) { 168 | $KeyVaultSecretUpdateAllowed = $true 169 | } 170 | else { 171 | Write-Output -InputObject "Secret update will be allowed first after (UTC): $(($KeyVaultSecret.Updated).ToUniversalTime().AddDays($KeyVaultUpdateFrequencyDays).ToString("yyyy-MM-dd HH:mm:ss"))" 172 | $KeyVaultSecretUpdateAllowed = $false 173 | } 174 | } 175 | } 176 | else { 177 | Write-Output -InputObject "Existing secret was not found, secret update will be allowed" 178 | $KeyVaultSecretUpdateAllowed = $true 179 | } 180 | 181 | # Continue if update of existing secret was allowed or if new should be created 182 | if ($KeyVaultSecretUpdateAllowed -eq $true) { 183 | Write-Output -InputObject "Secret update is allowed, SecretUpdateOverride value is $($SecretUpdateOverride)" 184 | 185 | # Generate a random password 186 | $Password = Invoke-PasswordGeneration -Length $PasswordLength -AllowedCharacters $PasswordAllowedCharacters 187 | $SecretValue = ConvertTo-SecureString -String $Password -AsPlainText -Force 188 | 189 | # Construct hash-table for Tags property 190 | $Tags = @{ 191 | "UserName" = $UserName 192 | "AzureADDeviceID" = $DeviceID 193 | "DeviceName" = $DeviceName 194 | } 195 | 196 | try { 197 | # Attempt to add secret to Key Vault 198 | Write-Output -InputObject "Attempting to commit secret with name '$($SecretName)' to vault" 199 | Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name $SecretName -SecretValue $SecretValue -ContentType $ContentType -Tags $Tags -ErrorAction Stop 200 | Write-Output -InputObject "Successfully committed secret to vault" 201 | $Body = $Password 202 | } 203 | catch [System.Exception] { 204 | Write-Warning -Message "Failed to commit key vault secret. Error message: $($_.Exception.Message)" 205 | $StatusCode = [HttpStatusCode]::BadRequest 206 | $Body = "Failed to commit secret to key vault" 207 | } 208 | } 209 | else { 210 | $StatusCode = [HttpStatusCode]::Forbidden 211 | $Body = "Secret update not allowed" 212 | } 213 | } 214 | else { 215 | Write-Output -InputObject "Trusted Azure AD device record validation for inbound request failed, record with deviceId '$($DeviceID)' is disabled" 216 | $StatusCode = [HttpStatusCode]::Forbidden 217 | $Body = "Disabled device record" 218 | } 219 | } 220 | else { 221 | Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not validate signed content from client" 222 | $StatusCode = [HttpStatusCode]::Forbidden 223 | $Body = "Untrusted request" 224 | } 225 | } 226 | else { 227 | Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not validate certificate SHA256 hash value" 228 | $StatusCode = [HttpStatusCode]::Forbidden 229 | $Body = "Untrusted request" 230 | } 231 | } 232 | else { 233 | Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not validate certificate thumbprint" 234 | $StatusCode = [HttpStatusCode]::Forbidden 235 | $Body = "Untrusted request" 236 | } 237 | } 238 | else { 239 | Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not find device with deviceId: $($DeviceID)" 240 | $StatusCode = [HttpStatusCode]::Forbidden 241 | $Body = "Untrusted request" 242 | } 243 | } 244 | 245 | # Associate values to output bindings by calling 'Push-OutputBinding'. 246 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 247 | StatusCode = $StatusCode 248 | Body = $Body 249 | }) -------------------------------------------------------------------------------- /Azure Functions/SetSecret/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "Request", 8 | "methods": [ 9 | "post" 10 | ] 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "Response" 16 | } 17 | ], 18 | "scriptFile": "SetSecret.ps1" 19 | } 20 | -------------------------------------------------------------------------------- /Azure Functions/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[2.*, 3.0.0)" 14 | }, 15 | "managedDependency": { 16 | "enabled": true 17 | } 18 | } -------------------------------------------------------------------------------- /Azure Functions/modules/AADDeviceTrust/AADDeviceTrust.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'AADDeviceTrust' 3 | # 4 | # Generated by: Nickolaj Andersen @NickolajA 5 | # 6 | # Generated on: 2022-01-01 7 | # 8 | 9 | @{ 10 | # Script module or binary module file associated with this manifest. 11 | RootModule = 'AADDeviceTrust.psm1' 12 | 13 | # Version number of this module. 14 | ModuleVersion = '1.0.0' 15 | 16 | # ID used to uniquely identify this module 17 | GUID = '52da9652-f13b-47d6-9836-4ecb6d4afb0a' 18 | 19 | # Author of this module 20 | Author = 'Nickolaj Andersen' 21 | 22 | # Company or vendor of this module 23 | CompanyName = 'MSEndpointMgr.com' 24 | 25 | # Copyright statement for this module 26 | Copyright = '(c) 2022 Nickolaj Andersen. All rights reserved.' 27 | 28 | # Description of the functionality provided by this module 29 | Description = 'Provides a set of functions to validate if a request against an API is made by a trusted Azure AD device.' 30 | 31 | # Minimum version of the Windows PowerShell engine required by this module 32 | PowerShellVersion = '5.0' 33 | 34 | # Modules that must be imported into the global environment prior to importing this module 35 | # RequiredModules = @("") 36 | 37 | # Assemblies that must be loaded prior to importing this module 38 | # RequiredAssemblies = @() 39 | 40 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 41 | FunctionsToExport = @("Get-AzureADDeviceAlternativeSecurityIds", 42 | "Get-AzureADDeviceRecord", 43 | "New-HashString", 44 | "Test-AzureADDeviceAlternativeSecurityIds", 45 | "Test-Encryption" 46 | ) 47 | 48 | # Variables to export from this module 49 | VariablesToExport = '*' 50 | 51 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 52 | AliasesToExport = @() 53 | 54 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 55 | PrivateData = @{ 56 | PSData = @{ 57 | # Tags applied to this module. These help with module discovery in online galleries. 58 | # Tags = @() 59 | 60 | # A URL to the license for this module. 61 | # LicenseUri = '' 62 | 63 | # A URL to the main website for this project. 64 | ProjectUri = 'https://github.com/MSEndpointMgr/AADDeviceTrust' 65 | 66 | # A URL to an icon representing this module. 67 | # IconUri = '' 68 | 69 | # ReleaseNotes of this module 70 | # ReleaseNotes = '' 71 | 72 | } # End of PSData hashtable 73 | 74 | } # End of PrivateData hashtable 75 | 76 | } 77 | 78 | -------------------------------------------------------------------------------- /Azure Functions/modules/AADDeviceTrust/AADDeviceTrust.psm1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Script that initiates the AADDeviceTrust module 4 | 5 | .NOTES 6 | Author: Nickolaj Andersen 7 | Contact: @NickolajA 8 | Website: https://www.msendpointmgr.com 9 | #> 10 | [CmdletBinding()] 11 | Param() 12 | Process { 13 | # Locate all the public and private function specific files 14 | $PublicFunctions = Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath "Public") -Filter "*.ps1" -ErrorAction SilentlyContinue 15 | $PrivateFunctions = Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath "Private") -Filter "*.ps1" -ErrorAction SilentlyContinue 16 | 17 | # Dot source the function files 18 | foreach ($FunctionFile in @($PublicFunctions + $PrivateFunctions)) { 19 | try { 20 | . $FunctionFile.FullName -ErrorAction Stop 21 | } 22 | catch [System.Exception] { 23 | Write-Error -Message "Failed to import function '$($FunctionFile.FullName)' with error: $($_.Exception.Message)" 24 | } 25 | } 26 | 27 | Export-ModuleMember -Function $PublicFunctions.BaseName -Alias * 28 | } -------------------------------------------------------------------------------- /Azure Functions/modules/AADDeviceTrust/Public/Get-AzureADDeviceAlternativeSecurityIds.ps1: -------------------------------------------------------------------------------- 1 | function Get-AzureADDeviceAlternativeSecurityIds { 2 | <# 3 | .SYNOPSIS 4 | Decodes Key property of an Azure AD device record into prefix, thumbprint and publickeyhash values. 5 | 6 | .DESCRIPTION 7 | Decodes Key property of an Azure AD device record into prefix, thumbprint and publickeyhash values. 8 | 9 | .PARAMETER Key 10 | Specify the 'key' property of the alternativeSecurityIds property retrieved from the Get-AzureADDeviceRecord function. 11 | 12 | .NOTES 13 | Author: Nickolaj Andersen 14 | Contact: @NickolajA 15 | Created: 2021-06-07 16 | Updated: 2021-06-07 17 | 18 | Version history: 19 | 1.0.0 - (2021-06-07) Function created 20 | #> 21 | param( 22 | [parameter(Mandatory = $true, HelpMessage = "Specify the 'key' property of the alternativeSecurityIds property retrieved from the Get-AzureADDeviceRecord function.")] 23 | [ValidateNotNullOrEmpty()] 24 | [string]$Key 25 | ) 26 | Process { 27 | $DecodedKey = [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($Key)) 28 | $PSObject = [PSCustomObject]@{ 29 | "Prefix" = $DecodedKey.SubString(0,21) 30 | "Thumbprint" = $DecodedKey.Split(">")[1].SubString(0,40) 31 | "PublicKeyHash" = $DecodedKey.Split(">")[1].SubString(40) 32 | } 33 | 34 | # Handle return response 35 | return $PSObject 36 | } 37 | } -------------------------------------------------------------------------------- /Azure Functions/modules/AADDeviceTrust/Public/Get-AzureADDeviceRecord.ps1: -------------------------------------------------------------------------------- 1 | function Get-AzureADDeviceRecord { 2 | <# 3 | .SYNOPSIS 4 | Retrieve an Azure AD device record. 5 | 6 | .DESCRIPTION 7 | Retrieve an Azure AD device record. 8 | 9 | .PARAMETER DeviceID 10 | Specify the Device ID of an Azure AD device record. 11 | 12 | .PARAMETER AuthToken 13 | Specify a hash table consisting of the authentication headers. 14 | 15 | .NOTES 16 | Author: Nickolaj Andersen 17 | Contact: @NickolajA 18 | Created: 2021-06-07 19 | Updated: 2022-01-01 20 | 21 | Version history: 22 | 1.0.0 - (2021-06-07) Function created 23 | 1.0.1 - (2022-01-01) Added support for passing in the authentication header table to the function 24 | #> 25 | param( 26 | [parameter(Mandatory = $true, HelpMessage = "Specify the Device ID of an Azure AD device record.")] 27 | [ValidateNotNullOrEmpty()] 28 | [string]$DeviceID, 29 | 30 | [parameter(Mandatory = $true, HelpMessage = "Specify a hash table consisting of the authentication headers.")] 31 | [ValidateNotNullOrEmpty()] 32 | [System.Collections.Hashtable]$AuthToken 33 | ) 34 | Process { 35 | $GraphURI = "https://graph.microsoft.com/v1.0/devices?`$filter=deviceId eq '$($DeviceID)'" 36 | $GraphResponse = (Invoke-RestMethod -Method "Get" -Uri $GraphURI -ContentType "application/json" -Headers $AuthToken -ErrorAction Stop).value 37 | 38 | # Handle return response 39 | return $GraphResponse 40 | } 41 | } -------------------------------------------------------------------------------- /Azure Functions/modules/AADDeviceTrust/Public/New-HashString.ps1: -------------------------------------------------------------------------------- 1 | function New-HashString { 2 | <# 3 | .SYNOPSIS 4 | Compute has from input value and return encoded Base64 string. 5 | 6 | .DESCRIPTION 7 | Compute has from input value and return encoded Base64 string. 8 | 9 | .PARAMETER Value 10 | Specify a Base64 encoded value for which a hash will be computed. 11 | 12 | .NOTES 13 | Author: Nickolaj Andersen 14 | Contact: @NickolajA 15 | Created: 2021-08-23 16 | Updated: 2021-08-23 17 | 18 | Version history: 19 | 1.0.0 - (2021-08-23) Function created 20 | #> 21 | param( 22 | [parameter(Mandatory = $true, HelpMessage = "Specify a Base64 encoded value for which a hash will be computed.")] 23 | [ValidateNotNullOrEmpty()] 24 | [string]$Value 25 | ) 26 | Process { 27 | # Convert from Base64 string to byte array 28 | $DecodedBytes = [System.Convert]::FromBase64String($Value) 29 | 30 | # Construct a new SHA256Managed object to be used when computing the hash 31 | $SHA256Managed = New-Object -TypeName "System.Security.Cryptography.SHA256Managed" 32 | 33 | # Compute the hash 34 | [byte[]]$ComputedHash = $SHA256Managed.ComputeHash($DecodedBytes) 35 | 36 | # Convert computed hash to Base64 string 37 | $ComputedHashString = [System.Convert]::ToBase64String($ComputedHash) 38 | 39 | # Handle return value 40 | return $ComputedHashString 41 | } 42 | } -------------------------------------------------------------------------------- /Azure Functions/modules/AADDeviceTrust/Public/Test-AzureADDeviceAlternativeSecurityIds.ps1: -------------------------------------------------------------------------------- 1 | function Test-AzureADDeviceAlternativeSecurityIds { 2 | <# 3 | .SYNOPSIS 4 | Validate the thumbprint and publickeyhash property values of the alternativeSecurityIds property from the Azure AD device record. 5 | 6 | .DESCRIPTION 7 | Validate the thumbprint and publickeyhash property values of the alternativeSecurityIds property from the Azure AD device record. 8 | 9 | .PARAMETER AlternativeSecurityIdKey 10 | Specify the alternativeSecurityIds.Key property from an Azure AD device record. 11 | 12 | .PARAMETER Type 13 | Specify the type of the AlternativeSecurityIdsKey object, e.g. Thumbprint or Hash. 14 | 15 | .PARAMETER Value 16 | Specify the value of the type to be validated. 17 | 18 | .NOTES 19 | Author: Nickolaj Andersen 20 | Contact: @NickolajA 21 | Created: 2021-06-07 22 | Updated: 2021-06-07 23 | 24 | Version history: 25 | 1.0.0 - (2021-06-07) Function created 26 | #> 27 | param( 28 | [parameter(Mandatory = $true, HelpMessage = "Specify the alternativeSecurityIds.Key property from an Azure AD device record.")] 29 | [ValidateNotNullOrEmpty()] 30 | [string]$AlternativeSecurityIdKey, 31 | 32 | [parameter(Mandatory = $true, HelpMessage = "Specify the type of the AlternativeSecurityIdsKey object, e.g. Thumbprint or Hash.")] 33 | [ValidateNotNullOrEmpty()] 34 | [ValidateSet("Thumbprint", "Hash")] 35 | [string]$Type, 36 | 37 | [parameter(Mandatory = $true, HelpMessage = "Specify the value of the type to be validated.")] 38 | [ValidateNotNullOrEmpty()] 39 | [string]$Value 40 | ) 41 | Process { 42 | # Construct custom object for alternativeSecurityIds property from Azure AD device record, used as reference value when compared to input value 43 | $AzureADDeviceAlternativeSecurityIds = Get-AzureADDeviceAlternativeSecurityIds -Key $AlternativeSecurityIdKey 44 | 45 | switch ($Type) { 46 | "Thumbprint" { 47 | # Validate match 48 | if ($Value -match $AzureADDeviceAlternativeSecurityIds.Thumbprint) { 49 | return $true 50 | } 51 | else { 52 | return $false 53 | } 54 | } 55 | "Hash" { 56 | # Convert from Base64 string to byte array 57 | $DecodedBytes = [System.Convert]::FromBase64String($Value) 58 | 59 | # Construct a new SHA256Managed object to be used when computing the hash 60 | $SHA256Managed = New-Object -TypeName "System.Security.Cryptography.SHA256Managed" 61 | 62 | # Compute the hash 63 | [byte[]]$ComputedHash = $SHA256Managed.ComputeHash($DecodedBytes) 64 | 65 | # Convert computed hash to Base64 string 66 | $ComputedHashString = [System.Convert]::ToBase64String($ComputedHash) 67 | 68 | # Validate match 69 | if ($ComputedHashString -like $AzureADDeviceAlternativeSecurityIds.PublicKeyHash) { 70 | return $true 71 | } 72 | else { 73 | return $false 74 | } 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Azure Functions/modules/AADDeviceTrust/Public/Test-Encryption.ps1: -------------------------------------------------------------------------------- 1 | function Test-Encryption { 2 | <# 3 | .SYNOPSIS 4 | Test the signature created with the private key by using the public key. 5 | 6 | .DESCRIPTION 7 | Test the signature created with the private key by using the public key. 8 | 9 | .PARAMETER PublicKeyEncoded 10 | Specify the Base64 encoded string representation of the Public Key. 11 | 12 | .PARAMETER Signature 13 | Specify the Base64 encoded string representation of the signature coming from the inbound request. 14 | 15 | .PARAMETER Content 16 | Specify the content string that the signature coming from the inbound request is based upon. 17 | 18 | .NOTES 19 | Author: Nickolaj Andersen / Thomas Kurth 20 | Contact: @NickolajA 21 | Created: 2021-06-07 22 | Updated: 2021-06-07 23 | 24 | Version history: 25 | 1.0.0 - (2021-06-07) Function created 26 | 27 | Credits to Thomas Kurth for sharing his original C# code. 28 | #> 29 | param( 30 | [parameter(Mandatory = $true, HelpMessage = "Specify the Base64 encoded string representation of the Public Key.")] 31 | [ValidateNotNullOrEmpty()] 32 | [string]$PublicKeyEncoded, 33 | 34 | [parameter(Mandatory = $true, HelpMessage = "Specify the Base64 encoded string representation of the signature coming from the inbound request.")] 35 | [ValidateNotNullOrEmpty()] 36 | [string]$Signature, 37 | 38 | [parameter(Mandatory = $true, HelpMessage = "Specify the content string that the signature coming from the inbound request is based upon.")] 39 | [ValidateNotNullOrEmpty()] 40 | [string]$Content 41 | ) 42 | Process { 43 | # Convert from Base64 string to byte array 44 | $PublicKeyBytes = [System.Convert]::FromBase64String($PublicKeyEncoded) 45 | 46 | # Convert signature from Base64 string 47 | [byte[]]$Signature = [System.Convert]::FromBase64String($Signature) 48 | 49 | # Extract the modulus and exponent based on public key data 50 | $ExponentData = [System.Byte[]]::CreateInstance([System.Byte], 3) 51 | $ModulusData = [System.Byte[]]::CreateInstance([System.Byte], 256) 52 | [System.Array]::Copy($PublicKeyBytes, $PublicKeyBytes.Length - $ExponentData.Length, $ExponentData, 0, $ExponentData.Length) 53 | [System.Array]::Copy($PublicKeyBytes, 9, $ModulusData, 0, $ModulusData.Length) 54 | 55 | # Construct RSACryptoServiceProvider and import modolus and exponent data as parameters to reconstruct the public key from bytes 56 | $PublicKey = [System.Security.Cryptography.RSACryptoServiceProvider]::Create(2048) 57 | $RSAParameters = $PublicKey.ExportParameters($false) 58 | $RSAParameters.Modulus = $ModulusData 59 | $RSAParameters.Exponent = $ExponentData 60 | $PublicKey.ImportParameters($RSAParameters) 61 | 62 | # Construct a new SHA256Managed object to be used when computing the hash 63 | $SHA256Managed = New-Object -TypeName "System.Security.Cryptography.SHA256Managed" 64 | 65 | # Construct new UTF8 unicode encoding object 66 | $UnicodeEncoding = [System.Text.UnicodeEncoding]::UTF8 67 | 68 | # Convert content to byte array 69 | [byte[]]$EncodedContentData = $UnicodeEncoding.GetBytes($Content) 70 | 71 | # Compute the hash 72 | [byte[]]$ComputedHash = $SHA256Managed.ComputeHash($EncodedContentData) 73 | 74 | # Verify the signature with the computed hash of the content using the public key 75 | $PublicKey.VerifyHash($ComputedHash, $Signature, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) 76 | } 77 | } -------------------------------------------------------------------------------- /Azure Functions/profile.ps1: -------------------------------------------------------------------------------- 1 | # Azure Functions profile.ps1 2 | # 3 | # This profile.ps1 will get executed every "cold start" of your Function App. 4 | # "cold start" occurs when: 5 | # 6 | # * A Function App starts up for the very first time 7 | # * A Function App starts up after being de-allocated due to inactivity 8 | # 9 | # You can define helper functions, run commands, or specify environment variables 10 | # NOTE: any variables defined that are not environment variables will get reset after the first execution 11 | 12 | # Authenticate with Azure PowerShell using MSI. 13 | # Remove this if you are not planning on using MSI or Azure PowerShell. 14 | if ($env:MSI_SECRET) { 15 | #Disable-AzContextAutosave 16 | Connect-AzAccount -Identity 17 | } 18 | Set-Item "Env:\SuppressAzurePowerShellBreakingChangeWarnings" "true" 19 | 20 | # Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell. 21 | # Enable-AzureRmAlias 22 | 23 | # You can also define functions or aliases that can be referenced in any of your PowerShell functions. -------------------------------------------------------------------------------- /Azure Functions/requirements.psd1: -------------------------------------------------------------------------------- 1 | # This file enables modules to be automatically managed by the Functions service. 2 | # See https://aka.ms/functionsmanageddependency for additional information. 3 | # 4 | @{ 5 | # For latest supported version, go to 'https://www.powershellgallery.com/packages/Az'. 6 | 'Az.Accounts' = '2.*' 7 | 'Az.KeyVault' = '4.*' 8 | 'WPNinjas.PasswordGeneration' = '1.*' 9 | } -------------------------------------------------------------------------------- /Deploy/Update/dev-update-function.bicep: -------------------------------------------------------------------------------- 1 | // Define parameters 2 | @description('Provide the name of the existing Function App that was given when CloudLAPS was initially deployed.') 3 | param FunctionAppName string 4 | @description('Provide the name of the existing portal Web App that was given when CloudLAPS was initially deployed.') 5 | param PortalWebAppName string 6 | @description('Provide the name of the existing Log Analytics workspace that was given when CloudLAPS was initially deployed.') 7 | param LogAnalyticsWorkspaceName string 8 | @description('Provide the name of the existing Key Vault that was given when CloudLAPS was initially deployed.') 9 | param KeyVaultName string 10 | @description('Provide the name of the existing Storage Account that was automatically given when CloudLAPS was initially deployed.') 11 | param StorageAccountName string 12 | @description('Provide the App registration application identifier that was created when CloudLAPS was initially deployed.') 13 | param ApplicationID string 14 | @description('Provide the number of days when password rotation is allowed. Default is 3.') 15 | param UpdateFrequencyDays string = '3' 16 | @description('Provide the default length of the generated local admin password. Default is 16.') 17 | param PasswordLength string = '16' 18 | @description('Provide the default character set to be used when generating the local admin password. Default is value is ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.:;,-_!?$%*=+&<>@#()23456789.') 19 | param PasswordAllowedCharacters string = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.:;,-_!?$%*=+&<>@#()23456789' 20 | 21 | // Automatically construct variables based on param input 22 | var FunctionAppInsightsName = '${FunctionAppName}-fa-ai' 23 | var PortalAppInsightsName = '${FunctionAppName}-wa-ai' 24 | var KeyVaultAppSettingsName = '${take(KeyVaultName, 21)}-as' 25 | 26 | // Define existing resources based on param input 27 | resource FunctionApp 'Microsoft.Web/sites@2022-03-01' existing = { 28 | name: FunctionAppName 29 | } 30 | resource PortalAppService 'Microsoft.Web/sites@2022-03-01' existing = { 31 | name: PortalWebAppName 32 | } 33 | resource LogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 34 | name: LogAnalyticsWorkspaceName 35 | } 36 | resource KeyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { 37 | name: KeyVaultName 38 | } 39 | resource StorageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' existing = { 40 | name: StorageAccountName 41 | } 42 | resource FunctionAppInsightsComponents 'Microsoft.Insights/components@2020-02-02' existing = { 43 | name: FunctionAppInsightsName 44 | } 45 | resource PortalAppInsightsComponents 'Microsoft.Insights/components@2020-02-02' existing = { 46 | name: PortalAppInsightsName 47 | } 48 | 49 | // Collect Log Analytics workspace properties to be added to Key Vault as secrets 50 | var LogAnalyticsWorkspaceId = LogAnalyticsWorkspace.properties.customerId 51 | var LogAnalyticsWorkspaceSharedKey = LogAnalyticsWorkspace.listKeys().primarySharedKey 52 | 53 | // Remove trailing forward slash from Key Vault uri property 54 | var KeyVaultUri = KeyVault.properties.vaultUri 55 | var KeyVaultUriNoSlash = substring(KeyVaultUri, 0, length(KeyVaultUri)-1) 56 | 57 | // Create Key Vault for Function App application settings 58 | resource KeyVaultAppSettings 'Microsoft.KeyVault/vaults@2022-07-01' = { 59 | name: KeyVaultAppSettingsName 60 | location: resourceGroup().location 61 | properties: { 62 | enabledForDeployment: false 63 | enabledForTemplateDeployment: false 64 | enabledForDiskEncryption: false 65 | tenantId: subscription().tenantId 66 | accessPolicies: [ 67 | { 68 | tenantId: FunctionApp.identity.tenantId 69 | objectId: FunctionApp.identity.principalId 70 | permissions: { 71 | secrets: [ 72 | 'get' 73 | ] 74 | } 75 | } 76 | { 77 | tenantId: PortalAppService.identity.tenantId 78 | objectId: PortalAppService.identity.principalId 79 | permissions: { 80 | secrets: [ 81 | 'get' 82 | ] 83 | } 84 | } 85 | ] 86 | sku: { 87 | name: 'standard' 88 | family: 'A' 89 | } 90 | } 91 | } 92 | 93 | // Construct secrets in Key Vault 94 | resource WorkspaceIdSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { 95 | name: '${KeyVaultAppSettingsName}/LogAnalyticsWorkspaceId' 96 | properties: { 97 | value: LogAnalyticsWorkspaceId 98 | } 99 | dependsOn: [ 100 | KeyVaultAppSettings 101 | ] 102 | } 103 | resource SharedKeySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { 104 | name: '${KeyVaultAppSettingsName}/LogAnalyticsWorkspaceSharedKey' 105 | properties: { 106 | value: LogAnalyticsWorkspaceSharedKey 107 | } 108 | dependsOn: [ 109 | KeyVaultAppSettings 110 | ] 111 | } 112 | 113 | // Construct appSettings resource for Function App and ensure default values including new ones are added 114 | resource FunctionAppSettings 'Microsoft.Web/sites/config@2022-03-01' = { 115 | name: '${FunctionApp.name}/appsettings' 116 | properties: { 117 | AzureWebJobsDashboard: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 118 | AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 119 | WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 120 | WEBSITE_CONTENTSHARE: toLower('CloudLAPS') 121 | WEBSITE_RUN_FROM_PACKAGE: '1' 122 | AzureWebJobsDisableHomepage: 'true' 123 | FUNCTIONS_EXTENSION_VERSION: '~3' 124 | FUNCTIONS_WORKER_PROCESS_COUNT: '3' 125 | PSWorkerInProcConcurrencyUpperBound: '10' 126 | APPINSIGHTS_INSTRUMENTATIONKEY: reference(FunctionAppInsightsComponents.id, '2020-02-02-preview').InstrumentationKey 127 | APPLICATIONINSIGHTS_CONNECTION_STRING: reference(FunctionAppInsightsComponents.id, '2020-02-02-preview').ConnectionString 128 | FUNCTIONS_WORKER_RUNTIME: 'powershell' 129 | UpdateFrequencyDays: UpdateFrequencyDays 130 | KeyVaultName: KeyVaultName 131 | DebugLogging: 'False' 132 | PasswordLength: PasswordLength 133 | PasswordAllowedCharacters: PasswordAllowedCharacters 134 | LogAnalyticsWorkspaceId: '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceId)' 135 | LogAnalyticsWorkspaceSharedKey: '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceSharedKey)' 136 | LogTypeClient: 'CloudLAPSClient' 137 | } 138 | dependsOn: [ 139 | FunctionAppZipDeploy 140 | ] 141 | } 142 | 143 | // Construct appSettings resource for CloudLAPS Portal and ensure default values including new ones are added 144 | resource PortalAppServiceAppSettings 'Microsoft.Web/sites/config@2022-03-01' = { 145 | name: '${PortalAppService.name}/appsettings' 146 | properties: { 147 | AzureWebJobsSecretStorageKeyVaultName: KeyVault.name 148 | APPLICATIONINSIGHTS_CONNECTION_STRING: reference(PortalAppInsightsComponents.id, '2020-02-02-preview').ConnectionString 149 | APPINSIGHTS_INSTRUMENTATIONKEY: reference(PortalAppInsightsComponents.id, '2020-02-02-preview').InstrumentationKey 150 | 'AzureAd:TenantId': subscription().tenantId 151 | 'AzureAd:ClientId': ApplicationID 152 | 'KeyVault:Uri': KeyVaultUriNoSlash 153 | 'LogAnalytics:WorkspaceId': '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceId)' 154 | 'LogAnalytics:SharedKey': '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceSharedKey)' 155 | 'LogAnalytics:LogType': 'CloudLAPSAudit' 156 | } 157 | dependsOn: [ 158 | PortalZipDeploy 159 | ] 160 | } 161 | 162 | // Add ZipDeploy for Function App 163 | resource FunctionAppZipDeploy 'Microsoft.Web/sites/extensions@2015-08-01' = { 164 | parent: FunctionApp 165 | name: 'ZipDeploy' 166 | properties: { 167 | packageUri: 'https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.2.0/CloudLAPS-FunctionApp1.2.0.zip' 168 | } 169 | } 170 | 171 | // Add ZipDeploy for CloudLAPS Portal 172 | resource PortalZipDeploy 'Microsoft.Web/sites/extensions@2015-08-01' = { 173 | parent: PortalAppService 174 | name: 'ZipDeploy' 175 | properties: { 176 | packageUri: 'https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.1.0/CloudLAPS-Portal1.1.0.zip' 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Deploy/Update/dev-update-function.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.11.1.770", 8 | "templateHash": "6986323175370347536" 9 | } 10 | }, 11 | "parameters": { 12 | "FunctionAppName": { 13 | "type": "string", 14 | "metadata": { 15 | "description": "Provide the name of the existing Function App that was given when CloudLAPS was initially deployed." 16 | } 17 | }, 18 | "PortalWebAppName": { 19 | "type": "string", 20 | "metadata": { 21 | "description": "Provide the name of the existing portal Web App that was given when CloudLAPS was initially deployed." 22 | } 23 | }, 24 | "LogAnalyticsWorkspaceName": { 25 | "type": "string", 26 | "metadata": { 27 | "description": "Provide the name of the existing Log Analytics workspace that was given when CloudLAPS was initially deployed." 28 | } 29 | }, 30 | "KeyVaultName": { 31 | "type": "string", 32 | "metadata": { 33 | "description": "Provide the name of the existing Key Vault that was given when CloudLAPS was initially deployed." 34 | } 35 | }, 36 | "StorageAccountName": { 37 | "type": "string", 38 | "metadata": { 39 | "description": "Provide the name of the existing Storage Account that was automatically given when CloudLAPS was initially deployed." 40 | } 41 | }, 42 | "ApplicationID": { 43 | "type": "string", 44 | "metadata": { 45 | "description": "Provide the App registration application identifier that was created when CloudLAPS was initially deployed." 46 | } 47 | }, 48 | "UpdateFrequencyDays": { 49 | "type": "string", 50 | "defaultValue": "3", 51 | "metadata": { 52 | "description": "Provide the number of days when password rotation is allowed. Default is 3." 53 | } 54 | }, 55 | "PasswordLength": { 56 | "type": "string", 57 | "defaultValue": "16", 58 | "metadata": { 59 | "description": "Provide the default length of the generated local admin password. Default is 16." 60 | } 61 | }, 62 | "PasswordAllowedCharacters": { 63 | "type": "string", 64 | "defaultValue": "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.:;,-_!?$%*=+&<>@#()23456789", 65 | "metadata": { 66 | "description": "Provide the default character set to be used when generating the local admin password. Default is value is ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.:;,-_!?$%*=+&<>@#()23456789." 67 | } 68 | } 69 | }, 70 | "variables": { 71 | "FunctionAppInsightsName": "[format('{0}-fa-ai', parameters('FunctionAppName'))]", 72 | "PortalAppInsightsName": "[format('{0}-wa-ai', parameters('FunctionAppName'))]", 73 | "KeyVaultAppSettingsName": "[format('{0}-as', take(parameters('KeyVaultName'), 21))]" 74 | }, 75 | "resources": [ 76 | { 77 | "type": "Microsoft.KeyVault/vaults", 78 | "apiVersion": "2022-07-01", 79 | "name": "[variables('KeyVaultAppSettingsName')]", 80 | "location": "[resourceGroup().location]", 81 | "properties": { 82 | "enabledForDeployment": false, 83 | "enabledForTemplateDeployment": false, 84 | "enabledForDiskEncryption": false, 85 | "tenantId": "[subscription().tenantId]", 86 | "accessPolicies": [ 87 | { 88 | "tenantId": "[reference(resourceId('Microsoft.Web/sites', parameters('FunctionAppName')), '2022-03-01', 'full').identity.tenantId]", 89 | "objectId": "[reference(resourceId('Microsoft.Web/sites', parameters('FunctionAppName')), '2022-03-01', 'full').identity.principalId]", 90 | "permissions": { 91 | "secrets": [ 92 | "get" 93 | ] 94 | } 95 | }, 96 | { 97 | "tenantId": "[reference(resourceId('Microsoft.Web/sites', parameters('PortalWebAppName')), '2022-03-01', 'full').identity.tenantId]", 98 | "objectId": "[reference(resourceId('Microsoft.Web/sites', parameters('PortalWebAppName')), '2022-03-01', 'full').identity.principalId]", 99 | "permissions": { 100 | "secrets": [ 101 | "get" 102 | ] 103 | } 104 | } 105 | ], 106 | "sku": { 107 | "name": "standard", 108 | "family": "A" 109 | } 110 | } 111 | }, 112 | { 113 | "type": "Microsoft.KeyVault/vaults/secrets", 114 | "apiVersion": "2022-07-01", 115 | "name": "[format('{0}/LogAnalyticsWorkspaceId', variables('KeyVaultAppSettingsName'))]", 116 | "properties": { 117 | "value": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', parameters('LogAnalyticsWorkspaceName')), '2022-10-01').customerId]" 118 | }, 119 | "dependsOn": [ 120 | "[resourceId('Microsoft.KeyVault/vaults', variables('KeyVaultAppSettingsName'))]" 121 | ] 122 | }, 123 | { 124 | "type": "Microsoft.KeyVault/vaults/secrets", 125 | "apiVersion": "2022-07-01", 126 | "name": "[format('{0}/LogAnalyticsWorkspaceSharedKey', variables('KeyVaultAppSettingsName'))]", 127 | "properties": { 128 | "value": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', parameters('LogAnalyticsWorkspaceName')), '2022-10-01').primarySharedKey]" 129 | }, 130 | "dependsOn": [ 131 | "[resourceId('Microsoft.KeyVault/vaults', variables('KeyVaultAppSettingsName'))]" 132 | ] 133 | }, 134 | { 135 | "type": "Microsoft.Web/sites/config", 136 | "apiVersion": "2022-03-01", 137 | "name": "[format('{0}/appsettings', parameters('FunctionAppName'))]", 138 | "properties": { 139 | "AzureWebJobsDashboard": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', parameters('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('StorageAccountName')), '2022-05-01').keys[0].value)]", 140 | "AzureWebJobsStorage": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', parameters('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('StorageAccountName')), '2022-05-01').keys[0].value)]", 141 | "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', parameters('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('StorageAccountName')), '2022-05-01').keys[0].value)]", 142 | "WEBSITE_CONTENTSHARE": "[toLower('CloudLAPS')]", 143 | "WEBSITE_RUN_FROM_PACKAGE": "1", 144 | "AzureWebJobsDisableHomepage": "true", 145 | "FUNCTIONS_EXTENSION_VERSION": "~3", 146 | "FUNCTIONS_WORKER_PROCESS_COUNT": "3", 147 | "PSWorkerInProcConcurrencyUpperBound": "10", 148 | "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName')), '2020-02-02-preview').InstrumentationKey]", 149 | "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName')), '2020-02-02-preview').ConnectionString]", 150 | "FUNCTIONS_WORKER_RUNTIME": "powershell", 151 | "UpdateFrequencyDays": "[parameters('UpdateFrequencyDays')]", 152 | "KeyVaultName": "[parameters('KeyVaultName')]", 153 | "DebugLogging": "False", 154 | "PasswordLength": "[parameters('PasswordLength')]", 155 | "PasswordAllowedCharacters": "[parameters('PasswordAllowedCharacters')]", 156 | "LogAnalyticsWorkspaceId": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceId)', variables('KeyVaultAppSettingsName'))]", 157 | "LogAnalyticsWorkspaceSharedKey": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceSharedKey)', variables('KeyVaultAppSettingsName'))]", 158 | "LogTypeClient": "CloudLAPSClient" 159 | }, 160 | "dependsOn": [ 161 | "[resourceId('Microsoft.Web/sites/extensions', parameters('FunctionAppName'), 'ZipDeploy')]" 162 | ] 163 | }, 164 | { 165 | "type": "Microsoft.Web/sites/config", 166 | "apiVersion": "2022-03-01", 167 | "name": "[format('{0}/appsettings', parameters('PortalWebAppName'))]", 168 | "properties": { 169 | "AzureWebJobsSecretStorageKeyVaultName": "[parameters('KeyVaultName')]", 170 | "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('Microsoft.Insights/components', variables('PortalAppInsightsName')), '2020-02-02-preview').ConnectionString]", 171 | "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.Insights/components', variables('PortalAppInsightsName')), '2020-02-02-preview').InstrumentationKey]", 172 | "AzureAd:TenantId": "[subscription().tenantId]", 173 | "AzureAd:ClientId": "[parameters('ApplicationID')]", 174 | "KeyVault:Uri": "[substring(reference(resourceId('Microsoft.KeyVault/vaults', parameters('KeyVaultName')), '2022-07-01').vaultUri, 0, sub(length(reference(resourceId('Microsoft.KeyVault/vaults', parameters('KeyVaultName')), '2022-07-01').vaultUri), 1))]", 175 | "LogAnalytics:WorkspaceId": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceId)', variables('KeyVaultAppSettingsName'))]", 176 | "LogAnalytics:SharedKey": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceSharedKey)', variables('KeyVaultAppSettingsName'))]", 177 | "LogAnalytics:LogType": "CloudLAPSAudit" 178 | }, 179 | "dependsOn": [ 180 | "[resourceId('Microsoft.Web/sites/extensions', parameters('PortalWebAppName'), 'ZipDeploy')]" 181 | ] 182 | }, 183 | { 184 | "type": "Microsoft.Web/sites/extensions", 185 | "apiVersion": "2015-08-01", 186 | "name": "[format('{0}/{1}', parameters('FunctionAppName'), 'ZipDeploy')]", 187 | "properties": { 188 | "packageUri": "https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.2.0/CloudLAPS-FunctionApp1.2.0.zip" 189 | } 190 | }, 191 | { 192 | "type": "Microsoft.Web/sites/extensions", 193 | "apiVersion": "2015-08-01", 194 | "name": "[format('{0}/{1}', parameters('PortalWebAppName'), 'ZipDeploy')]", 195 | "properties": { 196 | "packageUri": "https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.1.0/CloudLAPS-Portal1.1.0.zip" 197 | } 198 | } 199 | ] 200 | } -------------------------------------------------------------------------------- /Deploy/Update/prd-update-function.bicep: -------------------------------------------------------------------------------- 1 | // Define parameters 2 | @description('Provide the name of the existing Function App that was given when CloudLAPS was initially deployed.') 3 | param FunctionAppName string 4 | @description('Provide the name of the existing portal Web App that was given when CloudLAPS was initially deployed.') 5 | param PortalWebAppName string 6 | @description('Provide the name of the existing Log Analytics workspace that was given when CloudLAPS was initially deployed.') 7 | param LogAnalyticsWorkspaceName string 8 | @description('Provide the name of the existing Key Vault that was given when CloudLAPS was initially deployed.') 9 | param KeyVaultName string 10 | @description('Provide the name of the existing Storage Account that was automatically given when CloudLAPS was initially deployed.') 11 | param StorageAccountName string 12 | @description('Provide the App registration application identifier that was created when CloudLAPS was initially deployed.') 13 | param ApplicationID string 14 | @description('Provide the number of days when password rotation is allowed. Default is 3.') 15 | param UpdateFrequencyDays string = '3' 16 | @description('Provide the default length of the generated local admin password. Default is 16.') 17 | param PasswordLength string = '16' 18 | @description('Provide the default character set to be used when generating the local admin password. Default is value is ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.:;,-_!?$%*=+&<>@#()23456789.') 19 | param PasswordAllowedCharacters string = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.:;,-_!?$%*=+&<>@#()23456789' 20 | 21 | // Automatically construct variables based on param input 22 | var FunctionAppInsightsName = '${FunctionAppName}-fa-ai' 23 | var PortalAppInsightsName = '${FunctionAppName}-wa-ai' 24 | var KeyVaultAppSettingsName = '${take(KeyVaultName, 21)}-as' 25 | 26 | // Define existing resources based on param input 27 | resource FunctionApp 'Microsoft.Web/sites@2022-03-01' existing = { 28 | name: FunctionAppName 29 | } 30 | resource PortalAppService 'Microsoft.Web/sites@2022-03-01' existing = { 31 | name: PortalWebAppName 32 | } 33 | resource LogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 34 | name: LogAnalyticsWorkspaceName 35 | } 36 | resource KeyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { 37 | name: KeyVaultName 38 | } 39 | resource StorageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' existing = { 40 | name: StorageAccountName 41 | } 42 | resource FunctionAppInsightsComponents 'Microsoft.Insights/components@2020-02-02' existing = { 43 | name: FunctionAppInsightsName 44 | } 45 | resource PortalAppInsightsComponents 'Microsoft.Insights/components@2020-02-02' existing = { 46 | name: PortalAppInsightsName 47 | } 48 | 49 | // Collect Log Analytics workspace properties to be added to Key Vault as secrets 50 | var LogAnalyticsWorkspaceId = LogAnalyticsWorkspace.properties.customerId 51 | var LogAnalyticsWorkspaceSharedKey = LogAnalyticsWorkspace.listKeys().primarySharedKey 52 | 53 | // Remove trailing forward slash from Key Vault uri property 54 | var KeyVaultUri = KeyVault.properties.vaultUri 55 | var KeyVaultUriNoSlash = substring(KeyVaultUri, 0, length(KeyVaultUri)-1) 56 | 57 | // Create Key Vault for Function App application settings 58 | resource KeyVaultAppSettings 'Microsoft.KeyVault/vaults@2022-07-01' = { 59 | name: KeyVaultAppSettingsName 60 | location: resourceGroup().location 61 | properties: { 62 | enabledForDeployment: false 63 | enabledForTemplateDeployment: false 64 | enabledForDiskEncryption: false 65 | tenantId: subscription().tenantId 66 | accessPolicies: [ 67 | { 68 | tenantId: FunctionApp.identity.tenantId 69 | objectId: FunctionApp.identity.principalId 70 | permissions: { 71 | secrets: [ 72 | 'get' 73 | ] 74 | } 75 | } 76 | { 77 | tenantId: PortalAppService.identity.tenantId 78 | objectId: PortalAppService.identity.principalId 79 | permissions: { 80 | secrets: [ 81 | 'get' 82 | ] 83 | } 84 | } 85 | ] 86 | sku: { 87 | name: 'standard' 88 | family: 'A' 89 | } 90 | } 91 | } 92 | 93 | // Construct secrets in Key Vault 94 | resource WorkspaceIdSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { 95 | name: '${KeyVaultAppSettingsName}/LogAnalyticsWorkspaceId' 96 | properties: { 97 | value: LogAnalyticsWorkspaceId 98 | } 99 | dependsOn: [ 100 | KeyVaultAppSettings 101 | ] 102 | } 103 | resource SharedKeySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { 104 | name: '${KeyVaultAppSettingsName}/LogAnalyticsWorkspaceSharedKey' 105 | properties: { 106 | value: LogAnalyticsWorkspaceSharedKey 107 | } 108 | dependsOn: [ 109 | KeyVaultAppSettings 110 | ] 111 | } 112 | 113 | // Construct appSettings resource for Function App and ensure default values including new ones are added 114 | resource FunctionAppSettings 'Microsoft.Web/sites/config@2022-03-01' = { 115 | name: '${FunctionApp.name}/appsettings' 116 | properties: { 117 | AzureWebJobsDashboard: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 118 | AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 119 | WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 120 | WEBSITE_CONTENTSHARE: toLower('CloudLAPS') 121 | WEBSITE_RUN_FROM_PACKAGE: '1' 122 | AzureWebJobsDisableHomepage: 'true' 123 | FUNCTIONS_EXTENSION_VERSION: '~3' 124 | FUNCTIONS_WORKER_PROCESS_COUNT: '3' 125 | PSWorkerInProcConcurrencyUpperBound: '10' 126 | APPINSIGHTS_INSTRUMENTATIONKEY: reference(FunctionAppInsightsComponents.id, '2020-02-02-preview').InstrumentationKey 127 | APPLICATIONINSIGHTS_CONNECTION_STRING: reference(FunctionAppInsightsComponents.id, '2020-02-02-preview').ConnectionString 128 | FUNCTIONS_WORKER_RUNTIME: 'powershell' 129 | UpdateFrequencyDays: UpdateFrequencyDays 130 | KeyVaultName: KeyVaultName 131 | DebugLogging: 'False' 132 | PasswordLength: PasswordLength 133 | PasswordAllowedCharacters: PasswordAllowedCharacters 134 | LogAnalyticsWorkspaceId: '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceId)' 135 | LogAnalyticsWorkspaceSharedKey: '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceSharedKey)' 136 | LogTypeClient: 'CloudLAPSClient' 137 | } 138 | dependsOn: [ 139 | FunctionAppZipDeploy 140 | ] 141 | } 142 | 143 | // Construct appSettings resource for CloudLAPS Portal and ensure default values including new ones are added 144 | resource PortalAppServiceAppSettings 'Microsoft.Web/sites/config@2022-03-01' = { 145 | name: '${PortalAppService.name}/appsettings' 146 | properties: { 147 | AzureWebJobsSecretStorageKeyVaultName: KeyVault.name 148 | APPLICATIONINSIGHTS_CONNECTION_STRING: reference(PortalAppInsightsComponents.id, '2020-02-02-preview').ConnectionString 149 | APPINSIGHTS_INSTRUMENTATIONKEY: reference(PortalAppInsightsComponents.id, '2020-02-02-preview').InstrumentationKey 150 | 'AzureAd:TenantId': subscription().tenantId 151 | 'AzureAd:ClientId': ApplicationID 152 | 'KeyVault:Uri': KeyVaultUriNoSlash 153 | 'LogAnalytics:WorkspaceId': '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceId)' 154 | 'LogAnalytics:SharedKey': '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceSharedKey)' 155 | 'LogAnalytics:LogType': 'CloudLAPSAudit' 156 | } 157 | dependsOn: [ 158 | PortalZipDeploy 159 | ] 160 | } 161 | 162 | // Add ZipDeploy for Function App 163 | resource FunctionAppZipDeploy 'Microsoft.Web/sites/extensions@2015-08-01' = { 164 | parent: FunctionApp 165 | name: 'ZipDeploy' 166 | properties: { 167 | packageUri: 'https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.2.0/CloudLAPS-FunctionApp1.2.0.zip' 168 | } 169 | } 170 | 171 | // Add ZipDeploy for CloudLAPS Portal 172 | resource PortalZipDeploy 'Microsoft.Web/sites/extensions@2015-08-01' = { 173 | parent: PortalAppService 174 | name: 'ZipDeploy' 175 | properties: { 176 | packageUri: 'https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.1.0/CloudLAPS-Portal1.1.0.zip' 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Deploy/Update/prd-update-function.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.11.1.770", 8 | "templateHash": "6986323175370347536" 9 | } 10 | }, 11 | "parameters": { 12 | "FunctionAppName": { 13 | "type": "string", 14 | "metadata": { 15 | "description": "Provide the name of the existing Function App that was given when CloudLAPS was initially deployed." 16 | } 17 | }, 18 | "PortalWebAppName": { 19 | "type": "string", 20 | "metadata": { 21 | "description": "Provide the name of the existing portal Web App that was given when CloudLAPS was initially deployed." 22 | } 23 | }, 24 | "LogAnalyticsWorkspaceName": { 25 | "type": "string", 26 | "metadata": { 27 | "description": "Provide the name of the existing Log Analytics workspace that was given when CloudLAPS was initially deployed." 28 | } 29 | }, 30 | "KeyVaultName": { 31 | "type": "string", 32 | "metadata": { 33 | "description": "Provide the name of the existing Key Vault that was given when CloudLAPS was initially deployed." 34 | } 35 | }, 36 | "StorageAccountName": { 37 | "type": "string", 38 | "metadata": { 39 | "description": "Provide the name of the existing Storage Account that was automatically given when CloudLAPS was initially deployed." 40 | } 41 | }, 42 | "ApplicationID": { 43 | "type": "string", 44 | "metadata": { 45 | "description": "Provide the App registration application identifier that was created when CloudLAPS was initially deployed." 46 | } 47 | }, 48 | "UpdateFrequencyDays": { 49 | "type": "string", 50 | "defaultValue": "3", 51 | "metadata": { 52 | "description": "Provide the number of days when password rotation is allowed. Default is 3." 53 | } 54 | }, 55 | "PasswordLength": { 56 | "type": "string", 57 | "defaultValue": "16", 58 | "metadata": { 59 | "description": "Provide the default length of the generated local admin password. Default is 16." 60 | } 61 | }, 62 | "PasswordAllowedCharacters": { 63 | "type": "string", 64 | "defaultValue": "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.:;,-_!?$%*=+&<>@#()23456789", 65 | "metadata": { 66 | "description": "Provide the default character set to be used when generating the local admin password. Default is value is ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.:;,-_!?$%*=+&<>@#()23456789." 67 | } 68 | } 69 | }, 70 | "variables": { 71 | "FunctionAppInsightsName": "[format('{0}-fa-ai', parameters('FunctionAppName'))]", 72 | "PortalAppInsightsName": "[format('{0}-wa-ai', parameters('FunctionAppName'))]", 73 | "KeyVaultAppSettingsName": "[format('{0}-as', take(parameters('KeyVaultName'), 21))]" 74 | }, 75 | "resources": [ 76 | { 77 | "type": "Microsoft.KeyVault/vaults", 78 | "apiVersion": "2022-07-01", 79 | "name": "[variables('KeyVaultAppSettingsName')]", 80 | "location": "[resourceGroup().location]", 81 | "properties": { 82 | "enabledForDeployment": false, 83 | "enabledForTemplateDeployment": false, 84 | "enabledForDiskEncryption": false, 85 | "tenantId": "[subscription().tenantId]", 86 | "accessPolicies": [ 87 | { 88 | "tenantId": "[reference(resourceId('Microsoft.Web/sites', parameters('FunctionAppName')), '2022-03-01', 'full').identity.tenantId]", 89 | "objectId": "[reference(resourceId('Microsoft.Web/sites', parameters('FunctionAppName')), '2022-03-01', 'full').identity.principalId]", 90 | "permissions": { 91 | "secrets": [ 92 | "get" 93 | ] 94 | } 95 | }, 96 | { 97 | "tenantId": "[reference(resourceId('Microsoft.Web/sites', parameters('PortalWebAppName')), '2022-03-01', 'full').identity.tenantId]", 98 | "objectId": "[reference(resourceId('Microsoft.Web/sites', parameters('PortalWebAppName')), '2022-03-01', 'full').identity.principalId]", 99 | "permissions": { 100 | "secrets": [ 101 | "get" 102 | ] 103 | } 104 | } 105 | ], 106 | "sku": { 107 | "name": "standard", 108 | "family": "A" 109 | } 110 | } 111 | }, 112 | { 113 | "type": "Microsoft.KeyVault/vaults/secrets", 114 | "apiVersion": "2022-07-01", 115 | "name": "[format('{0}/LogAnalyticsWorkspaceId', variables('KeyVaultAppSettingsName'))]", 116 | "properties": { 117 | "value": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', parameters('LogAnalyticsWorkspaceName')), '2022-10-01').customerId]" 118 | }, 119 | "dependsOn": [ 120 | "[resourceId('Microsoft.KeyVault/vaults', variables('KeyVaultAppSettingsName'))]" 121 | ] 122 | }, 123 | { 124 | "type": "Microsoft.KeyVault/vaults/secrets", 125 | "apiVersion": "2022-07-01", 126 | "name": "[format('{0}/LogAnalyticsWorkspaceSharedKey', variables('KeyVaultAppSettingsName'))]", 127 | "properties": { 128 | "value": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', parameters('LogAnalyticsWorkspaceName')), '2022-10-01').primarySharedKey]" 129 | }, 130 | "dependsOn": [ 131 | "[resourceId('Microsoft.KeyVault/vaults', variables('KeyVaultAppSettingsName'))]" 132 | ] 133 | }, 134 | { 135 | "type": "Microsoft.Web/sites/config", 136 | "apiVersion": "2022-03-01", 137 | "name": "[format('{0}/appsettings', parameters('FunctionAppName'))]", 138 | "properties": { 139 | "AzureWebJobsDashboard": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', parameters('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('StorageAccountName')), '2022-05-01').keys[0].value)]", 140 | "AzureWebJobsStorage": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', parameters('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('StorageAccountName')), '2022-05-01').keys[0].value)]", 141 | "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', parameters('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('StorageAccountName')), '2022-05-01').keys[0].value)]", 142 | "WEBSITE_CONTENTSHARE": "[toLower('CloudLAPS')]", 143 | "WEBSITE_RUN_FROM_PACKAGE": "1", 144 | "AzureWebJobsDisableHomepage": "true", 145 | "FUNCTIONS_EXTENSION_VERSION": "~3", 146 | "FUNCTIONS_WORKER_PROCESS_COUNT": "3", 147 | "PSWorkerInProcConcurrencyUpperBound": "10", 148 | "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName')), '2020-02-02-preview').InstrumentationKey]", 149 | "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName')), '2020-02-02-preview').ConnectionString]", 150 | "FUNCTIONS_WORKER_RUNTIME": "powershell", 151 | "UpdateFrequencyDays": "[parameters('UpdateFrequencyDays')]", 152 | "KeyVaultName": "[parameters('KeyVaultName')]", 153 | "DebugLogging": "False", 154 | "PasswordLength": "[parameters('PasswordLength')]", 155 | "PasswordAllowedCharacters": "[parameters('PasswordAllowedCharacters')]", 156 | "LogAnalyticsWorkspaceId": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceId)', variables('KeyVaultAppSettingsName'))]", 157 | "LogAnalyticsWorkspaceSharedKey": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceSharedKey)', variables('KeyVaultAppSettingsName'))]", 158 | "LogTypeClient": "CloudLAPSClient" 159 | }, 160 | "dependsOn": [ 161 | "[resourceId('Microsoft.Web/sites/extensions', parameters('FunctionAppName'), 'ZipDeploy')]" 162 | ] 163 | }, 164 | { 165 | "type": "Microsoft.Web/sites/config", 166 | "apiVersion": "2022-03-01", 167 | "name": "[format('{0}/appsettings', parameters('PortalWebAppName'))]", 168 | "properties": { 169 | "AzureWebJobsSecretStorageKeyVaultName": "[parameters('KeyVaultName')]", 170 | "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('Microsoft.Insights/components', variables('PortalAppInsightsName')), '2020-02-02-preview').ConnectionString]", 171 | "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.Insights/components', variables('PortalAppInsightsName')), '2020-02-02-preview').InstrumentationKey]", 172 | "AzureAd:TenantId": "[subscription().tenantId]", 173 | "AzureAd:ClientId": "[parameters('ApplicationID')]", 174 | "KeyVault:Uri": "[substring(reference(resourceId('Microsoft.KeyVault/vaults', parameters('KeyVaultName')), '2022-07-01').vaultUri, 0, sub(length(reference(resourceId('Microsoft.KeyVault/vaults', parameters('KeyVaultName')), '2022-07-01').vaultUri), 1))]", 175 | "LogAnalytics:WorkspaceId": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceId)', variables('KeyVaultAppSettingsName'))]", 176 | "LogAnalytics:SharedKey": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceSharedKey)', variables('KeyVaultAppSettingsName'))]", 177 | "LogAnalytics:LogType": "CloudLAPSAudit" 178 | }, 179 | "dependsOn": [ 180 | "[resourceId('Microsoft.Web/sites/extensions', parameters('PortalWebAppName'), 'ZipDeploy')]" 181 | ] 182 | }, 183 | { 184 | "type": "Microsoft.Web/sites/extensions", 185 | "apiVersion": "2015-08-01", 186 | "name": "[format('{0}/{1}', parameters('FunctionAppName'), 'ZipDeploy')]", 187 | "properties": { 188 | "packageUri": "https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.2.0/CloudLAPS-FunctionApp1.2.0.zip" 189 | } 190 | }, 191 | { 192 | "type": "Microsoft.Web/sites/extensions", 193 | "apiVersion": "2015-08-01", 194 | "name": "[format('{0}/{1}', parameters('PortalWebAppName'), 'ZipDeploy')]", 195 | "properties": { 196 | "packageUri": "https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.1.0/CloudLAPS-Portal1.1.0.zip" 197 | } 198 | } 199 | ] 200 | } -------------------------------------------------------------------------------- /Deploy/cloudlaps.bicep: -------------------------------------------------------------------------------- 1 | // Define parameters 2 | @description('Provide the App registration application identifier.') 3 | param ApplicationID string 4 | @description('Provide a name for the Function App that consists of alphanumerics. Name must be globally unique in Azure and cannot start or end with a hyphen.') 5 | param FunctionAppName string 6 | @allowed([ 7 | 'Y1' 8 | 'EP1' 9 | 'EP2' 10 | 'EP3' 11 | ]) 12 | @description('Select the desired App Service Plan of the Function App. Select Y1 for free consumption based deployment.') 13 | param FunctionAppServicePlanSKU string = 'EP1' 14 | @description('Provide a name for the portal website that consists of alphanumerics. Name must be globally unique in Azure and cannot start or end with a hyphen.') 15 | param PortalWebAppName string 16 | @allowed([ 17 | 'B1' 18 | 'P1V2' 19 | 'P1V3' 20 | 'P2V2' 21 | 'P2V3' 22 | 'P3V2' 23 | 'P3V3' 24 | 'S1' 25 | 'S2' 26 | 'S3' 27 | 'P1' 28 | 'P2' 29 | 'P3' 30 | ]) 31 | @description('Select the desired App Service Plan for the portal website. Select B1, SKU for minimum cost. Recommended SKU for optimal performance and cost is S1.') 32 | param PortalAppServicePlanSKU string = 'S1' 33 | @minLength(3) 34 | @maxLength(24) 35 | @description('Provide a name for the Key Vault. Name must be globally unique in Azure and between 3-24 characters, containing only 0-9, a-z, A-Z, and - characters.') 36 | param KeyVaultName string 37 | @description('Provide a name for the Log Analytics workspace.') 38 | param LogAnalyticsWorkspaceName string 39 | @description('Provide any tags required by your organization (optional)') 40 | param Tags object = {} 41 | 42 | // Define variables 43 | var UniqueString = uniqueString(resourceGroup().id) 44 | var FunctionAppNameNoDash = replace(FunctionAppName, '-', '') 45 | var FunctionAppNameNoDashUnderScore = replace(FunctionAppNameNoDash, '_', '') 46 | var PortalWebAppNameNoDash = replace(PortalWebAppName, '-', '') 47 | var StorageAccountName = toLower('${take(FunctionAppNameNoDashUnderScore, 17)}${take(UniqueString, 5)}sa') 48 | var FunctionAppServicePlanName = '${FunctionAppName}-fa-plan' 49 | var PortalAppServicePlanName = toLower('${PortalWebAppName}-wa-plan') 50 | var FunctionAppInsightsName = '${FunctionAppName}-fa-ai' 51 | var PortalAppInsightsName = '${FunctionAppName}-wa-ai' 52 | var KeyVaultAppSettingsName = '${take(KeyVaultName, 21)}-as' 53 | 54 | // Create storage account for Function App 55 | resource StorageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = { 56 | name: StorageAccountName 57 | location: resourceGroup().location 58 | kind: 'StorageV2' 59 | sku: { 60 | name: 'Standard_LRS' 61 | } 62 | properties: { 63 | supportsHttpsTrafficOnly: true 64 | accessTier: 'Hot' 65 | allowBlobPublicAccess: false 66 | minimumTlsVersion: 'TLS1_2' 67 | allowSharedKeyAccess: true 68 | } 69 | tags: Tags 70 | } 71 | 72 | // Create app service plan for Function App 73 | resource FunctionAppServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { 74 | name: FunctionAppServicePlanName 75 | location: resourceGroup().location 76 | kind: 'Windows' 77 | sku: { 78 | name: FunctionAppServicePlanSKU 79 | } 80 | properties: {} 81 | tags: Tags 82 | } 83 | 84 | // Create application insights for Function App 85 | resource FunctionAppInsightsComponents 'Microsoft.Insights/components@2020-02-02' = { 86 | name: FunctionAppInsightsName 87 | location: resourceGroup().location 88 | kind: 'web' 89 | properties: { 90 | Application_Type: 'web' 91 | } 92 | tags: union(Tags, { 93 | 'hidden-link:${resourceId('Microsoft.Web/sites', FunctionAppInsightsName)}': 'Resource' 94 | }) 95 | } 96 | 97 | // Create function app 98 | resource FunctionApp 'Microsoft.Web/sites@2020-12-01' = { 99 | name: FunctionAppName 100 | location: resourceGroup().location 101 | kind: 'functionapp' 102 | identity: { 103 | type: 'SystemAssigned' 104 | } 105 | properties: { 106 | serverFarmId: FunctionAppServicePlan.id 107 | containerSize: 1536 108 | httpsOnly: true 109 | siteConfig: { 110 | ftpsState: 'Disabled' 111 | minTlsVersion: '1.2' 112 | powerShellVersion: '~7' 113 | scmType: 'None' 114 | appSettings: [ 115 | { 116 | name: 'AzureWebJobsDashboard' 117 | value: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 118 | } 119 | { 120 | name: 'AzureWebJobsStorage' 121 | value: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 122 | } 123 | { 124 | name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' 125 | value: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 126 | } 127 | { 128 | name: 'WEBSITE_CONTENTSHARE' 129 | value: toLower('CloudLAPS') 130 | } 131 | { 132 | name: 'WEBSITE_RUN_FROM_PACKAGE' 133 | value: '1' 134 | } 135 | { 136 | name: 'AzureWebJobsDisableHomepage' 137 | value: 'true' 138 | } 139 | { 140 | name: 'FUNCTIONS_EXTENSION_VERSION' 141 | value: '~3' 142 | } 143 | { 144 | name: 'FUNCTIONS_WORKER_PROCESS_COUNT' 145 | value: '3' 146 | } 147 | { 148 | name: 'PSWorkerInProcConcurrencyUpperBound' 149 | value: '10' 150 | } 151 | { 152 | name: 'APPINSIGHTS_INSTRUMENTATIONKEY' 153 | value: reference(FunctionAppInsightsComponents.id, '2020-02-02').InstrumentationKey 154 | } 155 | { 156 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' 157 | value: reference(FunctionAppInsightsComponents.id, '2020-02-02').ConnectionString 158 | } 159 | { 160 | name: 'FUNCTIONS_WORKER_RUNTIME' 161 | value: 'powershell' 162 | } 163 | ] 164 | } 165 | } 166 | tags: Tags 167 | } 168 | 169 | // Create Log Analytics workspace 170 | resource LogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-06-01' = { 171 | name: LogAnalyticsWorkspaceName 172 | location: resourceGroup().location 173 | properties: { 174 | sku: { 175 | name: 'PerGB2018' 176 | } 177 | } 178 | } 179 | 180 | // Create app service plan for CloudLAPS portal 181 | resource AppServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { 182 | name: PortalAppServicePlanName 183 | location: resourceGroup().location 184 | kind: 'Windows' 185 | sku: { 186 | name: PortalAppServicePlanSKU 187 | } 188 | properties: {} 189 | tags: Tags 190 | } 191 | 192 | // Create application insights for CloudLAPS portal 193 | resource PortalAppInsightsComponents 'Microsoft.Insights/components@2020-02-02' = { 194 | name: PortalAppInsightsName 195 | location: resourceGroup().location 196 | kind: 'web' 197 | properties: { 198 | Application_Type: 'web' 199 | } 200 | tags: union(Tags, { 201 | 'hidden-link:${resourceId('Microsoft.Web/sites', PortalWebAppName)}': 'Resource' 202 | }) 203 | } 204 | 205 | // Create app service for CloudLAPS portal 206 | resource PortalAppService 'Microsoft.Web/sites@2022-03-01' = { 207 | name: PortalWebAppNameNoDash 208 | location: resourceGroup().location 209 | identity: { 210 | type: 'SystemAssigned' 211 | } 212 | properties: { 213 | serverFarmId: AppServicePlan.id 214 | siteConfig: { 215 | alwaysOn: true 216 | metadata: [ 217 | { 218 | name: 'CURRENT_STACK' 219 | value: 'dotnetcore' 220 | } 221 | ] 222 | } 223 | } 224 | } 225 | 226 | // Create Key Vault for local admin passwords 227 | resource KeyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { 228 | name: KeyVaultName 229 | location: resourceGroup().location 230 | properties: { 231 | enabledForDeployment: false 232 | enabledForTemplateDeployment: false 233 | enabledForDiskEncryption: false 234 | tenantId: subscription().tenantId 235 | accessPolicies: [ 236 | { 237 | tenantId: FunctionApp.identity.tenantId 238 | objectId: FunctionApp.identity.principalId 239 | permissions: { 240 | secrets: [ 241 | 'get' 242 | 'set' 243 | ] 244 | } 245 | } 246 | { 247 | tenantId: PortalAppService.identity.tenantId 248 | objectId: PortalAppService.identity.principalId 249 | permissions: { 250 | secrets: [ 251 | 'get' 252 | ] 253 | } 254 | } 255 | ] 256 | sku: { 257 | name: 'standard' 258 | family: 'A' 259 | } 260 | } 261 | } 262 | 263 | // Create Key Vault for Function App application settings 264 | resource KeyVaultAppSettings 'Microsoft.KeyVault/vaults@2022-07-01' = { 265 | name: KeyVaultAppSettingsName 266 | location: resourceGroup().location 267 | properties: { 268 | enabledForDeployment: false 269 | enabledForTemplateDeployment: false 270 | enabledForDiskEncryption: false 271 | tenantId: subscription().tenantId 272 | accessPolicies: [ 273 | { 274 | tenantId: FunctionApp.identity.tenantId 275 | objectId: FunctionApp.identity.principalId 276 | permissions: { 277 | secrets: [ 278 | 'get' 279 | ] 280 | } 281 | } 282 | { 283 | tenantId: PortalAppService.identity.tenantId 284 | objectId: PortalAppService.identity.principalId 285 | permissions: { 286 | secrets: [ 287 | 'get' 288 | ] 289 | } 290 | } 291 | ] 292 | sku: { 293 | name: 'standard' 294 | family: 'A' 295 | } 296 | } 297 | } 298 | 299 | // Collect Log Analytics workspace properties to be added to Key Vault as secrets 300 | var LogAnalyticsWorkspaceId = LogAnalyticsWorkspace.properties.customerId 301 | var LogAnalyticsWorkspaceSharedKey = LogAnalyticsWorkspace.listKeys().primarySharedKey 302 | 303 | // Construct secrets in Key Vault 304 | resource WorkspaceIdSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { 305 | name: '${KeyVaultAppSettingsName}/LogAnalyticsWorkspaceId' 306 | properties: { 307 | value: LogAnalyticsWorkspaceId 308 | } 309 | dependsOn: [ 310 | KeyVaultAppSettings 311 | ] 312 | } 313 | resource SharedKeySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { 314 | name: '${KeyVaultAppSettingsName}/LogAnalyticsWorkspaceSharedKey' 315 | properties: { 316 | value: LogAnalyticsWorkspaceSharedKey 317 | } 318 | dependsOn: [ 319 | KeyVaultAppSettings 320 | ] 321 | } 322 | 323 | // Deploy application settings for CloudLAPS Function App 324 | resource FunctionAppSettings 'Microsoft.Web/sites/config@2022-03-01' = { 325 | name: '${FunctionApp.name}/appsettings' 326 | properties: { 327 | AzureWebJobsDashboard: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 328 | AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 329 | WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 330 | WEBSITE_CONTENTSHARE: toLower('CloudLAPS') 331 | WEBSITE_RUN_FROM_PACKAGE: '1' 332 | AzureWebJobsDisableHomepage: 'true' 333 | FUNCTIONS_EXTENSION_VERSION: '~3' 334 | FUNCTIONS_WORKER_PROCESS_COUNT: '3' 335 | PSWorkerInProcConcurrencyUpperBound: '10' 336 | APPINSIGHTS_INSTRUMENTATIONKEY: reference(FunctionAppInsightsComponents.id, '2020-02-02').InstrumentationKey 337 | APPLICATIONINSIGHTS_CONNECTION_STRING: reference(FunctionAppInsightsComponents.id, '2020-02-02').ConnectionString 338 | FUNCTIONS_WORKER_RUNTIME: 'powershell' 339 | UpdateFrequencyDays: '3' 340 | KeyVaultName: KeyVaultName 341 | DebugLogging: 'False' 342 | PasswordLength: '16' 343 | PasswordAllowedCharacters: 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.:;,-_!?$%*=+&<>@#()23456789' 344 | LogAnalyticsWorkspaceId: '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceId)' 345 | LogAnalyticsWorkspaceSharedKey: '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceSharedKey)' 346 | LogTypeClient: 'CloudLAPSClient' 347 | } 348 | dependsOn: [ 349 | FunctionAppZipDeploy 350 | ] 351 | } 352 | 353 | // Deploy application settings for CloudLAPS Portal 354 | resource PortalAppServiceAppSettings 'Microsoft.Web/sites/config@2022-03-01' = { 355 | name: '${PortalAppService.name}/appsettings' 356 | properties: { 357 | AzureWebJobsSecretStorageKeyVaultName: KeyVault.name 358 | APPLICATIONINSIGHTS_CONNECTION_STRING: reference(PortalAppInsightsComponents.id, '2020-02-02').ConnectionString 359 | APPINSIGHTS_INSTRUMENTATIONKEY: reference(PortalAppInsightsComponents.id, '2020-02-02').InstrumentationKey 360 | 'AzureAd:TenantId': subscription().tenantId 361 | 'AzureAd:ClientId': ApplicationID 362 | 'KeyVault:Uri': KeyVault.properties.vaultUri 363 | 'LogAnalytics:WorkspaceId': '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceId)' 364 | 'LogAnalytics:SharedKey': '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceSharedKey)' 365 | 'LogAnalytics:LogType': 'CloudLAPSAudit' 366 | } 367 | dependsOn: [ 368 | PortalZipDeploy 369 | ] 370 | } 371 | 372 | // Add ZipDeploy for Function App 373 | resource FunctionAppZipDeploy 'Microsoft.Web/sites/extensions@2015-08-01' = { 374 | parent: FunctionApp 375 | name: 'ZipDeploy' 376 | properties: { 377 | packageUri: 'https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.1.0/CloudLAPS-FunctionApp1.1.0.zip' 378 | } 379 | } 380 | 381 | // Add ZipDeploy for CloudLAPS Portal 382 | resource PortalZipDeploy 'Microsoft.Web/sites/extensions@2015-08-01' = { 383 | parent: PortalAppService 384 | name: 'ZipDeploy' 385 | properties: { 386 | packageUri: 'https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.1.0/CloudLAPS-Portal1.1.0.zip' 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /Deploy/cloudlaps.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.9.1.41621", 8 | "templateHash": "16334082401312218899" 9 | } 10 | }, 11 | "parameters": { 12 | "ApplicationID": { 13 | "type": "string", 14 | "metadata": { 15 | "description": "Provide the App registration application identifier." 16 | } 17 | }, 18 | "FunctionAppName": { 19 | "type": "string", 20 | "metadata": { 21 | "description": "Provide a name for the Function App that consists of alphanumerics. Name must be globally unique in Azure and cannot start or end with a hyphen." 22 | } 23 | }, 24 | "FunctionAppServicePlanSKU": { 25 | "type": "string", 26 | "defaultValue": "EP1", 27 | "metadata": { 28 | "description": "Select the desired App Service Plan of the Function App. Select Y1 for free consumption based deployment." 29 | }, 30 | "allowedValues": [ 31 | "Y1", 32 | "EP1", 33 | "EP2", 34 | "EP3" 35 | ] 36 | }, 37 | "PortalWebAppName": { 38 | "type": "string", 39 | "metadata": { 40 | "description": "Provide a name for the portal website that consists of alphanumerics. Name must be globally unique in Azure and cannot start or end with a hyphen." 41 | } 42 | }, 43 | "PortalAppServicePlanSKU": { 44 | "type": "string", 45 | "defaultValue": "S1", 46 | "metadata": { 47 | "description": "Select the desired App Service Plan for the portal website. Select B1, SKU for minimum cost. Recommended SKU for optimal performance and cost is S1." 48 | }, 49 | "allowedValues": [ 50 | "B1", 51 | "P1V2", 52 | "P1V3", 53 | "P2V2", 54 | "P2V3", 55 | "P3V2", 56 | "P3V3", 57 | "S1", 58 | "S2", 59 | "S3", 60 | "P1", 61 | "P2", 62 | "P3" 63 | ] 64 | }, 65 | "KeyVaultName": { 66 | "type": "string", 67 | "metadata": { 68 | "description": "Provide a name for the Key Vault. Name must be globally unique in Azure and between 3-24 characters, containing only 0-9, a-z, A-Z, and - characters." 69 | }, 70 | "maxLength": 24, 71 | "minLength": 3 72 | }, 73 | "LogAnalyticsWorkspaceName": { 74 | "type": "string", 75 | "metadata": { 76 | "description": "Provide a name for the Log Analytics workspace." 77 | } 78 | }, 79 | "Tags": { 80 | "type": "object", 81 | "defaultValue": {}, 82 | "metadata": { 83 | "description": "Provide any tags required by your organization (optional)" 84 | } 85 | } 86 | }, 87 | "variables": { 88 | "UniqueString": "[uniqueString(resourceGroup().id)]", 89 | "FunctionAppNameNoDash": "[replace(parameters('FunctionAppName'), '-', '')]", 90 | "FunctionAppNameNoDashUnderScore": "[replace(variables('FunctionAppNameNoDash'), '_', '')]", 91 | "PortalWebAppNameNoDash": "[replace(parameters('PortalWebAppName'), '-', '')]", 92 | "StorageAccountName": "[toLower(format('{0}{1}sa', take(variables('FunctionAppNameNoDashUnderScore'), 17), take(variables('UniqueString'), 5)))]", 93 | "FunctionAppServicePlanName": "[format('{0}-fa-plan', parameters('FunctionAppName'))]", 94 | "PortalAppServicePlanName": "[toLower(format('{0}-wa-plan', parameters('PortalWebAppName')))]", 95 | "FunctionAppInsightsName": "[format('{0}-fa-ai', parameters('FunctionAppName'))]", 96 | "PortalAppInsightsName": "[format('{0}-wa-ai', parameters('FunctionAppName'))]", 97 | "KeyVaultAppSettingsName": "[format('{0}-as', take(parameters('KeyVaultName'), 21))]" 98 | }, 99 | "resources": [ 100 | { 101 | "type": "Microsoft.Storage/storageAccounts", 102 | "apiVersion": "2021-02-01", 103 | "name": "[variables('StorageAccountName')]", 104 | "location": "[resourceGroup().location]", 105 | "kind": "StorageV2", 106 | "sku": { 107 | "name": "Standard_LRS" 108 | }, 109 | "properties": { 110 | "supportsHttpsTrafficOnly": true, 111 | "accessTier": "Hot", 112 | "allowBlobPublicAccess": false, 113 | "minimumTlsVersion": "TLS1_2", 114 | "allowSharedKeyAccess": true 115 | }, 116 | "tags": "[parameters('Tags')]" 117 | }, 118 | { 119 | "type": "Microsoft.Web/serverfarms", 120 | "apiVersion": "2022-03-01", 121 | "name": "[variables('FunctionAppServicePlanName')]", 122 | "location": "[resourceGroup().location]", 123 | "kind": "Windows", 124 | "sku": { 125 | "name": "[parameters('FunctionAppServicePlanSKU')]" 126 | }, 127 | "properties": {}, 128 | "tags": "[parameters('Tags')]" 129 | }, 130 | { 131 | "type": "Microsoft.Insights/components", 132 | "apiVersion": "2020-02-02", 133 | "name": "[variables('FunctionAppInsightsName')]", 134 | "location": "[resourceGroup().location]", 135 | "kind": "web", 136 | "properties": { 137 | "Application_Type": "web" 138 | }, 139 | "tags": "[union(parameters('Tags'), createObject(format('hidden-link:{0}', resourceId('Microsoft.Web/sites', variables('FunctionAppInsightsName'))), 'Resource'))]" 140 | }, 141 | { 142 | "type": "Microsoft.Web/sites", 143 | "apiVersion": "2020-12-01", 144 | "name": "[parameters('FunctionAppName')]", 145 | "location": "[resourceGroup().location]", 146 | "kind": "functionapp", 147 | "identity": { 148 | "type": "SystemAssigned" 149 | }, 150 | "properties": { 151 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('FunctionAppServicePlanName'))]", 152 | "containerSize": 1536, 153 | "httpsOnly": true, 154 | "siteConfig": { 155 | "ftpsState": "Disabled", 156 | "minTlsVersion": "1.2", 157 | "powerShellVersion": "~7", 158 | "scmType": "None", 159 | "appSettings": [ 160 | { 161 | "name": "AzureWebJobsDashboard", 162 | "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), '2021-02-01').keys[0].value)]" 163 | }, 164 | { 165 | "name": "AzureWebJobsStorage", 166 | "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), '2021-02-01').keys[0].value)]" 167 | }, 168 | { 169 | "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", 170 | "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), '2021-02-01').keys[0].value)]" 171 | }, 172 | { 173 | "name": "WEBSITE_CONTENTSHARE", 174 | "value": "[toLower('CloudLAPS')]" 175 | }, 176 | { 177 | "name": "WEBSITE_RUN_FROM_PACKAGE", 178 | "value": "1" 179 | }, 180 | { 181 | "name": "AzureWebJobsDisableHomepage", 182 | "value": "true" 183 | }, 184 | { 185 | "name": "FUNCTIONS_EXTENSION_VERSION", 186 | "value": "~3" 187 | }, 188 | { 189 | "name": "FUNCTIONS_WORKER_PROCESS_COUNT", 190 | "value": "3" 191 | }, 192 | { 193 | "name": "PSWorkerInProcConcurrencyUpperBound", 194 | "value": "10" 195 | }, 196 | { 197 | "name": "APPINSIGHTS_INSTRUMENTATIONKEY", 198 | "value": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName')), '2020-02-02').InstrumentationKey]" 199 | }, 200 | { 201 | "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", 202 | "value": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName')), '2020-02-02').ConnectionString]" 203 | }, 204 | { 205 | "name": "FUNCTIONS_WORKER_RUNTIME", 206 | "value": "powershell" 207 | } 208 | ] 209 | } 210 | }, 211 | "tags": "[parameters('Tags')]", 212 | "dependsOn": [ 213 | "[resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName'))]", 214 | "[resourceId('Microsoft.Web/serverfarms', variables('FunctionAppServicePlanName'))]", 215 | "[resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName'))]" 216 | ] 217 | }, 218 | { 219 | "type": "Microsoft.OperationalInsights/workspaces", 220 | "apiVersion": "2021-06-01", 221 | "name": "[parameters('LogAnalyticsWorkspaceName')]", 222 | "location": "[resourceGroup().location]", 223 | "properties": { 224 | "sku": { 225 | "name": "PerGB2018" 226 | } 227 | } 228 | }, 229 | { 230 | "type": "Microsoft.Web/serverfarms", 231 | "apiVersion": "2022-03-01", 232 | "name": "[variables('PortalAppServicePlanName')]", 233 | "location": "[resourceGroup().location]", 234 | "kind": "Windows", 235 | "sku": { 236 | "name": "[parameters('PortalAppServicePlanSKU')]" 237 | }, 238 | "properties": {}, 239 | "tags": "[parameters('Tags')]" 240 | }, 241 | { 242 | "type": "Microsoft.Insights/components", 243 | "apiVersion": "2020-02-02", 244 | "name": "[variables('PortalAppInsightsName')]", 245 | "location": "[resourceGroup().location]", 246 | "kind": "web", 247 | "properties": { 248 | "Application_Type": "web" 249 | }, 250 | "tags": "[union(parameters('Tags'), createObject(format('hidden-link:{0}', resourceId('Microsoft.Web/sites', parameters('PortalWebAppName'))), 'Resource'))]" 251 | }, 252 | { 253 | "type": "Microsoft.Web/sites", 254 | "apiVersion": "2022-03-01", 255 | "name": "[variables('PortalWebAppNameNoDash')]", 256 | "location": "[resourceGroup().location]", 257 | "identity": { 258 | "type": "SystemAssigned" 259 | }, 260 | "properties": { 261 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('PortalAppServicePlanName'))]", 262 | "siteConfig": { 263 | "alwaysOn": true, 264 | "metadata": [ 265 | { 266 | "name": "CURRENT_STACK", 267 | "value": "dotnetcore" 268 | } 269 | ] 270 | } 271 | }, 272 | "dependsOn": [ 273 | "[resourceId('Microsoft.Web/serverfarms', variables('PortalAppServicePlanName'))]" 274 | ] 275 | }, 276 | { 277 | "type": "Microsoft.KeyVault/vaults", 278 | "apiVersion": "2022-07-01", 279 | "name": "[parameters('KeyVaultName')]", 280 | "location": "[resourceGroup().location]", 281 | "properties": { 282 | "enabledForDeployment": false, 283 | "enabledForTemplateDeployment": false, 284 | "enabledForDiskEncryption": false, 285 | "tenantId": "[subscription().tenantId]", 286 | "accessPolicies": [ 287 | { 288 | "tenantId": "[reference(resourceId('Microsoft.Web/sites', parameters('FunctionAppName')), '2020-12-01', 'full').identity.tenantId]", 289 | "objectId": "[reference(resourceId('Microsoft.Web/sites', parameters('FunctionAppName')), '2020-12-01', 'full').identity.principalId]", 290 | "permissions": { 291 | "secrets": [ 292 | "get", 293 | "set" 294 | ] 295 | } 296 | }, 297 | { 298 | "tenantId": "[reference(resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash')), '2022-03-01', 'full').identity.tenantId]", 299 | "objectId": "[reference(resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash')), '2022-03-01', 'full').identity.principalId]", 300 | "permissions": { 301 | "secrets": [ 302 | "get" 303 | ] 304 | } 305 | } 306 | ], 307 | "sku": { 308 | "name": "standard", 309 | "family": "A" 310 | } 311 | }, 312 | "dependsOn": [ 313 | "[resourceId('Microsoft.Web/sites', parameters('FunctionAppName'))]", 314 | "[resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash'))]" 315 | ] 316 | }, 317 | { 318 | "type": "Microsoft.KeyVault/vaults", 319 | "apiVersion": "2022-07-01", 320 | "name": "[variables('KeyVaultAppSettingsName')]", 321 | "location": "[resourceGroup().location]", 322 | "properties": { 323 | "enabledForDeployment": false, 324 | "enabledForTemplateDeployment": false, 325 | "enabledForDiskEncryption": false, 326 | "tenantId": "[subscription().tenantId]", 327 | "accessPolicies": [ 328 | { 329 | "tenantId": "[reference(resourceId('Microsoft.Web/sites', parameters('FunctionAppName')), '2020-12-01', 'full').identity.tenantId]", 330 | "objectId": "[reference(resourceId('Microsoft.Web/sites', parameters('FunctionAppName')), '2020-12-01', 'full').identity.principalId]", 331 | "permissions": { 332 | "secrets": [ 333 | "get" 334 | ] 335 | } 336 | }, 337 | { 338 | "tenantId": "[reference(resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash')), '2022-03-01', 'full').identity.tenantId]", 339 | "objectId": "[reference(resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash')), '2022-03-01', 'full').identity.principalId]", 340 | "permissions": { 341 | "secrets": [ 342 | "get" 343 | ] 344 | } 345 | } 346 | ], 347 | "sku": { 348 | "name": "standard", 349 | "family": "A" 350 | } 351 | }, 352 | "dependsOn": [ 353 | "[resourceId('Microsoft.Web/sites', parameters('FunctionAppName'))]", 354 | "[resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash'))]" 355 | ] 356 | }, 357 | { 358 | "type": "Microsoft.KeyVault/vaults/secrets", 359 | "apiVersion": "2022-07-01", 360 | "name": "[format('{0}/LogAnalyticsWorkspaceId', variables('KeyVaultAppSettingsName'))]", 361 | "properties": { 362 | "value": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', parameters('LogAnalyticsWorkspaceName'))).customerId]" 363 | }, 364 | "dependsOn": [ 365 | "[resourceId('Microsoft.KeyVault/vaults', variables('KeyVaultAppSettingsName'))]", 366 | "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('LogAnalyticsWorkspaceName'))]" 367 | ] 368 | }, 369 | { 370 | "type": "Microsoft.KeyVault/vaults/secrets", 371 | "apiVersion": "2022-07-01", 372 | "name": "[format('{0}/LogAnalyticsWorkspaceSharedKey', variables('KeyVaultAppSettingsName'))]", 373 | "properties": { 374 | "value": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', parameters('LogAnalyticsWorkspaceName')), '2021-06-01').primarySharedKey]" 375 | }, 376 | "dependsOn": [ 377 | "[resourceId('Microsoft.KeyVault/vaults', variables('KeyVaultAppSettingsName'))]", 378 | "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('LogAnalyticsWorkspaceName'))]" 379 | ] 380 | }, 381 | { 382 | "type": "Microsoft.Web/sites/config", 383 | "apiVersion": "2022-03-01", 384 | "name": "[format('{0}/appsettings', parameters('FunctionAppName'))]", 385 | "properties": { 386 | "AzureWebJobsDashboard": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), '2021-02-01').keys[0].value)]", 387 | "AzureWebJobsStorage": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), '2021-02-01').keys[0].value)]", 388 | "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), '2021-02-01').keys[0].value)]", 389 | "WEBSITE_CONTENTSHARE": "[toLower('CloudLAPS')]", 390 | "WEBSITE_RUN_FROM_PACKAGE": "1", 391 | "AzureWebJobsDisableHomepage": "true", 392 | "FUNCTIONS_EXTENSION_VERSION": "~3", 393 | "FUNCTIONS_WORKER_PROCESS_COUNT": "3", 394 | "PSWorkerInProcConcurrencyUpperBound": "10", 395 | "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName')), '2020-02-02').InstrumentationKey]", 396 | "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName')), '2020-02-02').ConnectionString]", 397 | "FUNCTIONS_WORKER_RUNTIME": "powershell", 398 | "UpdateFrequencyDays": "3", 399 | "KeyVaultName": "[parameters('KeyVaultName')]", 400 | "DebugLogging": "False", 401 | "PasswordLength": "16", 402 | "PasswordAllowedCharacters": "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.:;,-_!?$%*=+&<>@#()23456789", 403 | "LogAnalyticsWorkspaceId": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceId)', variables('KeyVaultAppSettingsName'))]", 404 | "LogAnalyticsWorkspaceSharedKey": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceSharedKey)', variables('KeyVaultAppSettingsName'))]", 405 | "LogTypeClient": "CloudLAPSClient" 406 | }, 407 | "dependsOn": [ 408 | "[resourceId('Microsoft.Web/sites', parameters('FunctionAppName'))]", 409 | "[resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName'))]", 410 | "[resourceId('Microsoft.Web/sites/extensions', parameters('FunctionAppName'), 'ZipDeploy')]", 411 | "[resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName'))]" 412 | ] 413 | }, 414 | { 415 | "type": "Microsoft.Web/sites/config", 416 | "apiVersion": "2022-03-01", 417 | "name": "[format('{0}/appsettings', variables('PortalWebAppNameNoDash'))]", 418 | "properties": { 419 | "AzureWebJobsSecretStorageKeyVaultName": "[parameters('KeyVaultName')]", 420 | "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('Microsoft.Insights/components', variables('PortalAppInsightsName')), '2020-02-02').ConnectionString]", 421 | "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.Insights/components', variables('PortalAppInsightsName')), '2020-02-02').InstrumentationKey]", 422 | "AzureAd:TenantId": "[subscription().tenantId]", 423 | "AzureAd:ClientId": "[parameters('ApplicationID')]", 424 | "KeyVault:Uri": "[reference(resourceId('Microsoft.KeyVault/vaults', parameters('KeyVaultName'))).vaultUri]", 425 | "LogAnalytics:WorkspaceId": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceId)', variables('KeyVaultAppSettingsName'))]", 426 | "LogAnalytics:SharedKey": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceSharedKey)', variables('KeyVaultAppSettingsName'))]", 427 | "LogAnalytics:LogType": "CloudLAPSAudit" 428 | }, 429 | "dependsOn": [ 430 | "[resourceId('Microsoft.KeyVault/vaults', parameters('KeyVaultName'))]", 431 | "[resourceId('Microsoft.Insights/components', variables('PortalAppInsightsName'))]", 432 | "[resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash'))]", 433 | "[resourceId('Microsoft.Web/sites/extensions', variables('PortalWebAppNameNoDash'), 'ZipDeploy')]" 434 | ] 435 | }, 436 | { 437 | "type": "Microsoft.Web/sites/extensions", 438 | "apiVersion": "2015-08-01", 439 | "name": "[format('{0}/{1}', parameters('FunctionAppName'), 'ZipDeploy')]", 440 | "properties": { 441 | "packageUri": "https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.1.0/CloudLAPS-FunctionApp1.1.0.zip" 442 | }, 443 | "dependsOn": [ 444 | "[resourceId('Microsoft.Web/sites', parameters('FunctionAppName'))]" 445 | ] 446 | }, 447 | { 448 | "type": "Microsoft.Web/sites/extensions", 449 | "apiVersion": "2015-08-01", 450 | "name": "[format('{0}/{1}', variables('PortalWebAppNameNoDash'), 'ZipDeploy')]", 451 | "properties": { 452 | "packageUri": "https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.1.0/CloudLAPS-Portal1.1.0.zip" 453 | }, 454 | "dependsOn": [ 455 | "[resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash'))]" 456 | ] 457 | } 458 | ] 459 | } -------------------------------------------------------------------------------- /Deploy/dev-cloudlaps.bicep: -------------------------------------------------------------------------------- 1 | // Define parameters 2 | @description('Provide the App registration application identifier.') 3 | param ApplicationID string 4 | @description('Provide a name for the Function App that consists of alphanumerics. Name must be globally unique in Azure and cannot start or end with a hyphen.') 5 | param FunctionAppName string 6 | @allowed([ 7 | 'Y1' 8 | 'EP1' 9 | 'EP2' 10 | 'EP3' 11 | ]) 12 | @description('Select the desired App Service Plan of the Function App. Select Y1 for free consumption based deployment.') 13 | param FunctionAppServicePlanSKU string = 'EP1' 14 | @description('Provide a name for the portal website that consists of alphanumerics. Name must be globally unique in Azure and cannot start or end with a hyphen.') 15 | param PortalWebAppName string 16 | @allowed([ 17 | 'B1' 18 | 'P1V2' 19 | 'P1V3' 20 | 'P2V2' 21 | 'P2V3' 22 | 'P3V2' 23 | 'P3V3' 24 | 'S1' 25 | 'S2' 26 | 'S3' 27 | 'P1' 28 | 'P2' 29 | 'P3' 30 | ]) 31 | @description('Select the desired App Service Plan for the portal website. Select B1, SKU for minimum cost. Recommended SKU for optimal performance and cost is S1.') 32 | param PortalAppServicePlanSKU string = 'S1' 33 | @minLength(3) 34 | @maxLength(24) 35 | @description('Provide a name for the Key Vault. Name must be globally unique in Azure and between 3-24 characters, containing only 0-9, a-z, A-Z, and - characters.') 36 | param KeyVaultName string 37 | @description('Provide a name for the Log Analytics workspace.') 38 | param LogAnalyticsWorkspaceName string 39 | @description('Provide any tags required by your organization (optional)') 40 | param Tags object = {} 41 | 42 | // Define variables 43 | var UniqueString = uniqueString(resourceGroup().id) 44 | var FunctionAppNameNoDash = replace(FunctionAppName, '-', '') 45 | var FunctionAppNameNoDashUnderScore = replace(FunctionAppNameNoDash, '_', '') 46 | var PortalWebAppNameNoDash = replace(PortalWebAppName, '-', '') 47 | var StorageAccountName = toLower('${take(FunctionAppNameNoDashUnderScore, 17)}${take(UniqueString, 5)}sa') 48 | var FunctionAppServicePlanName = '${FunctionAppName}-fa-plan' 49 | var PortalAppServicePlanName = toLower('${PortalWebAppName}-wa-plan') 50 | var FunctionAppInsightsName = '${FunctionAppName}-fa-ai' 51 | var PortalAppInsightsName = '${FunctionAppName}-wa-ai' 52 | var KeyVaultAppSettingsName = '${take(KeyVaultName, 21)}-as' 53 | 54 | // Create storage account for Function App 55 | resource StorageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = { 56 | name: StorageAccountName 57 | location: resourceGroup().location 58 | kind: 'StorageV2' 59 | sku: { 60 | name: 'Standard_LRS' 61 | } 62 | properties: { 63 | supportsHttpsTrafficOnly: true 64 | accessTier: 'Hot' 65 | allowBlobPublicAccess: false 66 | minimumTlsVersion: 'TLS1_2' 67 | allowSharedKeyAccess: true 68 | } 69 | tags: Tags 70 | } 71 | 72 | // Create app service plan for Function App 73 | resource FunctionAppServicePlan 'Microsoft.Web/serverfarms@2021-01-15' = { 74 | name: FunctionAppServicePlanName 75 | location: resourceGroup().location 76 | kind: 'Windows' 77 | sku: { 78 | name: FunctionAppServicePlanSKU 79 | } 80 | tags: Tags 81 | } 82 | 83 | // Create application insights for Function App 84 | resource FunctionAppInsightsComponents 'Microsoft.Insights/components@2020-02-02-preview' = { 85 | name: FunctionAppInsightsName 86 | location: resourceGroup().location 87 | kind: 'web' 88 | properties: { 89 | Application_Type: 'web' 90 | } 91 | tags: union(Tags, { 92 | 'hidden-link:${resourceId('Microsoft.Web/sites', FunctionAppInsightsName)}': 'Resource' 93 | }) 94 | } 95 | 96 | // Create function app 97 | resource FunctionApp 'Microsoft.Web/sites@2020-12-01' = { 98 | name: FunctionAppName 99 | location: resourceGroup().location 100 | kind: 'functionapp' 101 | identity: { 102 | type: 'SystemAssigned' 103 | } 104 | properties: { 105 | serverFarmId: FunctionAppServicePlan.id 106 | containerSize: 1536 107 | httpsOnly: true 108 | siteConfig: { 109 | ftpsState: 'Disabled' 110 | minTlsVersion: '1.2' 111 | powerShellVersion: '~7' 112 | scmType: 'None' 113 | appSettings: [ 114 | { 115 | name: 'AzureWebJobsDashboard' 116 | value: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 117 | } 118 | { 119 | name: 'AzureWebJobsStorage' 120 | value: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 121 | } 122 | { 123 | name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' 124 | value: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 125 | } 126 | { 127 | name: 'WEBSITE_CONTENTSHARE' 128 | value: toLower('CloudLAPS') 129 | } 130 | { 131 | name: 'WEBSITE_RUN_FROM_PACKAGE' 132 | value: '1' 133 | } 134 | { 135 | name: 'AzureWebJobsDisableHomepage' 136 | value: 'true' 137 | } 138 | { 139 | name: 'FUNCTIONS_EXTENSION_VERSION' 140 | value: '~3' 141 | } 142 | { 143 | name: 'FUNCTIONS_WORKER_PROCESS_COUNT' 144 | value: '3' 145 | } 146 | { 147 | name: 'PSWorkerInProcConcurrencyUpperBound' 148 | value: '10' 149 | } 150 | { 151 | name: 'APPINSIGHTS_INSTRUMENTATIONKEY' 152 | value: reference(FunctionAppInsightsComponents.id, '2020-02-02-preview').InstrumentationKey 153 | } 154 | { 155 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' 156 | value: reference(FunctionAppInsightsComponents.id, '2020-02-02-preview').ConnectionString 157 | } 158 | { 159 | name: 'FUNCTIONS_WORKER_RUNTIME' 160 | value: 'powershell' 161 | } 162 | ] 163 | } 164 | } 165 | tags: Tags 166 | } 167 | 168 | // Create Log Analytics workspace 169 | resource LogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-10-01' = { 170 | name: LogAnalyticsWorkspaceName 171 | location: resourceGroup().location 172 | properties: { 173 | sku: { 174 | name: 'PerGB2018' 175 | } 176 | } 177 | } 178 | 179 | // Create app service plan for CloudLAPS portal 180 | resource AppServicePlan 'Microsoft.Web/serverfarms@2021-01-15' = { 181 | name: PortalAppServicePlanName 182 | location: resourceGroup().location 183 | kind: 'Windows' 184 | sku: { 185 | name: PortalAppServicePlanSKU 186 | } 187 | tags: Tags 188 | } 189 | 190 | // Create application insights for CloudLAPS portal 191 | resource PortalAppInsightsComponents 'Microsoft.Insights/components@2020-02-02-preview' = { 192 | name: PortalAppInsightsName 193 | location: resourceGroup().location 194 | kind: 'web' 195 | properties: { 196 | Application_Type: 'web' 197 | } 198 | tags: union(Tags, { 199 | 'hidden-link:${resourceId('Microsoft.Web/sites', PortalWebAppName)}': 'Resource' 200 | }) 201 | } 202 | 203 | // Create app service for CloudLAPS portal 204 | resource PortalAppService 'Microsoft.Web/sites@2020-06-01' = { 205 | name: PortalWebAppNameNoDash 206 | location: resourceGroup().location 207 | identity: { 208 | type: 'SystemAssigned' 209 | } 210 | properties: { 211 | serverFarmId: AppServicePlan.id 212 | siteConfig: { 213 | alwaysOn: true 214 | metadata: [ 215 | { 216 | name: 'CURRENT_STACK' 217 | value: 'dotnetcore' 218 | } 219 | ] 220 | } 221 | } 222 | } 223 | 224 | // Create Key Vault for local admin passwords 225 | resource KeyVault 'Microsoft.KeyVault/vaults@2019-09-01' = { 226 | name: KeyVaultName 227 | location: resourceGroup().location 228 | properties: { 229 | enabledForDeployment: false 230 | enabledForTemplateDeployment: false 231 | enabledForDiskEncryption: false 232 | tenantId: subscription().tenantId 233 | accessPolicies: [ 234 | { 235 | tenantId: FunctionApp.identity.tenantId 236 | objectId: FunctionApp.identity.principalId 237 | permissions: { 238 | secrets: [ 239 | 'get' 240 | 'set' 241 | ] 242 | } 243 | } 244 | { 245 | tenantId: PortalAppService.identity.tenantId 246 | objectId: PortalAppService.identity.principalId 247 | permissions: { 248 | secrets: [ 249 | 'get' 250 | ] 251 | } 252 | } 253 | ] 254 | sku: { 255 | name: 'standard' 256 | family: 'A' 257 | } 258 | } 259 | } 260 | 261 | // Create Key Vault for Function App application settings 262 | resource KeyVaultAppSettings 'Microsoft.KeyVault/vaults@2019-09-01' = { 263 | name: KeyVaultAppSettingsName 264 | location: resourceGroup().location 265 | properties: { 266 | enabledForDeployment: false 267 | enabledForTemplateDeployment: false 268 | enabledForDiskEncryption: false 269 | tenantId: subscription().tenantId 270 | accessPolicies: [ 271 | { 272 | tenantId: FunctionApp.identity.tenantId 273 | objectId: FunctionApp.identity.principalId 274 | permissions: { 275 | secrets: [ 276 | 'get' 277 | ] 278 | } 279 | } 280 | { 281 | tenantId: PortalAppService.identity.tenantId 282 | objectId: PortalAppService.identity.principalId 283 | permissions: { 284 | secrets: [ 285 | 'get' 286 | ] 287 | } 288 | } 289 | ] 290 | sku: { 291 | name: 'standard' 292 | family: 'A' 293 | } 294 | } 295 | } 296 | 297 | // Collect Log Analytics workspace properties to be added to Key Vault as secrets 298 | var LogAnalyticsWorkspaceId = LogAnalyticsWorkspace.properties.customerId 299 | var LogAnalyticsWorkspaceSharedKey = LogAnalyticsWorkspace.listKeys().primarySharedKey 300 | 301 | // Construct secrets in Key Vault 302 | resource WorkspaceIdSecret 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = { 303 | name: '${KeyVaultAppSettingsName}/LogAnalyticsWorkspaceId' 304 | properties: { 305 | value: LogAnalyticsWorkspaceId 306 | } 307 | dependsOn: [ 308 | KeyVaultAppSettings 309 | ] 310 | } 311 | resource SharedKeySecret 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = { 312 | name: '${KeyVaultAppSettingsName}/LogAnalyticsWorkspaceSharedKey' 313 | properties: { 314 | value: LogAnalyticsWorkspaceSharedKey 315 | } 316 | dependsOn: [ 317 | KeyVaultAppSettings 318 | ] 319 | } 320 | 321 | // Deploy application settings for CloudLAPS Function App 322 | resource FunctionAppSettings 'Microsoft.Web/sites/config@2020-06-01' = { 323 | name: '${FunctionApp.name}/appsettings' 324 | properties: { 325 | AzureWebJobsDashboard: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 326 | AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 327 | WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccount.listKeys().keys[0].value}' 328 | WEBSITE_CONTENTSHARE: toLower('CloudLAPS') 329 | WEBSITE_RUN_FROM_PACKAGE: '1' 330 | AzureWebJobsDisableHomepage: 'true' 331 | FUNCTIONS_EXTENSION_VERSION: '~3' 332 | FUNCTIONS_WORKER_PROCESS_COUNT: '3' 333 | PSWorkerInProcConcurrencyUpperBound: '10' 334 | APPINSIGHTS_INSTRUMENTATIONKEY: reference(FunctionAppInsightsComponents.id, '2020-02-02-preview').InstrumentationKey 335 | APPLICATIONINSIGHTS_CONNECTION_STRING: reference(FunctionAppInsightsComponents.id, '2020-02-02-preview').ConnectionString 336 | FUNCTIONS_WORKER_RUNTIME: 'powershell' 337 | UpdateFrequencyDays: '3' 338 | KeyVaultName: KeyVaultName 339 | DebugLogging: 'False' 340 | PasswordLength: '16' 341 | PasswordAllowedCharacters: 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.:;,-_!?$%*=+&<>@#()23456789' 342 | LogAnalyticsWorkspaceId: '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceId)' 343 | LogAnalyticsWorkspaceSharedKey: '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceSharedKey)' 344 | LogTypeClient: 'CloudLAPSClient' 345 | } 346 | dependsOn: [ 347 | FunctionAppZipDeploy 348 | ] 349 | } 350 | 351 | // Deploy application settings for CloudLAPS Portal 352 | resource PortalAppServiceAppSettings 'Microsoft.Web/sites/config@2020-06-01' = { 353 | name: '${PortalAppService.name}/appsettings' 354 | properties: { 355 | AzureWebJobsSecretStorageKeyVaultName: KeyVault.name 356 | APPLICATIONINSIGHTS_CONNECTION_STRING: reference(PortalAppInsightsComponents.id, '2020-02-02-preview').ConnectionString 357 | APPINSIGHTS_INSTRUMENTATIONKEY: reference(PortalAppInsightsComponents.id, '2020-02-02-preview').InstrumentationKey 358 | 'AzureAd:TenantId': subscription().tenantId 359 | 'AzureAd:ClientId': ApplicationID 360 | 'KeyVault:Uri': KeyVault.properties.vaultUri 361 | 'LogAnalytics:WorkspaceId': '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceId)' 362 | 'LogAnalytics:SharedKey': '@Microsoft.KeyVault(VaultName=${KeyVaultAppSettingsName};SecretName=LogAnalyticsWorkspaceSharedKey)' 363 | 'LogAnalytics:LogType': 'CloudLAPSAudit' 364 | } 365 | dependsOn: [ 366 | PortalZipDeploy 367 | ] 368 | } 369 | 370 | // Add ZipDeploy for Function App 371 | resource FunctionAppZipDeploy 'Microsoft.Web/sites/extensions@2015-08-01' = { 372 | parent: FunctionApp 373 | name: 'ZipDeploy' 374 | properties: { 375 | packageUri: 'https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.1.0/CloudLAPS-FunctionApp1.1.0.zip' 376 | } 377 | } 378 | 379 | // Add ZipDeploy for CloudLAPS Portal 380 | resource PortalZipDeploy 'Microsoft.Web/sites/extensions@2015-08-01' = { 381 | parent: PortalAppService 382 | name: 'ZipDeploy' 383 | properties: { 384 | packageUri: 'https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.0.0/CloudLAPS-Portal1.0.0.zip' 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /Deploy/dev-cloudlaps.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.4.1124.51302", 8 | "templateHash": "10904857489754353275" 9 | } 10 | }, 11 | "parameters": { 12 | "ApplicationID": { 13 | "type": "string", 14 | "metadata": { 15 | "description": "Provide the App registration application identifier." 16 | } 17 | }, 18 | "FunctionAppName": { 19 | "type": "string", 20 | "metadata": { 21 | "description": "Provide a name for the Function App that consists of alphanumerics. Name must be globally unique in Azure and cannot start or end with a hyphen." 22 | } 23 | }, 24 | "FunctionAppServicePlanSKU": { 25 | "type": "string", 26 | "defaultValue": "EP1", 27 | "metadata": { 28 | "description": "Select the desired App Service Plan of the Function App. Select Y1 for free consumption based deployment." 29 | }, 30 | "allowedValues": [ 31 | "Y1", 32 | "EP1", 33 | "EP2", 34 | "EP3" 35 | ] 36 | }, 37 | "PortalWebAppName": { 38 | "type": "string", 39 | "metadata": { 40 | "description": "Provide a name for the portal website that consists of alphanumerics. Name must be globally unique in Azure and cannot start or end with a hyphen." 41 | } 42 | }, 43 | "PortalAppServicePlanSKU": { 44 | "type": "string", 45 | "defaultValue": "S1", 46 | "metadata": { 47 | "description": "Select the desired App Service Plan for the portal website. Select B1, SKU for minimum cost. Recommended SKU for optimal performance and cost is S1." 48 | }, 49 | "allowedValues": [ 50 | "B1", 51 | "P1V2", 52 | "P1V3", 53 | "P2V2", 54 | "P2V3", 55 | "P3V2", 56 | "P3V3", 57 | "S1", 58 | "S2", 59 | "S3", 60 | "P1", 61 | "P2", 62 | "P3" 63 | ] 64 | }, 65 | "KeyVaultName": { 66 | "type": "string", 67 | "metadata": { 68 | "description": "Provide a name for the Key Vault. Name must be globally unique in Azure and between 3-24 characters, containing only 0-9, a-z, A-Z, and - characters." 69 | }, 70 | "maxLength": 24, 71 | "minLength": 3 72 | }, 73 | "LogAnalyticsWorkspaceName": { 74 | "type": "string", 75 | "metadata": { 76 | "description": "Provide a name for the Log Analytics workspace." 77 | } 78 | }, 79 | "Tags": { 80 | "type": "object", 81 | "defaultValue": {}, 82 | "metadata": { 83 | "description": "Provide any tags required by your organization (optional)" 84 | } 85 | } 86 | }, 87 | "variables": { 88 | "UniqueString": "[uniqueString(resourceGroup().id)]", 89 | "FunctionAppNameNoDash": "[replace(parameters('FunctionAppName'), '-', '')]", 90 | "FunctionAppNameNoDashUnderScore": "[replace(variables('FunctionAppNameNoDash'), '_', '')]", 91 | "PortalWebAppNameNoDash": "[replace(parameters('PortalWebAppName'), '-', '')]", 92 | "StorageAccountName": "[toLower(format('{0}{1}sa', take(variables('FunctionAppNameNoDashUnderScore'), 17), take(variables('UniqueString'), 5)))]", 93 | "FunctionAppServicePlanName": "[format('{0}-fa-plan', parameters('FunctionAppName'))]", 94 | "PortalAppServicePlanName": "[toLower(format('{0}-wa-plan', parameters('PortalWebAppName')))]", 95 | "FunctionAppInsightsName": "[format('{0}-fa-ai', parameters('FunctionAppName'))]", 96 | "PortalAppInsightsName": "[format('{0}-wa-ai', parameters('FunctionAppName'))]", 97 | "KeyVaultAppSettingsName": "[format('{0}-as', take(parameters('KeyVaultName'), 21))]" 98 | }, 99 | "resources": [ 100 | { 101 | "type": "Microsoft.Storage/storageAccounts", 102 | "apiVersion": "2021-02-01", 103 | "name": "[variables('StorageAccountName')]", 104 | "location": "[resourceGroup().location]", 105 | "kind": "StorageV2", 106 | "sku": { 107 | "name": "Standard_LRS" 108 | }, 109 | "properties": { 110 | "supportsHttpsTrafficOnly": true, 111 | "accessTier": "Hot", 112 | "allowBlobPublicAccess": false, 113 | "minimumTlsVersion": "TLS1_2", 114 | "allowSharedKeyAccess": true 115 | }, 116 | "tags": "[parameters('Tags')]" 117 | }, 118 | { 119 | "type": "Microsoft.Web/serverfarms", 120 | "apiVersion": "2021-01-15", 121 | "name": "[variables('FunctionAppServicePlanName')]", 122 | "location": "[resourceGroup().location]", 123 | "kind": "Windows", 124 | "sku": { 125 | "name": "[parameters('FunctionAppServicePlanSKU')]" 126 | }, 127 | "tags": "[parameters('Tags')]" 128 | }, 129 | { 130 | "type": "Microsoft.Insights/components", 131 | "apiVersion": "2020-02-02-preview", 132 | "name": "[variables('FunctionAppInsightsName')]", 133 | "location": "[resourceGroup().location]", 134 | "kind": "web", 135 | "properties": { 136 | "Application_Type": "web" 137 | }, 138 | "tags": "[union(parameters('Tags'), createObject(format('hidden-link:{0}', resourceId('Microsoft.Web/sites', variables('FunctionAppInsightsName'))), 'Resource'))]" 139 | }, 140 | { 141 | "type": "Microsoft.Web/sites", 142 | "apiVersion": "2020-12-01", 143 | "name": "[parameters('FunctionAppName')]", 144 | "location": "[resourceGroup().location]", 145 | "kind": "functionapp", 146 | "identity": { 147 | "type": "SystemAssigned" 148 | }, 149 | "properties": { 150 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('FunctionAppServicePlanName'))]", 151 | "containerSize": 1536, 152 | "httpsOnly": true, 153 | "siteConfig": { 154 | "ftpsState": "Disabled", 155 | "minTlsVersion": "1.2", 156 | "powerShellVersion": "~7", 157 | "scmType": "None", 158 | "appSettings": [ 159 | { 160 | "name": "AzureWebJobsDashboard", 161 | "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), '2021-02-01').keys[0].value)]" 162 | }, 163 | { 164 | "name": "AzureWebJobsStorage", 165 | "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), '2021-02-01').keys[0].value)]" 166 | }, 167 | { 168 | "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", 169 | "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), '2021-02-01').keys[0].value)]" 170 | }, 171 | { 172 | "name": "WEBSITE_CONTENTSHARE", 173 | "value": "[toLower('CloudLAPS')]" 174 | }, 175 | { 176 | "name": "WEBSITE_RUN_FROM_PACKAGE", 177 | "value": "1" 178 | }, 179 | { 180 | "name": "AzureWebJobsDisableHomepage", 181 | "value": "true" 182 | }, 183 | { 184 | "name": "FUNCTIONS_EXTENSION_VERSION", 185 | "value": "~3" 186 | }, 187 | { 188 | "name": "FUNCTIONS_WORKER_PROCESS_COUNT", 189 | "value": "3" 190 | }, 191 | { 192 | "name": "PSWorkerInProcConcurrencyUpperBound", 193 | "value": "10" 194 | }, 195 | { 196 | "name": "APPINSIGHTS_INSTRUMENTATIONKEY", 197 | "value": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName')), '2020-02-02-preview').InstrumentationKey]" 198 | }, 199 | { 200 | "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", 201 | "value": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName')), '2020-02-02-preview').ConnectionString]" 202 | }, 203 | { 204 | "name": "FUNCTIONS_WORKER_RUNTIME", 205 | "value": "powershell" 206 | } 207 | ] 208 | } 209 | }, 210 | "tags": "[parameters('Tags')]", 211 | "dependsOn": [ 212 | "[resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName'))]", 213 | "[resourceId('Microsoft.Web/serverfarms', variables('FunctionAppServicePlanName'))]", 214 | "[resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName'))]" 215 | ] 216 | }, 217 | { 218 | "type": "Microsoft.OperationalInsights/workspaces", 219 | "apiVersion": "2020-10-01", 220 | "name": "[parameters('LogAnalyticsWorkspaceName')]", 221 | "location": "[resourceGroup().location]", 222 | "properties": { 223 | "sku": { 224 | "name": "PerGB2018" 225 | } 226 | } 227 | }, 228 | { 229 | "type": "Microsoft.Web/serverfarms", 230 | "apiVersion": "2021-01-15", 231 | "name": "[variables('PortalAppServicePlanName')]", 232 | "location": "[resourceGroup().location]", 233 | "kind": "Windows", 234 | "sku": { 235 | "name": "[parameters('PortalAppServicePlanSKU')]" 236 | }, 237 | "tags": "[parameters('Tags')]" 238 | }, 239 | { 240 | "type": "Microsoft.Insights/components", 241 | "apiVersion": "2020-02-02-preview", 242 | "name": "[variables('PortalAppInsightsName')]", 243 | "location": "[resourceGroup().location]", 244 | "kind": "web", 245 | "properties": { 246 | "Application_Type": "web" 247 | }, 248 | "tags": "[union(parameters('Tags'), createObject(format('hidden-link:{0}', resourceId('Microsoft.Web/sites', parameters('PortalWebAppName'))), 'Resource'))]" 249 | }, 250 | { 251 | "type": "Microsoft.Web/sites", 252 | "apiVersion": "2020-06-01", 253 | "name": "[variables('PortalWebAppNameNoDash')]", 254 | "location": "[resourceGroup().location]", 255 | "identity": { 256 | "type": "SystemAssigned" 257 | }, 258 | "properties": { 259 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('PortalAppServicePlanName'))]", 260 | "siteConfig": { 261 | "alwaysOn": true, 262 | "metadata": [ 263 | { 264 | "name": "CURRENT_STACK", 265 | "value": "dotnetcore" 266 | } 267 | ] 268 | } 269 | }, 270 | "dependsOn": [ 271 | "[resourceId('Microsoft.Web/serverfarms', variables('PortalAppServicePlanName'))]" 272 | ] 273 | }, 274 | { 275 | "type": "Microsoft.KeyVault/vaults", 276 | "apiVersion": "2019-09-01", 277 | "name": "[parameters('KeyVaultName')]", 278 | "location": "[resourceGroup().location]", 279 | "properties": { 280 | "enabledForDeployment": false, 281 | "enabledForTemplateDeployment": false, 282 | "enabledForDiskEncryption": false, 283 | "tenantId": "[subscription().tenantId]", 284 | "accessPolicies": [ 285 | { 286 | "tenantId": "[reference(resourceId('Microsoft.Web/sites', parameters('FunctionAppName')), '2020-12-01', 'full').identity.tenantId]", 287 | "objectId": "[reference(resourceId('Microsoft.Web/sites', parameters('FunctionAppName')), '2020-12-01', 'full').identity.principalId]", 288 | "permissions": { 289 | "secrets": [ 290 | "get", 291 | "set" 292 | ] 293 | } 294 | }, 295 | { 296 | "tenantId": "[reference(resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash')), '2020-06-01', 'full').identity.tenantId]", 297 | "objectId": "[reference(resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash')), '2020-06-01', 'full').identity.principalId]", 298 | "permissions": { 299 | "secrets": [ 300 | "get" 301 | ] 302 | } 303 | } 304 | ], 305 | "sku": { 306 | "name": "standard", 307 | "family": "A" 308 | } 309 | }, 310 | "dependsOn": [ 311 | "[resourceId('Microsoft.Web/sites', parameters('FunctionAppName'))]", 312 | "[resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash'))]" 313 | ] 314 | }, 315 | { 316 | "type": "Microsoft.KeyVault/vaults", 317 | "apiVersion": "2019-09-01", 318 | "name": "[variables('KeyVaultAppSettingsName')]", 319 | "location": "[resourceGroup().location]", 320 | "properties": { 321 | "enabledForDeployment": false, 322 | "enabledForTemplateDeployment": false, 323 | "enabledForDiskEncryption": false, 324 | "tenantId": "[subscription().tenantId]", 325 | "accessPolicies": [ 326 | { 327 | "tenantId": "[reference(resourceId('Microsoft.Web/sites', parameters('FunctionAppName')), '2020-12-01', 'full').identity.tenantId]", 328 | "objectId": "[reference(resourceId('Microsoft.Web/sites', parameters('FunctionAppName')), '2020-12-01', 'full').identity.principalId]", 329 | "permissions": { 330 | "secrets": [ 331 | "get" 332 | ] 333 | } 334 | }, 335 | { 336 | "tenantId": "[reference(resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash')), '2020-06-01', 'full').identity.tenantId]", 337 | "objectId": "[reference(resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash')), '2020-06-01', 'full').identity.principalId]", 338 | "permissions": { 339 | "secrets": [ 340 | "get" 341 | ] 342 | } 343 | } 344 | ], 345 | "sku": { 346 | "name": "standard", 347 | "family": "A" 348 | } 349 | }, 350 | "dependsOn": [ 351 | "[resourceId('Microsoft.Web/sites', parameters('FunctionAppName'))]", 352 | "[resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash'))]" 353 | ] 354 | }, 355 | { 356 | "type": "Microsoft.KeyVault/vaults/secrets", 357 | "apiVersion": "2019-09-01", 358 | "name": "[format('{0}/LogAnalyticsWorkspaceId', variables('KeyVaultAppSettingsName'))]", 359 | "properties": { 360 | "value": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', parameters('LogAnalyticsWorkspaceName'))).customerId]" 361 | }, 362 | "dependsOn": [ 363 | "[resourceId('Microsoft.KeyVault/vaults', variables('KeyVaultAppSettingsName'))]", 364 | "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('LogAnalyticsWorkspaceName'))]" 365 | ] 366 | }, 367 | { 368 | "type": "Microsoft.KeyVault/vaults/secrets", 369 | "apiVersion": "2019-09-01", 370 | "name": "[format('{0}/LogAnalyticsWorkspaceSharedKey', variables('KeyVaultAppSettingsName'))]", 371 | "properties": { 372 | "value": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', parameters('LogAnalyticsWorkspaceName')), '2020-10-01').primarySharedKey]" 373 | }, 374 | "dependsOn": [ 375 | "[resourceId('Microsoft.KeyVault/vaults', variables('KeyVaultAppSettingsName'))]", 376 | "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('LogAnalyticsWorkspaceName'))]" 377 | ] 378 | }, 379 | { 380 | "type": "Microsoft.Web/sites/config", 381 | "apiVersion": "2020-06-01", 382 | "name": "[format('{0}/appsettings', parameters('FunctionAppName'))]", 383 | "properties": { 384 | "AzureWebJobsDashboard": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), '2021-02-01').keys[0].value)]", 385 | "AzureWebJobsStorage": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), '2021-02-01').keys[0].value)]", 386 | "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', variables('StorageAccountName'), listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), '2021-02-01').keys[0].value)]", 387 | "WEBSITE_CONTENTSHARE": "[toLower('CloudLAPS')]", 388 | "WEBSITE_RUN_FROM_PACKAGE": "1", 389 | "AzureWebJobsDisableHomepage": "true", 390 | "FUNCTIONS_EXTENSION_VERSION": "~3", 391 | "FUNCTIONS_WORKER_PROCESS_COUNT": "3", 392 | "PSWorkerInProcConcurrencyUpperBound": "10", 393 | "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName')), '2020-02-02-preview').InstrumentationKey]", 394 | "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName')), '2020-02-02-preview').ConnectionString]", 395 | "FUNCTIONS_WORKER_RUNTIME": "powershell", 396 | "UpdateFrequencyDays": "3", 397 | "KeyVaultName": "[parameters('KeyVaultName')]", 398 | "DebugLogging": "False", 399 | "PasswordLength": "16", 400 | "PasswordAllowedCharacters": "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz.:;,-_!?$%*=+&<>@#()23456789", 401 | "LogAnalyticsWorkspaceId": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceId)', variables('KeyVaultAppSettingsName'))]", 402 | "LogAnalyticsWorkspaceSharedKey": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceSharedKey)', variables('KeyVaultAppSettingsName'))]", 403 | "LogTypeClient": "CloudLAPSClient" 404 | }, 405 | "dependsOn": [ 406 | "[resourceId('Microsoft.Web/sites', parameters('FunctionAppName'))]", 407 | "[resourceId('Microsoft.Insights/components', variables('FunctionAppInsightsName'))]", 408 | "[resourceId('Microsoft.Web/sites/extensions', parameters('FunctionAppName'), 'ZipDeploy')]", 409 | "[resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName'))]" 410 | ] 411 | }, 412 | { 413 | "type": "Microsoft.Web/sites/config", 414 | "apiVersion": "2020-06-01", 415 | "name": "[format('{0}/appsettings', variables('PortalWebAppNameNoDash'))]", 416 | "properties": { 417 | "AzureWebJobsSecretStorageKeyVaultName": "[parameters('KeyVaultName')]", 418 | "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('Microsoft.Insights/components', variables('PortalAppInsightsName')), '2020-02-02-preview').ConnectionString]", 419 | "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.Insights/components', variables('PortalAppInsightsName')), '2020-02-02-preview').InstrumentationKey]", 420 | "AzureAd:TenantId": "[subscription().tenantId]", 421 | "AzureAd:ClientId": "[parameters('ApplicationID')]", 422 | "KeyVault:Uri": "[reference(resourceId('Microsoft.KeyVault/vaults', parameters('KeyVaultName'))).vaultUri]", 423 | "LogAnalytics:WorkspaceId": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceId)', variables('KeyVaultAppSettingsName'))]", 424 | "LogAnalytics:SharedKey": "[format('@Microsoft.KeyVault(VaultName={0};SecretName=LogAnalyticsWorkspaceSharedKey)', variables('KeyVaultAppSettingsName'))]", 425 | "LogAnalytics:LogType": "CloudLAPSAudit" 426 | }, 427 | "dependsOn": [ 428 | "[resourceId('Microsoft.KeyVault/vaults', parameters('KeyVaultName'))]", 429 | "[resourceId('Microsoft.Insights/components', variables('PortalAppInsightsName'))]", 430 | "[resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash'))]", 431 | "[resourceId('Microsoft.Web/sites/extensions', variables('PortalWebAppNameNoDash'), 'ZipDeploy')]" 432 | ] 433 | }, 434 | { 435 | "type": "Microsoft.Web/sites/extensions", 436 | "apiVersion": "2015-08-01", 437 | "name": "[format('{0}/{1}', parameters('FunctionAppName'), 'ZipDeploy')]", 438 | "properties": { 439 | "packageUri": "https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.1.0/CloudLAPS-FunctionApp1.1.0.zip" 440 | }, 441 | "dependsOn": [ 442 | "[resourceId('Microsoft.Web/sites', parameters('FunctionAppName'))]" 443 | ] 444 | }, 445 | { 446 | "type": "Microsoft.Web/sites/extensions", 447 | "apiVersion": "2015-08-01", 448 | "name": "[format('{0}/{1}', variables('PortalWebAppNameNoDash'), 'ZipDeploy')]", 449 | "properties": { 450 | "packageUri": "https://github.com/MSEndpointMgr/CloudLAPS/releases/download/1.0.0/CloudLAPS-Portal1.0.0.zip" 451 | }, 452 | "dependsOn": [ 453 | "[resourceId('Microsoft.Web/sites', variables('PortalWebAppNameNoDash'))]" 454 | ] 455 | } 456 | ] 457 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 MSEndpointMgr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Packages/Dev/CloudLAPS-FunctionApp1.1.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSEndpointMgr/CloudLAPS/43baa6f3c150ec8c0c06e871837cca75855579a8/Packages/Dev/CloudLAPS-FunctionApp1.1.0.zip -------------------------------------------------------------------------------- /Packages/Prod/CloudLAPS-FunctionApp1.0.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSEndpointMgr/CloudLAPS/43baa6f3c150ec8c0c06e871837cca75855579a8/Packages/Prod/CloudLAPS-FunctionApp1.0.0.zip -------------------------------------------------------------------------------- /Packages/Prod/CloudLAPS-Portal1.1.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSEndpointMgr/CloudLAPS/43baa6f3c150ec8c0c06e871837cca75855579a8/Packages/Prod/CloudLAPS-Portal1.1.0.zip -------------------------------------------------------------------------------- /Proactive Remediation/Detection.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Proaction Remediation script for CloudLAPS solution used within Endpoint Analytics with Microsoft Endpoint Manager to rotate a local administrator password. 4 | 5 | .DESCRIPTION 6 | This is the detection script for a Proactive Remediation in Endpoint Analytics used by the CloudLAPS solution. 7 | 8 | It will create an event log named CloudLAPS-Client if it doesn't already exist and ensure the remediation script is always triggered. 9 | 10 | .EXAMPLE 11 | .\Detection.ps1 12 | 13 | .NOTES 14 | FileName: Detection.ps1 15 | Author: Nickolaj Andersen 16 | Contact: @NickolajA 17 | Created: 2020-09-14 18 | Updated: 2020-09-14 19 | 20 | Version history: 21 | 1.0.0 - (2020-09-14) Script created 22 | #> 23 | Process { 24 | # Create new event log if it doesn't already exist 25 | $EventLogName = "CloudLAPS-Client" 26 | $EventLogSource = "CloudLAPS-Client" 27 | $CloudLAPSEventLog = Get-WinEvent -LogName $EventLogName -ErrorAction SilentlyContinue 28 | if ($CloudLAPSEventLog -eq $null) { 29 | try { 30 | New-EventLog -LogName $EventLogName -Source $EventLogSource -ErrorAction Stop 31 | } 32 | catch [System.Exception] { 33 | Write-Warning -Message "Failed to create new event log. Error message: $($_.Exception.Message)" 34 | } 35 | } 36 | 37 | # Trigger remediation script 38 | exit 1 39 | } -------------------------------------------------------------------------------- /Proactive Remediation/Remediate.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Proaction Remediation script for CloudLAPS solution used within Endpoint Analytics with Microsoft Endpoint Manager to rotate a local administrator password. 4 | 5 | .DESCRIPTION 6 | This is the remediation script for a Proactive Remediation in Endpoint Analytics used by the CloudLAPS solution. 7 | 8 | It will create a new local administrator account if it doesn't already exist on the device and call an Azure Function API defined in the 9 | script that will generate a new password, update a Secret in a defined Azure Key Vault and respond back with password to be either set or 10 | updated on the defined local administrator account. 11 | 12 | .NOTES 13 | FileName: Remediate.ps1 14 | Author: Nickolaj Andersen 15 | Contact: @NickolajA 16 | Created: 2020-09-14 17 | Updated: 2022-10-16 18 | 19 | Version history: 20 | 1.0.0 - (2020-09-14) Script created. 21 | 1.0.1 - (2021-10-07) Updated with output for extended details in MEM portal. 22 | 1.0.2 - (2022-01-01) Updated virtual machine array with 'Google Compute Engine'. 23 | 1.1.0 - (2022-01-08) Added support for new SendClientEvent function to send client events related to passwor rotation. 24 | 1.1.1 - (2022-01-27) Added validation check to test if device is either AAD joined or Hybrid Azure AD joined. 25 | 1.1.2 - (2022-09-15) Support for detecting the device registration certificate based on deviceId instead of thumbprint data in JoinInfo key. 26 | 1.2.0 - (2022-10-16) Added support to enforce password rotation of an existing device in CloudLAPS, after it has been re-provisioned. 27 | Also extended the main try and catch with additional HTTP response codes for more detailed error messages. 28 | #> 29 | Process { 30 | # Functions 31 | function Test-AzureADDeviceRegistration { 32 | <# 33 | .SYNOPSIS 34 | Determine if the device conforms to the requirement of being either Azure AD joined or Hybrid Azure AD joined. 35 | 36 | .DESCRIPTION 37 | Determine if the device conforms to the requirement of being either Azure AD joined or Hybrid Azure AD joined. 38 | 39 | .NOTES 40 | Author: Nickolaj Andersen 41 | Contact: @NickolajA 42 | Created: 2022-01-27 43 | Updated: 2022-01-27 44 | 45 | Version history: 46 | 1.0.0 - (2022-01-27) Function created 47 | #> 48 | Process { 49 | $AzureADJoinInfoRegistryKeyPath = "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\JoinInfo" 50 | if (Test-Path -Path $AzureADJoinInfoRegistryKeyPath) { 51 | return $true 52 | } 53 | else { 54 | return $false 55 | } 56 | } 57 | } 58 | 59 | function Get-AzureADDeviceID { 60 | <# 61 | .SYNOPSIS 62 | Get the Azure AD device ID from the local device. 63 | 64 | .DESCRIPTION 65 | Get the Azure AD device ID from the local device. 66 | 67 | .NOTES 68 | Author: Nickolaj Andersen 69 | Contact: @NickolajA 70 | Created: 2021-05-26 71 | Updated: 2021-05-26 72 | 73 | Version history: 74 | 1.0.0 - (2021-05-26) Function created 75 | 1.0.1 - (2022-09-15) Support for detecting the device registration certificate based on deviceId instead of thumbprint data in JoinInfo key 76 | #> 77 | Process { 78 | # Define Cloud Domain Join information registry path 79 | $AzureADJoinInfoRegistryKeyPath = "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\JoinInfo" 80 | 81 | # Retrieve the child key name that is the thumbprint of the machine certificate containing the device identifier guid 82 | $AzureADJoinInfoKey = Get-ChildItem -Path $AzureADJoinInfoRegistryKeyPath | Select-Object -ExpandProperty "PSChildName" 83 | if ($AzureADJoinInfoKey -ne $null) { 84 | # Match key data against GUID regex 85 | if ([guid]::TryParse($AzureADJoinInfoKey, $([ref][guid]::Empty))) { 86 | $AzureADJoinCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Subject -like "CN=$($AzureADJoinInfoKey)" } 87 | } 88 | else { 89 | $AzureADJoinCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Thumbprint -eq $AzureADJoinInfoKey } 90 | } 91 | 92 | # Retrieve the machine certificate based on thumbprint from registry key 93 | if ($AzureADJoinCertificate -ne $null) { 94 | # Determine the device identifier from the subject name 95 | $AzureADDeviceID = ($AzureADJoinCertificate | Select-Object -ExpandProperty "Subject") -replace "CN=", "" 96 | 97 | # Write event log entry with DeviceId 98 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Information -EventId 51 -Message "CloudLAPS: Azure AD device identifier: $($AzureADDeviceID)" 99 | 100 | # Handle return value 101 | return $AzureADDeviceID 102 | } 103 | } 104 | } 105 | } 106 | 107 | function Get-AzureADRegistrationCertificateThumbprint { 108 | <# 109 | .SYNOPSIS 110 | Get the thumbprint of the certificate used for Azure AD device registration. 111 | 112 | .DESCRIPTION 113 | Get the thumbprint of the certificate used for Azure AD device registration. 114 | 115 | .NOTES 116 | Author: Nickolaj Andersen 117 | Contributor: @JankeSkanke 118 | Contact: @NickolajA 119 | Created: 2021-06-03 120 | Updated: 2022-26-10 121 | 122 | Version history: 123 | 1.0.0 - (2021-06-03) Function created 124 | 1.1.0 - (2022-26-10) Added support for finding thumbprint for Cloud PCs @JankeSkanke 125 | #> 126 | Process { 127 | # Define Cloud Domain Join information registry path 128 | $AzureADJoinInfoRegistryKeyPath = "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\JoinInfo" 129 | # Retrieve the child key name that is the thumbprint of the machine certificate containing the device identifier guid 130 | $AzureADJoinInfoKey = Get-ChildItem -Path $AzureADJoinInfoRegistryKeyPath | Select-Object -ExpandProperty "PSChildName" 131 | # Retrieve the machine certificate based on thumbprint from registry key or Certificate (CloudPC) 132 | if ($AzureADJoinInfoKey -ne $null) { 133 | # Match key data against GUID regex for CloudPC Support 134 | if ([guid]::TryParse($AzureADJoinInfoKey, $([ref][guid]::Empty))) { 135 | #This is for CloudPC 136 | $AzureADJoinCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Subject -like "CN=$($AzureADJoinInfoKey)" } 137 | $AzureADJoinInfoThumbprint = $AzureADJoinCertificate.Thumbprint 138 | } 139 | else { 140 | # Retrieve the child key name that is the thumbprint of the machine certificate containing the device identifier guid (non-CloudPC) 141 | $AzureADJoinInfoThumbprint = Get-ChildItem -Path $AzureADJoinInfoRegistryKeyPath | Select-Object -ExpandProperty "PSChildName" 142 | } 143 | } 144 | # Handle return value 145 | return $AzureADJoinInfoThumbprint 146 | } 147 | } 148 | 149 | function New-RSACertificateSignature { 150 | <# 151 | .SYNOPSIS 152 | Creates a new signature based on content passed as parameter input using the private key of a certificate determined by it's thumbprint, to sign the computed hash of the content. 153 | 154 | .DESCRIPTION 155 | Creates a new signature based on content passed as parameter input using the private key of a certificate determined by it's thumbprint, to sign the computed hash of the content. 156 | The certificate used must be available in the LocalMachine\My certificate store, and must also contain a private key. 157 | 158 | .PARAMETER Content 159 | Specify the content string to be signed. 160 | 161 | .PARAMETER Thumbprint 162 | Specify the thumbprint of the certificate. 163 | 164 | .NOTES 165 | Author: Nickolaj Andersen / Thomas Kurth 166 | Contact: @NickolajA 167 | Created: 2021-06-03 168 | Updated: 2021-06-03 169 | 170 | Version history: 171 | 1.0.0 - (2021-06-03) Function created 172 | 173 | Credits to Thomas Kurth for sharing his original C# code. 174 | #> 175 | param( 176 | [parameter(Mandatory = $true, HelpMessage = "Specify the content string to be signed.")] 177 | [ValidateNotNullOrEmpty()] 178 | [string]$Content, 179 | 180 | [parameter(Mandatory = $true, HelpMessage = "Specify the thumbprint of the certificate.")] 181 | [ValidateNotNullOrEmpty()] 182 | [string]$Thumbprint 183 | ) 184 | Process { 185 | # Determine the certificate based on thumbprint input 186 | $Certificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Thumbprint -eq $CertificateThumbprint } 187 | if ($Certificate -ne $null) { 188 | if ($Certificate.HasPrivateKey -eq $true) { 189 | # Read the RSA private key 190 | $RSAPrivateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate) 191 | 192 | if ($RSAPrivateKey -ne $null) { 193 | if ($RSAPrivateKey -is [System.Security.Cryptography.RSACng]) { 194 | # Construct a new SHA256Managed object to be used when computing the hash 195 | $SHA256Managed = New-Object -TypeName "System.Security.Cryptography.SHA256Managed" 196 | 197 | # Construct new UTF8 unicode encoding object 198 | $UnicodeEncoding = [System.Text.UnicodeEncoding]::UTF8 199 | 200 | # Convert content to byte array 201 | [byte[]]$EncodedContentData = $UnicodeEncoding.GetBytes($Content) 202 | 203 | # Compute the hash 204 | [byte[]]$ComputedHash = $SHA256Managed.ComputeHash($EncodedContentData) 205 | 206 | # Create signed signature with computed hash 207 | [byte[]]$SignatureSigned = $RSAPrivateKey.SignHash($ComputedHash, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) 208 | 209 | # Convert signature to Base64 string 210 | $SignatureString = [System.Convert]::ToBase64String($SignatureSigned) 211 | 212 | # Handle return value 213 | return $SignatureString 214 | } 215 | } 216 | } 217 | } 218 | } 219 | } 220 | 221 | function Get-PublicKeyBytesEncodedString { 222 | <# 223 | .SYNOPSIS 224 | Returns the public key byte array encoded as a Base64 string, of the certificate where the thumbprint passed as parameter input is a match. 225 | 226 | .DESCRIPTION 227 | Returns the public key byte array encoded as a Base64 string, of the certificate where the thumbprint passed as parameter input is a match. 228 | The certificate used must be available in the LocalMachine\My certificate store. 229 | 230 | .PARAMETER Thumbprint 231 | Specify the thumbprint of the certificate. 232 | 233 | .NOTES 234 | Author: Nickolaj Andersen / Thomas Kurth 235 | Contact: @NickolajA 236 | Created: 2021-06-07 237 | Updated: 2021-06-07 238 | 239 | Version history: 240 | 1.0.0 - (2021-06-07) Function created 241 | 242 | Credits to Thomas Kurth for sharing his original C# code. 243 | #> 244 | param( 245 | [parameter(Mandatory = $true, HelpMessage = "Specify the thumbprint of the certificate.")] 246 | [ValidateNotNullOrEmpty()] 247 | [string]$Thumbprint 248 | ) 249 | Process { 250 | # Determine the certificate based on thumbprint input 251 | $Certificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Thumbprint -eq $Thumbprint } 252 | if ($Certificate -ne $null) { 253 | # Get the public key bytes 254 | [byte[]]$PublicKeyBytes = $Certificate.GetPublicKey() 255 | 256 | # Handle return value 257 | return [System.Convert]::ToBase64String($PublicKeyBytes) 258 | } 259 | } 260 | } 261 | 262 | function Get-ComputerSystemType { 263 | <# 264 | .SYNOPSIS 265 | Get the computer system type, either VM or NonVM. 266 | 267 | .DESCRIPTION 268 | Get the computer system type, either VM or NonVM. 269 | 270 | .NOTES 271 | Author: Nickolaj Andersen 272 | Contact: @NickolajA 273 | Created: 2021-06-07 274 | Updated: 2022-01-01 275 | 276 | Version history: 277 | 1.0.0 - (2021-06-07) Function created 278 | 1.0.1 - (2022-01-01) Updated virtual machine array with 'Google Compute Engine' 279 | #> 280 | Process { 281 | # Check if computer system type is virtual 282 | $ComputerSystemModel = Get-WmiObject -Class "Win32_ComputerSystem" | Select-Object -ExpandProperty "Model" 283 | if ($ComputerSystemModel -in @("Virtual Machine", "VMware Virtual Platform", "VirtualBox", "HVM domU", "KVM", "VMWare7,1", "Google Compute Engine")) { 284 | $ComputerSystemType = "VM" 285 | } 286 | else { 287 | $ComputerSystemType = "NonVM" 288 | } 289 | 290 | # Handle return value 291 | return $ComputerSystemType 292 | } 293 | } 294 | 295 | # Define the local administrator user name 296 | $LocalAdministratorName = "" 297 | 298 | # Construct the required URI for the Azure Function URL 299 | $SetSecretURI = "" 300 | $SendClientEventURI = "" 301 | 302 | # Control whether client-side events should be sent to Log Analytics workspace 303 | # Set to $true to enable this feature 304 | $SendClientEvent = $false 305 | 306 | # Define event log variables 307 | $EventLogName = "CloudLAPS-Client" 308 | $EventLogSource = "CloudLAPS-Client" 309 | 310 | # Validate that device is either Azure AD joined or Hybrid Azure AD joined 311 | if (Test-AzureADDeviceRegistration -eq $true) { 312 | # Intiate logging 313 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Information -EventId 10 -Message "CloudLAPS: Local administrator account password rotation started" 314 | 315 | # Retrieve variables required to build request header 316 | $SerialNumber = Get-WmiObject -Class "Win32_BIOS" | Select-Object -ExpandProperty "SerialNumber" 317 | $ComputerSystemType = Get-ComputerSystemType 318 | $AzureADDeviceID = Get-AzureADDeviceID 319 | $CertificateThumbprint = Get-AzureADRegistrationCertificateThumbprint 320 | $Signature = New-RSACertificateSignature -Content $AzureADDeviceID -Thumbprint $CertificateThumbprint 321 | $PublicKeyBytesEncoded = Get-PublicKeyBytesEncodedString -Thumbprint $CertificateThumbprint 322 | 323 | # Construct SetSecret function request header 324 | $SetSecretHeaderTable = [ordered]@{ 325 | DeviceName = $env:COMPUTERNAME 326 | DeviceID = $AzureADDeviceID 327 | SerialNumber = if (-not([string]::IsNullOrEmpty($SerialNumber)) -and ($SerialNumber -ne "System Serial Number")) { $SerialNumber } else { $env:COMPUTERNAME } # fall back to computer name if serial number is not present or equals "System Serial Number" 328 | Type = $ComputerSystemType 329 | Signature = $Signature 330 | Thumbprint = $CertificateThumbprint 331 | PublicKey = $PublicKeyBytesEncoded 332 | ContentType = "Local Administrator" 333 | UserName = $LocalAdministratorName 334 | SecretUpdateOverride = $false 335 | } 336 | 337 | # Construct SendClientEvent request header 338 | $SendClientEventHeaderTable = [ordered]@{ 339 | DeviceName = $env:COMPUTERNAME 340 | DeviceID = $AzureADDeviceID 341 | SerialNumber = if (-not([string]::IsNullOrEmpty($SerialNumber)) -and ($SerialNumber -ne "System Serial Number")) { $SerialNumber } else { $env:COMPUTERNAME } # fall back to computer name if serial number is not present or equals "System Serial Number" 342 | Signature = $Signature 343 | Thumbprint = $CertificateThumbprint 344 | PublicKey = $PublicKeyBytesEncoded 345 | PasswordRotationResult = "" 346 | DateTimeUtc = (Get-Date).ToUniversalTime().ToString() 347 | ClientEventMessage = "" 348 | } 349 | 350 | # Initiate exit code variable with default value if not errors are caught 351 | $ExitCode = 0 352 | 353 | # Initiate extended output variable 354 | $ExtendedOutput = [string]::Empty 355 | 356 | # Use TLS 1.2 connection when calling Azure Function 357 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 358 | 359 | try { 360 | # Check if existing local administrator user account exists 361 | $LocalAdministratorAccount = Get-LocalUser -Name $LocalAdministratorName -ErrorAction SilentlyContinue 362 | 363 | # Amend header table if local administrator account doesn't exist, enforce password creation for devices that were previously provisioned, but have been re-provisioned 364 | if ($LocalAdministratorAccount -eq $null) { 365 | $SetSecretHeaderTable["SecretUpdateOverride"] = $true 366 | } 367 | 368 | # Call Azure Function SetSecret to store new secret in Key Vault for current computer and have the randomly generated password returned 369 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Information -EventId 11 -Message "CloudLAPS: Calling Azure Function API for password generation and secret update" 370 | $APIResponse = Invoke-RestMethod -Method "POST" -Uri $SetSecretURI -Body ($SetSecretHeaderTable | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop 371 | 372 | if ([string]::IsNullOrEmpty($APIResponse)) { 373 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Error -EventId 13 -Message "CloudLAPS: Retrieved an empty response from Azure Function URL"; $ExitCode = 1 374 | } 375 | else { 376 | # Convert password returned from Azure Function API call to secure string 377 | $SecurePassword = ConvertTo-SecureString -String $APIResponse -AsPlainText -Force 378 | 379 | if ($LocalAdministratorAccount -eq $null) { 380 | # Create local administrator account 381 | try { 382 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Information -EventId 20 -Message "CloudLAPS: Local administrator account does not exist, attempt to create it" 383 | New-LocalUser -Name $LocalAdministratorName -Password $SecurePassword -PasswordNeverExpires -AccountNeverExpires -UserMayNotChangePassword -ErrorAction Stop 384 | 385 | try { 386 | # Add to local built-in security groups: Administrators (S-1-5-32-544) 387 | foreach ($Group in @("S-1-5-32-544")) { 388 | $GroupName = Get-LocalGroup -SID $Group | Select-Object -ExpandProperty "Name" 389 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Information -EventId 22 -Message "CloudLAPS: Adding local administrator account to security group '$($GroupName)'" 390 | Add-LocalGroupMember -SID $Group -Member $LocalAdministratorName -ErrorAction Stop 391 | } 392 | 393 | # Handle output for extended details in MEM portal 394 | $ExtendedOutput = "AdminAccountCreated" 395 | } 396 | catch [System.Exception] { 397 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Error -EventId 23 -Message "CloudLAPS: Failed to add '$($LocalAdministratorName)' user account as a member of local '$($GroupName)' group. Error message: $($PSItem.Exception.Message)"; $ExitCode = 1 398 | } 399 | } 400 | catch [System.Exception] { 401 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Error -EventId 21 -Message "CloudLAPS: Failed to create new '$($LocalAdministratorName)' local user account. Error message: $($PSItem.Exception.Message)"; $ExitCode = 1 402 | } 403 | } 404 | else { 405 | # Local administrator account already exists, reset password 406 | try { 407 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Information -EventId 30 -Message "CloudLAPS: Local administrator account exists, updating password" 408 | 409 | # Determine if changes are being made to the built-in local administrator account, if so don't attempt to set properties for password changes 410 | if ($LocalAdministratorAccount.SID -match "S-1-5-21-.*-500") { 411 | Set-LocalUser -Name $LocalAdministratorName -Password $SecurePassword -PasswordNeverExpires $true -ErrorAction Stop 412 | } 413 | else { 414 | Set-LocalUser -Name $LocalAdministratorName -Password $SecurePassword -PasswordNeverExpires $true -UserMayChangePassword $false -ErrorAction Stop 415 | } 416 | 417 | # Handle output for extended details in MEM portal 418 | $ExtendedOutput = "PasswordRotated" 419 | } 420 | catch [System.Exception] { 421 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Error -EventId 31 -Message "CloudLAPS: Failed to rotate password for '$($LocalAdministratorName)' local user account. Error message: $($PSItem.Exception.Message)"; $ExitCode = 1 422 | } 423 | } 424 | 425 | if (($SendClientEvent -eq $true) -and ($Error.Count -eq 0)) { 426 | # Amend header table with success parameters before sending client event 427 | $SendClientEventHeaderTable["PasswordRotationResult"] = "Success" 428 | $SendClientEventHeaderTable["ClientEventMessage"] = "Password rotation completed successfully" 429 | 430 | try { 431 | # Call Azure Functions SendClientEvent API to post client event 432 | $APIResponse = Invoke-RestMethod -Method "POST" -Uri $SendClientEventURI -Body ($SendClientEventHeaderTable | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop 433 | 434 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Information -EventId 50 -Message "CloudLAPS: Successfully sent client event to API. Message: $($SendClientEventHeaderTable["ClientEventMessage"])" 435 | } 436 | catch [System.Exception] { 437 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Error -EventId 51 -Message "CloudLAPS: Failed to send client event to API. Error message: $($PSItem.Exception.Message)"; $ExitCode = 1 438 | } 439 | } 440 | 441 | # Final event log entry 442 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Information -EventId 40 -Message "CloudLAPS: Local administrator account password rotation completed" 443 | } 444 | } 445 | catch [System.Exception] { 446 | switch ($PSItem.Exception.Response.StatusCode) { 447 | "Forbidden" { 448 | # Handle output for extended details in MEM portal 449 | $FailureResult = "NotAllowed" 450 | $FailureMessage = "Password rotation not allowed" 451 | $ExtendedOutput = $FailureResult 452 | 453 | # Write to event log and set exit code 454 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Warning -EventId 14 -Message "CloudLAPS: Forbidden, password was not allowed to be updated"; $ExitCode = 0 455 | } 456 | "BadRequest" { 457 | # Handle output for extended details in MEM portal 458 | $FailureResult = "BadRequest" 459 | $FailureMessage = "Password rotation failed with BadRequest" 460 | $ExtendedOutput = $FailureResult 461 | 462 | # Write to event log and set exit code 463 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Error -EventId 15 -Message "CloudLAPS: BadRequest, failed to update password"; $ExitCode = 1 464 | } 465 | "TooManyRequests" { 466 | # Handle output for extended details in MEM portal 467 | $FailureResult = "TooManyRequests" 468 | $FailureMessage = "Password rotation failed with TooManyRequests (throttled)" 469 | $ExtendedOutput = $FailureResult 470 | 471 | # Write to event log and set exit code 472 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Error -EventId 16 -Message "CloudLAPS: TooManyRequests returned by API, failed to update password"; $ExitCode = 1 473 | } 474 | "GatewayTimeout" { 475 | # Handle output for extended details in MEM portal 476 | $FailureResult = "GatewayTimeout" 477 | $FailureMessage = "Password rotation failed with GatewayTimeout" 478 | $ExtendedOutput = $FailureResult 479 | 480 | # Write to event log and set exit code 481 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Error -EventId 17 -Message "CloudLAPS: GatewayTimeout for API request, failed to update password"; $ExitCode = 1 482 | } 483 | default { 484 | # Handle output for extended details in MEM portal 485 | $FailureResult = "Failed" 486 | $FailureMessage = "Password rotation failed with unhandled exception '$($PSItem.Exception.Response.StatusCode)'" 487 | $ExtendedOutput = $FailureResult 488 | 489 | # Write to event log and set exit code 490 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Error -EventId 12 -Message "CloudLAPS: Call to Azure Function URI failed. Error message: $($PSItem.Exception.Message)"; $ExitCode = 1 491 | } 492 | } 493 | 494 | if ($SendClientEvent -eq $true) { 495 | # Amend header table with success parameters before sending client event 496 | $SendClientEventHeaderTable["PasswordRotationResult"] = $FailureResult 497 | $SendClientEventHeaderTable["ClientEventMessage"] = $FailureMessage 498 | 499 | try { 500 | # Call Azure Functions SendClientEvent API to post client event 501 | $APIResponse = Invoke-RestMethod -Method "POST" -Uri $SendClientEventURI -Body ($SendClientEventHeaderTable | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop 502 | 503 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Information -EventId 52 -Message "CloudLAPS: Successfully sent client event to API. Message: $($FailureMessage)" 504 | } 505 | catch [System.Exception] { 506 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Error -EventId 53 -Message "CloudLAPS: Failed to send client event to API. Error message: $($PSItem.Exception.Message)"; $ExitCode = 1 507 | } 508 | } 509 | } 510 | } 511 | else { 512 | Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Error -EventId 1 -Message "CloudLAPS: Azure AD device registration failed, device is not Azure AD joined or Hybrid Azure AD joined"; $ExitCode = 1 513 | 514 | # Handle output for extended details in MEM portal 515 | $ExtendedOutput = "DeviceRegistrationTestFailed" 516 | } 517 | 518 | # Write output for extended details in MEM portal 519 | Write-Output -InputObject $ExtendedOutput 520 | 521 | # Handle exit code 522 | exit $ExitCode 523 | } 524 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudLAPS 2 | -------------------------------------------------------------------------------- /Workbooks/CloudLAPS-AdminDetails-Template.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "Notebook/1.0", 3 | "items": [ 4 | { 5 | "type": 9, 6 | "content": { 7 | "version": "KqlParameterItem/1.0", 8 | "parameters": [ 9 | { 10 | "id": "e12b082e-be24-4a7d-8fd9-bae45f7b181c", 11 | "version": "KqlParameterItem/1.0", 12 | "name": "UserPrincipalName", 13 | "type": 1, 14 | "description": "Used for UPN pass through from parent workbook", 15 | "isRequired": true, 16 | "isHiddenWhenLocked": true, 17 | "timeContext": { 18 | "durationMs": 86400000 19 | } 20 | }, 21 | { 22 | "id": "db1e87f9-0ce1-4db7-bf61-d219f915c879", 23 | "version": "KqlParameterItem/1.0", 24 | "name": "User", 25 | "type": 1, 26 | "description": "Pass through parameter to display user details from parent workbook", 27 | "isRequired": true, 28 | "isHiddenWhenLocked": true, 29 | "timeContext": { 30 | "durationMs": 86400000 31 | } 32 | }, 33 | { 34 | "id": "b65a898c-456d-4aaf-a958-d9e846d66775", 35 | "version": "KqlParameterItem/1.0", 36 | "name": "TimeRange", 37 | "type": 4, 38 | "isRequired": true, 39 | "isHiddenWhenLocked": true, 40 | "typeSettings": { 41 | "selectableValues": [ 42 | { 43 | "durationMs": 300000 44 | }, 45 | { 46 | "durationMs": 900000 47 | }, 48 | { 49 | "durationMs": 1800000 50 | }, 51 | { 52 | "durationMs": 3600000 53 | }, 54 | { 55 | "durationMs": 14400000 56 | }, 57 | { 58 | "durationMs": 43200000 59 | }, 60 | { 61 | "durationMs": 86400000 62 | }, 63 | { 64 | "durationMs": 172800000 65 | }, 66 | { 67 | "durationMs": 259200000 68 | }, 69 | { 70 | "durationMs": 604800000 71 | }, 72 | { 73 | "durationMs": 1209600000 74 | }, 75 | { 76 | "durationMs": 2419200000 77 | }, 78 | { 79 | "durationMs": 2592000000 80 | }, 81 | { 82 | "durationMs": 5184000000 83 | }, 84 | { 85 | "durationMs": 7776000000 86 | } 87 | ] 88 | }, 89 | "timeContext": { 90 | "durationMs": 86400000 91 | } 92 | }, 93 | { 94 | "id": "4d23d49f-b283-41a6-bc7e-e885368d129c", 95 | "version": "KqlParameterItem/1.0", 96 | "name": "AppRegistrationName", 97 | "type": 1, 98 | "isHiddenWhenLocked": true 99 | } 100 | ], 101 | "style": "above", 102 | "queryType": 0, 103 | "resourceType": "microsoft.operationalinsights/workspaces" 104 | }, 105 | "name": "Template Parameters" 106 | }, 107 | { 108 | "type": 1, 109 | "content": { 110 | "json": "## Admin User Details - {TimeRange}\n\n---\n\n\n\n \n \n \n\n\n \n \n \n \n \n \n \n \n\n
Account Information
User{User}
UserPrincipalName{UserPrincipalName}
\n\n---\n\nBelow are detailed sign in details for the selected admin account." 111 | }, 112 | "name": "Admin User Details" 113 | }, 114 | { 115 | "type": 3, 116 | "content": { 117 | "version": "KqlItem/1.0", 118 | "query": "CloudLAPSAudit_CL\r\n| where UserPrincipalName_s contains \"{UserPrincipalName}\"\r\n| make-series Trend = count() default = 0 on TimeGenerated from {TimeRange:start} to {TimeRange:end} step {TimeRange:grain} by UserPrincipalName_s", 119 | "size": 1, 120 | "title": "Password Retrievals", 121 | "noDataMessageStyle": 3, 122 | "timeContext": { 123 | "durationMs": 2592000000 124 | }, 125 | "queryType": 0, 126 | "resourceType": "microsoft.operationalinsights/workspaces", 127 | "visualization": "timechart" 128 | }, 129 | "name": "Admin Password Retrievals" 130 | }, 131 | { 132 | "type": 3, 133 | "content": { 134 | "version": "KqlItem/1.0", 135 | "query": "SigninLogs \r\n| where UserPrincipalName contains \"{UserPrincipalName}\"\r\n| where AppDisplayName == \"{AppRegistrationName}\"\r\n| summarize count() by City = tostring(LocationDetails.city), Location = tostring(LocationDetails.countryOrRegion)\r\n| top 10 by City", 136 | "size": 3, 137 | "title": "Sign Ins - Top Locations", 138 | "timeContextFromParameter": "TimeRange", 139 | "queryType": 0, 140 | "resourceType": "microsoft.operationalinsights/workspaces", 141 | "visualization": "table" 142 | }, 143 | "name": "Sign Ins - Top 10 Locations" 144 | }, 145 | { 146 | "type": 3, 147 | "content": { 148 | "version": "KqlItem/1.0", 149 | "query": "SigninLogs \r\n| where UserPrincipalName contains \"{UserPrincipalName}\"\r\n| where AppDisplayName == \"{AppRegistrationName}\"\r\n| project TimeGenerated, UserPrincipalName, MultiFactor = tostring(Status.additionalDetails)\r\n| order by TimeGenerated desc", 150 | "size": 1, 151 | "title": "Sign In History", 152 | "timeContext": { 153 | "durationMs": 2592000000 154 | }, 155 | "showExportToExcel": true, 156 | "queryType": 0, 157 | "resourceType": "microsoft.operationalinsights/workspaces", 158 | "gridSettings": { 159 | "formatters": [ 160 | { 161 | "columnMatch": "MultiFactor", 162 | "formatter": 18, 163 | "formatOptions": { 164 | "thresholdsOptions": "icons", 165 | "thresholdsGrid": [ 166 | { 167 | "operator": "contains", 168 | "thresholdValue": "MFA requirement satisfied", 169 | "representation": "success", 170 | "text": "{0}{1}" 171 | }, 172 | { 173 | "operator": "contains", 174 | "thresholdValue": "MFA success", 175 | "representation": "success", 176 | "text": "{0}{1}" 177 | }, 178 | { 179 | "operator": "is Empty", 180 | "representation": "Unknown", 181 | "text": "Unknown MFA Status" 182 | }, 183 | { 184 | "operator": "Default", 185 | "thresholdValue": null, 186 | "representation": "2", 187 | "text": "{0}{1}" 188 | } 189 | ], 190 | "customColumnWidthSetting": "40ch" 191 | } 192 | } 193 | ] 194 | } 195 | }, 196 | "name": "User Details - Admin Sign Ins" 197 | } 198 | ], 199 | "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json" 200 | } --------------------------------------------------------------------------------