This is the fourth post in a multi-part post that describes using a GitHub Action and Terraform to deploy infrastructure in Azure. In this post I will create the Microsoft Entra Security Principal that the GitHub Action will use to authenticate to Azure.
In the Creating the Terraform State File in Azure post, I demonstrated logging into Azure using the “az login –use-device-code”. That will not work in a GitHub Action. The GitHub Action must use an Azure Security Principal.
And here’s where you need to make some decisions about security. You could just add the security principal to the Contributor and Storage Blob Data Contributor roles for the whole subscription. But that is a very broad set of permissions.
A more secure approach would be to create a custom role definition that grants just the permissions the security principal needs to create the Azure resources in your service. For example, if you are creating an API App Service, you will need read, write and delete permissions for an App Service Plan, App Service and depending on your architecture, a few other resources.
Creating the Custom Role Definition
Creating a custom role is easy. Identifying all the right permissions is a little painful. You will need to create a JSON document with all the permissions you need. Here’s what I came up with.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
{ "Name": "Terraform Operator", "IsCustom": true, "Description": "Can deploy resources using Terraform with state file stored in blob storage.", "Actions": [ "Microsoft.Resources/subscriptions/read", "Microsoft.Resources/subscriptions/resourceGroups/read", "Microsoft.Resources/subscriptions/resourceGroups/write", "Microsoft.Resources/subscriptions/resourceGroups/delete", "Microsoft.Web/sites/Read", "Microsoft.Web/sites/Write", "Microsoft.Web/sites/Delete", "Microsoft.Web/serverFarms/Read", "Microsoft.Web/serverFarms/Read", "Microsoft.Web/serverFarms/Read", "Microsoft.Storage/storageAccounts/read", "Microsoft.Storage/storageAccounts/write", "Microsoft.Storage/storageAccounts/delete", "Microsoft.KeyVault/vaults/read", "Microsoft.KeyVault/vaults/write", "Microsoft.KeyVault/vaults/delete", "Microsoft.KeyVault/vaults/deploy/action", "Microsoft.KeyVault/vaults/keys/read", "Microsoft.KeyVault/vaults/keys/write", "Microsoft.OperationalInsights/workspaces/read", "Microsoft.OperationalInsights/workspaces/write", "Microsoft.OperationalInsights/workspaces/delete", "Microsoft.Insights/Components/Write", "Microsoft.Insights/Components/Read", "Microsoft.Insights/Components/Delete", "Microsoft.Insights/Components/ApiKeys/Read" ], "NotActions": [], "DataActions": [ "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read", "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/Write", "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/Delete", "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/add/action", "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/move/action" ], "AssignableScopes": ["/subscriptions/<subscription id>"] } |
The Actions block defines the stuff that the security principal will be able to do. This includes managing Resource Groups, Web Sites, App Service Plans, Storage Accounts, Key Vaults, Log Analytic Environments and Application Insights. In the Data Actions block are permissions so the service account will be able to manage the .tfstate file.
I created a file named terraform-operator.json and pasted that JSON content into it. To create the Terraform Operator role, run this Azure CLI command from the directory that contains the terraform-operator.json file.
1 |
az role definition create --role-definition .\terraform-operator.json |
Creating the Security Principal
Next, create the security principal.
1 |
az ad sp create-for-rbac --name unicorn --role "Terraform Operator" --scopes /subscriptions/<subscription id> |
You will get a JSON blob returned to you that looks like to this.
GitHub Environments
GitHub supports secrets and variables that your GitHub Actions can use. These secrets and variables can be scoped to the entire repository (global scope) or can be scoped to specific environments. GitHub Environments are user defined scopes for variables and secrets. For example, you can define a GitHub Environment named “development” and add a variable named “RESOURCE_GROUP_NAME” that has the value “rg-unicorn-development”.
The Unicorn project will start out with only one physical environment in Azure, production. We could define the secrets and variables at the repository scope. But this would force a complicated refactor if the team scales and other environments need to be added. For that reason, the Unicorn project will define a “production” environment in GitHub.
The Federated Credential Subject
In OAuth, the Subject is a claim about the identity of a security principal. A security principal may be a user but in our case is the security principal we created above.
The Subject is kind of complicated, depending on whether you are deploying from a branch, environment or tag and which cloud provider you are using (Azure, Google Cloud, others). I’m going to focus on two scenarios: deploying from a branch and from an environment to Azure. If you have defined GitHub Environments, the pattern for the subject is:
repo:<organization-name>/<repository-name>:environment/<environment-name>
Example: repo:myorganization/unicorn:environment/production
If you have not defined GitHub environments, the pattern to follow for the subject is:
repo:<organization-name>/<repository-name>:ref:refs/heads/main
Example: repo:myorganization/unicorn:ref:refs/heads/main
The Subject is case sensitive, if you named your environment “Production”, don’t use “production” in your Subject.
Create a file named credential.json and copy the following content into it. I plan on using GitHub Environments, so I used the former pattern for the Subject.
1 2 3 4 5 6 7 8 9 |
{ "name": "Production", "issuer": "https://token.actions.githubusercontent.com", "subject": "repo:<organization-name>/<repository-name>:environment:production", "description": "", "audiences": [ "api://AzureADTokenExchange" ] } |
Replace the organization name and repository name in the subject property with your GitHub organization and repository name. You will also need the Object Id of the service principal that you just created.
Creating the Federated Credential
Use the Azure Portal to find the Object Id of the service principal. In the portal, navigate to Microsoft Entra Id. In the menu on the left, under the “Manage” heading, click on “App registration”. Click on “All Applications”, then paste the AppId returned in the JSON blob from the “az ad sp create-for-rback” command from above.
Click into the App registration object.
Then run the following command to create the federated credential.
1 |
az ad app federated-credential create --id 5beeaxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --parameters ./credential.json |
This command will return a small JSON blob.
Now inspect the Federated credential of the App Registration.
In this post, we created a custom role definition that has the least permissions for Terraform to a) manage the .tfstate file and b) to manage the resources that our service is made up of.
In the next post, I’ll show how to create the GitHub Action and configure it to use the security principal that was created in this post.