How to use Terraform to manage GitHub users with Azure SSO
In this step-by-step guide, we will be exploring the use of Terraform to administer users in your GitHub organisation, which is backed by Azure AD as an SSO provider.
Terraform 1.1.x
will be used throughout this tutorial, and so it is worth noting that that some configuration will look different if you are using a different minor version.
Please note that anything in angled brackets (<>
) will need to be populated with values for your desired outcome.
Pre-requisites
- A GitHub organisation
- An Azure Active Directory
- SSO is configured on your GitHub organisation and SSO is backed by Azure Active Directory
Setting up Azure AD authentication
To ensure that Terraform can perform AD actions on our behalf, we need to create a Service Principal with permissions to create / manage a group for our GitHub users. To do so, follow these steps:
- Create an Application Registration
- Access the Azure portal and navigate to
Azure Active Directory --> App registrations --> New registration
. - Enter the details as you prefer - for example:
4. Click Register
5. Navigate to <your app registration> --> Certificates & Secrets --> New Client Secret
6. Add a description and set an expiry date for the secret:
7. Note down the client_id
and client_secret
for use later:
8. Finally, grant this application the ability to manage Active Directory Groups. You may wish to remove this ability after the initial Terraform run has been completed.
Setting up GitHub authentication
We need to allow Terraform to perform actions in GitHub, at the repository level. To do this, we will use a PAT (Personal Access Token). For instructions on how to create one of these, refer to the official GitHub documentation.
Note the following when creating the PAT:
- It will need the
admin:org
scope - Ensure that the PAT code is created by an organisation admin
- Ensure that you have enabled it for use with SAML SSO
Terraform
Setup
To set up Terraform, we need to define our Terraform configuration, including:
- Terraform version
- Remote backend for state storage (optional but recommended)
- Required providers
- Provider configurations
This configuration will all be set in a provider.tf
file.
Terraform version
It is generally advisable to pin a specific version of Terraform to a set of configuration files. This is to ensure uniformity across different runtimes, but will also help to prevent any version conflicts that may cause issues in remote state files.
To pin your Terraform version, add the following to provider.tf
# provider.tfterraform {
required_version = “~> 1.1”
}
Adding remote backend
This is an optional but recommended next step. For all developers and CI/CD to keep in line, Terraform stores the status of resources in a file known as the “state”. This file is the source of truth for Terraform to manage your environment.
In my Terraform code, I will be using an AWS backend to store state, as Credera centrally stores Terraform state in a single S3 bucket. Alternatively, you can use a different backend such as Azure. For more information on different backends and some of the standard backends that can be used, see the official docs.
Your backend configuration should look something similar to the below:
# provider.tfterraform {
required_version = “~> 1.1”
backend “s3” {
bucket = “<s3-bucket-name>”
key = “<path-to-terraform-state>/github.tfstate”
region = “eu-west-1”
role_arn = “<role-to-write-to-s3-with>”
dynamodb_table = “<dynamo-db-table>”
}
}
Required providers
Since 0.13
, Terraform now allows the specification of required providers inside the Terraform configuration object. In this example, we will need two providers — GitHub and Azure AD:
# provider.tfterraform {
required_version = "~> 1.1" backend "s3" {
bucket = "<s3-bucket-name>"
key = "<path-to-terraform-state>/github.tfstate"
region = "eu-west-1"
role_arn = "<role-to-write-to-s3-with>"
dynamodb_table = "<dynamo-db-table>"
}required_providers {
github = {
source = "integrations/github"
version = "4.19.2"
}
azuread = {
source = "hashicorp/azuread"
version = "2.16.0"
}
}
Note that there are two Azure providers — be sure to configure the one which deals with Active Directory (`azuread`) and not the one which handles Azure cloud resources (`azurerm`).
Provider configurations
To complete our Terraform setup, we need to configure each provider with the relevant authentication to be able to deploy resources. To ensure that we aren’t revealing sensitive login details, we will use variables to configure our providers.
Create a variables.tf
file and add the following to it:
# variables.tfvariable "github_token" {
type = string
description = "Access token used to contact GitHub API"
}variable "github_organisation" {
type = string
description = "Organisation name to manage within GitHub"
}variable "azure_subscription_id" {
type = string
description = "Subscription ID used for Azure AD connection"
}variable "azure_client_id" {
type = string
description = "Client ID used for Azure AD connection"
}variable "azure_client_secret" {
type = string
description = "Client secret used for Azure AD connection"
}variable "azure_tenant_id" {
type = string
description = "Tenant ID used for Azure AD connection"
}
Next, add the following configuration to provider.tf
:
# provider.tf
provider "github" {
token = var.github_token
owner = var.github_organisation
}
provider "azuread" {
client_id = var.azure_client_id
client_secret = var.azure_client_secret
tenant_id = var.azure_tenant_id
}
For ease of use, you can create a terraform.tfvars
file with the values for these to prevent you from having to to enter them on every run.
Terraform: GitHub members
Finally, we can start adding Terraform resources. The first thing we will tackle is the GitHub members and their associated AD user. To do this, we can leverage the for_each
Terraform language feature to make everything nice and neat.
As a first step, we will define a local
block that contains a map of user emails as keys and their GitHub usernames as values:
# members.tf
locals {
members = {
"<AD email>": "benj-fletch"
…
}
}
Next, we can use this block to add users to the GitHub members list and import their AD account:
# members.tf
data "azuread_user" "github_ad_members" {
for_each = local.members
user_principal_name = each.key
}
resource "github_membership" "members" {
for_each = local.members
username = each.value
role = "member"
}
Terraform: GitHub admins
We can follow the same approach for GitHub admins / owners:
# owners.tf
data "azuread_user" "github_ad_owners" {
for_each = local.owners
user_principal_name = each.key
}
resource "github_membership" "admins" {
for_each = local.owners
username = each.value
role = "admin"
}
locals {
owners = {
"<AD email>": "benj-fletch"
…
}
}
Note that you should not add any entries for admins that are already present in members.tf
. Each user should only be in a single place.
Terraform: Azure Active Directory
We are now ready to configure our users to be added to the Active Directory group.
To simplify this process, we can leverage another feature of the Terraform language — for expressions. This expression allows us to use the output from our for_each
blocks in the previous sections, and populate a list with output attributes of those resource / data blocks.
# active-directory.tf
data "azuread_service_principal" "github-user-management-app" {
# You will have to use the app name you created when setting up Azure AD auth here
display_name = "<AD service principal name>"
}
resource "azuread_group" "github" {
name = "GitHub Members"
description = "Users of the github application" members = concat(
[for member in data.azuread_user.github_ad_members: member.id],
[for owner in data.azuread_user.github_ad_owners: owner.id]
)
owners = concat(
# Note we have to add the service principal as an owner of the group, to allow it to change the membership.
[data.azuread_service_principal.github-user-management-app.id],
[for owner in data.azuread_user.github_ad_owners: owner.id]
)
}
Running the Terraform
We now have the code that we need to create for our Terraform to work. To get everything to run, we can run the following commands:
Format: Ensures that our code follows consistent standards:
$ terraform fmt
Validate: Checks that configuration references are correct:
$ terraform validate
Plan: Provides a dry-run of what Terraform will do:
$ terraform plan
Apply: Run Terraform and create the resources, storing information in the state file:
$ terraform apply
This completes the basic implementation to manage your user accounts through Azure AD and Terraform.
Bonus one: Protecting members with GitHub Code Owners
It is a good idea to protect the modification of the owners.tf
file by enforcing changes to be reviewed by ‘code owners’. To do this, we can use the GitHub Code Owners feature.
Add a CODEOWNERS
file to the root of your repository, or to the docs/
or .github/
path, and add the owners.tf
file with some owners:
owners.tf @benj-fletch <…>
You can also use emails, should you prefer.
Next, add a branch protection to your repository that forces at least one code owner to approve PRs that involve the owners.tf
file:
Bonus two: Outside collaborators
There is one other type of GitHub user that you might want to manage — outside collaborators. These are people not strictly within your organisation but are those that you wish to reveal private repositories to. These users do not need to go through your SSO provider and should be handled slightly differently, as follows:
resource "github_repository_collaborator" "collaborators" {
for_each = local.collaborators
username = each.key
repository = each.value.repo
permission = each.value.permissions
}
locals {
collaborators = {
"benj-fletch": { "repo": "my-repo", "permissions": "pull" }
"benj-fletch": { "repo": "another-repo", "permissions": "pull" }
}
}
Note that the Terraform permissions do not match those in the GitHub UI.
Closing words
You now have the ability to manage access to your GitHub organisation via code, making it simple and easy to add new joiners to GitHub.
This can be taken a step further, with the ability to trigger changes using GitHub actions and issues, meaning that you can completely remove the need for users to change code to be added to an organisation.
Thanks for reading!
Interested in joining us?
Credera is currently hiring! View our open positions and apply here.
Got a question?
Please get in touch to speak to a member of our team.