Skip to content

Commit a66fba1

Browse files
committed
RBACs added
1 parent 35402ab commit a66fba1

7 files changed

Lines changed: 147 additions & 25 deletions

File tree

README.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ terraform validate
7575
terraform apply -auto-approve
7676
```
7777

78-
3) In Azure portal, open the Artifact Signing account and complete **Identity validation** (portal-only).
78+
3) Artifact Signing **identity verification / identity validation** (portal-only):
79+
80+
- In Azure portal, open the Artifact Signing account and complete **Identity validation**.
81+
- This is required by the service and cannot be automated by Terraform.
82+
- If the portal says you need **Artifact Signing Identity Verifier**, re-run Terraform (it assigns this role to the identity running `terraform apply` by default) and wait a few minutes for RBAC to propagate.
7983

8084
4) Copy the **Identity validation Id** from the portal and set it in Key Vault:
8185

@@ -112,14 +116,18 @@ terraform apply -auto-approve -var-file=terraform.tfvars -var-file=terraform.ado
112116

113117
Alternative: set `ado_enabled = true` and `ado_org_service_url` in `terraform.tfvars`.
114118

119+
> If you prefer variables over env vars, you can also set `$env:TF_VAR_ado_org_service_url = "https://dev.azure.com/<your-org>"`.
120+
115121
4) Terraform will:
116122
- create the Azure DevOps project + repo + YAML pipeline
117123
- create the AzureRM service connection using Workload Identity Federation (WIF)
118124
- read the generated WIF Issuer/Subject and create the Entra federated credential
119125

120126
Notes:
127+
- Artifact Signing identity validation remains **portal-only** even when Azure DevOps is Terraform-managed. Treat it as part of the "Deploy with Terraform" flow above (once completed, store the Id in Key Vault and the pipeline can create the certificate profile automatically).
121128
- The `WorkloadIdentityFederation` auth scheme requires your org feature to be enabled. If your org can’t use it yet, set `ado_service_endpoint_authentication_scheme = "ServicePrincipal"` and also set `TF_VAR_ado_service_principal_client_secret` for the service principal secret.
122129
- Terraform creates an empty repo. You still need to push this repo’s code into the Azure DevOps repo (Terraform will output the clone URL).
130+
- Least privilege: by default this repo does **not** grant RG `Contributor` to the Azure DevOps service principal. If you want the pipeline to auto-create the certificate profile, set `assign_contributor_role_to_ado_sp = true`.
123131

124132
## Pipeline
125133

@@ -144,8 +152,19 @@ Optional (only used when creating the certificate profile):
144152
### Azure Key Vault for pipeline variables
145153

146154
Terraform creates a Key Vault by default and wires RBAC so:
147-
- your current identity can set secrets
148-
- the Azure DevOps service principal can read secrets
155+
- your current identity (the identity running `terraform apply`) can set secrets (`Key Vault Secrets Officer`)
156+
- the Azure DevOps service principal can read secrets (`Key Vault Secrets User`)
157+
158+
> This Key Vault is **RBAC-enabled** (`rbac_authorization_enabled = true`). You will see access under **Key Vault → Access control (IAM)** (not under “Access policies”).
159+
160+
Least privilege options:
161+
- Set `keyvault_populate_secrets = false` if you do not want Terraform to write secrets into Key Vault (you can manage secrets yourself and/or set pipeline variables another way).
162+
- The pipeline does not attempt to self-assign RBAC by default (it only does if `pipeline_attempt_rbac_assignment = true`).
163+
- If you want to view **Keys** and/or **Certificates** in the Azure portal, opt in to RBAC for your current identity:
164+
- `keyvault_grant_keys_access_to_current = true` (assigns `Key Vault Crypto User`)
165+
- `keyvault_grant_certificates_access_to_current = true` (assigns `Key Vault Certificates User`)
166+
- Or, if you want the simplest “make the portal work” option (broad permissions):
167+
- `keyvault_grant_administrator_to_current = true` (assigns `Key Vault Administrator`)
149168

150169
Terraform also populates these Key Vault secrets during `terraform apply`:
151170
- `artifactSigningEndpoint`

terraform-infrastructure/main.tf

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,24 @@ locals {
3333
length(trimspace(var.ado_wif_subject == null ? "" : var.ado_wif_subject)) > 0
3434
) ? trimspace(var.ado_wif_subject == null ? "" : var.ado_wif_subject) : try(azuredevops_serviceendpoint_azurerm.azurerm[0].workload_identity_federation_subject, null)
3535

36+
# Object id for the ADO service principal, regardless of whether Terraform created it
37+
# or you provided an existing client id.
38+
ado_sp_object_id_effective = var.ado_enabled ? (
39+
var.ado_create_app ? try(azuread_service_principal.ado_sp[0].object_id, null) : try(data.azuread_service_principal.ado_sp_existing[0].object_id, null)
40+
) : null
41+
3642
keyvault_name_input = var.keyvault_name != null ? trimspace(var.keyvault_name) : ""
3743
}
3844

3945
data "azurerm_client_config" "current" {}
4046

4147
data "azurerm_subscription" "current" {}
4248

49+
data "azuread_service_principal" "ado_sp_existing" {
50+
count = var.ado_enabled && !var.ado_create_app ? 1 : 0
51+
client_id = local.ado_sp_client_id
52+
}
53+
4354
resource "azurerm_resource_group" "rg" {
4455
name = var.resource_group_name
4556
location = var.location
@@ -62,6 +73,14 @@ resource "azapi_resource" "code_signing_account" {
6273
}
6374
}
6475

76+
resource "azurerm_role_assignment" "current_identity_verifier" {
77+
count = var.assign_identity_verifier_role_to_current ? 1 : 0
78+
79+
scope = azapi_resource.code_signing_account.id
80+
role_definition_name = "Artifact Signing Identity Verifier"
81+
principal_id = data.azurerm_client_config.current.object_id
82+
}
83+
6584
resource "azapi_resource" "certificate_profile" {
6685
count = var.identity_validation_id == null ? 0 : 1
6786
type = "Microsoft.CodeSigning/codeSigningAccounts/certificateProfiles@2025-10-13"
@@ -101,27 +120,27 @@ resource "azuread_application_federated_identity_credential" "ado_fic" {
101120
}
102121

103122
resource "azurerm_role_assignment" "ado_account_signer" {
104-
count = var.assign_signer_role_to_ado_sp && var.ado_enabled && var.ado_create_app ? 1 : 0
123+
count = var.assign_signer_role_to_ado_sp_at_account_scope && var.ado_enabled ? 1 : 0
105124

106125
scope = azapi_resource.code_signing_account.id
107126
role_definition_name = "Artifact Signing Certificate Profile Signer"
108-
principal_id = azuread_service_principal.ado_sp[0].object_id
127+
principal_id = local.ado_sp_object_id_effective
109128
}
110129

111130
resource "azurerm_role_assignment" "ado_profile_signer" {
112-
count = var.assign_signer_role_to_ado_sp && var.ado_enabled && var.ado_create_app && length(azapi_resource.certificate_profile) > 0 ? 1 : 0
131+
count = var.assign_signer_role_to_ado_sp && var.ado_enabled && length(azapi_resource.certificate_profile) > 0 ? 1 : 0
113132

114133
scope = azapi_resource.certificate_profile[0].id
115134
role_definition_name = "Artifact Signing Certificate Profile Signer"
116-
principal_id = azuread_service_principal.ado_sp[0].object_id
135+
principal_id = local.ado_sp_object_id_effective
117136
}
118137

119138
resource "azurerm_role_assignment" "ado_rg_contributor" {
120-
count = var.assign_contributor_role_to_ado_sp && var.ado_enabled && var.ado_create_app ? 1 : 0
139+
count = var.assign_contributor_role_to_ado_sp && var.ado_enabled ? 1 : 0
121140

122141
scope = azurerm_resource_group.rg.id
123142
role_definition_name = "Contributor"
124-
principal_id = azuread_service_principal.ado_sp[0].object_id
143+
principal_id = local.ado_sp_object_id_effective
125144
}
126145

127146
resource "random_string" "kv_suffix" {
@@ -155,17 +174,38 @@ resource "azurerm_key_vault" "kv" {
155174
}
156175

157176
resource "azurerm_role_assignment" "kv_secrets_officer_current" {
158-
count = var.keyvault_enabled ? 1 : 0
177+
count = var.keyvault_enabled && var.keyvault_populate_secrets ? 1 : 0
159178
scope = azurerm_key_vault.kv[0].id
160179
role_definition_name = "Key Vault Secrets Officer"
161180
principal_id = data.azurerm_client_config.current.object_id
162181
}
163182

183+
resource "azurerm_role_assignment" "kv_crypto_user_current" {
184+
count = var.keyvault_enabled && var.keyvault_grant_keys_access_to_current ? 1 : 0
185+
scope = azurerm_key_vault.kv[0].id
186+
role_definition_name = "Key Vault Crypto User"
187+
principal_id = data.azurerm_client_config.current.object_id
188+
}
189+
190+
resource "azurerm_role_assignment" "kv_certificates_user_current" {
191+
count = var.keyvault_enabled && var.keyvault_grant_certificates_access_to_current ? 1 : 0
192+
scope = azurerm_key_vault.kv[0].id
193+
role_definition_name = "Key Vault Certificates User"
194+
principal_id = data.azurerm_client_config.current.object_id
195+
}
196+
197+
resource "azurerm_role_assignment" "kv_administrator_current" {
198+
count = var.keyvault_enabled && var.keyvault_grant_administrator_to_current ? 1 : 0
199+
scope = azurerm_key_vault.kv[0].id
200+
role_definition_name = "Key Vault Administrator"
201+
principal_id = data.azurerm_client_config.current.object_id
202+
}
203+
164204
resource "azurerm_role_assignment" "kv_secrets_user_ado" {
165-
count = var.keyvault_enabled && var.ado_enabled && var.ado_create_app ? 1 : 0
205+
count = var.keyvault_enabled && var.ado_enabled ? 1 : 0
166206
scope = azurerm_key_vault.kv[0].id
167207
role_definition_name = "Key Vault Secrets User"
168-
principal_id = azuread_service_principal.ado_sp[0].object_id
208+
principal_id = local.ado_sp_object_id_effective
169209
}
170210

171211
resource "time_sleep" "kv_rbac_propagation" {
@@ -179,7 +219,7 @@ resource "time_sleep" "kv_rbac_propagation" {
179219
}
180220

181221
resource "azurerm_key_vault_secret" "artifact_signing_endpoint" {
182-
count = var.keyvault_enabled ? 1 : 0
222+
count = var.keyvault_enabled && var.keyvault_populate_secrets ? 1 : 0
183223
key_vault_id = azurerm_key_vault.kv[0].id
184224
name = "artifactSigningEndpoint"
185225
value = local.artifact_signing_endpoint != null ? local.artifact_signing_endpoint : " "
@@ -191,7 +231,7 @@ resource "azurerm_key_vault_secret" "artifact_signing_endpoint" {
191231
}
192232

193233
resource "azurerm_key_vault_secret" "artifact_signing_account_name" {
194-
count = var.keyvault_enabled ? 1 : 0
234+
count = var.keyvault_enabled && var.keyvault_populate_secrets ? 1 : 0
195235
key_vault_id = azurerm_key_vault.kv[0].id
196236
name = "artifactSigningAccountName"
197237
value = azapi_resource.code_signing_account.name
@@ -203,7 +243,7 @@ resource "azurerm_key_vault_secret" "artifact_signing_account_name" {
203243
}
204244

205245
resource "azurerm_key_vault_secret" "artifact_signing_certificate_profile_name" {
206-
count = var.keyvault_enabled ? 1 : 0
246+
count = var.keyvault_enabled && var.keyvault_populate_secrets ? 1 : 0
207247
key_vault_id = azurerm_key_vault.kv[0].id
208248
name = "artifactSigningCertificateProfileName"
209249
value = var.certificate_profile_name
@@ -215,7 +255,7 @@ resource "azurerm_key_vault_secret" "artifact_signing_certificate_profile_name"
215255
}
216256

217257
resource "azurerm_key_vault_secret" "artifact_signing_identity_validation_id" {
218-
count = var.keyvault_enabled ? 1 : 0
258+
count = var.keyvault_enabled && var.keyvault_populate_secrets ? 1 : 0
219259
key_vault_id = azurerm_key_vault.kv[0].id
220260
name = "artifactSigningIdentityValidationId"
221261
value = local.identity_validation_id_trimmed != "" ? local.identity_validation_id_trimmed : " "
@@ -324,7 +364,7 @@ resource "azuredevops_variable_group" "signing" {
324364

325365
variable {
326366
name = "adoServicePrincipalObjectId"
327-
value = var.ado_create_app ? azuread_service_principal.ado_sp[0].object_id : ""
367+
value = var.pipeline_attempt_rbac_assignment && local.ado_sp_object_id_effective != null ? local.ado_sp_object_id_effective : ""
328368
}
329369
}
330370

terraform-infrastructure/outputs.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ output "ado_app_client_id" {
3535
}
3636

3737
output "ado_sp_object_id" {
38-
value = var.ado_enabled && var.ado_create_app ? azuread_service_principal.ado_sp[0].object_id : null
39-
description = "Object ID for the service principal. Useful for troubleshooting RBAC."
38+
value = var.ado_enabled ? local.ado_sp_object_id_effective : null
39+
description = "Object ID for the Azure DevOps service principal used by the service connection (useful for troubleshooting RBAC)."
4040
}
4141

4242
output "ado_project_id" {

terraform-infrastructure/provider.tf

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,9 @@ provider "azuread" {}
4040
provider "azuredevops" {
4141
# Provider blocks can't be conditional. Use a syntactically-valid placeholder URL when
4242
# Azure DevOps resources are disabled so non-ADO applies still work.
43-
org_service_url = var.ado_enabled ? var.ado_org_service_url : "https://dev.azure.com/unused"
43+
# When ado_enabled=true and ado_org_service_url is null/empty, the azuredevops provider can
44+
# read AZDO_ORG_SERVICE_URL from the environment.
45+
org_service_url = var.ado_enabled ? (
46+
length(trimspace(var.ado_org_service_url == null ? "" : var.ado_org_service_url)) > 0 ? trimspace(var.ado_org_service_url) : null
47+
) : "https://dev.azure.com/unused"
4448
}

terraform-infrastructure/terraform.ado.tfvars

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ ado_service_connection_name = "sc-artifact-signing"
2525
# Default: secretless auth (requires org feature enabled)
2626
ado_service_endpoint_authentication_scheme = "WorkloadIdentityFederation"
2727

28+
# Least-privilege notes:
29+
# - By default, this repo no longer grants RG Contributor to the ADO SP.
30+
# If you want the PIPELINE to auto-create the certificate profile (to avoid a second terraform apply),
31+
# you can opt in by setting this to true:
32+
# assign_contributor_role_to_ado_sp = true
33+
#
34+
# - By default, the pipeline will NOT attempt to self-assign RBAC via az role assignment create.
35+
# (RBAC should be handled by Terraform instead.)
36+
# pipeline_attempt_rbac_assignment = true
37+
2838
# If your org can't use WIF yet, switch to ServicePrincipal and provide the secret via:
2939
# $env:TF_VAR_ado_service_principal_client_secret = "..."
3040
# ado_service_endpoint_authentication_scheme = "ServicePrincipal"

terraform-infrastructure/terraform.tfvars

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ keyvault_enabled = true
1616
# Optional override (must be globally unique):
1717
# keyvault_name = "kvREPLACE_ME"
1818

19+
# If you want full portal access (Secrets/Keys/Certificates) without needing separate RBAC roles,
20+
# grant the identity running `terraform apply` the Key Vault Administrator role.
21+
keyvault_grant_administrator_to_current = true
22+
1923
# Azure DevOps Workload Identity Federation (optional)
2024
ado_enabled = false
2125
ado_org_service_url = "https://dev.azure.com/REPLACE_ME"

terraform-infrastructure/variables.tf

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,15 @@ variable "ado_org_service_url" {
9696
validation {
9797
condition = (
9898
var.ado_enabled == false || (
99-
length(trimspace(var.ado_org_service_url == null ? "" : var.ado_org_service_url)) > 0 &&
100-
can(regex("^https://dev\\.azure\\.com/[^\\s/]+$", trimspace(var.ado_org_service_url == null ? "" : var.ado_org_service_url))) &&
101-
!strcontains(upper(trimspace(var.ado_org_service_url == null ? "" : var.ado_org_service_url)), "REPLACE_ME")
99+
# Either provide ado_org_service_url explicitly, OR leave it null/empty and let
100+
# the azuredevops provider read AZDO_ORG_SERVICE_URL from the environment.
101+
length(trimspace(var.ado_org_service_url == null ? "" : var.ado_org_service_url)) == 0 || (
102+
can(regex("^https://dev\\.azure\\.com/[^\\s/]+$", trimspace(var.ado_org_service_url == null ? "" : var.ado_org_service_url))) &&
103+
!strcontains(upper(trimspace(var.ado_org_service_url == null ? "" : var.ado_org_service_url)), "REPLACE_ME")
104+
)
102105
)
103106
)
104-
error_message = "When ado_enabled=true you must set ado_org_service_url to a real org URL like https://dev.azure.com/your-org (not REPLACE_ME)."
107+
error_message = "When ado_enabled=true set ado_org_service_url to a real org URL like https://dev.azure.com/your-org (not REPLACE_ME), or leave it null/empty and set env var AZDO_ORG_SERVICE_URL."
105108
}
106109
}
107110

@@ -232,10 +235,28 @@ variable "assign_signer_role_to_ado_sp" {
232235
default = true
233236
}
234237

238+
variable "assign_signer_role_to_ado_sp_at_account_scope" {
239+
type = bool
240+
description = "If true, assigns 'Artifact Signing Certificate Profile Signer' to the Azure DevOps service principal at the Code Signing ACCOUNT scope. This is broader than profile-scope; prefer false for least privilege."
241+
default = false
242+
}
243+
244+
variable "assign_identity_verifier_role_to_current" {
245+
type = bool
246+
description = "If true, assigns 'Artifact Signing Identity Verifier' on the Artifact Signing account to the identity running terraform apply. Required to complete identity validation in the Azure portal."
247+
default = true
248+
}
249+
235250
variable "assign_contributor_role_to_ado_sp" {
236251
type = bool
237252
description = "If true, assigns Contributor at the resource group scope to the Azure DevOps service principal. Required if you want the pipeline to create the certificate profile (so you don't need a second terraform apply)."
238-
default = true
253+
default = false
254+
}
255+
256+
variable "pipeline_attempt_rbac_assignment" {
257+
type = bool
258+
description = "If true, Terraform passes the ADO service principal object id to the pipeline so it can attempt az role assignment create. Prefer false for least privilege (do RBAC in Terraform instead)."
259+
default = false
239260
}
240261

241262
variable "keyvault_enabled" {
@@ -244,6 +265,30 @@ variable "keyvault_enabled" {
244265
default = true
245266
}
246267

268+
variable "keyvault_populate_secrets" {
269+
type = bool
270+
description = "If true, Terraform writes the artifactSigning* secrets into Key Vault during apply (requires data-plane RBAC on the vault). Set false for least privilege if you prefer managing secrets outside Terraform."
271+
default = true
272+
}
273+
274+
variable "keyvault_grant_keys_access_to_current" {
275+
type = bool
276+
description = "If true, grants the identity running terraform apply permission to view/use Key Vault Keys via RBAC (assigns 'Key Vault Crypto User'). Default false (least privilege)."
277+
default = false
278+
}
279+
280+
variable "keyvault_grant_certificates_access_to_current" {
281+
type = bool
282+
description = "If true, grants the identity running terraform apply permission to view/use Key Vault Certificates via RBAC (assigns 'Key Vault Certificates User'). Default false (least privilege)."
283+
default = false
284+
}
285+
286+
variable "keyvault_grant_administrator_to_current" {
287+
type = bool
288+
description = "If true, grants the identity running terraform apply full Key Vault administration rights via RBAC (assigns 'Key Vault Administrator'). Broad; prefer the narrower key/cert toggles when possible. Default false."
289+
default = false
290+
}
291+
247292
variable "keyvault_name" {
248293
type = string
249294
description = "Optional Key Vault name override (globally unique, 3-24 chars, alphanumeric). If null/empty and keyvault_enabled=true, Terraform generates a name."

0 commit comments

Comments
 (0)