diff --git a/quickstart-templates/workload-identity-creates-workload-identities/README.md b/quickstart-templates/workload-identity-creates-workload-identities/README.md new file mode 100644 index 0000000..bcf1169 --- /dev/null +++ b/quickstart-templates/workload-identity-creates-workload-identities/README.md @@ -0,0 +1,70 @@ +# Create Workload Identity Federation that can create other workload identity federations + +## The Idea + +Create federated identity credentials (FIC) at appropriate scopes and controls to deploy company and project specific resource. The process is rooted in a Git pull request review process in order to introduce checkpoints, audit trail and repeatability to process. This should restrict the need for manual work requiring high privileges. + +## Example scenario + +A high-privilege user creates an initial, bootstrap workload identity federation token between Azure and a certain GitHub repository. The workload in this repository is then allowed to access Microsoft Entra to create further workload identity federation tokens and assign them to other repositories. + +The goal is to create a GitHub repository that is privileged enough to create further, more specific federated identifier according to needs of specific projects. This way creating resources can be support by pull request reviews. Together with application roles and restricting the federated identity credential (FIC) access to a certain repository, one can reduce the "blast radius" for any given project and repository and human operators. + +## Benefits + +- **Enhanced Security:** Restricting high-privilege usage reduces the attack surface and potential for unauthorized access. +- **Scalability:** Facilitates the growth of projects by enabling the creation of new identity federations as needed. +- **Efficient Management:** Streamlines the process of managing resource access across multiple projects and teams. + +## Usage could look something like this + +### az cli + +```sh +# Sign in to Azure. +az login + +# Sign in to GitHub. +gh auth login + +# Deploy the Bicep template and capture the output as JSON. +deploymentOutput=$(az deployment tenant create --name bootstrap --location WestEurope --template-file ./main.bicep --parameters ./main.bicepparam --output json) + +# Extract appId from the deployment output using jq. +appId=$(echo $deploymentOutput | jq -r '.properties.outputs.appId.value') + +# The GitHub repository information. +gitHubOwner="myRoot" +gitHubRepo="myRepo" + +# Set GitHub secrets using the GitHub CLI +gh secret set AZURE_CLIENT_ID --repo "${gitHubOwner}/${gitHubRepo}" --body "$appId" +gh secret set AZURE_TENANT_ID --repo "${gitHubOwner}/${gitHubRepo}" --body "$(az account show --query tenantId --output tsv)" +``` + +### PowerShell + +```powershell + +# Sign in to Azure. +Connect-AzAccount + +# Sign in to GitHub. +gh auth login + +# Deploy the Bicep template and capture the output as a JSON string. +$deploymentOutput = New-AzTenantDeployment -Name bootstrap -Location "WestEurope" TemplateFile ./main.bicep -TemplateParametersFile ./main.bicepparam -Verbose + +# Extract appId from the deployment output. +$appId = $deploymentOutput.Properties.Outputs.appId.Value + +# The GitHub repository information. +$gitHubOwner = "myRoot" +$gitHubRepo = "myRepo" + +# Set GitHub secrets using the GitHub CLI. +gh secret set AZURE_CLIENT_ID --repo "$gitHubOwner/$gitHubRepo" --body $appId + +$tenantId = (Get-AzTenant).Id +gh secret set AZURE_TENANT_ID --repo "$gitHubOwner/$gitHubRepo" --body $tenantId +``` diff --git a/quickstart-templates/workload-identity-creates-workload-identities/bicepconfig.json b/quickstart-templates/workload-identity-creates-workload-identities/bicepconfig.json new file mode 100644 index 0000000..8c461ef --- /dev/null +++ b/quickstart-templates/workload-identity-creates-workload-identities/bicepconfig.json @@ -0,0 +1,5 @@ +{ + "experimentalFeaturesEnabled": { + "extensibility": true + } +} \ No newline at end of file diff --git a/quickstart-templates/workload-identity-creates-workload-identities/main.bicep b/quickstart-templates/workload-identity-creates-workload-identities/main.bicep new file mode 100644 index 0000000..4bccd7c --- /dev/null +++ b/quickstart-templates/workload-identity-creates-workload-identities/main.bicep @@ -0,0 +1,86 @@ +targetScope = 'tenant' +extension microsoftGraph + + +@description('The display name for the application.') +param applicationIdentityRegistrationDisplayName string + +@description('The name for the application.') +param applicationIdentityRegistrationName string + +@description('The owner of the organization that it assigned to a workload identity that is used by GitHub Actions to deploy further resources.') +param gitHubOwner string + +@description('The GitHub repository that is assigned to a workload identity that is used by GitHub Actions to deploy further resources.') +param gitHubRepo string + +@description('Subject of the GitHub Actions workflow\'s federated identity credentials (FIC) that is checked before issuing an Entra ID access token to access Azure resources. GitHub Actions subject examples can be found in https://docs.github.com/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims.') +var gitHubActionsFederatedIdentitySubject = 'repo:${gitHubOwner}/${gitHubRepo}:ref:refs/heads/main' + + +// The githubOIDCProvider and microsoftEntraAudience are well-known values that are used to +// create the federated identity credentials for the GitHub Actions application so the two systems +// can properly authenticate and authorize between each other. For other OIDC enabled systems you need to check +// their documentation to find out the correct values so the federated identity credentials can be created +// to connect to Azure. +var githubOIDCProvider = 'https://token.actions.githubusercontent.com' +var microsoftEntraAudience = 'api://AzureADTokenExchange' + +// In order for the Azure and GitHub Actions to communicate with each other, an +// application for GitHub Actions is created to Azure Entra. This application is then +// assigned an application role that allows it to access the Microsoft Graph API. +resource githubActionsApplication 'Microsoft.Graph/applications@v1.0' = { + uniqueName: applicationIdentityRegistrationName + displayName: applicationIdentityRegistrationDisplayName + + // This creates the federated identity credentials for the GitHub Actions application. + resource githubFederatedIdentityCredential 'federatedIdentityCredentials@v1.0' = { + name: '${githubActionsApplication.uniqueName}/githubFederatedIdentityCredential' + audiences: [microsoftEntraAudience] + description: 'Identity for application to deploy the root identity infrastructure.' + issuer: githubOIDCProvider + subject: gitHubActionsFederatedIdentitySubject + } +} + + +// This is the Azure service principal the GitHub Actions application is assigned to. +resource gitHubIdentityActionsServicePrincipal 'Microsoft.Graph/servicePrincipals@v1.0' = { + displayName: applicationIdentityRegistrationDisplayName + appId: githubActionsApplication.appId +} + + +// The identifier '00000003-0000-0000-c000-000000000000' is a well-known identifier +// for Microsoft first part application 'Microsoft Graph', see more at +// https://learn.microsoft.com/en-us/troubleshoot/azure/entra/entra-id/governance/verify-first-party-apps-sign-in. +resource microsoftGraphServicePrincipal 'Microsoft.Graph/servicePrincipals@v1.0' existing = { + appId: '00000003-0000-0000-c000-000000000000' +} + + +// This searches the used application role permission by its friendly name. There should be only one, +// so the fist one is taken to be used in the symbolic name. +// See at https://learn.microsoft.com/en-us/graph/permissions-reference#applicationreadwriteownedby +// for the actual description. Look also at https://www.azadvertizer.net/azEntraIdAPIpermissionsAdvertizer.html?targetPermissionId=18a4783c-866b-4cc7-a460-3d5e5662c884 +// how one can search for permissions GUIDs. Using clear names is clearer. +// +// TODO: At the moment 'Application.ReadWrite.All' is used because there's a bug in +// Azure. See at https://github.com/microsoftgraph/msgraph-bicep-types/issues/142 and at +// https://learn.microsoft.com/en-us/graph/permissions-reference#approleassignmentreadwriteall. +param appRoleName string = 'Application.ReadWrite.All' +var graphAppRoles = microsoftGraphServicePrincipal.appRoles +var appRoleDetail = filter(graphAppRoles, graphAppRoles => graphAppRoles.value == appRoleName)[0] + + +// Assign the GitHub Actions service principal the Application.ReadWrite.OwnedBy permission, +// which allows the service principal to be able to create and manage the applications and service principals is creates/owns. +resource symbolicname 'Microsoft.Graph/appRoleAssignedTo@v1.0' = { + appRoleId: appRoleDetail.id + principalId: gitHubIdentityActionsServicePrincipal.id + resourceId: microsoftGraphServicePrincipal.id +} + +// This outputs the application ID of the GitHub Actions application. So, for instance, this can be +// used in a PowerShell or a Bash Script to set the AZURE_CLIENT_ID environment variable in GitHub. +output appId string = githubActionsApplication.appId diff --git a/quickstart-templates/workload-identity-creates-workload-identities/main.bicepparam b/quickstart-templates/workload-identity-creates-workload-identities/main.bicepparam new file mode 100644 index 0000000..bc6f84b --- /dev/null +++ b/quickstart-templates/workload-identity-creates-workload-identities/main.bicepparam @@ -0,0 +1,13 @@ +using './main.bicep' + +@description('The display name for the application.') +param applicationIdentityRegistrationDisplayName = 'GitHub Actions Example Application Deployer' + +@description('The name for the application.') +param applicationIdentityRegistrationName = 'root-appident-deployer' + +@description('The owner of the organization that it assigned to a workload identity that is used by GitHub Actions to deploy further resources.') +param gitHubOwner = 'github-owner' + +@description('The GitHub repository that is assigned to a workload identity that is used by GitHub Actions to deploy further resources.') +param gitHubRepo = 'github-repo'