diff --git a/oidc/.env b/oidc/.env new file mode 100644 index 0000000..a6e2b1e --- /dev/null +++ b/oidc/.env @@ -0,0 +1,12 @@ +# change values as needed +name="tfscaffold" +suffix="dev" +location="northeurope" + +spName="sp-$name-$suffix" +rg="rg-$name-$suffix" +tag="$suffix" +saName="stac0${name}0${suffix}" +scName="blob0${name}0${suffix}" + +saSku="Standard_ZRS" diff --git a/oidc/.env.powershell b/oidc/.env.powershell new file mode 100644 index 0000000..7d74521 --- /dev/null +++ b/oidc/.env.powershell @@ -0,0 +1,5 @@ +# change values as needed +name=tfscaffold +suffix=dev +location=northeurope +saSku=Standard_ZRS diff --git a/oidc/Readme.md b/oidc/Readme.md new file mode 100644 index 0000000..e9e2034 --- /dev/null +++ b/oidc/Readme.md @@ -0,0 +1,50 @@ +# Terraform scaffold for Azure + +This repo contains everything to get started with Terraform on Azure. It sets you up to use the `azurerm` backend with Service Principal authentication via OIDC. + +[Terraform Backend Docs for azurerm](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm#backend-azure-ad-service-principal-or-user-assigned-managed-identity-via-oidc-workload-identity-federation) + +## What you will get +After executing the below steps you will get: + +- a service principal used to run Terraform on behalf +- a Storage Container used to store the Terraform state file + +## Requirements + +This project requires the following: + +- Bash or PowerShell (you can use [Azure Cloud Shell](http://shell.azure.com/)) +- for Bash you need to have [jq](https://stedolan.github.io/jq/) installed +- Azure CLI (authenticated) +- the executing user needs Subscription owner access (to give owner access to the Service Principal for creating managed identities and assigning roles) as well as the Application Developer role in AAD (to create the Service Principal) + +## Get started with Bash + +Execute the following steps to get started: + +1. Authenticate against Azure by executing `az login` +1. Optional: Export your Tenant (`tenantId`) and Subscription ID (`subscriptionId`) if you don't like to deploy with your `az` defaults. +1. Customize `.env` based on your needs and naming conventions (Make sure you met all [Azure naming rules and restrictions](https://docs.microsoft.com/azure/azure-resource-manager/management/resource-name-rules)). +1. Update the \ in `federated_credential.json`. +1. Execute `up.sh` to deploy everything needed +1. Grant admin consent for the created app registrations (Terraform will then be allowed to create app registrations and groups in Azure AD). This needs Azure Active Directory global admin access. Find more details on how to grant consent [here](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/grant-admin-consent). + +#TODO +## Get started with PowerShell + +Execute the following steps to get started: + +1. Authenticate against Azure by executing `az login` +1. Optional: Create environment variables for Tenant (`tenantId`) and Subscription ID (`subscriptionId`) or call the script with the parameters `-tenantId` and `-subscriptionId` if you don't like to deploy with your `az` defaults. +1. Customize `.env.powershell` based on your needs and naming conventions (Make sure you met all [Azure naming rules and restrictions](https://docs.microsoft.com/azure/azure-resource-manager/management/resource-name-rules)). +1. Update the \ in `federated_credential.json`. +1. Execute `up.ps1` to deploy everything needed +1. Grant admin consent for the created app registrations (Terraform will then be allowed to create app registrations and groups in Azure AD). This needs Azure Active Directory global admin access. Find more details on how to grant consent [here](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/grant-admin-consent). + + +## Disclaimer + +The `up.sh` script asks you whether you would like to map our Partner ID to the created Service Principal. Feel free to opt-out or remove the marked lines if you don't like to support us. + +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/oidc/federated_credential.json b/oidc/federated_credential.json new file mode 100644 index 0000000..9e91661 --- /dev/null +++ b/oidc/federated_credential.json @@ -0,0 +1,9 @@ +{ + "name": "-service-connection", + "issuer": "https://vstoken.dev.azure.com/", + "subject": "sc:////", + "description": "Terraform pipeline", + "audiences": [ + "api://AzureADTokenExchange" + ] +} \ No newline at end of file diff --git a/oidc/resources.bicep b/oidc/resources.bicep new file mode 100644 index 0000000..e9db47b --- /dev/null +++ b/oidc/resources.bicep @@ -0,0 +1,61 @@ +param sa_name string +param sa_sku string +param sc_name string +param tag string +param location string + +resource tf_sa 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: sa_name + location: location + tags: { + environment: tag + managedBy: 'tfScaffolding' + } + sku: { + name: sa_sku + } + kind: 'StorageV2' + properties: { + networkAcls: { + bypass: 'AzureServices' + virtualNetworkRules: [] + ipRules: [] + defaultAction: 'Allow' + } + supportsHttpsTrafficOnly: true + minimumTlsVersion: 'TLS1_2' + encryption: { + services: { + file: { + enabled: true + } + blob: { + enabled: true + } + } + keySource: 'Microsoft.Storage' + } + } +} + +resource tf_sb 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = { + parent: tf_sa + name: 'default' + properties: { + deleteRetentionPolicy: { + enabled: true + days: 30 + allowPermanentDelete: false + } + containerDeleteRetentionPolicy: { + enabled: true + days: 30 + allowPermanentDelete: false + } + } +} + +resource tf_sc 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = { + parent: tf_sb + name: sc_name +} diff --git a/oidc/up.ps1 b/oidc/up.ps1 new file mode 100644 index 0000000..d1c6eec --- /dev/null +++ b/oidc/up.ps1 @@ -0,0 +1,152 @@ +[CmdletBinding()] +param ( + [Parameter()] + [string] + $subscriptionId = $env:subscriptionId, + + [Parameter()] + [string] + $tenantId = $env:tenantId +) + +# Error trapping +trap { + Write-Host "Error on line $($($_.InvocationInfo.ScriptLineNumber)): $($_.Exception.Message)" + exit 1 +} + +# If $subscriptionId is not set, try to set it using the az CLI +# If $subscriptionId is still not set after that, throw an error +if (-not $subscriptionId) { + $subscriptionId = az account show --query id -o tsv + if (-not $subscriptionId) { + throw "Failed to obtain subscription ID" + } +} +Write-Host "Subscription ID set to $subscriptionId" + +# If $tenantId is not set, try to set it using the az CLI +# If $tenantId is still not set after that, throw an error +if (-not $tenantId) { + $tenantId = az account show --query homeTenantId -o tsv + if (-not $tenantId) { + throw "Failed to obtain tenant ID" + } +} +Write-Host "Tenant ID set to $tenantId" + +# Load independent variables from .env.powershell file +$envVars = Get-Content .env.powershell | Out-String | ConvertFrom-StringData + +# Declare dependent variables +$spName = "sp-$($envVars['name'])-$($envVars['suffix'])" +$rg = "rg-$($envVars['name'])-$($envVars['suffix'])" +$tag = $envVars['suffix'] +$saName = "stac0$($envVars['name'])0$($envVars['suffix'])" +$scName = "blob0$($envVars['name'])0$($envVars['suffix'])" + +# Set subscription +az account set --subscription "$subscriptionId" + +# Creates resource group +az group create ` + --name $rg ` + --location "$($envVars['location'])" ` + --tags environment="$tag" ` + --subscription "$subscriptionId" +if (-not $?) { + throw "Failed to create resource group" +} +Write-Host "Resource group created..." + +# Creates a service principal if it doesn't exist +# Needs to be owner to create managed identities and assign roles +$sp = az ad sp list --display-name $spName --query "[].displayName" -o tsv +if ($sp -eq $spName) { + Write-Host "Service principal already exists..." + $spId = az ad sp list --display-name $spName --query "[].appId" -o tsv +} +else { + $sp = az ad sp create-for-rbac ` + --name $spName ` + --role "Owner" ` + --scopes "/subscriptions/$subscriptionId" ` + --years 99 | ConvertFrom-Json + Write-Host "Service principal created..." + # Set service principal id variable + $spId = $sp.appId + $parametersPath = "./federated_credential.json" + az ad app federated-credential create --id $spId --parameters $parametersPath + Write-Host "Federated credential created..." +} + +# Add ADD API permissions - Group.ReadWrite.All, GroupMember.ReadWrite.All, User.Read.All +az ad app permission add ` + --id "$spId" ` + --api 00000003-0000-0000-c000-000000000000 ` + --api-permissions ` + 62a82d76-70ea-41e2-9197-370581804d09=Role ` + dbaae8cf-10b5-4b86-a4a1-f871c94c6695=Role ` + df021288-bdef-4463-88db-98f22de89214=Role +if (-not $?) { + throw "Failed to add ADD API permissions" +} +Write-Host "ADD API permissions added..." + +# Update roles +az role assignment create ` + --assignee "$spId" ` + --scope "/subscriptions/$subscriptionId" ` + --role "Monitoring Metrics Publisher" +if (-not $?) { + throw "Failed to update roles" +} +Write-Host "Roles updated..." + +# Get local user +$userId = az ad signed-in-user show --query id -o tsv +if (-not $?) { + throw "Failed to get local user" +} +Write-Host "Local user fetched..." + +# Creates resources +az deployment group create ` + --name "$($envVars['name'])" ` + --resource-group "$rg" ` + --template-file ./resources.bicep ` + --subscription "$subscriptionId" ` + --mode Incremental ` + --parameters ` + sa_name="$saName" ` + sa_sku="$($envVars['saSku'])" ` + sc_name="$scName" ` + tag="$tag" ` + location="$($envVars['location'])" +if (-not $?) { + throw "Failed to create deployment" +} +Write-Host "Deployment created..." + +# Update roles +az role assignment create ` + --assignee "$spId" ` + --scope "/subscriptions/$subscriptionId/resourceGroups/$rg/providers/Microsoft.Storage/storageAccounts/$saName" ` + --role "Storage Blob Data Owner" +if (-not $?) { + throw "Failed to update roles" +} +Write-Host "Roles updated..." + +# Map Partner ID (optional) +Write-Host "---" +$response = Read-Host "Do you like to map our Partner ID? [y/N]" +if ($response -imatch "^(y|yes)$") { + az extension add --name managementpartner + az login --tenant "$tenantId" --service-principal -u "$spId" -p "$spSecret" + az managementpartner create --partner-id 3699617 + az logout + Write-Host "---" + Write-Host "Please login." + az login +} diff --git a/oidc/up.sh b/oidc/up.sh new file mode 100755 index 0000000..7be5bac --- /dev/null +++ b/oidc/up.sh @@ -0,0 +1,118 @@ +#!/bin/bash +# Used to bootstrap infrastructure required by Terraform + +set -e # Exit on error +set -o pipefail # Exit on pipeline failure + +# Check for jq installation +if ! command -v jq >/dev/null; then + echo "Error: jq is not installed." + exit 1 +fi + +# Central error handling +error_handler() { + echo "Error on line $1" + exit 1 +} + +trap 'error_handler $LINENO' ERR + +# Check and export subscription/tenant if needed +if [[ -z "$subscriptionId" ]]; then + export subscriptionId=$(az account show --query id -o tsv) + [[ -n "$subscriptionId" ]] && echo "Subscription exported..." || exit 1 +else + echo "Subscription details are set..." +fi + +if [[ -z "$tenantId" ]]; then + export tenantId=$(az account show --query homeTenantId -o tsv) + [[ -n "$tenantId" ]] && echo "Tenant exported..." || exit 1 +else + echo "Tenant details are set..." +fi + +# Sources variables +if [[ -f ".env" ]]; then + source .env +fi + +# Set subscription +az account set --subscription "$subscriptionId" + +# Creates resource group +az group create --name "$rg" \ + --location "$location" \ + --tags environment="$tag" \ + --subscription "$subscriptionId" +echo "Resources group created..." + +# create service principal if not exists already +# Needs to be owner to create managed identities and assign roles +if [[ $(az ad sp list --display-name $spName --query "[].displayName" -o tsv) = "$spName" ]]; then +echo "Service principal already exists..." +export spId=$(az ad sp list --display-name $spName --query "[].appId" -o tsv) +else + export sp=$(az ad sp create-for-rbac \ + --name "$spName" \ + --role="Owner" \ + --scopes="/subscriptions/$subscriptionId") +echo "Service principal created..." +export spSecret=$(echo "$sp" | jq -r '.password') +export spId=$(echo "$sp" | jq -r '.appId') +# Create federated credential +az ad app federated-credential create --id "$spId" --parameters ./federated_credential.json +echo "Federated credential created..." +fi + +# Add API permissions - Group.ReadWrite.All, GroupMember.ReadWrite.All, User.Read.All +az ad app permission add \ + --id "$spId" \ + --api 00000003-0000-0000-c000-000000000000 \ + --api-permissions \ + 62a82d76-70ea-41e2-9197-370581804d09=Role \ + dbaae8cf-10b5-4b86-a4a1-f871c94c6695=Role \ + df021288-bdef-4463-88db-98f22de89214=Role +echo "Service principal authorized..." + +# Update roles +az role assignment create \ + --assignee "$spId" \ + --scope "/subscriptions/$subscriptionId" \ + --role "Monitoring Metrics Publisher" +echo "Service principal role updated..." + +# Creates resources +az deployment group create \ + --name "$name" \ + --resource-group "$rg" \ + --template-file ./resources.bicep \ + --subscription "$subscriptionId" \ + --mode Incremental \ + --parameters "sa_name=$saName" \ + "sa_sku=$saSku" \ + "sc_name=$scName" \ + "tag=$tag" \ + "location=$location" +echo "Deployment created..." + +# Add Storage Blob Data Owner role assignment +az role assignment create \ + --assignee "$spId" \ + --role "Storage Blob Data Owner" \ + --scope "/subscriptions/$subscriptionId/resourceGroups/$rg/providers/Microsoft.Storage/storageAccounts/$saName" +echo "Role for Service Principal created..." + +# Map Partner ID (optional) +echo "---" +read -r -p "Do you like to map our Partner ID? [y/N] " response +if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then + az extension add --name managementpartner + az login --tenant "$tenantId" --service-principal -u "$spId" -p "$spSecret" + az managementpartner create --partner-id 3699617 + az logout + echo "---" + echo "Please login." + az login +fi \ No newline at end of file