Stop Pasting Refresh Tokens Into Your Scripts: Azure Key Vault for Multi-Tenant PowerShell
TL;DR
- Hardcoding app registration details and refresh tokens into PowerShell scripts is a security risk that Azure Key Vault eliminates by storing and serving secrets at runtime.
- The Secure Application Model uses GDAP relationships and refresh tokens to obtain access tokens for downstream customer tenants, the same architecture tools like CIPP rely on.
- Storing secrets in Key Vault enables headless automation, letting multi-tenant security scripts run on a schedule without anyone pasting credentials.
- Azure Key Vault names must be globally unique and 32 characters or fewer.
- A worked example loops every Partner Center customer over GDAP and audits mailboxes for forwarding inbox rules, with all credentials pulled from Key Vault.
If your multi-tenant PowerShell scripts have an app secret or a refresh token sitting in plaintext somewhere, that is the part of your automation most likely to end badly. This guide covers using Azure Key Vault to manage secrets securely in single-tenant or multi-tenant scripts, so you can access authentication tokens and other sensitive information needed to reach downstream customer environments without ever hardcoding them. As a bonus, it unlocks headless automation: scripts that run on a schedule with no one in the loop.
Where these secrets come from: the Secure Application Model
Most MSPs use the Secure Application Model whether or not they call it that. Tools like CIPP rely on this architecture to obtain access tokens for downstream customer environments through established GDAP relationships. As an example, we wrote a multi-tenant script (opens in new tab) that loops through every Partner Center customer and inspects user mailboxes for inbox rules, specifically forwarding rules, as part of regular security audits.

If you have not set up the Secure Application Model, two earlier write-ups cover it end to end:
Why secret management is not optional here
Scripts that need app registration details or refresh tokens are handling the keys to every customer tenant you manage. Storing those secrets locally, or pasting them into the script body, puts them in front of anyone who can read the file. Azure Key Vault stores and serves these artifacts securely, removing that exposure, and the same mechanism lets scripts authenticate non-interactively so they can run on a schedule behind the scenes.
Loading your secrets into Key Vault
This can be done by hand, but the script below creates a Key Vault and loads the Secure Application Model secrets into it in one pass:
#PARAMETERS
param (
[Parameter(Mandatory=$false)]
[string] $AppId,
[Parameter(Mandatory=$false)]
[string] $AppSecret,
[Parameter(Mandatory=$false)]
[string] $TenantId,
[Parameter(Mandatory=$false)]
[string] $refreshToken,
[Parameter(Mandatory=$false)]
[string] $PartnerRefreshToken,
[Parameter(Mandatory=$false)]
[string] $AppDisplayName
)
$ErrorActionPreference = "SilentlyContinue"
#See if Az.Accounts and Az.KeyVault Powershell module is installed and if not install it
Write-Host "Checking for Az module"
$AzModule = Get-Module -Name Az.Accounts -ListAvailable
if ($null -eq $AzModule) {
Write-Host "Az module not found, installing now"
Install-Module -Name Az.Accounts -Force -AllowClobber
}
else {
Write-Host "Az module found"
}
$AzModule = Get-Module -Name Az.KeyVault -ListAvailable
if ($null -eq $AzModule) {
Write-Host "Az.KeyVault module not found, installing now"
Install-Module -Name Az.KeyVault -Force -AllowClobber
}
else {
Write-Host "Az.KeyVault module found"
}
#Connect to Azure, sign in with Global Admin Credentials
Connect-AzAccount
#Set values for Key Vault Name and Resource Group and Location
$KeyVaultName = Read-Host -Prompt "Enter the name you want the key vault to be called. Make this unique"
$ResourceGroupName = Read-Host -Prompt "Enter the name of the resource group you want the key vault to be in"
$Location = Read-Host -Prompt "Enter the location you want the key vault to be in, i.e. East US"
#Create the Key Vault
try {
$KeyVault = New-AzKeyVault -VaultName $KeyVaultName -ResourceGroupName $ResourceGroupName -Location $Location
Write-Host "Key Vault created successfully" -ForegroundColor Green
}
catch {
Write-Host "Failed to create Key Vault" -ForegroundColor Red
Write-Host $_.Exception.Message
break
}
#Create the secrets based on the params in the key vault
#AppID
try{
$secretvalue = ConvertTo-SecureString $AppId -AsPlainText -Force
$secret = Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppID" -SecretValue $secretvalue
Write-Host "AppID secret created successfully" -ForegroundColor Green
}
catch {
Write-Host "Failed to create AppID secret" -ForegroundColor Red
Write-Host $_.Exception.Message
return
}
#AppSecret
try{
$secretvalue = ConvertTo-SecureString $AppSecret -AsPlainText -Force
$secret = Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppSecret" -SecretValue $secretvalue
Write-Host "AppSecret secret created successfully" -ForegroundColor Green
}
catch {
Write-Host "Failed to create AppSecret secret" -ForegroundColor Red
Write-Host $_.Exception.Message
return
}
#TenantID
try{
$secretvalue = ConvertTo-SecureString $TenantId -AsPlainText -Force
$secret = Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "TenantID" -SecretValue $secretvalue
Write-Host "TenantID secret created successfully" -ForegroundColor Green
}
catch {
Write-Host "Failed to create TenantID secret" -ForegroundColor Red
Write-Host $_.Exception.Message
return
}
#RefreshToken
try{
$secretvalue = ConvertTo-SecureString $refreshToken -AsPlainText -Force
$secret = Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "RefreshToken" -SecretValue $secretvalue
Write-Host "RefreshToken secret created successfully" -ForegroundColor Green
}
catch {
Write-Host "Failed to create RefreshToken secret" -ForegroundColor Red
Write-Host $_.Exception.Message
return
}
#PartnerRefreshToken
try{
$secretvalue = ConvertTo-SecureString $PartnerRefreshToken -AsPlainText -Force
$secret = Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "PartnerRefreshToken" -SecretValue $secretvalue
Write-Host "PartnerRefreshToken secret created successfully" -ForegroundColor Green
}
catch {
Write-Host "Failed to create PartnerRefreshToken secret" -ForegroundColor Red
Write-Host $_.Exception.Message
return
}
#AppDisplayName
try{
$secretvalue = ConvertTo-SecureString $AppDisplayName -AsPlainText -Force
$secret = Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppDisplayName" -SecretValue $secretvalue
Write-Host "AppDisplayName secret created successfully" -ForegroundColor Green
}
catch {
Write-Host "Failed to create AppDisplayName secret" -ForegroundColor Red
Write-Host $_.Exception.Message
return
}Make sure your Key Vault name is globally unique. It also needs to be 32 characters or fewer. The result of a successful run:

Pulling secrets back at runtime
With the secrets stored, scripts retrieve them securely instead of carrying them. Here is how to modify a script to pull the secrets from the vault:
#PARAMETERS
param (
[Parameter(Mandatory=$false)]
[string] $KeyVaultName
)
$ErrorActionPreference = "SilentlyContinue"
#See if Az.Accounts and Az.KeyVault Powershell module is installed and if not install it
Write-Host "Checking for Az module"
$AzModule = Get-Module -Name Az.Accounts -ListAvailable
if ($null -eq $AzModule) {
Write-Host "Az module not found, installing now"
Install-Module -Name Az.Accounts -Force -AllowClobber
}
else {
Write-Host "Az module found"
}
$AzModule = Get-Module -Name Az.KeyVault -ListAvailable
if ($null -eq $AzModule) {
Write-Host "Az.KeyVault module not found, installing now"
Install-Module -Name Az.KeyVault -Force -AllowClobber
}
else {
Write-Host "Az.KeyVault module found"
}
#Connect to Azure, sign in with Global Admin Credentials
try{
Connect-AzAccount
Write-Host "Connected to Azure" -ForegroundColor Green
}
catch {
Write-Host "Failed to connect to Azure" -ForegroundColor Red
Write-Host $_.Exception.Message
break
}
try {
$AppId = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppID" -AsPlainText
Write-Host "Got AppID from Key Vault" -ForegroundColor Green
}
catch {
Write-Host "Failed to get AppID from Key Vault" -ForegroundColor Red
Write-Host $_.Exception.Message
break
}
try {
$AppSecret = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppSecret" -AsPlainText
Write-Host "Got AppSecret from Key Vault" -ForegroundColor Green
}
catch {
Write-Host "Failed to get AppSecret from Key Vault" -ForegroundColor Red
Write-Host $_.Exception.Message
break
}
try {
$TenantId = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "TenantId" -AsPlainText
Write-Host "Got TenantId from Key Vault" -ForegroundColor Green
}
catch {
Write-Host "Failed to get TenantId from Key Vault" -ForegroundColor Red
Write-Host $_.Exception.Message
break
}
try {
$refreshToken = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "RefreshToken" -AsPlainText
Write-Host "Got RefreshToken from Key Vault" -ForegroundColor Green
}
catch {
Write-Host "Failed to get RefreshToken from Key Vault" -ForegroundColor Red
Write-Host $_.Exception.Message
break
}This prompts for your Azure credentials and authenticates securely using the stored secrets. An example of it inside a script:

Putting it to work: tokens and a full audit script
From here, the retrieved variables mint access tokens to run multi-tenant scripts. Two example functions, one using the refresh token for delegated access and one using client credentials for app-permission access:
function Get-GraphAccessToken ($TenantId) {
$Body = @{
'tenant' = $TenantId
'client_id' = $AppId
'scope' = 'https://graph.microsoft.com/.default'
'client_secret' = $AppSecret
'grant_type' = 'refresh_token'
'refresh_token' = $refreshToken
}
$Params = @{
'Uri' = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
'Method' = 'Post'
'ContentType' = 'application/x-www-form-urlencoded'
'Body' = $Body
}
$AuthResponse = Invoke-RestMethod @Params
Write-Host "Got accestoken for $AppID"
return $AuthResponse.access_token
}
function Get-AppPermAccessToken ($TenantId) {
$Body = @{
'tenant' = $TenantId
'client_id' = $AppId
'scope' = 'https://graph.microsoft.com/.default'
'client_secret' = $AppSecret
'grant_type' = 'client_credentials'
}
$Params = @{
'Uri' = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
'Method' = 'Post'
'ContentType' = 'application/x-www-form-urlencoded'
'Body' = $Body
}
$AuthResponse = Invoke-RestMethod @Params
Write-Host "Got accestoken for $AppID"
return $AuthResponse.access_token
}And the full multi-tenant inbox rule script, which pulls every secret from Key Vault, loops all GDAP customers, retrieves each mailbox, and exports any forwarding, redirect, or move-to-folder rules to CSV alongside an audit log:
#PARAMETERS
param (
[Parameter(Mandatory=$false)]
[string] $KeyVaultName
)
$ErrorActionPreference = "SilentlyContinue"
#See if Az.Accounts and Az.KeyVault Powershell module is installed and if not install it
Write-Host "Checking for Az module"
$AzModule = Get-Module -Name Az.Accounts -ListAvailable
if ($null -eq $AzModule) {
Write-Host "Az module not found, installing now"
Install-Module -Name Az.Accounts -Force -AllowClobber
}
else {
Write-Host "Az module found"
}
$AzModule = Get-Module -Name Az.KeyVault -ListAvailable
if ($null -eq $AzModule) {
Write-Host "Az.KeyVault module not found, installing now"
Install-Module -Name Az.KeyVault -Force -AllowClobber
}
else {
Write-Host "Az.KeyVault module found"
}
#Connect to Azure, sign in with Global Admin Credentials
try{
Connect-AzAccount
Write-Host "Connected to Azure" -ForegroundColor Green
}
catch {
Write-Host "Failed to connect to Azure" -ForegroundColor Red
Write-Host $_.Exception.Message
break
}
try {
$AppId = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppID" -AsPlainText
Write-Host "Got AppID from Key Vault" -ForegroundColor Green
}
catch {
Write-Host "Failed to get AppID from Key Vault" -ForegroundColor Red
Write-Host $_.Exception.Message
break
}
try {
$AppSecret = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppSecret" -AsPlainText
Write-Host "Got AppSecret from Key Vault" -ForegroundColor Green
}
catch {
Write-Host "Failed to get AppSecret from Key Vault" -ForegroundColor Red
Write-Host $_.Exception.Message
break
}
try {
$TenantId = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "TenantId" -AsPlainText
Write-Host "Got TenantId from Key Vault" -ForegroundColor Green
}
catch {
Write-Host "Failed to get TenantId from Key Vault" -ForegroundColor Red
Write-Host $_.Exception.Message
break
}
try {
$refreshToken = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "RefreshToken" -AsPlainText
Write-Host "Got RefreshToken from Key Vault" -ForegroundColor Green
}
catch {
Write-Host "Failed to get RefreshToken from Key Vault" -ForegroundColor Red
Write-Host $_.Exception.Message
break
}
# Function to get an access token for Microsoft Graph
function Get-GraphAccessToken ($TenantId) {
$Body = @{
'tenant' = $TenantId
'client_id' = $AppId
'scope' = 'https://graph.microsoft.com/.default'
'client_secret' = $AppSecret
'grant_type' = 'refresh_token'
'refresh_token' = $refreshToken
}
$Params = @{
'Uri' = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
'Method' = 'Post'
'ContentType' = 'application/x-www-form-urlencoded'
'Body' = $Body
}
$AuthResponse = Invoke-RestMethod @Params
Write-Host "Got accestoken for $AppID"
return $AuthResponse.access_token
}
function Get-AppPermAccessToken ($TenantId) {
$Body = @{
'tenant' = $TenantId
'client_id' = $AppId
'scope' = 'https://graph.microsoft.com/.default'
'client_secret' = $AppSecret
'grant_type' = 'client_credentials'
}
$Params = @{
'Uri' = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
'Method' = 'Post'
'ContentType' = 'application/x-www-form-urlencoded'
'Body' = $Body
}
$AuthResponse = Invoke-RestMethod @Params
Write-Host "Got accestoken for $AppID"
return $AuthResponse.access_token
}
# Initialize a list to hold audit logs
$AuditLogs = [System.Collections.Generic.List[Object]]::new()
#function to record an audit log of events
function Write-Log {
param (
[string]$customerName,
[string]$EventName,
[string]$Status,
[string]$Message
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$Result = @{
'Customer Name' = $CustomerName
'Timestamp' = $timestamp
'Event' = $EventName
'Status' = $Status
'Message' = $Message
}
$ReportLine = New-Object PSObject -Property $Result
$AuditLogs.Add($ReportLine)
# Optionally, you can also display this log message in the console
Write-Host $logMessage
}
# Initialize a list to hold the results
$Report = [System.Collections.Generic.List[Object]]::new()
# Function for adding output to the list
Function ExportCSV {
Param($MailBoxName, $UPN, $InboxRule, $CustomerName)
if ($InboxRule.actions.ForwardTo.Count -gt 0) {
# Collect email addresses into an array
$emailAddresses = $InboxRule.actions.ForwardTo | ForEach-Object { $_.emailaddress.address }
# Join email addresses with a comma
$ForwardTo = $emailAddresses -join ','
}
else {
$ForwardTo = $null
}
if ($InboxRule.actions.redirectTo.Count -gt 0) {
#loop through the fw to addresses and join them with a comma
$redirects = $InboxRule.actions.RedirectTo | ForEach-Object { $_.emailaddress.address }
$RedirectTo = $redirects -join ','
}
else {
$RedirectTo = $null
}
if ($InboxRule.actions.forwardAsAttachmentTo.Count -gt 0) {
#loop through the fw to addresses and join them with a comma
$forwardAsAttachmentTo = $InboxRule.actions.forwardAsAttachmentTo | ForEach-Object { $_.emailaddress.address }
$forwardAsAttachmentTo = $forwardAsAttachmentTo -join ','
}
else {
$forwardAsAttachmentTo = $null
}
If ($InboxRule.actions.MoveToFolder -ne $null) {
$MoveToFolder = $true
}
else {
$MoveToFolder = $null
}
$Result = @{
'Customer Name' = $CustomerName
'Mailbox Name' = $MailBoxName
'UPN' = $UPN
'Inbox Rule Name' = $InboxRule.displayName
'Enabled' = $InboxRule.isEnabled
'Forward To' = $ForwardTo
'Redirect To' = $RedirectTo
'Forward As Attachment To' = $forwardAsAttachmentTo
'Move To Folder' = $MoveToFolder
'Delete Message' = $InboxRule.actions.delete
'Mark As Read' = $InboxRule.actions.markAsRead
}
$ReportLine = New-Object PSObject -Property $Result
$Report.Add($ReportLine)
}
# Get the access token
try {
$AccessToken = Get-GraphAccessToken -TenantId $TenantId
Write-Host "Got partner access token for Microsoft Graph" -ForegroundColor Green
Write-Log -customerName "Partner Tenant" -EventName "Get-GraphAccessToken" -Status "Success" -Message "Got partner access token for Microsoft Graph"
}
catch {
Write-Host "Failed to partner access token for Microsoft Graph" -ForegroundColor Red
$ErrorMessage = $_.Exception.Message
Write-Log -customerName "Partner Tenant" -EventName "Get-GraphAccessToken" -Status "Fail" -Message $ErrorMessage
break
}
# Define header with authorization token
$Headers = @{
'Authorization' = "Bearer $AccessToken"
'Content-Type' = 'application/json'
}
# Get GDAP Customers
try{
$GraphUrl = "https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminCustomers"
$Customers = Invoke-RestMethod -Uri $GraphUrl -Headers $Headers -Method Get
#Write-Host a list of the customer display Names
Write-Host "Customers:" -ForegroundColor Green
$Customers.value.displayName
Write-Log -customerName "Partner Tenant" -EventName "Get-GDAPCustomers" -Status "Success" -Message "Got GDAP Customers"
}catch{
Write-Host "Failed to get GDAP Customers" -ForegroundColor Red
$ErrorMessage = $_.Exception.Message
Write-Log -customerName "Partner Tenant" -EventName "Get-GDAPCustomers" -Status "Fail" -Message $ErrorMessage
break
}
#Loop Through Each Customer, Connect to Exchange Online and Get Mailboxes
$Customers.value | ForEach-Object {
$Customer = $_
$CustomerName = $Customer.displayName
$CustomerTenantId = $Customer.id
# Display the customer name
Write-Host "Processing Customer: $CustomerName" -ForegroundColor Green
# Get the access token for the customer with Refresh token
try {
Write-Host "Getting Graph Token" -ForegroundColor Green
$GraphAccessToken = Get-GraphAccessToken -TenantId $CustomerTenantId
Write-Log -customerName $CustomerName -EventName "Get Graph Token for customer" -Status "Success" -Message "Got access token for Microsoft Graph"
}
catch {
$ErrorMSg = $_.Exception.Message
Write-Host "Failed to get Graph Token for $CustomerName - $ErrorMsg"
Write-Log -customerName $CustomerName -EventName "Get Graph Token for customer" -Status "Fail" -Message $_.Exception.Message
return
}
# Get the access token for the customer with client credentials
try {
Write-Host "Getting Application Permission Graph Token" -ForegroundColor Green
$AppPermAccessToken = Get-AppPermAccessToken -TenantId $CustomerTenantId
Write-Log -customerName $CustomerName -EventName "Get Application Permission Graph Token for customer" -Status "Success" -Message "Got access token for Microsoft Graph"
}
catch {
$ErrorMSg = $_.Exception.Message
Write-Host "Failed to get Application Permission Graph Token for $CustomerName - $ErrorMsg"
Write-Log -customerName $CustomerName -EventName "Get Application Permission Graph Token for customer" -Status "Fail" -Message $_.Exception.Message
return
}
try{
# Use the graph access token to call the graph API to get all mailboxes
$GraphUrl = "https://graph.microsoft.com/beta/reports/getMailboxUsageDetail(period='D7')?`$format=application/json"
$Headers = @{
'Authorization' = "Bearer $GraphAccessToken"
'Content-Type' = 'application/json'
}
$Mailboxes = Invoke-RestMethod -Uri $GraphUrl -Headers $Headers -Method Get
Write-Log -customerName $CustomerName -EventName "Get Mailboxes" -Status "Success" -Message "Retrieved mailboxes"
} catch {
$ErrorMSg = $_.Exception.Message
Write-Host "Failed to get mailboxes for $CustomerName - $ErrorMsg" -ForegroundColor Red
Write-Log -customerName $CustomerName -EventName "Get Mailboxes" -Status "Fail" -Message $_.Exception.Message
return
}
#Reset headers for the application permission token
$Headers = @{
'Authorization' = "Bearer $AppPermAccessToken"
'Content-Type' = 'application/json'
}
#loop thright the mailboxes to get the inbox rules
$Mailboxes.value | ForEach-Object {
Write-Host "Processing Mailbox: $($_.displayName)"
$MailBoxName = $_.DisplayName
$UPN = $_.UserPrincipalName
try{
$GraphInboxRuleURL = "https://graph.microsoft.com/v1.0/users/$UPN/mailFolders/inbox/messageRules"
$InboxRules = (Invoke-RestMethod -Uri $GraphInboxRuleURL -Headers $Headers -Method Get).value
#if there are any inbox rules, loop through them and export them to CSV
if ($InboxRules.Count -gt 0) {
$InboxRules | ForEach-Object {
Write-Host "Exporting Inbox Rule: $($_.displayName) for $UPN" -ForegroundColor Green
ExportCSV -MailBoxName $MailBoxName -UPN $UPN -InboxRule $_ -CustomerName $CustomerName
}
Write-Log -customerName $CustomerName -EventName "Get Inbox Rules-$UPN" -Status "Success" -Message "Retrieved inbox rules"
} else{
Write-Log -customerName $CustomerName -EventName "Get Inbox Rules-$UPN" -Status "Success" -Message "No Inbox rules for $UPN"
}
} catch {
$ErrorMSg = $_.Exception.Message
Write-Host "Failed to get inbox rules for $UPN - $ErrorMsg" -ForegroundColor Red
Write-Log -customerName $CustomerName -EventName "Get Inbox Rules-$UPN" -Status "Fail" -Message $_.Exception.Message
return
}
}
}
# When displaying and exporting, select the properties in the desired order
$Report | Select-Object 'Customer Name', 'Mailbox Name', 'UPN', 'Inbox Rule Name', 'Enabled', 'Forward To', 'Redirect To', 'Forward As Attachment To', 'Move To Folder', 'Delete Message', 'Mark As Read' | Out-GridView
$path = "$((Get-Location).Path)\Inbox Rules $(Get-Date -Format 'yyyy-MM-dd').csv"
$Report | Select-Object 'Customer Name', 'Mailbox Name', 'UPN', 'Inbox Rule Name', 'Enabled', 'Forward To', 'Redirect To', 'Forward As Attachment To', 'Move To Folder', 'Delete Message', 'Mark As Read' | Export-Csv -Path $path -NoTypeInformation -Encoding UTF8
$AuditLogPath = "$((Get-Location).Path)\Inbox Rules $(Get-Date -Format 'yyyy-MM-dd') Audit Log.csv"
$AuditLogs | Select-Object 'Customer Name', 'Timestamp', 'Event', 'Status', 'Message' | Export-CSV -Path $AuditLogPath -NoTypeInformation
Write-Host "Report saved to $Path" -ForegroundColor CyanThe payoff
Azure Key Vault in your PowerShell scripts hardens the automation by keeping sensitive information out of code, and it streamlines the workflow by serving secrets automatically, so scripts run smoothly without manual entry of credentials and can run unattended on a schedule.
Frequently asked questions
Why use Azure Key Vault instead of storing secrets in a script or local file?
Local storage and copy-paste expose app secrets and refresh tokens to anyone who reads the file or the script. Key Vault stores them encrypted, serves them at runtime to an authenticated session, and lets the same script run headless on a schedule without secrets ever living in code.
What secrets does the Secure Application Model need stored?
The setup script loads AppID, AppSecret, TenantID, RefreshToken, PartnerRefreshToken, and AppDisplayName into the vault. Runtime scripts then retrieve the subset they need to mint Graph access tokens per customer tenant.
Are there naming constraints on the Key Vault?
Yes. The vault name must be globally unique across Azure and 32 characters or fewer, or creation fails.
Automating audits is good. Automating the full assessment is better.
Instead of maintaining multi-tenant scripts for every check, CloudCapsule runs 250+ CIS-mapped controls against each tenant in 60 seconds and reports the results across your whole customer base.
See it in action
Written by
Nick Ross
CEO · Microsoft MVP · Founder, T-Minus 365
Nick is not just a CEO, he's a respected thought leader and influencer in the MSP space. Tens of thousands of MSPs learn through his YouTube channel, T-Minus365. Nick has been honored as a three-time Microsoft MVP for his educational content; his expertise and influence are the backbone of our mission, ensuring that you are in the best hands when it comes to security.
Nick joined Pax8 in 2017, where he would ultimately oversee product management for PSA and Microsoft integrations. Following his tenure at Pax8, Nick has continued to demonstrate his leadership prowess as an executive at various MSPs, culminating in his most recent role at Sourcepass.
Nick holds a Bachelor's Degree in Business Management from Florida State University, as well as a Minor Degree in Entrepreneurship. In his free time, Nick is an avid hiker, reader, and fitness-junkie.


