Skip to content

huntandhackett/IaC-EntraID

Repository files navigation

IaC-EntraID

This repository instantiates sample IaC for Entra ID using Terraform. It includes examples for the creation of:

  • Groups
  • Directory roles
  • Access packages
  • Named locations
  • Administrative units
  • Conditional access policies

This repository is intended to supplement the "How to move from ClickOps to fully automated Microsoft Entra ID infrastructure" blogpost. Refer to the blogpost for further explanations on the design of this repo.

This sample code demonstrates the usage of modules (see modules/ dir), workaround implementation via Graph SDK when functionality is missing from the azuread Terraform providers (see scripts/ dir), creation of pipelines (see cicd/ dir), etc.

Note that this is sample code, so it is not exhaustive. Tenant-specific values have been removed from this code and are set in <placeholders_like_these>.

While this repository is made publically available on Github, note that it is designed for usage in Azure DevOps.

Directory structure

IaC-EntraID/
│   └── resources/
│   │       └── branching.PNG
│   └── cicd/
│   │       └── azure-pipelines-mergePR-prod.yml
│   │       └── azure-pipelines-mergePR-test.yml
│   │       └── azure-pipelines-openPR-prod.yml
│   │       └── azure-pipelines-openPR-test.yml
│   │       └── openPR-template.yml
│   │       └── mergePR-template.yml
│   │       └── variables-template.yml
│   │       └── vmss-image.sh
│   └── scripts/
│   │       └── authentication.py
│   │       └── configure_au_dymamic_membership.py
│   └── modules/
│   │   └── access-packages/
│   │       └── access_packages.tf
│   │       └── variables.tf
└── main.tf
└── provider.tf
└── variables.tf
└── test.tfvars
└── prod.tfvars
└── prod.tfbackend
└── test.tfbackend
└── terraform.rego
└── README.md

This repo manages infrastructure in both test and prod. This infrastructure is for the most part identical, except particular environment-specific configuration differences that are imported through variables.tf via their respective .tfvars files.

The terraform statefiles of the test and prod environments are stored seperately in each tenant's respective storage accounts, under a resource group defined in the respective .tfbackend files.

Within modules, reusable configuration is stored per resource in Entra ID. For example, the modules/access_packages directory defines the creation of a catalog storing the resources used for access packages, the access package policy and its membership. For each new access package that has to be created, this module can be called with the appropriate variables.

The files related to CICD are stored in the cicd/ directory. Two main pipelines are defined: azure-pipelines-mergePR.yml describes the pipeline for merged PRs and open-PR-template.yml describes the pipeline for open PRs. The latter template is instantiated for the test branch of this repo in azure-pipelines-open-PR-test.yml and for the main branch of this repo in azure-pipelines-open-PR-main.yml. The environment-relevant variables are set for these files through the variables-template.yml. For further explanation of our CICD, see section CICD.

The resources/ directory contains supplementary images for this README.md file.

The scripts/ directory contains Python scripts that implement logic that is not yet available in the Terraform provider, but is available in the underlying Graph API. The scripts/authentication.py script handles the authentication to the Azure DevOps Service Connection (explained in section CICD) after which subsequent API calls can be made.

Local development

WARNING: This section should not be relevant as part of regular code development, as Terraform infrastructure should only be changed through the CICD pipeline. A local terraform apply in either test or in prod should not be performed. However, we provide this documentation on how to develop locally for the sake of completeness - there might be situations where we need to e.g. import resources into the existing infrastructure, or remove resources.

Service principal + certificate

A service principal can be used to authenticate to the APIs with a certificate. To authenticate, create a certificate and provide the client ID, client certificate path and password, tenant ID and subscription ID. An example in PowerShell:

$env:ARM_CLIENT_ID = "<insert-client-id>"
$env:ARM_CLIENT_CERTIFICATE_PATH = "<insert-client-certificate-path>"
$env:ARM_CLIENT_CERTIFICATE_PASSWORD = "<insert-client-certificate-password>"
$env:ARM_TENANT_ID = "<insert-tenant-id>"
$env:ARM_SUBSCRIPTION_ID = "<insert-subscription-id>"

You can subsequently initialise Terraform: terraform init -reconfigure -backend-config="<env>.tfbackend"

How to run

Make sure to use the -var-file="<env>.tfvars" flag when running your terraform plan or apply. This will import the .tfvars file that is relevant to the environment that you are working with.

For example, to create a terraform plan in the test tenant, you should run terraform plan -var-file="test.tfvars"

Workaround Python implementation

Some logic is not yet available in the azuread Terraform provider, but is available in the underlying Microsoft Graph API. We therefore implement any missing pieces that are exposed through the Microsoft Graph API with Python scrips (found in the scripts directory).

Local development

The Python scripts can be executed standalone and tested locally, as follows:

  • For package dependancies, install the relevant libraries in a local Python virtual environment. The packages required are specified in the Install package dependancies for Python scripts scripts in the cicd/openPR-template.yml and cicd/azure-pipelines-mergePR.yml pipelines.
  • For the Graph API authentication, use the CertificateCredential class to perform the authentication in scripts/authenticate_to_graph.py instead of the AzurePipelinesCredential class. The same Service Principal from section Local development can be used for this.
  • Any environment variables imported from the CICD pipeline (os.environ[<VAR>]) can be manually set.

CICD

CICD for this repository is handled through Azure DevOps. Our repository maintains 2 protected branches:

  • main - Any commits merged here instantiate infrastucture in the production tenant.
  • test - Any commits merged here instantiate infrastucture in the test tenant.

When making changes, you should first open a PR to merge into test, and once successfully tested in the test tenant, open a PR to merge into main. This means you can not create a PR from a feature branch directly into main, see below:

screenshot

Authentication

To authenticate the connection to Azure, we created service connections in Azure DevOps for the test environment and the production environment:

  • <read_only_service_connection_name_in_test>
  • <service_connection_name_in_test>
  • <read_only_service_connection_name_in_prod>
  • <service_connection_name_in_prod>

These service connections use a Service Principal Federated Credential to obtain the relevant API permissions to execute resource creation. Thus for each service connection per environment, a respective service principal has been created:

  • <read_only_service_principal_name_in_test>*
  • <service_principal_name_in_test>
  • <read_only_service_principal_name_in_prod>*
  • <service_principal_name_in_prod>

These service principals exist in both the test and production tenants. To allow the service connection to impersonate this service principal, a federated credential has been created for these service principals in both the test and production tenants.

*The API permissions associated with this service principal are all read-only, except for 1.

Pipelines

The two main activities from which we launch CICD are open pull requests (CI), and merged pull requests (CD). To handle this, we have 4 pipelines in Azure DevOps:

Pipeline Authorised to use these service connections: Federated credential set up with this service principal:
Open PR on test <read_only_service_connection_name_in_test> <read_only_service_principal_name_in_test>
Open PR on main (prod) <read_only_service_connection_name_in_prod> <read_only_service_principal_name_in_prod>
Merge PR on test <service_connection_name_in_test> <service_principal_name_in_test>
Merge PR on main (prod) <service_connection_name_in_prod> <service_principal_name_in_prod>

The seperation of pipelines has been done to ensure each pipeline is only authorised to use the service connection dedicated to its environment, and with the relevant API permissions. This means when a PR is opened (and thus only a Terraform plan needs to be generated), read-only permissions should be sufficient. However, when a PR is merged (and Terraform apply needs to occur), write permissions are needed.

When we open a PR on test or main (or make any new commits to an existing open PR on these branches), the following is triggered by our Open PR on * pipeline:

  1. Terraform code is validated.
  2. Terraform scan for vulnerabilities is run.
  3. A Terraform plan is created in the relevant environment.
  4. Infra evaluation is done on the plan against custom QA principles (see terraform.rego).

Additionally, on PRs that are opened on main, the following occurs:

  1. A check is carried out to ensure the source branch opening this pull request was test. This is to protect main from any changes that try to bypass being tested out in the test tenant first.
  2. The PR requires approval from at least 1 reviewer.

After the Open PR on * build validation completes successfully, the PR is ready to be merged. This triggers the Merge PR on * pipeline:

  1. A Terraform plan is created in the relevant environment.
  2. A manual validation step waits for approval before changing the infrastucture.
  3. The infrastructure is changed in the relevant environment with a Terraform apply.

NOTE: There is a distinction between the approval that happens on the Open PR on main pipeline and the approval that happens on the Merge PR on * pipeline. The former is a branch policy which asks for a code-review of the PR, where a colleague agrees that your code is OK to merge into main, while the latter is a manual validation step in the pipeline to prevent the changes being automatically/immidiately rolled out. This is done so that you can control when the changes occur in the infrastructure.

Virtual Machine Scale Set (VMSS) agents

The agents running in the CICD pipelines are VMSS agents. This means Agent Pools have been created in Azure DevOps in test and prod, and permissions have been granted for these pools to access the aforementioned pipelines that are intended for their environments.

The motivation to run VMSS agents instead of Microsoft-hosted agents is to ensure that we don't have to open our storage account to public network access - the VMSS agents are able to access the storage account in the CICD pipeline through a private endpoint.

The custom image the VM uses has been created by installing the tools described in cicd\vmss-image.sh. For this purpose, an Azure compute gallery was created, with associated VM image definition and image versioning.

To keep the VMSS agents up to date, a custom script extension runs the following script on the VMs on each boot:

#!/bin/bash

for i in {1..3}; do
  apt update -y && break
  sleep 5
done
apt full-upgrade -y
reboot

This script is stored in the storage account associated with this repo. This has been configured for usage by the VMSS as follows: VMSS -> Settings -> Extensions + applications -> '+' -> Custom script for linux.

How to generate a new VM image version

If you need to generate a new version of the VM image, you can do this as follow:

  1. In the Azure portal, navigate to the VM image defintion created for this project.
  2. Under the tab Versions, select Create VM alongside the latest version of the VM image.
  3. Create the VM and name it according to the change, and access it via Bastion.
  4. Make the desired changes to the VM.
  5. To deprovision the VM, run:
sudo waagent -deprovision+user -force
  1. Close the Bastion session. In the Azure portal, stop the VM.
  2. Select Capture -> Create an image from the VM.
  3. In the Azure portal, search for the VMSS created for this project. Go to Settings -> Operating system -> Change image reference. Select the latest version of the VM image you created.
  4. Once successful, delete all resources associated with the VM you created (e.g. disks, keys, vm, IPs, etc.)

Manually-created supporting infrastructure

This repo requires the following supporting infrastructure that has been manually-created (due to chicken/egg):

  • Service principals for authentication
    • <read_only_service_principal_name_in_test> and <read_only_service_principal_name_in_prod> with associated read-only Graph API permissions.
    • <service_principal_name_in_test> and <service_principal_name_in_prod> with associated write- Graph API permissions.
  • Azure DevOps service connections
    • <read_only_service_connection_name_in_test>
    • <service_connection_name_in_test>
    • <read_only_service_connection_name_in_prod>
    • <service_connection_name_in_prod>
  • Resource group in test and in prod
  • Storage account storing state file, Terraform plan and VM upgrade script in test and in prod. This has been configured with versioning, and files are stored for 30 days before deletion. The Terraform Authenticator SP has been granted the Storage Blob Data Contributor IAM role on this storage account to allow uploading of files. The storage account has containers to store the state file, the terraform plan and the VM upgrade script.
  • Virtual Machine Scale Set created with the following properties:
    • Orchestration mode: Uniform
    • Instance count: 0
    • Instance size: Standard_DS1_v2
    • OS disk type: standard SSD (LRS)
    • Ephemeral OS disk: OS cache placement
    • Load balancing options: None
    • Boot diagnostics: Enabled
    • The image of the machine is decribed in the subsequent bullet point.
  • Azure compute gallery, containing a VM image definition with a given VM image version. This image is of a VM which has been installed with the tools described in cicd\vmss-image.sh and has the following properties:
    • Storage account type: Standard HDD LRS
    • Size: Standard_DS1_v2
    • Enable vTPM: ✓
    • Security type: Trusted launch
  • Virtual Network and corresponding subnet and NSG
  • Private endpoint on storage account. This is necessary to allow the VMSS to access the storage account privately without opening up public access.

About

Sample IaC for Entra ID.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages