{"id":12878,"date":"2026-08-26T09:47:16","date_gmt":"2026-08-26T16:47:16","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/cosmosdb\/?p=12878"},"modified":"2026-08-27T07:45:14","modified_gmt":"2026-08-27T14:45:14","slug":"i-need-my-ci-cd-pipeline-to-access-cosmos-db-without-using-a-secret","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/cosmosdb\/i-need-my-ci-cd-pipeline-to-access-cosmos-db-without-using-a-secret\/","title":{"rendered":"I Need My CI\/CD Pipeline to Access Cosmos DB Without Using a Secret"},"content":{"rendered":"<p><em><span class=\"TextRun SCXW26541042 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW26541042 BCX8\">In the last post we walked through <\/span><\/span><a class=\"Hyperlink SCXW26541042 BCX8\" href=\"https:\/\/devblogs.microsoft.com\/cosmosdb\/i-enabled-rbac-and-everything-broke-what-did-i-do-wrong\/\" target=\"_blank\" rel=\"noreferrer noopener\"><span class=\"TextRun Underlined SCXW26541042 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"none\"><span class=\"NormalTextRun SCXW26541042 BCX8\" data-ccp-charstyle=\"Hyperlink\">why RBAC broke your app and how to fix it<\/span><\/span><\/a><span class=\"TextRun SCXW26541042 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW26541042 BCX8\">. Your app is now running on a managed identity, keys are off, and everything is green. Then your build pipeline runs, and it needs to seed test data. Suddenly <\/span><span class=\"NormalTextRun SCXW26541042 BCX8\">you&#8217;re<\/span><span class=\"NormalTextRun SCXW26541042 BCX8\"> staring at a secret in your CI settings and wondering if you just undid all that work. As we continue this security series, <\/span><span class=\"NormalTextRun SCXW26541042 BCX8\">let&#8217;s<\/span><span class=\"NormalTextRun SCXW26541042 BCX8\"> get your pipeline to Cosmos DB with no secret anywhere.<\/span><\/span><\/em><span class=\"EOP Selected SCXW26541042 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<h3 aria-level=\"2\"><span data-contrast=\"none\">The situation<\/span><span data-ccp-props=\"{&quot;134245418&quot;:true,&quot;134245529&quot;:true,&quot;335559738&quot;:160,&quot;335559739&quot;:80}\">\u00a0<\/span><\/h3>\n<p><span data-contrast=\"auto\">You did the hard part. Your app uses <\/span><code><span data-contrast=\"auto\">DefaultAzureCredential<\/span><\/code><span data-contrast=\"auto\">, you assigned a data plane role, and you set <\/span><code><span data-contrast=\"auto\">disableLocalAuth: true<\/span><\/code><span data-contrast=\"auto\">. Passwordless, done.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">Then you look at your CI\/CD pipeline and find this sitting in your repository secrets:<\/span><\/p>\n<p><code><span class=\"TextRun SCXW18458778 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW18458778 BCX8\">AZURE_CLIENT_SECRET = &lt;<\/span><span class=\"NormalTextRun SCXW18458778 BCX8\">a very long<\/span><span class=\"NormalTextRun SCXW18458778 BCX8\"> string that expires in 6 months&gt;<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW18458778 BCX8\"><span class=\"SCXW18458778 BCX8\">\u00a0<\/span><br class=\"SCXW18458778 BCX8\" \/><\/span><span class=\"TextRun SCXW18458778 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW18458778 BCX8\">COSMOS_CONNECTION_STRING = <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW18458778 BCX8\">AccountEndpoint<\/span><span class=\"NormalTextRun SCXW18458778 BCX8\">=https:\/\/...;<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW18458778 BCX8\">AccountKey<\/span><span class=\"NormalTextRun SCXW18458778 BCX8\">=...<\/span><\/span><span class=\"EOP Selected SCXW18458778 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/p>\n<p><span data-contrast=\"auto\">That connection string doesn&#8217;t even work anymore \u2014 local auth is off. And the client secret is exactly the kind of long-lived credential you just spent a sprint eliminating from your app. It sits in a settings page, it gets copied into a runbook, someone pastes it in a chat, and in six months it expires at 2am during a release.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">There is a better answer, and it&#8217;s not &#8220;store the secret somewhere nicer.&#8221; It&#8217;s <\/span><b><span data-contrast=\"auto\">workload identity federation<\/span><\/b><span data-contrast=\"auto\">: your pipeline proves who it is with a short-lived OIDC token issued by GitHub or Azure DevOps, and Entra ID trades that token for an Azure access token. No secret is stored anywhere. Nothing expires. Nothing to rotate.<\/span><\/p>\n<h3 aria-level=\"2\"><span data-contrast=\"none\">The mental model: three things have to line up<\/span><span data-ccp-props=\"{&quot;134245418&quot;:true,&quot;134245529&quot;:true,&quot;335559738&quot;:160,&quot;335559739&quot;:80}\">\u00a0<\/span><\/h3>\n<p><span data-contrast=\"auto\">Before touching any YAML, get this picture straight. When your pipeline talks to Cosmos DB, three separate things have to be true:<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<ol>\n<li><b><span data-contrast=\"auto\">Trust<\/span><\/b><span data-contrast=\"auto\"> \u2014 Entra ID has to believe the token your CI platform hands it. That&#8217;s the federated credential.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<li aria-setsize=\"-1\" data-leveltext=\"%1.\" data-font=\"\" data-listid=\"4\" data-list-defn-props=\"{&quot;335552541&quot;:0,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769242&quot;:[65533,0],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;%1.&quot;,&quot;469777815&quot;:&quot;hybridMultilevel&quot;}\" data-aria-posinset=\"2\" data-aria-level=\"1\"><b><span data-contrast=\"auto\">Control plane authorization<\/span><\/b><span data-contrast=\"auto\"> \u2014 if the pipeline deploys infrastructure (creates the account, databases, containers), it needs an Azure RBAC role like <\/span><span data-contrast=\"auto\">DocumentDB Account Contributor<\/span><span data-contrast=\"auto\">.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<li aria-setsize=\"-1\" data-leveltext=\"%1.\" data-font=\"\" data-listid=\"4\" data-list-defn-props=\"{&quot;335552541&quot;:0,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769242&quot;:[65533,0],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;%1.&quot;,&quot;469777815&quot;:&quot;hybridMultilevel&quot;}\" data-aria-posinset=\"2\" data-aria-level=\"1\"><b><span data-contrast=\"auto\">Data plane authorization<\/span><\/b><span data-contrast=\"auto\"> \u2014 if the pipeline reads, writes, or seeds <\/span><i><span data-contrast=\"auto\">items<\/span><\/i><span data-contrast=\"auto\">, it needs a Cosmos DB data plane role assignment.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<\/ol>\n<p><span data-contrast=\"auto\">Most people set up #1, get excited that <\/span><code><span data-contrast=\"auto\">az login<\/span><\/code><span data-contrast=\"auto\"> works, and then get blindsided by a <\/span><code><span data-contrast=\"auto\">403<\/span><\/code><span data-contrast=\"auto\"> because they never did #3. If that sounds familiar, the previous post has the full breakdown of why control plane and data plane are separate systems.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-content\/uploads\/sites\/52\/2026\/09\/Blog-4-2.jpg\"><img decoding=\"async\" class=\"aligncenter wp-image-12896 size-large\" src=\"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-content\/uploads\/sites\/52\/2026\/09\/Blog-4-2-1024x1024.jpg\" alt=\"Hand-drawn infographic titled \u201cPasswordless CI\/CD to Azure Cosmos DB,\u201d showing a GitHub Actions workflow using OIDC authentication, Microsoft Entra ID, and Azure Cosmos DB. The diagram illustrates the token flow between GitHub Actions and Entra ID, configuration steps for federated credentials and Cosmos DB access, security practices such as avoiding client secrets and connection strings, and a sample data plane API connection.\" width=\"1024\" height=\"1024\" srcset=\"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-content\/uploads\/sites\/52\/2026\/09\/Blog-4-2-1024x1024.jpg 1024w, https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-content\/uploads\/sites\/52\/2026\/09\/Blog-4-2-300x300.jpg 300w, https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-content\/uploads\/sites\/52\/2026\/09\/Blog-4-2-150x150.jpg 150w, https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-content\/uploads\/sites\/52\/2026\/09\/Blog-4-2-768x768.jpg 768w, https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-content\/uploads\/sites\/52\/2026\/09\/Blog-4-2-24x24.jpg 24w, https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-content\/uploads\/sites\/52\/2026\/09\/Blog-4-2-48x48.jpg 48w, https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-content\/uploads\/sites\/52\/2026\/09\/Blog-4-2-96x96.jpg 96w, https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-content\/uploads\/sites\/52\/2026\/09\/Blog-4-2.jpg 1254w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><\/p>\n<h3 aria-level=\"2\"><span data-contrast=\"none\">GitHub Actions: the walkthrough<\/span><span data-ccp-props=\"{&quot;134245418&quot;:true,&quot;134245529&quot;:true,&quot;335559738&quot;:160,&quot;335559739&quot;:80}\">\u00a0<\/span><\/h3>\n<h4 aria-level=\"3\"><span data-contrast=\"none\">Step 1: Create an identity for the pipeline<\/span><span data-ccp-props=\"{&quot;134245418&quot;:true,&quot;134245529&quot;:true,&quot;335559738&quot;:160,&quot;335559739&quot;:80}\">\u00a0<\/span><\/h4>\n<p><span data-contrast=\"auto\">You can federate either an app registration or a user-assigned managed identity. I&#8217;ll use an app registration here because it&#8217;s the most common, but a user-assigned managed identity works the same way and is worth considering if you want the identity to live in a resource group with the rest of your infrastructure.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p aria-level=\"3\"><code><span class=\"TextRun SCXW168844416 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW168844416 BCX8\">APP_ID=$(<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW168844416 BCX8\">az<\/span><span class=\"NormalTextRun SCXW168844416 BCX8\"> ad app create --display-name \"<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW168844416 BCX8\">gh<\/span><span class=\"NormalTextRun SCXW168844416 BCX8\">-actions-<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW168844416 BCX8\">myrepo<\/span><span class=\"NormalTextRun SCXW168844416 BCX8\">\" --query <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW168844416 BCX8\">appId<\/span><span class=\"NormalTextRun SCXW168844416 BCX8\"> -o <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW168844416 BCX8\">tsv<\/span><span class=\"NormalTextRun SCXW168844416 BCX8\">)<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW168844416 BCX8\"><span class=\"SCXW168844416 BCX8\">\u00a0<\/span><br class=\"SCXW168844416 BCX8\" \/><\/span><span class=\"TextRun SCXW168844416 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW168844416 BCX8\">az ad <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW168844416 BCX8\">sp<\/span><span class=\"NormalTextRun SCXW168844416 BCX8\"> create --id \"$APP_ID\"<\/span><\/span><span class=\"EOP Selected SCXW168844416 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/p>\n<h4 aria-level=\"3\"><span data-contrast=\"none\">Step 2: Add the federated credential<\/span><span data-ccp-props=\"{&quot;134245418&quot;:true,&quot;134245529&quot;:true,&quot;335559738&quot;:160,&quot;335559739&quot;:80}\">\u00a0<\/span><\/h4>\n<p><span data-contrast=\"auto\">This is where you tell Entra ID which workflow is allowed to use this identity. The <\/span><span data-contrast=\"auto\">subject<\/span><span data-contrast=\"auto\"> is the important field \u2014 it has to match the OIDC token GitHub issues, exactly.<\/span><\/p>\n<p><code><span class=\"TextRun SCXW204982456 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SpellingErrorV2Themed SCXW204982456 BCX8\">az<\/span><span class=\"NormalTextRun SCXW204982456 BCX8\"> ad app federated-credential create \\<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW204982456 BCX8\"><span class=\"SCXW204982456 BCX8\">\u00a0<\/span><br class=\"SCXW204982456 BCX8\" \/><\/span><span class=\"TextRun SCXW204982456 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW204982456 BCX8\">\u00a0 --id \"$APP_ID\" \\<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW204982456 BCX8\"><span class=\"SCXW204982456 BCX8\">\u00a0<\/span><br class=\"SCXW204982456 BCX8\" \/><\/span><span class=\"TextRun SCXW204982456 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW204982456 BCX8\">\u00a0 --parameters '{<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW204982456 BCX8\"><span class=\"SCXW204982456 BCX8\">\u00a0<\/span><br class=\"SCXW204982456 BCX8\" \/><\/span><span class=\"TextRun SCXW204982456 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW204982456 BCX8\">\u00a0\u00a0\u00a0 \"name\": \"<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW204982456 BCX8\">github<\/span><span class=\"NormalTextRun SCXW204982456 BCX8\">-prod-env\",<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW204982456 BCX8\"><span class=\"SCXW204982456 BCX8\">\u00a0<\/span><br class=\"SCXW204982456 BCX8\" \/><\/span><span class=\"TextRun SCXW204982456 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW204982456 BCX8\">\u00a0\u00a0\u00a0 \"issuer\": \"https:\/\/token.actions.githubusercontent.com\",<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW204982456 BCX8\"><span class=\"SCXW204982456 BCX8\">\u00a0<\/span><br class=\"SCXW204982456 BCX8\" \/><\/span><span class=\"TextRun SCXW204982456 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW204982456 BCX8\">\u00a0\u00a0\u00a0 \"subject\": \"<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW204982456 BCX8\">repo:my-org<\/span><span class=\"NormalTextRun SCXW204982456 BCX8\">\/<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW204982456 BCX8\">my-repo:environment:production<\/span><span class=\"NormalTextRun SCXW204982456 BCX8\">\",<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW204982456 BCX8\"><span class=\"SCXW204982456 BCX8\">\u00a0<\/span><br class=\"SCXW204982456 BCX8\" \/><\/span><span class=\"TextRun SCXW204982456 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW204982456 BCX8\">\u00a0\u00a0\u00a0 \"audiences\": [\"<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW204982456 BCX8\">api<\/span><span class=\"NormalTextRun SCXW204982456 BCX8\">:\/\/<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW204982456 BCX8\">AzureADTokenExchange<\/span><span class=\"NormalTextRun SCXW204982456 BCX8\">\"]<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW204982456 BCX8\"><span class=\"SCXW204982456 BCX8\">\u00a0<\/span><br class=\"SCXW204982456 BCX8\" \/><\/span><span class=\"TextRun SCXW204982456 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW204982456 BCX8\">\u00a0 }'<\/span><\/span><span class=\"EOP Selected SCXW204982456 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/p>\n<p><span data-contrast=\"auto\">A few notes on <\/span><code><span data-contrast=\"auto\">subject<\/span><\/code><span data-contrast=\"auto\">, because this is where people miss:<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<table style=\"border-collapse: collapse; width: 100%; height: 96px;\">\n<tbody>\n<tr style=\"height: 24px;\">\n<td style=\"width: 50%; height: 24px;\"><strong><span class=\"TextRun SCXW37180359 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW37180359 BCX8\">What you want<\/span><\/span><span class=\"EOP Selected SCXW37180359 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/strong><\/td>\n<td style=\"width: 50%; height: 24px;\"><strong>Subject<\/strong><\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 50%; height: 24px;\">A specific branch<\/td>\n<td style=\"width: 50%; height: 24px;\"><code><span class=\"TextRun SCXW127617819 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SpellingErrorV2Themed SCXW127617819 BCX8\" data-ccp-charstyle=\"HTML Code\">repo:my-org<\/span><span class=\"NormalTextRun SCXW127617819 BCX8\" data-ccp-charstyle=\"HTML Code\">\/<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW127617819 BCX8\" data-ccp-charstyle=\"HTML Code\">my-repo:ref:refs<\/span><span class=\"NormalTextRun SCXW127617819 BCX8\" data-ccp-charstyle=\"HTML Code\">\/heads\/main<\/span><\/span><span class=\"EOP Selected SCXW127617819 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 50%; height: 24px;\"><span class=\"TextRun SCXW152346134 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW152346134 BCX8\">A GitHub environment<\/span><\/span><span class=\"EOP Selected SCXW152346134 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/td>\n<td style=\"width: 50%; height: 24px;\"><code><span class=\"TextRun SCXW89726255 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SpellingErrorV2Themed SCXW89726255 BCX8\" data-ccp-charstyle=\"HTML Code\">repo:my-org<\/span><span class=\"NormalTextRun SCXW89726255 BCX8\" data-ccp-charstyle=\"HTML Code\">\/<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW89726255 BCX8\" data-ccp-charstyle=\"HTML Code\">my-repo:environment:production<\/span><\/span><span class=\"EOP Selected SCXW89726255 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 50%; height: 24px;\"><span class=\"TextRun SCXW267323264 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW267323264 BCX8\">Pull request builds<\/span><\/span><span class=\"EOP Selected SCXW267323264 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/td>\n<td style=\"width: 50%; height: 24px;\"><code><span class=\"TextRun SCXW141549619 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SpellingErrorV2Themed SCXW141549619 BCX8\" data-ccp-charstyle=\"HTML Code\">repo:my-org<\/span><span class=\"NormalTextRun SCXW141549619 BCX8\" data-ccp-charstyle=\"HTML Code\">\/<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW141549619 BCX8\" data-ccp-charstyle=\"HTML Code\">my-repo:pull_request<\/span><\/span><span class=\"EOP Selected SCXW141549619 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><span class=\"TextRun SCXW212014815 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><strong><span class=\"NormalTextRun SCXW212014815 BCX8\">Pr<\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">efer <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">scoping <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">the <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">federated <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">credential<\/span> <span class=\"NormalTextRun SCXW212014815 BCX8\">to a <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">GitHub <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">environment<\/span><\/strong><span class=\"NormalTextRun SCXW212014815 BCX8\">.<\/span><\/span><span class=\"TextRun SCXW212014815 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"> <span class=\"NormalTextRun SCXW212014815 BCX8\">B<\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">ranch<\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">-based<\/span><span class=\"NormalTextRun SCXW212014815 BCX8\"> subjects <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">don&#8217;t<\/span><span class=\"NormalTextRun SCXW212014815 BCX8\"> support wildcards, so <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">every <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">new <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">release branch requires another <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">federated credential<\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">. Environment-based subjects are easier to manage, support approvals and branch protection, and remain stable as branches come and go. You can assign<\/span><span class=\"NormalTextRun SCXW212014815 BCX8\"> up to 20 federated credentials <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">to a single<\/span><span class=\"NormalTextRun SCXW212014815 BCX8\"> identity, so <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">plan their <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">use <\/span><span class=\"NormalTextRun SCXW212014815 BCX8\">carefully<\/span><\/span><span class=\"TextRun SCXW212014815 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW212014815 BCX8\">.<\/span><\/span><span class=\"EOP Selected SCXW212014815 BCX8\" data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:300}\">\u00a0<\/span><\/p>\n<h4 aria-level=\"3\"><span data-contrast=\"none\">Step 3: Check which subject format your repo uses<\/span><span data-ccp-props=\"{&quot;134245418&quot;:true,&quot;134245529&quot;:true,&quot;335559738&quot;:160,&quot;335559739&quot;:80}\">\u00a0<\/span><\/h4>\n<p><span data-contrast=\"auto\">This one is new and it will quietly break setups that used to work.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">The OIDC spec requires subject claims to be locally unique and never reassigned. The old format used only organization and repository\u202f<\/span><i><span data-contrast=\"auto\">names<\/span><\/i><span data-contrast=\"auto\">, which meant a recycled namespace could produce the same subject value under a different owner. To close that hole,\u202f<\/span><b><span data-contrast=\"auto\">repositories created after July 15, 2026 use an immutable default subject format that includes the owner ID and the repository ID.<\/span><\/b><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<ul>\n<li aria-setsize=\"-1\" data-leveltext=\"\uf0b7\" data-font=\"Symbol\" data-listid=\"1\" data-list-defn-props=\"{&quot;335552541&quot;:1,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769226&quot;:&quot;Symbol&quot;,&quot;469769242&quot;:[8226],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;\uf0b7&quot;,&quot;469777815&quot;:&quot;multilevel&quot;}\" data-aria-posinset=\"1\" data-aria-level=\"1\"><span data-contrast=\"auto\">Previous format:\u202f<code>repo:sample-org\/octo-repo:ref:refs\/heads\/main<\/code><\/span><code><span data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/li>\n<\/ul>\n<ul>\n<li aria-setsize=\"-1\" data-leveltext=\"\uf0b7\" data-font=\"Symbol\" data-listid=\"1\" data-list-defn-props=\"{&quot;335552541&quot;:1,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769226&quot;:&quot;Symbol&quot;,&quot;469769242&quot;:[8226],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;\uf0b7&quot;,&quot;469777815&quot;:&quot;multilevel&quot;}\" data-aria-posinset=\"2\" data-aria-level=\"1\"><span data-contrast=\"auto\">Immutable format:\u202f<code>repo:sample-org@123456\/octo-repo@456789:ref:refs\/heads\/main<\/code><\/span><code><span data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/li>\n<\/ul>\n<p><span data-contrast=\"auto\">The\u202f@\u202fseparator is used because\u202f@\u202fcan&#8217;t appear in a GitHub username or repository name. Repositories created before July 15, 2026 keep the previous format unless they opt in, at the organization or repository level, through the OIDC settings UI or REST API.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">Two consequences worth internalizing:<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<ol>\n<li><b><span data-contrast=\"auto\">A new repo needs the new format.<\/span><\/b><span data-contrast=\"auto\">\u202fIf you copy a federated credential from an older repo into a repo you created last month, the subject won&#8217;t match and the exchange will fail.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<li aria-setsize=\"-1\" data-leveltext=\"%1.\" data-font=\"\" data-listid=\"5\" data-list-defn-props=\"{&quot;335552541&quot;:0,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769242&quot;:[65533,0],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;%1.&quot;,&quot;469777815&quot;:&quot;multilevel&quot;}\" data-aria-posinset=\"2\" data-aria-level=\"1\"><b><span data-contrast=\"auto\">Renames and transfers after July 15, 2026 move the repository to the immutable format.<\/span><\/b><span data-contrast=\"auto\">\u202fSo a rename doesn&#8217;t just change the name inside your subject string, it changes the shape of the whole claim. If someone renames the repo, expect to rewrite the credential, not just edit it.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<\/ol>\n<p><span data-contrast=\"auto\">Immutable subject claims aren&#8217;t available on GitHub Enterprise Server. And if you customize claims with\u202f<code>include_claim_keys,<\/code> the owner and repo IDs are always included in the\u202f<code>repo<\/code>\u202fsegment for repositories on the immutable format. You can&#8217;t remove them.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">The reliable move here is to stop guessing: print the actual subject your workflow produces (see suspect #1) and register that.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<h4 aria-level=\"3\"><span data-contrast=\"none\">Step 4: Grant control plane access (only if the pipeline deploys infra)<\/span><span data-ccp-props=\"{&quot;134245418&quot;:true,&quot;134245529&quot;:true,&quot;335559738&quot;:160,&quot;335559739&quot;:80}\">\u00a0<\/span><\/h4>\n<p aria-level=\"3\"><code><span class=\"TextRun SCXW254469588 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SpellingErrorV2Themed SCXW254469588 BCX8\">az<\/span><span class=\"NormalTextRun SCXW254469588 BCX8\"> role assignment <\/span><span class=\"NormalTextRun ContextualSpellingAndGrammarErrorV2Themed SCXW254469588 BCX8\">create<\/span><span class=\"NormalTextRun SCXW254469588 BCX8\"> \\<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW254469588 BCX8\"><span class=\"SCXW254469588 BCX8\">\u00a0<\/span><br class=\"SCXW254469588 BCX8\" \/><\/span><span class=\"TextRun SCXW254469588 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW254469588 BCX8\">\u00a0 --assignee \"$APP_ID\" \\<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW254469588 BCX8\"><span class=\"SCXW254469588 BCX8\">\u00a0<\/span><br class=\"SCXW254469588 BCX8\" \/><\/span><span class=\"TextRun SCXW254469588 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW254469588 BCX8\">\u00a0 --role \"<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW254469588 BCX8\">DocumentDB<\/span><span class=\"NormalTextRun SCXW254469588 BCX8\"> Account Contributor\" \\<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW254469588 BCX8\"><span class=\"SCXW254469588 BCX8\">\u00a0<\/span><br class=\"SCXW254469588 BCX8\" \/><\/span><span class=\"TextRun SCXW254469588 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW254469588 BCX8\">\u00a0 --scope \"\/subscriptions\/$SUB_ID\/<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW254469588 BCX8\">resourceGroups<\/span><span class=\"NormalTextRun SCXW254469588 BCX8\">\/$RESOURCE_GROUP\"<\/span><\/span><span class=\"EOP Selected SCXW254469588 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/p>\n<p aria-level=\"3\">Skip this entirely if the pipeline only touches data. Not every pipeline needs to manage the account.<\/p>\n<h4 aria-level=\"3\">Ste<span data-contrast=\"none\">p 5: Grant data plane access (this is the one people forget)<\/span><span data-ccp-props=\"{&quot;134245418&quot;:true,&quot;134245529&quot;:true,&quot;335559738&quot;:160,&quot;335559739&quot;:80}\">\u00a0<\/span><\/h4>\n<p><span class=\"TextRun SCXW84987983 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW84987983 BCX8\">PRINCIPAL_ID=$(<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW84987983 BCX8\">az<\/span><span class=\"NormalTextRun SCXW84987983 BCX8\"> ad <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW84987983 BCX8\">sp<\/span><span class=\"NormalTextRun SCXW84987983 BCX8\"> show &#8211;id &#8220;$APP_ID&#8221; &#8211;query id -o <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW84987983 BCX8\">tsv<\/span><span class=\"NormalTextRun SCXW84987983 BCX8\">)<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW84987983 BCX8\"><span class=\"SCXW84987983 BCX8\">\u00a0<\/span><br class=\"SCXW84987983 BCX8\" \/><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW84987983 BCX8\"><span class=\"SCXW84987983 BCX8\">\u00a0<\/span><br class=\"SCXW84987983 BCX8\" \/><\/span><code><span class=\"TextRun SCXW84987983 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW84987983 BCX8\">az cosmosdb sql role assignment create \\<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW84987983 BCX8\"><span class=\"SCXW84987983 BCX8\">\u00a0<\/span><br class=\"SCXW84987983 BCX8\" \/><\/span><span class=\"TextRun SCXW84987983 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW84987983 BCX8\">\u00a0 --account-name \"$ACCOUNT\" \\<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW84987983 BCX8\"><span class=\"SCXW84987983 BCX8\">\u00a0<\/span><br class=\"SCXW84987983 BCX8\" \/><\/span><span class=\"TextRun SCXW84987983 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW84987983 BCX8\">\u00a0 --resource-group \"$RESOURCE_GROUP\" \\<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW84987983 BCX8\"><span class=\"SCXW84987983 BCX8\">\u00a0<\/span><br class=\"SCXW84987983 BCX8\" \/><\/span><span class=\"TextRun SCXW84987983 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW84987983 BCX8\">\u00a0 --role-definition-id \"00000000-0000-0000-0000-000000000002\" \\<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW84987983 BCX8\"><span class=\"SCXW84987983 BCX8\">\u00a0<\/span><br class=\"SCXW84987983 BCX8\" \/><\/span><span class=\"TextRun SCXW84987983 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW84987983 BCX8\">\u00a0 --scope \"\/<\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW84987983 BCX8\">dbs<\/span><span class=\"NormalTextRun SCXW84987983 BCX8\">\/orders-test\" \\<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW84987983 BCX8\"><span class=\"SCXW84987983 BCX8\">\u00a0<\/span><br class=\"SCXW84987983 BCX8\" \/><\/span><span class=\"TextRun SCXW84987983 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW84987983 BCX8\">\u00a0 --principal-id \"$PRINCIPAL_ID\"<\/span><\/span><span class=\"EOP Selected SCXW84987983 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/p>\n<p><span data-contrast=\"auto\">That role definition ID is the built-in <\/span><b><span data-contrast=\"auto\">Cosmos DB Built-in Data Contributor<\/span><\/b><span data-contrast=\"auto\">. If the pipeline only runs read-only smoke tests, use <\/span><span data-contrast=\"auto\">&#8230;0001<\/span><span data-contrast=\"auto\"> (Data Reader) instead. And note the scope: I pointed it at the test database, not <\/span><span data-contrast=\"auto\">\/<\/span><span data-contrast=\"auto\">. Your CI identity runs unattended on every push \u2014 it&#8217;s the last identity that should have account-wide write access.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<h4 aria-level=\"3\"><span data-contrast=\"none\">Step 6: Store the identifiers (not secrets)<\/span><span data-ccp-props=\"{&quot;134245418&quot;:true,&quot;134245529&quot;:true,&quot;335559738&quot;:160,&quot;335559739&quot;:80}\">\u00a0<\/span><\/h4>\n<p><span data-contrast=\"auto\">Add these to your repository or environment <\/span><b><span data-contrast=\"auto\">variables<\/span><\/b><span data-contrast=\"auto\">:<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<ul>\n<li aria-setsize=\"-1\" data-leveltext=\"\u2022\" data-font=\"\" data-listid=\"3\" data-list-defn-props=\"{&quot;335552541&quot;:1,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769242&quot;:[8226],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;\u2022&quot;,&quot;469777815&quot;:&quot;hybridMultilevel&quot;}\" data-aria-posinset=\"1\" data-aria-level=\"1\"><code><span data-contrast=\"auto\">AZURE_CLIENT_ID<\/span><\/code><span data-contrast=\"auto\"> \u2014 the app ID from step 1<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<\/ul>\n<ul>\n<li aria-setsize=\"-1\" data-leveltext=\"\u2022\" data-font=\"\" data-listid=\"3\" data-list-defn-props=\"{&quot;335552541&quot;:1,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769242&quot;:[8226],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;\u2022&quot;,&quot;469777815&quot;:&quot;hybridMultilevel&quot;}\" data-aria-posinset=\"2\" data-aria-level=\"1\"><code><span data-contrast=\"auto\">AZURE_TENANT_ID<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/li>\n<\/ul>\n<ul>\n<li aria-setsize=\"-1\" data-leveltext=\"\u2022\" data-font=\"\" data-listid=\"3\" data-list-defn-props=\"{&quot;335552541&quot;:1,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769242&quot;:[8226],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;\u2022&quot;,&quot;469777815&quot;:&quot;hybridMultilevel&quot;}\" data-aria-posinset=\"3\" data-aria-level=\"1\"><code><span data-contrast=\"auto\">AZURE_SUBSCRIPTION_ID<\/span><\/code><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<\/ul>\n<p><span data-contrast=\"auto\">None of these are secrets. They&#8217;re identifiers. They&#8217;re useless without a token from your specific repository and environment. Plenty of teams still put them in <\/span><span data-contrast=\"auto\">secrets<\/span><span data-contrast=\"auto\"> out of habit, and that&#8217;s fine, but understanding that they&#8217;re <\/span><i><span data-contrast=\"auto\">not<\/span><\/i><span data-contrast=\"auto\"> sensitive is the point of the whole exercise.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<h4 aria-level=\"3\">Step 7: The workflow<\/h4>\n<pre><code>name: Integration tests\r\n\r\non:\r\n  push:\r\n    branches: [main]\r\n\r\npermissions:\r\n  id-token: write   # required to request the OIDC token\r\n  contents: read\r\n\r\njobs:\r\n  test:\r\n    runs-on: ubuntu-latest\r\n    environment: production   # must match your federated credential subject\r\n\r\n    steps:\r\n      - uses: actions\/checkout@v4\r\n\r\n      - uses: azure\/login@v2\r\n        with:\r\n          client-id: ${{ vars.AZURE_CLIENT_ID }}\r\n          tenant-id: ${{ vars.AZURE_TENANT_ID }}\r\n          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}\r\n\r\n      - name: Run integration tests against Cosmos DB\r\n        env:\r\n          COSMOS_ENDPOINT: https:\/\/my-account.documents.azure.com:443\/\r\n        run: dotnet test<\/code><\/pre>\n<p><span data-contrast=\"auto\">Notice what&#8217;s missing: <\/span><span data-contrast=\"auto\">client-secret<\/span><span data-contrast=\"auto\">. And notice what your test code looks like \u2014 exactly the same as your app:<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><code><span class=\"TextRun SCXW115791077 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW115791077 BCX8\">var client = new <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW115791077 BCX8\">CosmosClient<\/span><span class=\"NormalTextRun SCXW115791077 BCX8\">(<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW115791077 BCX8\"><span class=\"SCXW115791077 BCX8\">\u00a0<\/span><br class=\"SCXW115791077 BCX8\" \/><\/span><span class=\"TextRun SCXW115791077 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW115791077 BCX8\">\u00a0\u00a0\u00a0 <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW115791077 BCX8\">accountEndpoint<\/span><span class=\"NormalTextRun SCXW115791077 BCX8\">: <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW115791077 BCX8\">Environment.GetEnvironmentVariable<\/span><span class=\"NormalTextRun SCXW115791077 BCX8\">(\"COSMOS_ENDPOINT\"),<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW115791077 BCX8\"><span class=\"SCXW115791077 BCX8\">\u00a0<\/span><br class=\"SCXW115791077 BCX8\" \/><\/span><span class=\"TextRun SCXW115791077 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW115791077 BCX8\">\u00a0\u00a0\u00a0 <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW115791077 BCX8\">tokenCredential<\/span><span class=\"NormalTextRun SCXW115791077 BCX8\">: new <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW115791077 BCX8\">DefaultAzureCredential<\/span><span class=\"NormalTextRun SCXW115791077 BCX8\">());<\/span><\/span><span class=\"EOP Selected SCXW115791077 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/p>\n<p><span data-contrast=\"auto\">azure\/login@v2<\/span><span data-contrast=\"auto\"> logs the Azure CLI in on the runner, and <\/span><code><span data-contrast=\"auto\">DefaultAzureCredential<\/span><\/code><span data-contrast=\"auto\"> picks that up through its Azure CLI credential. Same code in CI, same code in production, same code on your laptop. That&#8217;s the payoff.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">Missing <\/span><span data-contrast=\"auto\">permissions: id-token: write<\/span><span data-contrast=\"auto\"> is the single most common reason this step fails, and the error message is not obvious about it.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<h3 aria-level=\"2\"><span data-contrast=\"none\">Azure Pipelines: the same idea, fewer steps<\/span><span data-ccp-props=\"{&quot;134245418&quot;:true,&quot;134245529&quot;:true,&quot;335559738&quot;:160,&quot;335559739&quot;:80}\">\u00a0<\/span><\/h3>\n<p><span data-contrast=\"auto\">Azure DevOps does most of this for you. Create a service connection of type <\/span><b><span data-contrast=\"auto\">Azure Resource Manager<\/span><\/b><span data-contrast=\"auto\"> and choose <\/span><b><span data-contrast=\"auto\">Workload Identity federation (automatic)<\/span><\/b><span data-contrast=\"auto\">. Azure DevOps creates the app registration and the federated credential for you, with the subject bound to your project and service connection.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">Then assign the roles exactly as above \u2014 find the service connection&#8217;s principal ID in Entra ID, and run the same <\/span><span data-contrast=\"auto\"><code>az cosmosdb sql role<\/code> assignment create<\/span><span data-contrast=\"auto\"> command. Azure DevOps sets up trust; it does <\/span><b><span data-contrast=\"auto\">not<\/span><\/b><span data-contrast=\"auto\"> know anything about Cosmos DB data plane roles. That part is still on you.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><code><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">trigger:<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><span class=\"SCXW181617136 BCX8\">\u00a0<\/span><br class=\"SCXW181617136 BCX8\" \/><\/span><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">\u00a0 branches:<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><span class=\"SCXW181617136 BCX8\">\u00a0<\/span><br class=\"SCXW181617136 BCX8\" \/><\/span><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">\u00a0\u00a0\u00a0 <\/span><span class=\"NormalTextRun ContextualSpellingAndGrammarErrorV2Themed SCXW181617136 BCX8\">include:<\/span><span class=\"NormalTextRun SCXW181617136 BCX8\"> [main]<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><span class=\"SCXW181617136 BCX8\">\u00a0<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><br class=\"SCXW181617136 BCX8\" \/><\/span><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">steps:<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><span class=\"SCXW181617136 BCX8\">\u00a0<\/span><br class=\"SCXW181617136 BCX8\" \/><\/span><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">\u00a0 - task: AzureCLI@2<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><span class=\"SCXW181617136 BCX8\">\u00a0<\/span><br class=\"SCXW181617136 BCX8\" \/><\/span><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">\u00a0\u00a0\u00a0 <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW181617136 BCX8\">displayName<\/span><span class=\"NormalTextRun SCXW181617136 BCX8\">: Run integration tests<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><span class=\"SCXW181617136 BCX8\">\u00a0<\/span><br class=\"SCXW181617136 BCX8\" \/><\/span><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">\u00a0\u00a0\u00a0 inputs:<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><span class=\"SCXW181617136 BCX8\">\u00a0<\/span><br class=\"SCXW181617136 BCX8\" \/><\/span><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">\u00a0\u00a0\u00a0\u00a0\u00a0 <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW181617136 BCX8\">azureSubscription<\/span><span class=\"NormalTextRun SCXW181617136 BCX8\">: 'my-workload-identity-connection'<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><span class=\"SCXW181617136 BCX8\">\u00a0<\/span><br class=\"SCXW181617136 BCX8\" \/><\/span><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">\u00a0\u00a0\u00a0\u00a0\u00a0 <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW181617136 BCX8\">scriptType<\/span><span class=\"NormalTextRun SCXW181617136 BCX8\">: bash<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><span class=\"SCXW181617136 BCX8\">\u00a0<\/span><br class=\"SCXW181617136 BCX8\" \/><\/span><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">\u00a0\u00a0\u00a0\u00a0\u00a0 <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW181617136 BCX8\">scriptLocation<\/span><span class=\"NormalTextRun SCXW181617136 BCX8\">: inlineScript<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><span class=\"SCXW181617136 BCX8\">\u00a0<\/span><br class=\"SCXW181617136 BCX8\" \/><\/span><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">\u00a0\u00a0\u00a0\u00a0\u00a0 <\/span><span class=\"NormalTextRun SpellingErrorV2Themed SCXW181617136 BCX8\">inlineScript<\/span><span class=\"NormalTextRun SCXW181617136 BCX8\">: |<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><span class=\"SCXW181617136 BCX8\">\u00a0<\/span><br class=\"SCXW181617136 BCX8\" \/><\/span><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 dotnet test<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><span class=\"SCXW181617136 BCX8\">\u00a0<\/span><br class=\"SCXW181617136 BCX8\" \/><\/span><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">\u00a0\u00a0\u00a0 env:<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW181617136 BCX8\"><span class=\"SCXW181617136 BCX8\">\u00a0<\/span><br class=\"SCXW181617136 BCX8\" \/><\/span><span class=\"TextRun SCXW181617136 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW181617136 BCX8\">\u00a0\u00a0\u00a0\u00a0\u00a0 COSMOS_ENDPOINT: https:\/\/my-account.documents.azure.com:443\/<\/span><\/span><\/code><span class=\"EOP Selected SCXW181617136 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">Anything you run inside <\/span><code><span data-contrast=\"auto\">AzureCLI@2<\/span><\/code><span data-contrast=\"auto\"> inherits the CLI login, so <\/span><code><span data-contrast=\"auto\">DefaultAzureCredential<\/span><\/code><span data-contrast=\"auto\"> works the same way it does in GitHub Actions. If your test process runs outside that task, it won&#8217;t have a credential \u2014 see suspect #3 below.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">One housekeeping note: if you have older service connections created with a service principal and secret, they still have a secret sitting in Azure DevOps with an expiry date. Converting them to workload identity federation is a supported in-place operation and worth doing.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<h3 aria-level=\"2\"><span data-contrast=\"none\">The five usual suspects<\/span><span data-ccp-props=\"{&quot;134245418&quot;:true,&quot;134245529&quot;:true,&quot;335559738&quot;:160,&quot;335559739&quot;:80}\">\u00a0<\/span><\/h3>\n<ol>\n<li><code><strong>AADSTS70021: No matching federated identity record found<\/strong><\/code><\/li>\n<\/ol>\n<p><span data-contrast=\"auto\">Your subject doesn&#8217;t match. This is a string comparison, and it is unforgiving.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">It&#8217;s worth knowing\u202f<\/span><i><span data-contrast=\"auto\">why<\/span><\/i><span data-contrast=\"auto\">\u202fthis is so easy to get wrong: when you create a federated identity credential with an incorrect subject,\u202f<\/span><b><span data-contrast=\"auto\">it is created successfully, with no error.<\/span><\/b><span data-contrast=\"auto\">\u202fEntra ID doesn&#8217;t validate it against anything, because there&#8217;s nothing to validate it against yet. The mistake only surfaces when a real token exchange fails.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:240,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">So don&#8217;t eyeball it. Print the actual subject your workflow produces and compare it character by character to what you registered.\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">Common mismatches:<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:240,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<ul>\n<li aria-setsize=\"-1\" data-leveltext=\"\uf0b7\" data-font=\"Symbol\" data-listid=\"6\" data-list-defn-props=\"{&quot;335552541&quot;:1,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769226&quot;:&quot;Symbol&quot;,&quot;469769242&quot;:[8226],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;\uf0b7&quot;,&quot;469777815&quot;:&quot;multilevel&quot;}\" data-aria-posinset=\"1\" data-aria-level=\"1\"><span data-contrast=\"auto\">You registered\u202f<\/span><code><span data-contrast=\"auto\">environment:production<\/span><\/code><span data-contrast=\"auto\">\u202fbut the job has no\u202f<\/span><code><span data-contrast=\"auto\">environment:<\/span><\/code><span data-contrast=\"auto\">\u202fkey, or has\u202f<\/span><code><span data-contrast=\"auto\">environment: prod<\/span><\/code><span data-contrast=\"auto\">.<\/span><span data-ccp-props=\"{&quot;134233117&quot;:true,&quot;134233118&quot;:true,&quot;201341983&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/li>\n<\/ul>\n<ul>\n<li aria-setsize=\"-1\" data-leveltext=\"\uf0b7\" data-font=\"Symbol\" data-listid=\"6\" data-list-defn-props=\"{&quot;335552541&quot;:1,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769226&quot;:&quot;Symbol&quot;,&quot;469769242&quot;:[8226],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;\uf0b7&quot;,&quot;469777815&quot;:&quot;multilevel&quot;}\" data-aria-posinset=\"2\" data-aria-level=\"1\"><span data-contrast=\"auto\">You registered\u202f<\/span><code><span data-contrast=\"auto\">ref:refs\/heads\/main<\/span><\/code><span data-contrast=\"auto\">\u202fand the run was triggered on a tag, which produces\u202f<\/span><code><span data-contrast=\"auto\">ref:refs\/tags\/v1.0<\/span><\/code><span data-contrast=\"auto\">.<\/span><span data-ccp-props=\"{&quot;134233117&quot;:true,&quot;134233118&quot;:true,&quot;201341983&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/li>\n<\/ul>\n<ul>\n<li aria-setsize=\"-1\" data-leveltext=\"\uf0b7\" data-font=\"Symbol\" data-listid=\"6\" data-list-defn-props=\"{&quot;335552541&quot;:1,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769226&quot;:&quot;Symbol&quot;,&quot;469769242&quot;:[8226],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;\uf0b7&quot;,&quot;469777815&quot;:&quot;multilevel&quot;}\" data-aria-posinset=\"3\" data-aria-level=\"1\"><span data-contrast=\"auto\">The job references an environment, so the subject contains the environment and\u202f<\/span><i><span data-contrast=\"auto\">not<\/span><\/i><span data-contrast=\"auto\">\u202fthe branch. Environment wins.<\/span><span data-ccp-props=\"{&quot;134233117&quot;:true,&quot;134233118&quot;:true,&quot;201341983&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/li>\n<\/ul>\n<ul>\n<li aria-setsize=\"-1\" data-leveltext=\"\uf0b7\" data-font=\"Symbol\" data-listid=\"6\" data-list-defn-props=\"{&quot;335552541&quot;:1,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769226&quot;:&quot;Symbol&quot;,&quot;469769242&quot;:[8226],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;\uf0b7&quot;,&quot;469777815&quot;:&quot;multilevel&quot;}\" data-aria-posinset=\"4\" data-aria-level=\"1\"><span data-contrast=\"auto\">The repo was renamed or transferred after July 15, 2026, so it moved to the immutable subject format and your old subject can&#8217;t match anymore.<\/span><span data-ccp-props=\"{&quot;134233117&quot;:true,&quot;134233118&quot;:true,&quot;201341983&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/li>\n<\/ul>\n<ul>\n<li aria-setsize=\"-1\" data-leveltext=\"\uf0b7\" data-font=\"Symbol\" data-listid=\"6\" data-list-defn-props=\"{&quot;335552541&quot;:1,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769226&quot;:&quot;Symbol&quot;,&quot;469769242&quot;:[8226],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;\uf0b7&quot;,&quot;469777815&quot;:&quot;multilevel&quot;}\" data-aria-posinset=\"5\" data-aria-level=\"1\"><span data-contrast=\"auto\">The repo is new (created after July 15, 2026) and you copied a subject from an older repo that&#8217;s still on the legacy format.<\/span><span data-ccp-props=\"{&quot;134233117&quot;:true,&quot;134233118&quot;:true,&quot;201341983&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/li>\n<\/ul>\n<ul>\n<li aria-setsize=\"-1\" data-leveltext=\"\uf0b7\" data-font=\"Symbol\" data-listid=\"6\" data-list-defn-props=\"{&quot;335552541&quot;:1,&quot;335559685&quot;:720,&quot;335559991&quot;:360,&quot;469769226&quot;:&quot;Symbol&quot;,&quot;469769242&quot;:[8226],&quot;469777803&quot;:&quot;left&quot;,&quot;469777804&quot;:&quot;\uf0b7&quot;,&quot;469777815&quot;:&quot;multilevel&quot;}\" data-aria-posinset=\"6\" data-aria-level=\"1\"><span data-contrast=\"auto\">Your environment name contains a colon. Any\u202f<\/span><span data-contrast=\"auto\">:<\/span><span data-contrast=\"auto\">\u202fin a metadata value is escaped to\u202f<\/span><code><span data-contrast=\"auto\">%3A<\/span><\/code><span data-contrast=\"auto\">, so\u202f<\/span><code><span data-contrast=\"auto\">Production:V1<\/span><\/code><span data-contrast=\"auto\">\u202fappears in the subject as\u202f<\/span><code><span data-contrast=\"auto\">Production%3AV1<\/span><\/code><span data-contrast=\"auto\">.<\/span><span data-ccp-props=\"{&quot;134233117&quot;:true,&quot;134233118&quot;:true,&quot;201341983&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/li>\n<\/ul>\n<ol start=\"2\">\n<li aria-level=\"3\"><strong><code>az login<\/code> works, Cosmos DB returns 403<\/strong><\/li>\n<\/ol>\n<p><span data-contrast=\"auto\">Trust is fine. Authorization isn&#8217;t. You almost certainly did the Azure RBAC assignment and skipped the data plane one. Check:<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><code><span class=\"TextRun SCXW174723895 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SpellingErrorV2Themed SpellingErrorHighlight SCXW174723895 BCX8\">az<\/span><span class=\"NormalTextRun SCXW174723895 BCX8\"> cosmosdb sql role assignment list \\<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW174723895 BCX8\"><span class=\"SCXW174723895 BCX8\">\u00a0<\/span><br class=\"SCXW174723895 BCX8\" \/><\/span><span class=\"TextRun SCXW174723895 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW174723895 BCX8\">\u00a0 --account-name \"$ACCOUNT\" \\<\/span><\/span><span class=\"LineBreakBlob BlobObject DragDrop SCXW174723895 BCX8\"><span class=\"SCXW174723895 BCX8\">\u00a0<\/span><br class=\"SCXW174723895 BCX8\" \/><\/span><span class=\"TextRun SCXW174723895 BCX8\" lang=\"EN-US\" xml:lang=\"EN-US\" data-contrast=\"auto\"><span class=\"NormalTextRun SCXW174723895 BCX8\">\u00a0 --resource-group \"$RESOURCE_GROUP\"<\/span><\/span><span class=\"EOP Selected SCXW174723895 BCX8\" data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/p>\n<p><span data-contrast=\"auto\">If the pipeline&#8217;s principal ID isn&#8217;t in that output, that&#8217;s your bug. Data plane assignments never show up in <\/span><code><span data-contrast=\"auto\">az role assignment list<\/span><\/code><span data-contrast=\"auto\"> or in the portal&#8217;s Access control (IAM) blade \u2014 they&#8217;re a separate system with a separate command.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<ol start=\"3\">\n<li aria-level=\"3\"><strong> The credential works in one step but not another<\/strong><\/li>\n<\/ol>\n<p><span data-contrast=\"auto\">The OIDC login belongs to the shell session on the runner. If your tests run somewhere that shell can&#8217;t reach \u2014 inside a Docker container you started, in a separate job, on a different runner \u2014 the credential doesn&#8217;t follow.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">For containers, either pass the token through explicitly or run the container with the relevant environment variables mapped. For separate jobs, each job needs its own <\/span><code><span data-contrast=\"auto\">azure\/login<\/span><\/code><span data-contrast=\"auto\"> step and its own <\/span><code><span data-contrast=\"auto\">id-token: write<\/span><\/code><span data-contrast=\"auto\"> permission. Jobs don&#8217;t inherit login state from each other.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<ol start=\"4\">\n<li aria-level=\"3\"><strong> Your Bicep or ARM template still calls listKeys()<\/strong><\/li>\n<\/ol>\n<p><span data-contrast=\"auto\">This one is sneaky, and it&#8217;s specific to Cosmos DB. Plenty of templates end with something like:<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><code><span data-contrast=\"auto\">output connectionString string = listKeys(cosmosAccount.id, '2024-11-15').primaryMasterKey<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/code><\/p>\n<p><span data-contrast=\"auto\">Once <\/span><code><span data-contrast=\"auto\">disableLocalAuth<\/span><\/code><span data-contrast=\"auto\"> is <\/span><span data-contrast=\"auto\">true<\/span><span data-contrast=\"auto\">, that call fails, and it takes the whole deployment with it \u2014 after your infrastructure changes have partially applied. Search your templates for <\/span><code><span data-contrast=\"auto\">listKeys<\/span><\/code><span data-contrast=\"auto\">, <\/span><code><span data-contrast=\"auto\">listConnectionStrings<\/span><\/code><span data-contrast=\"auto\">, and <\/span><code><span data-contrast=\"auto\">primaryMasterKey<\/span><\/code><span data-contrast=\"auto\">, and delete those outputs. Your app doesn&#8217;t need them anymore.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<ol start=\"5\">\n<li aria-level=\"3\"><strong> Pull request builds from forks get nothing<\/strong><\/li>\n<\/ol>\n<p><span data-contrast=\"auto\">A fork&#8217;s workflow run doesn&#8217;t receive an OIDC token for your repository, by design. If your PR validation needs Cosmos DB, either run those tests only on branches in the repo, or use the\u00a0<\/span><a href=\"https:\/\/learn.microsoft.com\/azure\/cosmos-db\/emulator\"><span data-contrast=\"none\">Azure Cosmos DB emulator<\/span><\/a><span data-contrast=\"auto\"> for fork PRs and save the real account for post-merge. Don&#8217;t work around it by adding a secret \u2014 that&#8217;s the exact hole federation is closing.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<h3 aria-level=\"2\"><strong>A fast diagnostic order\u00a0<\/strong><\/h3>\n<p><span data-contrast=\"auto\">When your pipeline fails, work down this list:<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<ol>\n<li><span data-contrast=\"auto\">Does the job have <\/span><code><span data-contrast=\"auto\">permissions: id-token: write<\/span><\/code><span data-contrast=\"auto\">?<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<li><span data-contrast=\"auto\">Does the <\/span><code><span data-contrast=\"auto\">subject<\/span><\/code><span data-contrast=\"auto\"> on the federated credential exactly match what this run produces?<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<li><span data-contrast=\"auto\">Does the pipeline&#8217;s principal have a <\/span><b><span data-contrast=\"auto\">data plane<\/span><\/b><span data-contrast=\"auto\"> role assignment, not just an Azure RBAC one?<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<li><span data-contrast=\"auto\">Does the data plane scope cover the database and container the pipeline actually touches?<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<li><span data-contrast=\"auto\">Is the failing step running outside the shell that did the Azure login?<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<li><span data-contrast=\"auto\">Is anything in your templates or scripts still asking for a key?<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<\/ol>\n<h3 aria-level=\"2\"><span data-contrast=\"none\">Wrapping up<\/span><span data-ccp-props=\"{&quot;134245418&quot;:true,&quot;134245529&quot;:true,&quot;335559738&quot;:160,&quot;335559739&quot;:80}\">\u00a0<\/span><\/h3>\n<p><span data-contrast=\"auto\">The instinct when a pipeline needs access is to reach for a secret, because that&#8217;s what pipelines have always used. But a CI\/CD identity is a great candidate for federation: it runs in a known place, on known events, on behalf of a known repository. That&#8217;s exactly the information an OIDC token carries, and exactly what Entra ID can verify without anything being stored.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">Set it up once and there&#8217;s nothing to rotate, nothing to leak, and nothing to expire at 2am. Your pipeline authenticates the same way your app does, with the same code, and the &#8220;who has access to production data&#8221; question finally has an answer you can query.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<p aria-level=\"3\"><strong>Your turn\u00a0<\/strong><\/p>\n<ul>\n<li><span data-contrast=\"auto\">Open your CI settings right now and look for <\/span><span data-contrast=\"auto\">A<code>ZURE_CLIENT_SECRET,<\/code><\/span><code><span data-contrast=\"auto\">COSMOS_CONNECTION_STRING<\/span><\/code><span data-contrast=\"auto\">, or anything with <\/span><span data-contrast=\"auto\">AccountKey<\/span><span data-contrast=\"auto\"> in it. Whatever you find, that&#8217;s your first migration.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<li><span data-contrast=\"auto\">Set up federation on a non-production pipeline first and scope the data plane role to a test database rather than <\/span><span data-contrast=\"auto\">\/<\/span><span data-contrast=\"auto\">.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<li><span data-contrast=\"auto\">Grep your Bicep and ARM templates for <\/span><code><span data-contrast=\"auto\">listKeys<\/span><\/code><span data-contrast=\"auto\"> before you flip <\/span><code><span data-contrast=\"auto\">disableLocalAuth<\/span><\/code><span data-contrast=\"auto\">. It&#8217;s the step people skip.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<li><span data-contrast=\"auto\">Stuck on a subject claim that won&#8217;t match, or a <code>403<\/code> you can&#8217;t explain? Drop your scenario in the comments and I&#8217;ll help you dig in.<\/span><span data-ccp-props=\"{}\">\u00a0<\/span><\/li>\n<\/ul>\n<p><i><span data-contrast=\"auto\">If this series saved you a debugging session, share it with whoever owns your build pipeline. Their expiring secret will thank you.<\/span><\/i><span data-ccp-props=\"{}\">\u00a0<\/span><\/p>\n<h2><strong>About Azure Cosmos DB<\/strong><\/h2>\n<p>Azure Cosmos DB is a fully managed and serverless NoSQL and vector database for modern app development, including AI applications. With its SLA-backed speed and availability as well as instant dynamic scalability, it is ideal for real-time NoSQL and MongoDB applications that require high performance and distributed computing over massive volumes of NoSQL and vector data.<\/p>\n<p>To stay in the loop on Azure Cosmos DB updates, follow us on\u00a0<a href=\"https:\/\/twitter.com\/AzureCosmosDB\" target=\"_blank\" rel=\"noopener\">X<\/a>,\u00a0<a href=\"https:\/\/aka.ms\/AzureCosmosDBYouTube\" target=\"_blank\" rel=\"noopener\">YouTube<\/a>, and\u00a0<a href=\"https:\/\/www.linkedin.com\/company\/azure-cosmos-db\/\" target=\"_blank\" rel=\"noopener\">LinkedIn<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In the last post we walked through why RBAC broke your app and how to fix it. Your app is now running on a managed identity, keys are off, and everything is green. Then your build pipeline runs, and it needs to seed test data. Suddenly you&#8217;re staring at a secret in your CI settings [&hellip;]<\/p>\n","protected":false},"author":72078,"featured_media":12893,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1980,14,667],"tags":[499,668],"class_list":["post-12878","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-azure-cosmos-db","category-core-sql-api","category-security","tag-azure-cosmos-db","tag-security"],"acf":[],"blog_post_summary":"<p>In the last post we walked through why RBAC broke your app and how to fix it. Your app is now running on a managed identity, keys are off, and everything is green. Then your build pipeline runs, and it needs to seed test data. Suddenly you&#8217;re staring at a secret in your CI settings [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-json\/wp\/v2\/posts\/12878","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-json\/wp\/v2\/users\/72078"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-json\/wp\/v2\/comments?post=12878"}],"version-history":[{"count":3,"href":"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-json\/wp\/v2\/posts\/12878\/revisions"}],"predecessor-version":[{"id":12901,"href":"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-json\/wp\/v2\/posts\/12878\/revisions\/12901"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-json\/wp\/v2\/media\/12893"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-json\/wp\/v2\/media?parent=12878"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-json\/wp\/v2\/categories?post=12878"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cosmosdb\/wp-json\/wp\/v2\/tags?post=12878"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}