次の方法で共有


Using Key Vault Secrets in PowerShell

Interacting with Key Vault through the standard cmdlets is very simple and straight forward, but what happens when I want to use the Key Vault functions that are not exposed in this way such as encrypting or signing a value using the key stored in the key vault? I was experimenting with some ideas to try and keep separation of knowledge when it comes to operation vs. development and found the excessively heavy dependence on .NET knowledge I see in most explanations seems a bit overkill for non-developers. Trying to find a better way I dissected some code samples for Linux disk encryption and came up with the following technique.

First was figuring out the most appropriate way to authenticate. In most of the examples I see on the internet people are using the ClientCredential class from .NET, but this comes with a bit of a cost. The cost I am talking about is the importing of the types which can be done, or a requirement to at least run the Azure login powershell cmdlet. Obviously, this does not align with my goal of reducing the .NET knowledge. The technique I decided on was to first perform a failed access to my Key Vault which will reply with an endpoint to authenticate against.

 function Get-OAuth2Uri
(
  [string]$vaultName
)
{
  $response = try { Invoke-RestMethod -Method GET -Uri "https://$vaultName.vault.azure.net/keys" -Headers @{} } catch { $_.Exception.Response }
  $authHeader = $response.Headers['www-authenticate']
  $endpoint = [regex]::match($authHeader, 'authorization="(.*?)"').Groups[1].Value

  return "$endpoint/oauth2/token"
}

This finds the endpoint for my key vault and appends the oauth2 specific portion of the URL providing me with a location to authenticate against which can change and my scripts should continue to function. The one requirement that this technique has for the next step is to already have registered an application identity with Key Vault as I have done in my setting up Key Vault description previously. Assuming I have the Azure Active Directory Client Id and Client Secret I can continue without issue.

 function Get-AccessToken
(
  [string]$vaultName,
  [string]$aadClientId,
  [string]$aadClientSecret
)
{
  $oath2Uri = Get-OAuth2Uri -vaultName $vaultName

  $body = 'grant_type=client_credentials'
  $body += '&client_id=' + $aadClientId
  $body += '&client_secret=' + [Uri]::EscapeDataString($aadClientSecret)
  $body += '&resource=' + [Uri]::EscapeDataString("https://vault.azure.net")

  $response = Invoke-RestMethod -Method POST -Uri $oath2Uri -Headers @{} -Body $body

  return $response.access_token
}

With the above function, I get the access token for the Key Vault based on my Client Id’s permissions. Now the challenging portions are done. Yes, that is the hardest part. To make use of this for the simple REST APIs exposed via the GET verb I can use the following technique.

 function Get-Keys
(
  [string]$accessToken,
  [string]$vaultName
)
{
  $headers = @{ 'Authorization' = "Bearer $accessToken" }
  $queryUrl = "https://$vaultName.vault.azure.net/keys" + '?api-version=2016-10-01'

  $keyResponse = Invoke-RestMethod -Method GET -Uri $queryUrl -Headers $headers

  return $keyResponse.value
}

In this function, I retrieve a list of all the keys in my Key Vault. The important parts to pay attention to here are

  1. The Authorization header is set in the hash table as a bearer token and the access token retrieved from my Get-AccessToken function
  2. The Invoke-RestMethod cmdlet does all of the heavy listing

Now to handle the important POST methods for encrypting and decrypting data using a key that is in my Key Vault letting it do what it is meant to do, keep my keys safe!

 function Encrypt-ByteArray
(
  [string]$accessToken,
  [string]$vaultName,
  [string]$keyName,
  [string]$keyVersion,
  [byte[]]$plainArray
)
{
  $base64Array = [Convert]::ToBase64String($plainArray)
  $queryUrl = "https://$vaultName.vault.azure.net/keys/$keyName/$keyVersion" + '/encrypt?api-version=2016-10-01'   
  $headers = @{ 'Authorization' = "Bearer $accessToken"; "Content-Type" = "application/json" }

  $bodyObject = @{ "alg" = "RSA-OAEP"; "value" = $base64Array }
  $bodyJson = ConvertTo-Json -InputObject $bodyObject

  $response = Invoke-RestMethod -Method POST -Ur $queryUrl -Headers $headers -Body $bodyJson

  return $response.value
}

When performing the post methods there is a common theme of converting to Base64 strings which just makes sense so we do not introduce any invalid characters etc. and creating a JSON body as you can see here. For the most part the Post Methods will have the same body structure but there are a few that vary slightly such as Verifying data signatures.

Now all I must do is decrypt the data as you can see here.

 function Decrypt-ByteArray
(
  [string]$accessToken,
  [string]$vaultName,
  [string]$keyName,
  [string]$keyVersion,
  [string]$cipher
)
{
  $queryUrl = "https://$vaultName.vault.azure.net/keys/$keyName/$keyVersion" + '/decrypt?api-version=2016-10-01'       
  $headers = @{ 'Authorization' = "Bearer $accessToken"; "Content-Type" = "application/json" }

  $bodyObject = @{ "alg" = "RSA-OAEP"; "value" = $cipher }
  $bodyJson = ConvertTo-Json -InputObject $bodyObject

  $response = Invoke-RestMethod -Method POST -Ur $queryUrl -Headers $headers -Body $bodyJson
  $base64Array = $response.value

  # This next section fixes missing characters on the base64
  $missingCharacters = $base64Array.Length % 4

  if($missingCharacters -gt 0)
  {
    $missingString = New-Object System.String -ArgumentList @( '=', $missingCharacters )
    $base64Array = $base64Array + $missingString       
  }

  return [Convert]::FromBase64String($base64Array)
}

You can see that this is almost the same as the Encrypt-ByteArray with a change the query string and one piece of code identified as missing characters. I will say it bothers me that this is needed but as much as I looked I saw tons of people hitting this in general encrypting and decrypting of base 64 strings without any great answers. It turns out that for some reason the process of encrypting and decrypting removes the ‘=’ characters that represent padding on a base 64 string to the length that is divisible by 4 (all base 64 encoded strings must have a length that is divisible by 4 to be valid). So as a workaround I figure out the number of missing characters and pad it myself with the ‘=’ character. After several tests this technique seems to hold up.

I can now use secrets from my Key Vault in my PowerShell scripts etc. for operations without a great knowledge of .NET. To really simplify things, I will wrap all the REST API in this. Obviously, the downside to this is that I need to register an application for use by my infrastructure team.