CAM.psm1
# CONSTRUCTORS <# .SYNOPSIS Creates a new CAMConfig object .DESCRIPTION Use this function to create a new CAMConfig object to pass to functions within the module if you dont want to read from a configuration file. .PARAMETER AADApplicationId The id of your AAD Application that has access to the KeyVault. .PARAMETER AADApplicationKey An encrypted key for authenticating to the AAD Application. You will need either a key or a certificate and password to authenticate to the AAD Application. .PARAMETER TenantId The TenantId of the Azure Service Principal registered to your AAD Application. .PARAMETER KeyVaultCertificate The name of the certificate file you are using to authenticate to the AAD Application. .PARAMETER KeyVaultCertificatePassword The password of the certificate file you are using to authenticate to the AAD Application. .PARAMETER KeyVault The KeyVault you wish to retrieve certificates from. .PARAMETER Environment The environment name for the current service the Module is being run from. #> function New-CAMConfig() { param( [parameter(mandatory=$true)] [string]$AADApplicationId, [parameter(ParameterSetName="KeyAuth", mandatory=$true)] [parameter(mandatory=$false)] [AllowEmptyString()] [SecureString]$AADApplicationKey, [parameter(mandatory=$true)] [string]$TenantId, [parameter(ParameterSetName="CertificateAuth", mandatory=$true)] [parameter(mandatory=$false)] [AllowEmptyString()] [string]$KeyVaultCertificate, [parameter(ParameterSetName="CertificateAuth", mandatory=$true)] [parameter(mandatory=$false)] [AllowEmptyString()] [SecureString]$KeyVaultCertificatePassword, [parameter(mandatory=$true)] [string]$KeyVault, [parameter(mandatory=$true)] [string]$Environment, [parameter(mandatory=$false)] [string]$ApiAADApplicationId, [parameter(mandatory=$false)] [string]$ApiBaseUrl, [parameter(mandatory=$false)] [PSCustomObject]$SID, [parameter()] [bool]$LogToWindowsEventLog = $false ) return [PSCustomObject]@{ PSTypeName = "CAMConfig" AADApplicationId = $AADApplicationId AADApplicationKey = $AADapplicationKey TenantId = $TenantId KeyVaultCertificate = $KeyVaultCertificate KeyVaultCertificatePassword = $KeyVaultCertificatePassword KeyVault = $KeyVault Environment = $Environment ApiAADApplicationId = $ApiAADApplicationId APIBaseUrl = $ApiBaseUrl SID = $SID LogToWindowsEventLog = $LogToWindowsEventLog } } # END CONSTRUCTORS # LOG FUNCTIONS function Write-InfoLog { param( [parameter(Mandatory=$true)] [string]$Message, [parameter(Mandatory=$true)] [int]$EventId, [parameter()] [bool]$OnlyEvent, [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) Write-CAMEventLog -Message $Message -Type "Information" -EventId $EventId -OnlyEvent $OnlyEvent -CAMConfig $CAMConfig } function Write-WarningLog { param( [parameter(Mandatory=$true)] [string]$Message, [parameter(Mandatory=$true)] [int]$EventId, [parameter()] [bool]$OnlyEvent, [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) Write-CAMEventLog -Message $Message -Type "Warning" -EventId $EventId -Error $true -OnlyEvent $OnlyEvent -CAMConfig $CAMConfig } function Write-ErrorLog { param( [parameter(Mandatory=$true)] [string]$Message, [parameter(Mandatory=$true)] [int]$EventId, [parameter()] [bool]$OnlyEvent, [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) Write-CAMEventLog -Message $Message -Type "Error" -EventId $EventId -OnlyEvent $OnlyEvent -CAMConfig $CAMConfig } function Write-CAMEventLog { param( [parameter(Mandatory=$true)] [string]$Message, [parameter(Mandatory=$true)] [string]$Type, [parameter(Mandatory=$true)] [int]$EventId, [parameter()] [bool]$Error, [parameter()] [bool]$OnlyEvent, [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) if (!$OnlyEvent){ if (!$Error) { Write-Output $Message } else { Write-Error $Message } } if ($CAMConfig.LogToWindowsEventLog){ if (((Get-EventLog -List).Log.Contains("CertificateAllocationModule"))){ New-EventLog -LogName "Application" -Source "CertificateAllocationModule" } Write-EventLog -LogName Application -EventID $EventId ` -EntryType $Type -Source "CertificateAllocationModule" -Message $Message } } # END LOG FUNCTIONS # API FUNCTIONS function Update-Manifest { param( [parameter()] [PSCustomObject]$Manifest, [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) #Get list of the latest versions of each certificate $OverallCertificatesList = Get-ApiCertificateVersionList -CAMConfig $CAMConfig if ($OverallCertificatesList -eq $null) { return $Manifest } #iterate through certificate objects in manifest if ($null -ne $Manifest.certificates) { foreach ($Certificate in $Manifest.Certificates) { if ($Certificate.DeployStrategy -ne "Ignore") { # get latest api entry data $update = $true # Check if API entry latest version is in the manifest foreach($certVersion in $Certificate.certVersions) { # check that this certificate is listed as active in the manifest if ($certVersion.Deploy -notcontains $false){ # check that this certificate is on the Overall list $certIsCurrent = $OverallCertificatesList | Where-Object { $_[1] -contains $certVersion[0].certVersion } if ($certIsCurrent) { $update = $false } else { $newVersionId = ($OverallCertificatesList | Where-Object { $_[0] -contains $Certificate.certName })[1] } } } if ($update){ # the api entry has a new version not present in the manifest # Add the certificate version to the manifest with deploy=true <# $NewVersion = @{} # iterate through current certificate version and copy properties $Certificate.certVersions[0].PsObject.Properties | foreach-object { $NewVersion.Add($_.name, $_.value) } # update certVersion property and deploy property $NewVersion.certVersion = $newVersionId if ($NewVersion.Deploy -eq @("False")) { $NewVersion.Deploy -eq @("True") } # prepend NewVersion $Certificate.certVersions = , (New-Object PSObject -Property $NewVersion) + $Certificate.certVersions if ($Certificate.DeployStrategy -eq "Persist"){ # Set the second most recent certificate deploy=false if ($Certificate.certVersions[1]){ $Certificate.certVersions[1].Deploy = @("True") } } else { if ($Certificate.certVersions[1]){ $Certificate.certVersions[1].Deploy = @("False") } } # if a third (or more) certificate version exists, delete it $Certificate.certVersions = @($Certificate.certVersions[0] , $Certificate.certVersions[1]) } } } } # Iterate through secret objects in manifest if ($null -ne $Manifest.Secrets) { foreach ($Certificate in $Manifest.Secrets) { if ($Certificate.DeployStrategy -ne "Ignore") { # get latest api entry data $update = $true # Check if API entry latest version is in the manifest foreach($certVersion in $Certificate.certVersions) { # check that this certificate is listed as active in the manifest if ($certVersion.Deploy -notcontains $false){ # check that this certificate is on the Overall list $certIsCurrent = $OverallCertificatesList | Where-Object { $_[1] -contains $certVersion[0].certVersion } if ($certIsCurrent) { $update = $false } else { $newVersionId = ($OverallCertificatesList | Where-Object { $_[0] -contains $Certificate.certName })[1] } } } if ($update) { # the api entry has a new version not present in the manifest # Add the certificate version to the manifest with deploy=true $NewVersion = @{} # iterate through current certificate version and copy properties $Certificate.certVersions[0].PsObject.Properties | foreach-object { $NewVersion.Add($_.name, $_.value) } # update certVersion property and deploy property $NewVersion.certVersion = $newVersionId if ($NewVersion.Deploy -eq @("False")) { $NewVersion.Deploy -eq @("True") } # prepend NewVersion $Certificate.certVersions = , (New-Object PSObject -Property $NewVersion) + $Certificate.certVersions if ($Certificate.DeployStrategy -eq "Persist"){ # Set the second most recent certificate deploy=false if ($Certificate.certVersions[1]){ $Certificate.certVersions[1].Deploy = @("True") } } else { if ($Certificate.certVersions[1]){ $Certificate.certVersions[1].Deploy = @("False") } } # if a third (or more) certificate version exists, delete it $Certificate.certVersions = @($Certificate.certVersions[0] , $Certificate.certVersions[1]) } } } } # Update the manifest in the Key Vault Set-AzureKeyVaultSecret -VaultName $CAMConfig.KeyVault -SecretName "$($CAMConfig.KeyVault)-Manifest" -SecretValue (($Manifest | ConvertTo-Json -Depth 4) | ConvertTo-SecureString -AsPlainText -Force) return $Manifest } function Get-ApiCertificateVersionList(){ param( [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $VersionList = @() foreach ($SID in $CAMConfig.SID){ $Url = "$($CAMConfig.ApiBaseUrl)/v1/api/services/$($SID.SID)/certificates/ReadyToDeploy" $Token = Acquire-Token -CAMConfig $CAMConfig for ($x = 0; $x -lt 3; $x++){ try { $Response = (Invoke-WebRequest $url -TimeoutSec 30 -Headers @{ "Authorization"="Bearer $Token" }).Content | ConvertFrom-Json continue } catch { Write-InfoLog -Message "CAM: Unable to reach url: $url" -EventId 2017 -OnlyEvent $true -CAMConfig $CAMConfig if ($x -eq 0) { # we have exhausted 3 retries with no results return $null } } } # iterate through certificates and find latest versions id foreach ($entry in $Response.Certificates) { # iterate through multiple versions stored in an entry foreach ($version in $entry.Versions){ # if latest version and ready, add it to the version list with the cert name if ($version.Latest -eq $true -and $version.Ready -eq $true){ $VersionList += , @($entry.CertName,$version.VersionId) } } } } return $VersionList } function Acquire-Token { param( [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig, [parameter()] [string]$Path = (Get-Item -Path ".\").FullName ) $clientId = $CAMConfig.AADApplicationId $resourceId = $CAMConfig.ApiAADApplicationId $authority = "https://login.microsoftonline.com/$($CAMConfig.TenantId)" $authenticationContext = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext]::new($authority) if ($CAMConfig.AADApplicationKey) { $clientCredential = [Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential]::new($clientId, $CAMConfig.AADApplicationKey) $Token = $authenticationContext.AcquireToken($resourceId, $clientCredential).AccessToken } else { $pfx = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("$($Path)\$($CAMConfig.KeyVaultCertificate).pfx", $CAMConfig.KeyVaultCertificatePassword) $clientCredential = [Microsoft.IdentityModel.Clients.ActiveDirectory.ClientAssertionCertificate]::new($clientId,$pfx) $Token = $authenticationContext.AcquireToken($resourceId, [Microsoft.IdentityModel.Clients.ActiveDirectory.ClientAssertionCertificate]$clientCredential).AccessToken } return $Token } # END API FUNCTIONS # SETUP FUNCTIONS # Script level variable for fallbacks during development, should store these in config or pass in to individual functions. $script:CAMConfig = New-CamConfig -AADApplicationId "MyAADApp" -AADApplicationKey ("MyAADKey" | ConvertTo-SecureString -AsPlainText -force) -TenantId "12345" -KeyVault "MyKeyVault" -Environment "PROD" <# .SYNOPSIS Install AAD Application certificate from an optionally given path .DESCRIPTION Install the certificate you will use to authenticate to your AAD Application. If the certificate is not stored in the same directory as the module pass in the path to the directory it is in with the "Path" parameter. .PARAMETER Path The Path to the directory that houses the certificate you will use to authenticate to your AAD Application. This defaults to the current directory. .PARAMETER CAMConfig (optional) A configuration object used to override the fallback variable and any present CAMConfig file. .EXAMPLE C:\PS> Install-AADAppCertificate -Path "C:\Certificates\AADApp" -CAMConfig $CustomConfig #> function Install-AADAppCertificate() { Param( [parameter()] [string]$Path = (Get-Item -Path ".\").FullName, [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) if (Test-Path "$($Path)\$($CAMConfig.KeyVaultCertificate).pfx") { try { $Pfx = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $Pfx.Import("$($Path)\$($CAMConfig.KeyVaultCertificate).pfx", $CAMConfig.KeyVaultCertificatePassword, "PersistKeySet") if (-not $Pfx.FriendlyName) { $Pfx.FriendlyName = $CAMConfig.KeyVaultCertificate } $Store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My", "LocalMachine") $Store.Open("MaxAllowed") $Store.Add($Pfx) $Store.Close() $PfxFriendlyName = $Pfx.FriendlyName $Pfx.Dispose() return $PfxFriendlyName } catch { Write-ErrorLog -Message "certificate $($CAMConfig.KeyVaultCertificate) could not be imported with given password"` -EventId 2001 -OnlyEvent $true -CAMConfig $CAMConfig throw "certificate $($CAMConfig.KeyVaultCertificate) could not be imported with given password" } } else { Write-ErrorLog -Message "AAD App certificate was not found at $($Path)"` -EventId 2002 -OnlyEvent $true -CAMConfig $CAMConfig throw "AAD App certificate was not found at $($Path)" } } <# .SYNOPSIS Read the CAMconfig file from an optionally given path, or pass in your own custom configuration object. .DESCRIPTION Read the CAMconfig file from an optionally given path, or pass in your own custom configuration object to override the fallback variable and any present CAMconfig file. .PARAMETER Path The Path to the directory that houses the CAMConfig you will use. This defaults to the current directory. .PARAMETER CAMConfig (optional) A configuration object used to override the fallback variable and any present configuration files. .EXAMPLE C:\PS> Read-CAMConfig -Path "C:\CAM\Config" -CAMConfig $CustomConfig #> function Read-CAMConfig() { param( [parameter()] [string]$Path = (Get-Item -Path ".\").FullName, [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) if ($CAMConfig -ne $script:CAMConfig) { $script:CAMConfig.AADApplicationID = $CAMConfig.AADApplicationId $script:CAMConfig.AADApplicationKey = $CAMConfig.AADApplicationkey $script:CAMConfig.TenantId = $CAMConfig.TenantId $script:CAMConfig.KeyVaultCertificate = $CAMConfig.KeyVaultCertificate $script:CAMConfig.KeyVaultCertificatePassword = $CAMConfig.KeyVaultCertificatePassword $script:CAMConfig.KeyVault = $CAMConfig.KeyVault $script:CAMConfig.Environment = $CAMConfig.Environment $script:CAMConfig.ApiAADApplicationId = $CAMConfig.ApiAADApplicationId $script:CAMConfig.ApiBaseUrl = $CAMConfig.ApiBaseUrl $script:CAMConfig.SID = $CAMConfig.SID $script:CAMConfig.LogToWindowsEventLog = $CAMConfig.LogToWindowsEventLog return } if (Test-Path "$($Path)\CAMConfig.json") { try { $Json = Get-Content -Raw -Path "$($Path)\CAMConfig.json" | ConvertFrom-Json # reset hardcoded fallback values $script:CAMConfig.AADApplicationID = $Json.AADApplicationId if ($Json.AADApplicationkey) { $script:CAMConfig.AADApplicationkey = ($Json.AADApplicationkey | ConvertTo-SecureString -AsPlainText -Force) } $script:CAMConfig.TenantId = $Json.TenantId $script:CAMConfig.KeyVaultCertificate = $Json.KeyVaultCertificate if ($Json.KeyVaultCertificatePassword) { $script:CAMConfig.KeyVaultCertificatePassword = ($Json.KeyVaultCertificatePassword | ConvertTo-SecureString -AsPlainText -Force) } $script:CAMConfig.KeyVault = $Json.KeyVault $script:CAMConfig.Environment = $Json.Environment if ($Json.ApiBaseUrl) { $script:CAMConfig.ApiBaseUrl = $Json.ApiBaseUrl } if ($Json.SID) { $script:CAMConfig.SID = $Json.SID } if ($Json.ApiAADApplicationId) { $script:CAMConfig.ApiAADApplicationId = $Json.ApiAADApplicationId } if ($Json.LogToWindowsEventLog) { $script:CAMConfig.LogToWindowsEventLog = $Json.LogToWindowsEventLog } else { $script:CAMConfig.LogToWindowsEventLog = $false } return $true } catch { Write-WarningLog -Message "Unable to read config at $($Path)\CAMConfig.json, defaulting to hardcoded fallback values."` -EventId 2003 -CAMConfig $CAMConfig } } else { Write-WarningLog -Message "Unable to read config at $($Path)\CAMConfig.json, defaulting to hardcoded fallback values."` -EventId 2003 -CAMConfig $CAMConfig } } <# .SYNOPSIS Schedule CAM to run every 5 minutes .DESCRIPTION Create a scheduled task that will run the CAM module's Install-KVCertificates cmdlet at intervals, with 5 minutes being the default. .PARAMETER LocalManifest (optional) The path to the local manifest to be used instead of a manifest in the KeyVault. .PARAMETER Frequency The frequency in minutes that the module should be run. This defaults to 5 minutes. .PARAMETER Path The path to the directory that houses the CAM module you will use. This defaults to the current directory. .EXAMPLE C:\PS> New-CAMSchedule -Path "C:\CAM" #> function New-CAMSchedule() { param( [parameter()] [string]$LocalManifest, [parameter()] [int]$Frequency = 5, [parameter()] [string]$Path = (Get-Item -Path ".\").FullName ) try { if ($LocalManifest) { $LocalManifest = " -LocalManifest " + '"' + $LocalManifest + '"' } $action = New-ScheduledTaskAction -Execute 'Powershell.exe' -WorkingDirectory "$Path" -Argument "Import-Module .\CAM.psm1; Install-KVCertificates$LocalManifest" $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes $Frequency) Register-ScheduledTask -Action $action -Trigger $trigger -TaskName "CAM" -RunLevel Highest return $true } catch { Write-WarningLog -Message "Unable to schedule CAM task. Error: $_" -EventId 2004 } } # END SETUP FUNCTIONS # AUTH FUNCTIONS <# .SYNOPSIS Function for authenticating using user profile .DESCRIPTION Authenticate to azure using your personal azure account credentials. If there is a .ctx or .json azure profile file available in the directory the function is run from it will default to authenticating with it. .EXAMPLE C:\PS> Authenticate-WithUserProfile #> function Authenticate-WithUserProfile() { # Log in to Azure $Path = (Get-Item -Path ".\").FullName if (Test-Path "$Path\myAzureRmProfile.json") { Select-AzureRmProfile -Path "$Path\myAzureRmProfile.json" -ErrorAction Stop } elseif (Test-Path "$Path\profile.ctx") { Import-AzureRmContext -Path "$Path\profile.ctx" -ErrorAction Stop } else { Write-InfoLog -Message "Please log into Azure now" -EventId 1001 Login-AzureRMAccount -ErrorAction stop } } <# .SYNOPSIS Function for authenticating to AAD through AAD app certificate .DESCRIPTION Authenticate to AAD Application using a certificate that has been whitelisted with an applicable service principal. .PARAMETER CAMConfig (optional) A configuration object used to override the fallback variable and any present configuration files. .EXAMPLE C:\PS> Authenticate-WithCertificate -CAMConfig $CustomConfig #> function Authenticate-WithCertificate() { param( [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) try { $KeyVaultCertificateThumbprint = (Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.FriendlyName -match $CAMConfig.KeyVaultCertificate}).Thumbprint Login-AzureRmAccount -ServicePrincipal -CertificateThumbprint $KeyVaultCertificateThumbprint -ApplicationId $CAMConfig.AADApplicationID -TenantId $CAMConfig.TenantId -ErrorAction Stop } catch { Write-ErrorLog "Unable to login with Certificate $($CAMConfig.KeyVaultCertificate). Error: $_" -Message -EventId 2005 -OnlyEvent $true -CAMConfig $CAMConfig throw "Unable to login with Certificate $($CAMConfig.KeyVaultCertificate). Error: $_" } } <# .SYNOPSIS Function for authenticating to AAD through AAD app key .DESCRIPTION Authenticate to AAD Application using a encrypted key that has been whitelisted with an applicable service principal. .PARAMETER CAMConfig (optional) A configuration object used to override the fallback variable and any present configuration files. .PARAMETER Key (optional) An AAD application key to be used if it differs from the one being used in the CAMConfig. .EXAMPLE C:\PS> Authenticate-WithKey -CAMConfig $CustomConfig #> function Authenticate-WithKey() { param( [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig, [parameter()] [SecureString]$Key = $CAMConfig.AADApplicationKey ) try { $Credential = New-Object System.Management.Automation.PSCredential($CAMConfig.AADApplicationID, $Key) Login-AzureRmAccount -Credential $Credential -Tenant $CAMConfig.TenantId -ServicePrincipal -ErrorAction Stop } catch { Write-ErrorLog "Unable to login with Key. Error: $_" -Message -EventId 2006 -OnlyEvent $true -CAMConfig $CAMConfig throw "Unable to login with Key. Error: $_" } } <# .SYNOPSIS Function for trying to authenticate to AAD app with Certificate, Key, or User profile. .DESCRIPTION Authenticate to azure by attempting to authenticate first to an AAD Application with either a whitelisted key or certificate. If that fails, attempt to use local users credentials. .PARAMETER CAMConfig (optional) A configuration object used to override the fallback variable and any present configuration files. .EXAMPLE C:\PS> Authenticate-ToKeyVault -CAMConfig $CustomConfig #> function Authenticate-ToKeyVault() { param( [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) Try { if ($CAMConfig.KeyVaultCertificate) { Authenticate-WithCertificate -CAMConfig $CAMConfig } else { Authenticate-WithKey -CAMConfig $CAMConfig } } # If that doesn't work use local user profile Catch { Authenticate-WithUserProfile } } # END AUTH FUNCTIONS <# .SYNOPSIS Install and delete certificates as specified within the manifest file provided or saved in the KeyVault. .DESCRIPTION Ensure the module is authenticated to an AAD Application. Load a manifest file stored in the KeyVault or provided locally. Break apart the manifest into a secret and certificate section. Iterate through the secrets and download or delete the certificate as specified in the certificateVersion "deploy" property. Repeat this process for the certificate section. .PARAMETER LocalManifest (optional) The full path to a valid json manifest file to be used in place of a passed in object or manifest available in the KeyVault. .PARAMETER Manifest (optional) A PSCustom object containing the manifest information to override a local or keyvault sourced manifest. .PARAMETER CAMConfig (optional) A configuration object used to override the fallback variable and any present configuration files. .EXAMPLE C:\PS> Install-KVCertificates -LocalManifest "C:\CAM\testingmanifests\test.json" -CAMConfig $CustomConfig #> function Install-KVCertificates() { param( [parameter()] [string]$LocalManifest, [parameter()] $Manifest, [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) #Read CAMConfig object if present and update fallback values if ($CAMConfig -ne $script:CAMConfig) { Read-CAMConfig -CAMConfig $CAMConfig | Out-Null } else { Read-CAMConfig | Out-Null } #if ($Manifest.KeyVault) { # $CAMConfig.KeyVault = $Manifest.KeyVault #} Write-InfoLog -Message "CAM: Config loaded" -EventId 1002 -CAMConfig $CAMConfig #If certificate authentication is being used, install the required certificate if ($CAMConfig.KeyVaultCertificate -and $CAMConfig.KeyVaultCertificatePassword) { Install-AADAppCertificate -CAMConfig $CAMConfig | Out-Null } #Authenticate with AAD App and KeyVault Authenticate-ToKeyVault -CAMConfig $CAMConfig | Out-Null Write-InfoLog -Message "CAM: Authenticated to KeyVault" -EventId 1003 -CAMConfig $CAMConfig # local path passed in if ($LocalManifest) { $json = Get-Content -Raw -Path "$($localmanifest)" | ConvertFrom-Json } # object passed in elseif ($Manifest) { if ($Manifest.gettype().tostring() -eq "System.Management.Automation.PSCustomObject") { $json = $Manifest } else { Write-ErrorLog "CAM: Manifest object was not of type System.Management.Automation.PSCustomObject" -EventId 2007 -CAMConfig $CAMConfig return } } # retrieve manifest from keyvault else { # Load manifest from the KeyVault $manifestName = "$($CAMConfig.KeyVault)-manifest" $manifest = Get-AzureKeyVaultSecret -VaultName $CAMConfig.KeyVault -Name $manifestName -ErrorAction Stop $json = $manifest.SecretValueText | ConvertFrom-Json Write-InfoLog -Message "CAM: Manifest retrieved from Key Vault" if ($CAMConfig.ApiBaseUrl) { # if manifest has not been updated within the last hour, check API if ($manifest.attributes.updated -lt ((Get-Date)-(New-Timespan -Hours 1))) { try { # create an updated verison of manifest $update = (Update-Manifest -Manifest $json -CAMConfig $CAMConfig) # reset our json if succesfully updated $json = $update Write-InfoLog -Message "CAM: Manifest was out of date, and updated in Key Vault" -EventId 1010 } catch { Write-WarningLog -Message "CAM: Failed to update manifest in Key Vault. Error: $_" -EventId 2018 -CAMConfig $CAMConfig } } } } $DefaultKeyVault = $CAMConfig.KeyVault Write-InfoLog -Message "CAM: $($ManifestName)Manifest loaded" -EventId 1004 -CAMConfig $CAMConfig # Iterate through Certificates section if ($null -ne $json.certificates) { foreach ($Certificate in $json.Certificates) { $CertificateName = $Certificate.CertName $CertificateVersions = $Certificate.CertVersions $KeyStorageFlags = "DefaultKeySet" $PublicKeyOnly = $false if ($Certificate.KeyStorageFlags) { $KeyStorageFlags = $Certificate.KeyStorageFlags } if ($Certificate.PublicKeyOnly){ $PublicKeyOnly = $true } # Iterate through Certificate versions foreach ($CertificateVersion in $CertificateVersions) { $CertificateStoreLocation = "LocalMachine" if ($CertificateVersion.StoreLocation) { $CertificateStoreLocation = $CertificateVersion.StoreLocation } $CertificateStoreName = "My" if ($CertificateVersion.StoreName) { $CertificateStoreName = $CertificateVersion.StoreName } $downloaded = $false # Iterate through deployment array to decide if cert should be installed or deleted foreach ($deployment in $CertificateVersion.Deploy) { if ($deployment -eq "True" -or $deployment -eq $CAMConfig.Environment) { # Install Certificate Write-InfoLog -Message "CAM: Installing Certificate: $($CertificateName)" -EventId 1005 -CAMConfig $CAMConfig Install-KVCertificateObject -CertName $CertificateName -CertVersion $CertificateVersion.CertVersion ` -CertStoreName $CertificateStoreName -CertStoreLocation $CertificateStoreLocation ` -KeyStorageFlags $KeyStorageFlags -Export $Certificate.Export -PublicKeyOnly $PublicKeyOnly -CAMConfig $CAMConfig # Grant user access to private keys if ($null -ne $Certificate.GrantAccess) { Grant-CertificateAccess -CertName $CertificateName -User $CertificateVersion.GrantAccess -CertStoreName $CertificateStoreName ` -CertStoreLocation $CertificateStoreLocation } $downloaded = $true } } # Delete Certificate if (!$downloaded) { Write-InfoLog -Message "CAM: Deleting Certificate: $($CertificateName)" -EventId 1006 -CAMConfig $CAMConfig $RetrievedCertificate = Get-AzureKeyVaultCertificate -VaultName $CAMConfig.KeyVault -Name $CertificateName -Version $CertificateVersion.CertVersion Remove-Certificate -certName $CertificateName -CertStoreLocation $CertificateStoreLocation ` -CertStoreName $CertificateStoreName -certThumbprint $RetrievedCertificate.Thumbprint } } } } # Iterate through Secrets section if ($null -ne $json.Secrets) { foreach ($Secret in $json.Secrets) { $CertificateName = $Secret.CertName $CertificateVersions = $Secret.CertVersions $Unstructured = $false $KeyStorageFlags = "DefaultKeySet" $PublicKeyOnly = $false if ($Secret.Unstructured) { $Unstructured = $true } if ($Secret.KeyStorageFlags) { $KeyStorageFlags = $Secret.KeyStorageFlags } if ($Secret.PublicKeyOnly){ $PublicKeyOnly = $true } if ($Secret.KeyVault) { $CAMConfig.KeyVault = $Secret.KeyVault } else { $CAMConfig.KeyVault = $DefaultKeyVault } # Iterate through Certificate versions foreach ($CertificateVersion in $CertificateVersions) { $CertificateStoreLocation = "LocalMachine" if ($CertificateVersion.StoreLocation) { $CertificateStoreLocation = $CertificateVersion.StoreLocation } $CertificateStoreName = "My" if ($CertificateVersion.StoreName) { $CertificateStoreName = $CertificateVersion.StoreName } $download = $false # Iterate through deployment array to decide if cert should be downloaded or deleted foreach ($deployment in $CertificateVersion.Deploy) { if ($deployment -eq "True" -or $deployment -eq $CAMConfig.Environment) { # Install Certificate Write-InfoLog -Message "CAM: Installing Certificate: $($CertificateName)" -EventId 1005 -CAMConfig $CAMConfig Install-KVSecretObject -CertName $CertificateName -CertVersion $CertificateVersion.CertVersion ` -CertStoreName $CertificateStoreName -CertStoreLocation $CertificateStoreLocation -Unstructured $Unstructured ` -KeyStorageFlags $KeyStorageFlags -Export $Secret.Export -PublicKeyOnly $PublicKeyOnly -CAMConfig $CAMConfig # Grant user access to private keys if ($null -ne $Secret.GrantAccess) { Grant-CertificateAccess -CertName $CertificateName -User $CertificateVersion.GrantAccess -CertStoreName $CertificateStoreName ` -CertStoreLocation $CertificateStoreLocation } $download = $true } } # Delete Certificate if (!$download) { Write-InfoLog -Message "CAM: Deleting Certificate: $($CertificateName)" -EventId 1006 -CAMConfig $CAMConfig $Thumbprint = Get-SecretThumbprint -CertName $CertificateName -CertVersion $CertificateVersion.CertVersion -Unstructured $Unstructured -CAMConfig $CAMConfig Remove-Certificate -CertName $CertificateName -CertStoreLocation $CertificateStoreLocation` -CertStoreName $CertificateStoreName -CertThumbprint $Thumbprint } } } } } <# .SYNOPSIS This function will download and install a certificate object from a key vault and install it on local machine. .DESCRIPTION This script runs through key vault commands to download a certificate object from a key vault and install it locally. .PARAMETER CertName Secret name in key vault. .PARAMETER CertVersion (optional) Version GUID of the secret you want to retrieve. .PARAMETER Export (optional) Path to folder where the public key should be exported as .cert file. .PARAMETER CertStoreName (optional) Certificate Store Name that you would like the certificate installed to. Defaults to "My" .PARAMETER CertStoreLocation (optional) Certificate Store Location that you would like the certificate installed to. Defaults to "LocalMachine" .PARAMETER KeyStorageFlags (optional) Key storage flags to be used when the certificate is imported to the store. Defaults to "PersistKeySet" .PARAMETER ReturnOutput (optional) A switch to indicate you want the HashTable returned with the Friendly Name and Thumbprint of the certificate .PARAMETER CAMConfig (optional) A configuration object used to override the fallback variable and any present configuration files. .EXAMPLE C:\PS> Install-KVCertificateObject -CertName "MyCertificate" -CertVersion "0000-0000-0000-0000" ` -CertStoreName "My" -CertStoreLocation "LocalMachine" -CAMConfig $CustomConfig #> function Install-KVCertificateObject() { param( [parameter(Mandatory=$true)] [string]$CertName, [parameter()] [string]$CertVersion, [parameter()] [string]$Export, [parameter()] [string]$CertStoreName = "My", [parameter()] [string]$CertStoreLocation = "LocalMachine", [parameter()] [string]$KeyStorageFlags = "DefaultKeySet", [parameter()] [bool]$PublicKeyOnly = $false, [parameter()] [switch]$ReturnOutput, [parameter()] [switch]$SkipAuth, [parameter()] $CAMConfig = $script:CAMConfig ) if (!$SkipAuth) { if (!(LoggedIn -CAMConfig $CAMConfig)) { Authenticate-ToKeyVault -CAMConfig $CAMConfig } } if ($CertVersion) { $Cert = Get-PrivateKeyVaultCert -CertName $CertName -CertVersion $CertVersion -CAMConfig $CamConfig } else { $Cert = Get-PrivateKeyVaultCert -CertName $CertName -CAMConfig $CamConfig } if (-not $Cert) { Write-ErrorLog -Message "CAM: Certificate $($certName) does not exist in $($CAMConfig.KeyVault) KeyVault" -EventId 2008 -CAMConfig $CAMConfig return } try { $CertBytes = [Convert]::FromBase64String($Cert.SecretValueText) $Pfx = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertBytes, "", $keyStorageFlags) if ($PublicKeyOnly -and $Pfx.HasPrivateKey) { $Pfx.PrivateKey = $null } } catch { Write-ErrorLog -Message "CAM: Certificate $Certname could not be imported with password. Error: $_" -EventId 2009 -CAMConfig $CAMConfig return } $Pfx.FriendlyName = $CertName $Store = New-Object System.Security.Cryptography.X509Certificates.X509Store($CertStoreName, $CertStoreLocation) $Store.Open("MaxAllowed") $Store.Add($Pfx) $Store.Close() if ($Export) { $Bytes = $Pfx.Export("Cert") [IO.File]::WriteAllBytes("$Export\$CertName.cer", $Bytes) } $Output = @{ FriendlyName=$pfx.FriendlyName Thumbprint=$pfx.Thumbprint } $Pfx.Dispose() Write-InfoLog -Message "CAM: Installed Certificate $($CertName) to $CertStoreLocation\$CertStoreName store" -EventId 1007 -CAMConfig $CAMConfig if ($ReturnOutput) { return $Output } } <# .SYNOPSIS This function will download and install a certificate from a json object that was encrypted and stored as a KeyVault secret. .DESCRIPTION This function will download and install certificates that have been stored in KeyVault as encrypted json objects. These json objects are made up of two properties. "data" which stores the raw certificate data, and "password" which stores the password to the Pfx. .PARAMETER CertName Cert name in key vault .PARAMETER CertVersion (optional) Version GUID of the secret you want to retrieve. .PARAMETER Export (optional) Path to folder where the public key should be exported as .cert file. .PARAMETER CertStoreName (optional) Certificate Store Name that you would like the certificate installed to. Defaults to "My" .PARAMETER CertStoreLocation (optional) Certificate Store Location that you would like the certificate installed to. Defaults to "LocalMachine" .PARAMETER KeyStorageFlags (optional) Key storage flags to be used when the certificate is imported to the store. Defaults to "PersistKeySet" .PARAMETER Unstructured If true, will download the secret without disassembling it as a JSON object, and import with no password. Defaults to "false" .PARAMETER ReturnOutput (optional) A switch to indicate you want the HashTable returned with the Friendly Name and Thumbprint of the certificate .PARAMETER CAMConfig (optional) A configuration object used to override the fallback variable and any present configuration files. .EXAMPLE C:\PS> Install-KVSecretObject -CertName "MyCertificate" -CertVersion "0000-0000-0000-0000" ` -CertStoreName "My" -CertStoreLocation "LocalMachine" -CAMConfig $CustomConfig #> function Install-KVSecretObject() { param( [parameter(mandatory=$true)] [string]$CertName, [parameter()] [string]$CertVersion, [parameter()] [string]$Export, [parameter()] [string]$CertStoreName = "My", [parameter()] [string]$CertStoreLocation = "LocalMachine", [parameter()] [string]$keyStorageFlags = "DefaultKeySet", [parameter()] [bool]$PublicKeyOnly = $false, [parameter()] [switch]$ReturnOutput, [parameter()] [switch]$SkipAuth, [parameter()] [bool]$Unstructured = $false, [parameter()] $CAMConfig = $script:CAMConfig ) if (!$SkipAuth) { if (!(LoggedIn -CAMConfig $CAMConfig)) { Authenticate-ToKeyVault -CAMConfig $CAMConfig } } if ($CertVersion) { $Secret = Get-PrivateKeyVaultCert -CertName $CertName -CertVersion $CertVersion -CAMConfig $CamConfig } else { $Secret = Get-PrivateKeyVaultCert -CertName $CertName -CAMConfig $CamConfig } if (-not $Secret) { Write-ErrorLog -Message "CAM: Certificate $($certName) does not exist in $($CAMConfig.KeyVault) KeyVault" -EventId 2008 -CAMConfig $CAMConfig return } if ($Unstructured) { $CertBytes = [Convert]::FromBase64String($Secret.SecretValueText) } else { try { $KvSecretBytes = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Secret.SecretValueText)) $CertJson = $KvSecretBytes | ConvertFrom-Json $Password = $CertJson.password $CertBytes = [System.Convert]::FromBase64String($CertJson.data) } catch { Write-ErrorLog -Message "CAM: Certificate $($CertName) has invalid JSON, Unable to install" -EventId 2010 -CAMConfig $CAMConfig return } } try { if ($Unstructured) { $Pfx = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertBytes, '', $keyStorageFlags) } else { $Pfx = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertBytes, $Password, $keyStorageFlags) } if ($PublicKeyOnly -and $Pfx.HasPrivateKey) { $Pfx.PrivateKey = $null } } catch { Write-ErrorLog -Message "CAM: Certificate $Certname could not be imported with password. Error: $_" -EventId 2009 -CAMConfig $CAMConfig return } $Pfx.FriendlyName = $CertName $Store = New-Object System.Security.Cryptography.X509Certificates.X509Store($CertStoreName, $CertStoreLocation) $Store.Open("MaxAllowed") $Store.Add($Pfx) $Store.Close() if ($Export) { $Bytes = $Pfx.Export("Cert") [IO.File]::WriteAllBytes("$Export\$CertName.cer", $Bytes) } $Output = @{ FriendlyName=$pfx.FriendlyName Thumbprint=$pfx.Thumbprint } $Pfx.Dispose() Write-InfoLog -Message "CAM: Installed Certificate $($CertName) to $CertStoreLocation\$CertStoreName store" -EventId 1007 -CAMConfig $CAMConfig if ($ReturnOutput) { return $Output } } <# .SYNOPSIS This function retrives a KeyVault secret based on the name and version. If the version is not specified it retrieves the most current version. .PARAMETER CertName Cert name in key vault .PARAMETER CertVersion (optional) Version GUID of the secret you want to retrieve. .PARAMETER CAMConfig (optional) A configuration object used to override the fallback variable and any present configuration files. .EXAMPLE C:\PS> Get-PrivateKeyVaultCert -CertName "MyCertificate" -CertVersion "0000-0000-0000-0000" -CAMConfig $CustomConfig #> function Get-PrivateKeyVaultCert() { param( [parameter(Mandatory=$true)] [string]$CertName, [parameter()] [string]$CertVersion, [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) if ($CertVersion) { return Get-AzureKeyVaultSecret -VaultName $CAMConfig.KeyVault -Name $CertName -Version $CertVersion } else { return Get-AzureKeyVaultSecret -VaultName $CAMConfig.KeyVault -Name $CertName } } <# .SYNOPSIS This function lets you remove a cert from a provided store location and store name. .DESCRIPTION If this function is not provided a thumbprint, it will search the provided store location and store name for certificates with a friendly name that match the provided CertName parameter and select the one with the earliest expiry. Once the certificate has been selected by earliest expiry or provided thumbprint, it is removed from the store. .PARAMETER CertName The friendly name of the certificate you would like to remove. .PARAMETER CertStoreLocation The store location where the certificate is installed. .PARAMETER CertStoreName The store name where the certificate is installed. .PARAMETER CertThumbprint (optional) The thumbprint of the certificate you would like to remove. If not provided, the certificate that matches the CertName parameter with the earliest expiry will be selected. .EXAMPLE C:\PS> Remove-Certificate -CertName "MyCertificate" -CertStoreLocation "LocalMachine" -CertStoreName "My" -CertificateThumbprint "0000000000000000000000000000" #> function Remove-Certificate() { param( [parameter()] [string]$CertName, [parameter(mandatory=$true)] [string]$CertStoreLocation, [parameter(mandatory=$true)] [string]$CertStoreName, [parameter()] [string]$CertThumbprint ) try { if (-not $CertThumbprint) { try { #find the certificate with the earliest expiry of those matching the CertName parameter $CertLocation = (Get-ChildItem "Cert:\$CertStoreLocation\$CertStoreName" | Where-Object {$_.FriendlyName -match $CertName} | Sort-Object -Property NotAfter)[0].PSPath Write-WarningLog -Message "CAM: Certificate Thumbprint not provided for $CertName, attempting to delete certificate with earliest expiry." ` -EventId 2011 -CAMConfig $CAMConfig } catch { Write-ErrorLog -Message "CAM: Certificate $($certName) does not exist in $($CAMConfig.KeyVault) KeyVault" -EventId 2008 -CAMConfig $CAMConfig } } else { #Create path to cert $CertLocation = "Cert:\" + $CertStoreLocation + "\" + $CertStoreName + "\" + $CertThumbprint } if (test-path $CertLocation) { Remove-Item $CertLocation Write-InfoLog -Message "CAM: Certificate $($CertName) deleted from $($CertStoreLocation)\$($CertStoreName) store" -EventId 1008 -CAMConfig $CAMConfig } else { Write-ErrorLog -Message "CAM: Certificate $($CertName) does not exist in $($CertStoreLocation)\$($CertStoreName) store" -EventId 2012 -CAMConfig $CAMConfig } } catch { Write-ErrorLog -Message "CAM: Failed to delete certificate $($CertLocation). Error: $_" -EventId 2013 -CAMConfig $CAMConfig } } <# .SYNOPSIS This function grants access to a supplied user for a certificates private key. .DESCRIPTION This function grants access to a supplied user (defaulted to Network Service) for a certificates private key. .PARAMETER CertName The friendly name of the certificate you would like to remove. .PARAMETER User (optional) The user you want to give access to. .PARAMETER CertStoreName The store name where the certificate is installed. .PARAMETER CertStoreLocation The store location where the certificate is installed. .EXAMPLE C:\PS> Grant-CertificateAccess -CertName "MyCertificate" -User "Network Service" -CertStoreLocation "LocalMachine" -CertStoreName "My" #> function Grant-CertificateAccess() { param( [parameter(mandatory=$true)] [string]$CertName, [parameter()] [string]$User = "Network Service", [parameter()] [string]$CertStoreName = "My", [parameter()] [string]$CertStoreLocation = "LocalMachine" ) try { $Certificate = (Get-ChildItem "Cert:\$CertStoreLocation\$CertStoreName" ` | Where-Object {$_.FriendlyName -eq $CertName}).PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName if ($Certificate) { $keyPath = $env:ProgramData + "\Microsoft\Crypto\RSA\MachineKeys\" $fullpath = $keypath+$Certificate try { $acl=(Get-Item $fullpath -ErrorAction Stop).GetAccessControl('Access') } catch { Write-ErrorLog -Message "CAM: Unable to find Machine Key path for certificate $($CertName), Grant-CertificateAccess failed." -EventId 2014 -CAMConfig $CAMConfig return } $permission=$User, "Read", "Allow" $accessRule=new-object System.Security.AccessControl.FileSystemAccessRule $permission $acl.SetAccessRule($accessRule) try { Set-Acl -Path $fullPath -AclObject $acl Write-InfoLog -Message "CAM: Granted access to $User for certificate $CertName in $CertStoreLocation\$CertStoreName store." -EventId 1009 -CAMConfig $CAMConfig } catch { Write-ErrorLog -Message "CAM: Unable to grant access to $User for certificate $CertName in $CertStoreLocation\$CertStoreName store. Error: $_" ` -EventId 2015 -CAMConfig $CAMConfig } } } catch { Write-ErrorLog -Message "CAM: Grant-CertificateAccess failed. Error: $_" -EventId 2016 -CAMConfig $CAMConfig } } <# .SYNOPSIS This function retrieves a thumbprint from a secret object. .DESCRIPTION This function retrieves a secret object from the KeyVault and then returns its thumbprint. It is only used to identify the thumbprint of a secret object to be deleted. .PARAMETER CertName The friendly name of the certificate you would like to remove. .PARAMETER CertVersion (optional) Version GUID of the secret you want to retrieve. .PARAMETER Unstructured If true, will download the secret without disassembling it as a JSON object, and import with no password. Defaults to "false" .PARAMETER CAMConfig (optional) A configuration object used to override the fallback variable and any present configuration files. .EXAMPLE C:\PS> Get-SecretThumbprint -CertName "MyCertificate" -CertVersion "0000-0000-0000-0000" -CAMConfig $CustomConfig #> function Get-SecretThumbprint() { param( [parameter(mandatory=$true)] [string]$CertName, [parameter()] [string]$CertVersion, [parameter()] [bool]$Unstructured, [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) if (-not $CertVersion) { $Secret = Get-AzureKeyVaultSecret -VaultName $CAMConfig.KeyVault -Name $CertName -ErrorAction Stop } else { $Secret = Get-AzureKeyVaultSecret -VaultName $CAMConfig.KeyVault -Name $CertName -Version $CertVersion -ErrorAction Stop } $Password = '' if (-not $Secret) { Write-ErrorLog -Message "CAM: Certificate $($certName) does not exist in $($CAMConfig.KeyVault) KeyVault" -EventId 2008 -CAMConfig $CAMConfig return } if ($Unstructured) { $CertBytes = [Convert]::FromBase64String($Secret.SecretValueText) } else { try { $KvSecretBytes = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Secret.SecretValueText)) $CertJson = $KvSecretBytes | ConvertFrom-Json $Password = $CertJson.password $CertBytes = [System.Convert]::FromBase64String($CertJson.data) } catch { Write-ErrorLog -Message "CAM: Certificate $($CertName) has invalid JSON, Unable to install" -EventId 2010 -CAMConfig $CAMConfig return } } try { $Pfx = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertBytes, $Password, "PersistKeySet") } catch { Write-ErrorLog -Message "CAM: Certificate $Certname could not be imported with password. Error: $_" -EventId 2009 -CAMConfig $CAMConfig return } $Thumbprint = $Pfx.Thumbprint $Pfx.Dispose() return $Thumbprint } <# .SYNOPSIS This function checks if the powershell session has been authenticated by the context provided in the CAMConfig .DESCRIPTION This function checks to see if the current powershell session has been authenticated with the same TenantId as specified in the CAMConfig. .PARAMETER CAMConfig (optional) A configuration object used to override the fallback variable and any present configuration files. .EXAMPLE C:\PS> LoggedIn -CAMConfig $CustomConfig #> function LoggedIn() { param( [parameter()] [PSTypeName("CAMConfig")]$CAMConfig = $script:CAMConfig ) $Context = Get-AzureRmContext if ($null -ne $Context.Tenant -and $Context.Tenant.Id -eq $CAMConfig.TenantId) { return $true } return $false } Export-ModuleMember -Function New-CamConfig Export-ModuleMember -Function Install-AADAppCertificate Export-ModuleMember -Function Read-CAMConfig Export-ModuleMember -Function New-CAMSchedule Export-ModuleMember -Function Authenticate-WithUserProfile Export-ModuleMember -Function Authenticate-WithCertificate Export-ModuleMember -Function Authenticate-WithKey Export-ModuleMember -Function Authenticate-ToKeyVault Export-ModuleMember -Function Grant-CertificateAccess Export-ModuleMember -Function Install-KVCertificates Export-ModuleMember -Function Install-KVCertificateObject Export-ModuleMember -Function Install-KVSecretObject Export-ModuleMember -Function Remove-Certificate |