
Here's a scenario that should make any data engineer uncomfortable: a Power Automate flow connecting to a production SQL database, an external REST API, and an SFTP server — all with credentials stored directly in the flow's connection configuration. The flow works perfectly until the DBA rotates the database password as part of quarterly security compliance. The flow breaks silently at 2 AM, nobody notices until morning, and by the time the on-call engineer figures out what happened, six hours of data haven't been processed. Then someone discovers the old password is visible in plain text inside the connection's authentication settings, which have been exported and shared across the team.
This situation is not hypothetical. It's the default outcome when Power Automate credentials aren't managed deliberately. And as flows graduate from personal productivity tools to production infrastructure — handling financial transactions, patient data, customer-facing operations — the credential management problem moves from "technical debt" to "active liability." The good news is that Azure provides exactly the right primitives to solve this properly: Key Vault for centralized secret storage, Managed Identities for passwordless authentication, and a zero-trust architecture that means no secrets ever need to live inside your flow definitions at all.
By the end of this lesson, you will be able to design and implement production-grade credential management for Power Automate flows. You'll understand not just the mechanical steps, but the underlying security model well enough to defend your architecture choices in a review, adapt when you hit platform limitations, and rotate credentials without any flow downtime.
What you'll learn:
Before working through this lesson, you should be comfortable with:
You'll need access to an Azure subscription with permissions to create Key Vaults and Managed Identities, and a Power Platform environment with a premium license (the Key Vault connector requires a premium connector).
Before reaching for solutions, it's worth being precise about what problem we're solving, because "credentials in Power Automate" is actually several distinct problems that require different approaches.
Problem 1: Static credentials in connection configurations. When you create a Power Automate connection to SQL Server, SharePoint, or a custom API, those credentials are stored in the Power Platform connection store. They're encrypted at rest, but they're tied to a specific identity and don't rotate automatically. If that user's password changes, or their account is disabled, every flow using that connection breaks simultaneously.
Problem 2: Credentials embedded in flow actions. This is worse. When a flow makes an HTTP call with an API key in the Authorization header, that key often ends up hard-coded in the flow action configuration. It's visible to anyone who can view the flow, it shows up in flow run history if you're not careful, and rotating it means manually finding and updating every flow that uses it.
Problem 3: The bootstrap problem. You need credentials to access your credential store. If you're using Key Vault to store secrets, how does Power Automate authenticate to Key Vault without itself having credentials that need to be stored somewhere? This is where Managed Identities solve a fundamental architectural problem.
Problem 4: Audit and access control. Who accessed which secret, when, and from which flow? With credentials stored in connections or flow configurations, the answer is essentially "we have no idea." Key Vault gives you a complete audit trail.
Understanding these four problems separately helps you make better architectural decisions, because the right solution for hard-coded API keys is different from the right solution for rotating database credentials.
Key Vault is often described as "a place to store secrets," which undersells how it actually works. Understanding the internal model matters because it affects how you design your rotation strategy.
Key Vault has three distinct object types: Secrets, Keys, and Certificates. For Power Automate integration, you'll primarily work with Secrets, but the distinction matters.
A Secret in Key Vault is a name-value pair with versioning. When you update a secret — say, rotating an API key — Key Vault doesn't overwrite the old value. It creates a new version with a new version ID, while marking the previous version as superseded. The secret's name stays constant, but there's now a history of all values it has ever held. This version history is critical for rotation strategies, because it means you can always retrieve the previous version if a rotation fails midway.
Secret Name: salesforce-api-key
├── Version: 8f3a2b1c (current, created 2024-01-15)
├── Version: 7e2a1b0c (previous, created 2023-10-15)
└── Version: 6d1a0b9c (older, created 2023-07-15)
When Power Automate retrieves a secret by name without specifying a version, it always gets the current version. When it specifies a version ID, it gets exactly that version regardless of what the current one is. This distinction will be important when we talk about atomic rotation.
Key Vault access is controlled through one of two models: Vault Access Policies (the legacy model) and Azure Role-Based Access Control (RBAC). Microsoft is pushing everyone toward RBAC, and for good reason — it integrates with the same permission model you use everywhere else in Azure, it supports deny assignments, and it's auditable at the resource level. For new implementations, always use the RBAC model.
The relevant RBAC roles for Power Automate integration are:
Key Vault Secrets User — can read secret values. This is what your flows need.Key Vault Secrets Officer — can create, update, and delete secrets. This is what your rotation automation needs.Key Vault Reader — can read metadata but not secret values. Useful for monitoring flows.The separation between these roles is meaningful. Your production flows that only consume secrets should only have Key Vault Secrets User. The rotation flow that updates secrets needs Key Vault Secrets Officer. Never give a consumption flow more permission than it needs — this limits blast radius if a flow is compromised or behaves unexpectedly.
Important: Key Vault has a soft-delete feature that's now enabled by default and cannot be disabled. This means deleted secrets (and deleted Key Vaults) go into a recoverable state for a configurable retention period (7–90 days). This is almost always what you want, but it affects rotation cleanup scripts — you can't immediately reuse a deleted secret name without explicitly purging it.
The bootstrap problem — needing credentials to get credentials — is solved by Managed Identities. A Managed Identity is an identity in Microsoft Entra ID that Azure manages automatically. The secret key for this identity never exists anywhere you can see it; Azure generates it, rotates it, and uses it internally to issue tokens. You grant the identity permissions to resources (like Key Vault), and then services that have the identity assigned can authenticate to those resources without needing any credentials at all.
There are two types of Managed Identities:
System-Assigned Managed Identity: Created automatically for a specific Azure resource and tied to its lifecycle. When the resource is deleted, the identity is deleted. One-to-one relationship between resource and identity.
User-Assigned Managed Identity: Created independently as a standalone Azure resource. Can be assigned to multiple resources. When a resource is deleted, the identity persists. This is usually what you want for Power Automate scenarios because it gives you more control.
Here's where Power Automate fits into this picture, because it's more nuanced than it first appears.
Power Automate itself is a SaaS service — you can't assign a Managed Identity directly to Power Automate the way you'd assign one to an Azure Function or a Logic App. Instead, the integration works in one of three ways depending on your architecture:
In this pattern, Power Automate calls a custom connector backed by Azure API Management (APIM). APIM has a Managed Identity, and APIM is the one that actually authenticates to Key Vault. Power Automate authenticates to APIM using OAuth (typically with Entra ID), and APIM uses its Managed Identity to retrieve secrets and pass them back. This is the most enterprise-grade pattern, with the most control.
The built-in Key Vault connector for Power Automate uses OAuth with a service principal to authenticate to Key Vault. While this isn't technically a Managed Identity, it behaves similarly — you configure it once with the service principal credentials, and Power Automate handles token lifecycle automatically.
Power Automate calls an Azure Function (which has a Managed Identity assigned) via HTTP. The Function retrieves the secret from Key Vault using its Managed Identity and returns it to the flow. This is useful when you need server-side logic around secret access — logging, validation, or transforming the secret before returning it.
For most teams, Pattern 2 (the native Key Vault connector) is the right starting point. Pattern 1 and 3 are worth considering when you need audit logging beyond what Key Vault provides natively, when you need to add business logic to secret access, or when you're using the Government or sovereign clouds where connector availability differs.
Let's implement Pattern 2 in detail, then come back to Pattern 3 for more advanced scenarios.
In the Azure portal, navigate to Key Vaults and create a new vault. Use a naming convention that signals its purpose and environment — something like kv-powerautomate-prod-eastus. In the Access Configuration tab, select "Azure role-based access control" as the permission model. Enable soft delete (it should be enabled by default) and configure a 90-day retention period for production.
Under Networking, consider whether you want a public endpoint or private endpoint. For initial setup, public with selected network access (whitelisted IPs or VNet integration) is a reasonable balance. Full private endpoint requires additional networking configuration that's beyond the scope here, but note that it's available if your security posture requires it.
Rather than using an existing user account, create a dedicated service principal. In Microsoft Entra ID, navigate to App Registrations and create a new registration called something like sp-powerautomate-keyvault-prod. Note the Application (client) ID and Directory (tenant) ID.
Under Certificates & Secrets, create a new client secret. Give it a name like powerautomate-keyvault-secret and set an expiry of 24 months (Key Vault connector secrets have their own rotation that we'll handle separately). Copy the secret value immediately — you'll never see it again after you leave this page.
Warning: The service principal's client secret itself now needs to be stored somewhere. This is an unavoidable bootstrap problem unless you're using Pattern 1 or 3. The pragmatic answer: store it in Power Platform environment variables (using a secret type, which stores it in Dataverse encrypted), and accept that this one credential is the root of your trust chain. Rotate it annually as a minimum.
In your Key Vault, go to Access Control (IAM) and add a role assignment. Select "Key Vault Secrets User" and assign it to the service principal you just created. This is the only permission the principal needs for read-only secret access.
If you're building a rotation flow (which we will), create a second service principal sp-powerautomate-keyvault-rotation-prod and grant it "Key Vault Secrets Officer". Keep these separate so your production consumption flows can never accidentally modify secrets.
In Key Vault, create the secrets your flows need. Use a consistent naming convention that encodes the target system and secret type:
sql-connstring-crm-prod
sftp-password-vendor-acme-prod
salesforce-client-secret-prod
smtp-api-key-sendgrid-prod
Avoid generic names like api-key or password — when you have dozens of secrets, you need to understand what each one is for without clicking into it.
Set activation and expiration dates on secrets. Key Vault will alert you when secrets are approaching expiration, and expired secrets cannot be retrieved (the API returns an error), which enforces rotation discipline. For a 90-day rotation cycle, set expiration to 95 days, which gives you a 5-day window to rotate before things break.
In Power Automate, create a new connection to Azure Key Vault. You'll be prompted for:
Once connected, you can test the connection by using the "Get Secret" action to retrieve one of the secrets you created. If it returns the value, your authentication chain is working.
With the connection configured, here's how secret retrieval actually works in a flow, along with important nuances about performance and error handling.
The basic pattern is: at the start of a flow, retrieve all required secrets, store them in variables, and use those variables throughout the flow. Don't retrieve secrets inline mid-flow unless you have a specific reason — batching secret retrievals at the start minimizes the number of Key Vault API calls and makes flow logic cleaner.
Here's what a robust secret retrieval pattern looks like structurally:
Trigger: Recurrence (Daily at 2 AM)
Action: Get Secret - sql-connstring-crm-prod
Scope: Retrieve secret value
Action: Get Secret - sftp-password-vendor-acme-prod
Scope: Retrieve secret value
Action: Get Secret - salesforce-client-secret-prod
Scope: Retrieve secret value
Action: Initialize Variable - varSqlConnectionString
Type: String
Value: @{body('Get_Secret_-_sql-connstring-crm-prod')?['value']}
Action: Initialize Variable - varSftpPassword
Type: String
Value: @{body('Get_Secret_-_sftp-password-vendor-acme-prod')?['value']}
Action: Initialize Variable - varSalesforceClientSecret
Type: String
Value: @{body('Get_Secret_-_salesforce-client-secret-prod')?['value']}
[... rest of flow logic using variables ...]
Notice a few things here. First, the secret values are extracted from the response body using the ?['value'] path expression with the null-conditional operator — if the action somehow returns no body (which can happen with transient errors), this prevents a cryptic expression error and instead gives you a null value you can check.
Second, the secrets are immediately stored in variables. This is important because it means all downstream actions reference the variable, not the action output. This matters for two reasons: if you need to use the secret in 20 places, you have one variable to update if the retrieval logic changes; and it makes the flow more readable because varSftpPassword is self-documenting in a way that body('Get_Secret_...')?['value'] is not.
Here's where many implementations go wrong: they don't handle the case where Key Vault is unavailable or returns an error. In production, this will happen — Key Vault has an SLA of 99.9%, which means roughly 8.7 hours of downtime per year. If your flow runs frequently, some runs will hit that window.
Configure the Get Secret action with retry policy:
Then add error handling with a "Configure run after" setting on the downstream actions. If secret retrieval fails after all retries, you want the flow to fail loudly and notify the operations team, not silently skip the work.
Action: Condition - Did Secret Retrieval Succeed?
Expression: @equals(actions('Get_Secret_-_sql-connstring-crm-prod')?['status'], 'Succeeded')
If Yes: [Continue with main flow logic]
If No:
Action: Post Message to Teams Channel
Channel: #alerts-production
Message: "CRITICAL: Flow 'CRM Data Sync' failed to retrieve secrets from Key Vault.
Run ID: @{workflow()?['run']?['name']}
Failure reason: @{actions('Get_Secret_-_sql-connstring-crm-prod')?['error']?['message']}
Manual intervention required."
Action: Terminate
Status: Failed
Code: SECRET_RETRIEVAL_FAILED
Message: Key Vault unavailable
Tip: Be careful about what you include in error messages. The failure message from Key Vault is safe to log — it will say things like "Access denied" or "Secret not found" but won't contain the secret value itself. Never log the secret value, even in error conditions.
Power Automate's run history is a tremendous debugging tool, and also a potential security liability. By default, every action's inputs and outputs are stored in run history, which means your secret values could be visible to anyone who can view the flow's run history.
There are two mitigations:
Mark action outputs as secure. In the Get Secret action settings, enable "Secure outputs." This causes the action's output to appear as [REDACTED] in run history. Do this for every action that touches a secret value.
Mark variable initialization as secure. Unfortunately, you can't directly secure a variable in Power Automate the way you can secure action outputs. The workaround is to avoid storing raw secrets in regular variables if run history security is critical. Instead, use the secret inline in the action that needs it, sourced directly from the secured action output. The tradeoff is less readable flow logic, so this is a judgment call based on your security requirements.
For the variable pattern, at minimum enable secure outputs on all Get Secret actions and restrict who can view flow run history through Power Platform environment roles.
Credential rotation is where the theory meets operational reality. Rotating a credential sounds simple — generate new secret, update Key Vault, update the target system — but in production there's a gap between "I've updated the secret in Key Vault" and "all flows are now using the new secret" that can cause failures if not handled carefully.
The atomic rotation pattern solves this with a two-phase approach that Key Vault's versioning model is specifically designed to support.
Phase 1: Pre-stage the new credential
Generate the new credential (new API key, new password, etc.) but don't invalidate the old one yet. Store the new credential in Key Vault as a new version of the secret. At this point, Key Vault has two valid versions:
If the target system supports this — many APIs allow two active keys simultaneously for exactly this reason — you can update Key Vault to point to version N+1 immediately. Running flows that retrieved version N at the start of their run will complete successfully using the old credential. New flow runs will retrieve version N+1 automatically (since they always get the current version).
Phase 2: Invalidate the old credential
After a safety window — long enough for all in-flight runs using the old credential to complete — revoke the old credential from the target system. Version N in Key Vault still exists but the target system will reject it. This doesn't matter because no running flow should be using it anymore.
Here's what this looks like as a Power Automate rotation flow:
Trigger: Recurrence (Every 90 days) or Manual trigger
// Step 1: Generate new credential in target system
Action: HTTP - POST to Salesforce API to rotate client secret
URI: https://login.salesforce.com/services/oauth2/token
Method: POST
Headers: Content-Type: application/x-www-form-urlencoded
Body: [authentication payload using current credentials]
Action: Parse JSON - Parse new credential response
Schema: {
"type": "object",
"properties": {
"client_secret": { "type": "string" },
"expires_at": { "type": "string" }
}
}
// Step 2: Update Key Vault with new credential
Action: HTTP - PUT new secret version to Key Vault
Method: PUT
URI: https://{vault-name}.vault.azure.net/secrets/salesforce-client-secret-prod?api-version=7.4
Authentication: Active Directory OAuth
Authority: https://login.microsoftonline.com/
Tenant: {tenant-id}
Audience: https://vault.azure.net
Client ID: {rotation-sp-client-id}
Secret: {rotation-sp-secret} // from env variable
Body: {
"value": "@{body('Parse_JSON')?['client_secret']}",
"attributes": {
"enabled": true,
"exp": {unix timestamp 90 days from now}
}
}
// Step 3: Verify new credential works
Action: HTTP - Test new credential against Salesforce
[Use the new secret directly from the parse response to test auth]
Action: Condition - Did verification succeed?
If No:
// Rollback: the old Key Vault version is still valid
// Revoke the new credential from Salesforce
// Send alert
Action: Terminate (Failed)
If Yes:
// Step 4: Wait for in-flight flows to complete
Action: Delay - 30 minutes
// Step 5: Revoke old credential from Salesforce
Action: HTTP - Revoke old Salesforce client secret
// Step 6: Log successful rotation
Action: HTTP - POST to audit log
Critical: The rotation flow itself needs credentials to authenticate to Key Vault (to write the new secret) and to the target system (to generate the new credential). These bootstrapping credentials for the rotation flow should live in Power Platform environment variables and be rotated manually on a longer cycle (annually). Accept that you have a small set of "root credentials" that require human rotation — the goal is to minimize their number and scope, not eliminate them entirely.
Some systems only support a single active credential. Rotating these is riskier because there's an inherent gap between invalidating the old credential and propagating the new one to all consumers.
For these systems, you need a maintenance window approach:
PUT /flows/{id} endpoint with state: "Suspended")The flow suspension/resumption can itself be automated in another flow that has permissions to call the Power Platform API. This turns a manual maintenance window into a scheduled automation — you just need to define the maintenance window time and which flows are in scope.
For teams that need more control — custom audit logging, secret transformation, or true Managed Identity authentication without a service principal — the Azure Function broker pattern is worth the additional complexity.
The architecture is:
Power Automate Flow
↓ (HTTP call with Entra ID token)
Azure Function (System-Assigned Managed Identity)
↓ (Managed Identity token, no credentials)
Azure Key Vault
↓ (returns secret value)
Azure Function
↓ (returns secret to caller)
Power Automate Flow
The Function is the only component that ever touches Key Vault. Power Automate authenticates to the Function using Entra ID, and the Function's Managed Identity authenticates to Key Vault. No credentials appear anywhere in the Power Automate flow configuration.
Here's the key function code (C#):
[FunctionName("GetSecret")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req,
ILogger log)
{
// Validate the caller's Entra ID token
string authHeader = req.Headers["Authorization"];
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
return new UnauthorizedResult();
string token = authHeader.Substring("Bearer ".Length);
// Validate token against your tenant and expected audience
var validationParameters = new TokenValidationParameters
{
ValidAudience = Environment.GetEnvironmentVariable("FUNCTION_APP_CLIENT_ID"),
ValidIssuer = $"https://login.microsoftonline.com/{tenantId}/v2.0",
IssuerSigningKeys = await GetSigningKeysAsync() // fetch from OIDC endpoint
};
ClaimsPrincipal principal;
try
{
principal = new JwtSecurityTokenHandler()
.ValidateToken(token, validationParameters, out _);
}
catch (SecurityTokenException ex)
{
log.LogWarning("Token validation failed: {Error}", ex.Message);
return new UnauthorizedResult();
}
// Get the secret name from the request
string secretName = req.Query["secretName"];
if (string.IsNullOrEmpty(secretName) || !IsAllowedSecretName(principal, secretName))
{
log.LogWarning("Caller {User} requested unauthorized secret {Secret}",
principal.Identity?.Name, secretName);
return new ForbidResult();
}
// Retrieve from Key Vault using Managed Identity
var credential = new DefaultAzureCredential();
var client = new SecretClient(
new Uri($"https://{vaultName}.vault.azure.net"),
credential);
KeyVaultSecret secret = await client.GetSecretAsync(secretName);
// Log the access for audit
log.LogInformation("Secret {SecretName} retrieved by {Caller} at {Time}",
secretName, principal.Identity?.Name, DateTimeOffset.UtcNow);
return new OkObjectResult(new { value = secret.Value });
}
private static bool IsAllowedSecretName(ClaimsPrincipal principal, string secretName)
{
// Implement your access control logic here
// For example, check the caller's groups or roles against a policy
var allowedSecrets = GetAllowedSecretsForCaller(principal);
return allowedSecrets.Contains(secretName);
}
The IsAllowedSecretName function is where you implement your least-privilege policy. Rather than giving Power Automate access to every secret in the vault, you define — in code — which flow identities can access which secrets. This is more granular than Key Vault RBAC, which operates at the vault or secret level but not on which callers can access which specific secrets.
In Power Automate, calling this function uses the HTTP action with Entra ID authentication:
Action: HTTP - Get Secret from Broker
Method: GET
URI: https://{function-app-name}.azurewebsites.net/api/GetSecret?secretName=salesforce-client-secret-prod
Authentication: Active Directory OAuth
Authority: https://login.microsoftonline.com/{tenant-id}/
Audience: api://{function-app-client-id}
Client ID: {power-automate-service-principal-client-id}
Secret: {service-principal-secret} // stored in env variable
The key advantage here is that Key Vault's audit log will show the Function's Managed Identity as the accessor — not Power Automate — and your Function's own logs will show which Power Automate caller requested which secret. You get a two-level audit trail that's impossible to achieve with the native Key Vault connector.
"Zero Trust" is often used as marketing language, but it has a specific technical meaning that's directly applicable here: never trust any identity by default, verify explicitly, use least-privilege access, and assume breach.
Applied to Power Automate, this means:
Never trust that a secret is still valid. Even if a secret was valid when you retrieved it 10 minutes ago, downstream systems can revoke it without warning. Build retry logic with fresh secret retrieval on authentication failures:
Action: HTTP - Call CRM API
// uses varCrmApiKey retrieved at flow start
Action: Condition - Did API call return 401?
If Yes:
// Re-retrieve the secret — it may have been rotated mid-flow
Action: Get Secret - crm-api-key-prod
Action: Set Variable - varCrmApiKey
Value: @{body('Get_Secret_-_crm-api-key-prod')?['value']}
// Retry the API call once
Action: HTTP - Call CRM API (Retry)
Action: Condition - Did retry return 401?
If Yes:
// Now it's a genuine authentication failure
Action: Terminate (Failed) with alert
Verify identity at every boundary. When Power Automate calls your Function broker, the Function validates the caller's token. When the Function calls Key Vault, Key Vault validates the Managed Identity token. No hop trusts the previous hop — each boundary has its own authentication check.
Scope secrets to minimum necessary lifetime. A flow that runs hourly doesn't need a secret that's valid for a week. If you're generating short-lived credentials (JWT tokens, SAS tokens, OAuth bearer tokens), generate them as close to usage as possible and don't store them longer than needed. Power Automate variables exist only for the duration of the flow run, which makes them naturally scoped — they can't leak to other flow runs.
Log everything. Key Vault's diagnostic settings can be configured to stream audit logs to Log Analytics. Enable this and create alerts for:
Here's a Kusto query for the Key Analytics workspace that surfaces suspicious access patterns:
AzureDiagnostics
| where ResourceType == "VAULTS"
| where OperationName == "SecretGet"
| where ResultType != "Success"
| summarize FailureCount = count() by CallerIPAddress, identity_claim_appid_g, bin(TimeGenerated, 1h)
| where FailureCount > 5
| order by FailureCount desc
This flags any caller that's failing to retrieve secrets more than 5 times per hour — which could indicate a misconfigured flow, a compromised identity trying incorrect secret names, or an expired service principal credential.
In this exercise, you'll build a complete secret-managed flow that connects to a REST API (we'll use the publicly available JSONPlaceholder API to simulate a CRM), retrieves data, and posts a summary to a Teams channel. The focus is on implementing all the security patterns correctly, not the business logic.
Part 1: Azure Resources
Create a Key Vault named kv-exercise-[yourname]-dev in your subscription using RBAC access control model.
Create a service principal sp-pa-exercise-dev in Entra ID with a client secret. Record the client ID, secret value, and your tenant ID.
Assign Key Vault Secrets User role to the service principal on your Key Vault.
Create the following secrets in Key Vault:
jsonplaceholder-base-url with value https://jsonplaceholder.typicode.comteams-webhook-url with your Teams channel's incoming webhook URLexercise-api-key with value Bearer demo-key-12345 (simulating an API key)Enable diagnostic logging on the Key Vault and stream to a Log Analytics workspace.
Part 2: Power Automate Environment Variables
KeyVaultClientId with the service principal's client ID.KeyVaultClientSecret with the service principal's client secret.KeyVaultTenantId with your tenant ID.Build the flow:
Trigger: Manual trigger (for testing)
Add three Get Secret actions to retrieve all three secrets. Enable Secure Outputs on each. Name them descriptively.
Add error handling: after the secret retrieval actions, add a condition checking if all three succeeded (use and(equals(actions('Get_..._base_url')?['status'],'Succeeded'), ...). If any failed, terminate with a failure message.
Initialize variables for each secret value.
Add an HTTP action to call @{variables('varBaseUrl')}/posts?userId=1 with the Authorization header set to @{variables('varApiKey')}. Enable Secure Inputs (since the API key is in the header).
Parse the JSON response using a schema derived from the API's actual response structure.
Compose a summary: count of posts returned, the title of the first post, and the timestamp.
Add an HTTP action to POST to the Teams webhook URL from @{variables('varWebhookUrl')} with an Adaptive Card payload containing the summary.
Add a final HTTP action to write an audit log entry to Log Analytics using the Data Collector API.
In Key Vault, update the exercise-api-key secret with a new value: Bearer demo-key-99999. This creates version 2.
Run the flow. Verify it retrieves the new value (version 2) automatically.
In Key Vault, disable version 1 of the secret (you can do this in the Versions tab by selecting the old version and disabling it). This simulates invalidating the old key.
Run the flow again and verify it still works — it was already using version 2.
Check the Key Vault audit logs in Log Analytics and find the two SecretGet events from your flow runs.
[REDACTED] for all secret values, not actual valuesThis almost always means one of three things: a naming mismatch (the flow is requesting salesforce-api-key but the secret is named salesforce-api-key-prod), the service principal doesn't have the Key Vault Secrets User role assigned on the correct vault, or the secret is disabled or expired. Check the Key Vault audit log first — it will show you the exact secret name that was requested and whether access was denied at the auth level or the secret-doesn't-exist level.
The Get Secret action can return a 200 status (success) but with an empty value if the secret's value itself is an empty string. This can happen after a botched rotation where someone accidentally set the secret to an empty value. Add a validation step after secret retrieval: if(empty(variables('varSomeSecret')), ...) and fail fast if any secret is unexpectedly empty.
The Power Automate Key Vault connection caches the service principal credentials. After rotating the service principal's own client secret, you need to update the connection in Power Automate with the new secret value, then re-save all flows that use that connection. This is the main operational burden of the service principal approach. Set a calendar reminder 30 days before the service principal secret expires — don't discover it's expired when flows start failing.
When using the HTTP connector to call Key Vault directly (instead of the built-in connector), the most common issue is the OAuth audience. Key Vault requires the audience to be exactly https://vault.azure.net — including the trailing slash in some SDK versions, no trailing slash in others. Try both. Also verify that the OAuth token request is going to https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token with scope=https://vault.azure.net/.default.
If you're passing a secret value through an intermediate action (like a Compose or a string interpolation) that doesn't have Secure Outputs enabled, the secret can reappear. Audit every action between secret retrieval and secret usage. Any action whose output could contain the secret value needs Secure Outputs enabled. Pay particular attention to Compose actions and Parse JSON actions — these are common places where secrets re-surface.
This is why you kept the old Key Vault version. If your rotation flow fails after updating Key Vault but before revoking the old credential from the target system, you're in the best possible failure state: Key Vault has the new credential (which works), and the target system still accepts the old credential (which no longer appears in Key Vault as current). Running flows will start using the new credential automatically. The old credential will eventually expire. You can manually complete the cleanup — revoke the old credential from the target system — without any urgency.
Key Vault has throttling limits: 2000 GET operations per 10 seconds per region (as of the current limits, which you should verify in the Azure docs as they change). If you have many high-frequency flows all retrieving secrets individually, you can hit this limit. Mitigations: cache secrets for the flow run duration (which you're already doing with variables), consider a secret caching layer in an Azure Function, or spread flows across multiple Key Vaults geographically.
You've covered a lot of ground. Let's consolidate what you've learned and where this takes you.
The fundamental insight is that credentials in production Power Automate flows need to be treated as infrastructure, not configuration. The Key Vault plus service principal pattern solves the visibility problem (secrets aren't in flow definitions), the rotation problem (new versions are automatically picked up), and the audit problem (every access is logged). The Azure Function broker pattern extends this to solve the bootstrap problem more completely and adds fine-grained access control that Key Vault RBAC alone can't provide.
Credential rotation isn't a one-time event — it's an ongoing operational process. The two-phase rotation pattern (pre-stage the new credential while the old one is still valid, then invalidate the old one after a safety window) is universally applicable and worth implementing as a template flow that you parameterize for each credential type. Your rotation schedules should be driven by Key Vault's expiration alerts, not by memory or calendar reminders.
Zero-trust applied to Power Automate means: least-privilege service principals, scoped per environment and purpose; short-lived secrets where possible; validation at every authentication boundary; and enough logging to reconstruct any access event from the audit trail.
Where to go next:
Start with the exercise as written, then extend it in two directions. First, implement the Azure Function broker pattern for at least one secret — the operational complexity is worth understanding hands-on before you need to deploy it under pressure. Second, build a rotation flow for one of your real production credentials, starting with a non-critical system where a failed rotation won't cause a production incident.
From a broader learning perspective, the next logical steps are:
The security posture you've built here — centralized secrets, Managed Identity authentication, automated rotation, comprehensive audit logging — isn't just good for Power Automate. It's a pattern that applies across your entire data platform. Get it right here, and you have a template for every service that touches sensitive credentials.