You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Support secretless managed-identity Terraform provider auth in addition to SPN client secret #496
Prerequisite for #173, the ephemeral vnext → main sandbox validation gate.
The root configuration currently authenticates all Azure providers only with a service-principal client secret. This issue adds managed-identity provider authentication while preserving the current service-principal-secret path as the default for interactive/manual execution.
Goal
Support these selectable root authentication modes across azurerm, azapi, and azuread:
Secret (default) — preserve arm_client_id plus TF_VAR_arm_client_secret unchanged.
Managed identity (msi) — use a pre-provisioned user-assigned managed identity (UAMI), selected by arm_client_id and attached to the Terraform execution environment, without a long-lived secret.
Acceptance criteria
arm_auth_mode accepts secret (default) and msi, and consistently configures all three providers.
arm_client_secret is required only for secret mode; the existing manual flow and bootstrap helpers remain valid without changes.
A pre-provisioned UAMI, selected by arm_client_id, is required in MSI mode. It has the required Azure RBAC and Microsoft Graph group-management permissions before apply.
The optional mssql setup uses jumpwin1's separate system-assigned identity in both provider-authentication modes and never passes a service-principal secret to the VM.
Managed-identity mode is validated on an execution environment with the UAMI attached.
terraform validate, TFLint, and the updated root documentation pass.
Excluded scope
OIDC is not a Terraform provider mode for the current CD architecture. A GitHub-hosted job may use a narrowly scoped OIDC identity only to start jumplinux2; Terraform and the complete test suite execute on the self-hosted runner using an attached UAMI. The runner-local vWAN/P2S test retrieves its client credential from the sandbox Key Vault after public access is disabled, so a direct GitHub-hosted Terraform/test design requires separate credential and network work.
The root configuration does not provision the UAMI or grant its Azure RBAC/Microsoft Graph permissions. Those are pre-apply execution-environment prerequisites.
Terminology note: "CD" as referenced here (and throughout #173's sub-issues) means an ephemeral plan/apply validation gate for the vnext → main merge — it provisions a throwaway sandbox, runs tests, and is normally torn down on success. It is not a pipeline that keeps a persistent environment continuously up to date. See the terminology clarification on #173 for the full definition: #173 (comment)
The current CD gate requires the full unit and integration suite. A GitHub-hosted runner could use OIDC to run Terraform and dispatch many VM-side tests, but it cannot complete the required vWAN/P2S integration test after the apply barrier disables public access to the sandbox Key Vault: that runner-local test retrieves the P2S client private key from Key Vault.
For #173, GitHub-hosted OIDC is retained only for the narrowly scoped action that starts the self-hosted runner VM. jumplinux2 then uses managed identity for Terraform, its private state backend, and tests. Consequently, #496 should preserve the service-principal-secret default and add managed-identity provider support, but must not add an OIDC Terraform auth mode. A future direct GitHub-hosted path needs a separate secure P2S credential and state/test-network design.
Managed-identity mode is limited to the existing system-assigned identity on the rg-devops-iac self-hosted runner (jumplinux2). User-assigned managed identities are intentionally not supported.
arm_client_id is exclusively the service-principal client ID for secret mode and is rejected in MSI mode. This keeps the automated deployment contract narrow and aligned with #173.
A full sandbox was deployed and tested to confirm the proposed changes preserve the existing service-principal client-secret behavior without regression.
Total runtime was roughly 52 minutes with no errors. All three providers (azurerm, azapi, azuread) authenticated successfully using the client secret, confirming that passing client_id unconditionally and gating client_secret and use_msi on the authentication mode does not alter secret-mode behavior.
Automated tests
Invoke-UnitTests.ps1 was run against all deployed modules after confirming every VM was running.
The most important result is the Azure SQL path, since this change removes the service-principal secret from the VM run-command contract in every authentication mode:
azurerm_virtual_machine_run_command.create_mssql_db_user completed successfully while receiving only the server FQDN, database name, and VM name. No client ID, tenant ID, or protected secret was passed to the VM.
Create-AzSqlDbUser.ps1 acquired its Azure SQL access token through IMDS using the jumpwin1 system-assigned managed identity.
The Azure SQL: jumpwin1 -> testdb integration test passed 4 of 4 checks, confirming the contained database user was created correctly and can read from the database.
This demonstrates that the unified managed-identity approach for the SQL database user works even when Terraform itself is authenticating with a client secret, so there is a single code path rather than one per authentication mode.
Scope note
Live validation of msi mode is not included here. It requires a pre-provisioned user-assigned managed identity that has been granted the necessary Azure RBAC and Microsoft Graph group-management permissions in advance, so it will be validated separately.
A second sandbox was deployed and tested using arm_auth_mode = "msi" to validate the managed
identity path. This complements the secret mode evidence posted earlier.
Environment
arm_auth_mode: msi
arm_client_id: the client id of a pre-provisioned user-assigned managed identity (UAMI)
attached to the Terraform execution environment
No service principal secret was present.TF_VAR_arm_client_secret was unset for the entire
run, and arm_client_secret was omitted from terraform.tfvars
Modules enabled: vnet_app and mssql (plus the always-on vnet_shared), matching the secret
mode baseline exactly
UAMI preparation
Only two grants had to be made in advance, both to the UAMI:
Azure RBAC Owner at subscription scope.Owner rather than Contributor, because the
configuration creates role assignments and therefore needs Microsoft.Authorization/roleAssignments/write.
Microsoft Graph Group.ReadWrite.All application permission (app role 62a82d76-70ea-41e2-9197-370581804d09), required by azuread_group.sql_admins. This is an
admin-consent operation and needed an elevated directory role to grant.
Notably, the data-plane storage roles did not need pre-granting. modules/vnet-app/locals.tf
assigns Storage Blob Data Contributor and Storage File Data Privileged Contributor to data.azurerm_client_config.current.object_id, which resolves to the UAMI in MSI mode, so Terraform
grants them to itself during the apply.
Provider authentication confirmed before apply
The saved plan recorded module.vnet_shared.azurerm_role_assignment.roles["kv_secrets_officer_spn"] with a principal_id
equal to the UAMI's object id. That value is derived from data.azurerm_client_config.current.object_id, so it confirms the azurerm provider authenticated
as the managed identity. The service principal object id appeared nowhere in the plan.
Identical resource count to the secret mode run, with no errors.
Microsoft Graph write path confirmed
azuread_group.sql_admins was created successfully, and both intended members were added:
the interactive user (user_object_id)
the jumpwin1managed identity service principal
This exercises the Group.ReadWrite.All grant end to end: the managed identity created a security
group and added a service principal to it, with no client secret involved.
Note for anyone verifying this manually: az ad group member list may show only the user. Graph
omits member types the caller's token cannot read from the heterogeneous /members collection
rather than returning an error. Querying the typed cast /members/microsoft.graph.servicePrincipal reveals the managed identity, and the Terraform state
for the resource lists both members.
This matches the secret mode result exactly, including the Azure SQL: jumpwin1 -> testdb
integration test that verifies the contained database user created by Create-AzSqlDbUser.ps1
through an IMDS-acquired token.
An SMB read/write check in vnet-app failed on the first attempt and passed on re-run. It was a
scheduled task timeout unrelated to provider authentication, and is tracked separately.
Identities in use
In MSI mode the service principal is not used at all. Three separate identities are involved:
Identity
Purpose
User-assigned managed identity
azurerm, azapi, and azuread providers
Terraform host system-assigned identity
Terraform state backend access
jumpwin1 system-assigned identity
Azure SQL contained user via IMDS
Conclusion
Both authentication modes are now validated against the same module set, with identical results:
192 resources added and 41 of 41 checks passing. secret remains the default and is unchanged,
and msi enables a fully secretless deployment.
Both authentication modes were validated end to end against the same module set, with identical results: 192 resources added and 41 of 41 tests passing in each. Evidence for both runs is recorded in the comments above.
secret remains the unchanged default; msi enables a fully secretless deployment. OIDC was intentionally excluded, per the architecture correction on #173.
Activity
doherty100 commentedon Sep 17, 2026
Terminology note: "CD" as referenced here (and throughout #173's sub-issues) means an ephemeral plan/apply validation gate for the
vnext → mainmerge — it provisions a throwaway sandbox, runs tests, and is normally torn down on success. It is not a pipeline that keeps a persistent environment continuously up to date. See the terminology clarification on #173 for the full definition: #173 (comment)doherty100 commentedon Sep 21, 2026
Scope clarification: exclude OIDC provider authentication
The current CD gate requires the full unit and integration suite. A GitHub-hosted runner could use OIDC to run Terraform and dispatch many VM-side tests, but it cannot complete the required vWAN/P2S integration test after the apply barrier disables public access to the sandbox Key Vault: that runner-local test retrieves the P2S client private key from Key Vault.
For #173, GitHub-hosted OIDC is retained only for the narrowly scoped action that starts the self-hosted runner VM.
jumplinux2then uses managed identity for Terraform, its private state backend, and tests. Consequently, #496 should preserve the service-principal-secret default and add managed-identity provider support, but must not add an OIDC Terraform auth mode. A future direct GitHub-hosted path needs a separate secure P2S credential and state/test-network design.[-]Support secretless (managed identity / OIDC) Terraform provider auth in addition to SPN client secret[/-][+]Support secretless managed-identity Terraform provider auth in addition to SPN client secret[/+]doherty100 commentedon Sep 22, 2026
Scope refinement: system-assigned MSI only
Managed-identity mode is limited to the existing system-assigned identity on the
rg-devops-iacself-hosted runner (jumplinux2). User-assigned managed identities are intentionally not supported.arm_client_idis exclusively the service-principal client ID forsecretmode and is rejected in MSI mode. This keeps the automated deployment contract narrow and aligned with #173.doherty100 commentedon Sep 22, 2026
Test evidence: default
secretauthentication modeA full sandbox was deployed and tested to confirm the proposed changes preserve the existing service-principal client-secret behavior without regression.
Environment
issue-496-secretless-terraform-provider-autharm_auth_mode:secret(the default, i.e. the variable was not set explicitly)arm_client_idplusTF_VAR_arm_client_secretenvironment variable, exactly as documented todayvnet_appandmssql(plus the always-onvnet_shared)azurermremote state backendTerraform apply
Total runtime was roughly 52 minutes with no errors. All three providers (
azurerm,azapi,azuread) authenticated successfully using the client secret, confirming that passingclient_idunconditionally and gatingclient_secretanduse_msion the authentication mode does not alter secret-mode behavior.Automated tests
Invoke-UnitTests.ps1was run against all deployed modules after confirming every VM was running.Validation of the secretless Azure SQL bootstrap
The most important result is the Azure SQL path, since this change removes the service-principal secret from the VM run-command contract in every authentication mode:
azurerm_virtual_machine_run_command.create_mssql_db_usercompleted successfully while receiving only the server FQDN, database name, and VM name. No client ID, tenant ID, or protected secret was passed to the VM.Create-AzSqlDbUser.ps1acquired its Azure SQL access token through IMDS using thejumpwin1system-assigned managed identity.Azure SQL: jumpwin1 -> testdbintegration test passed 4 of 4 checks, confirming the contained database user was created correctly and can read from the database.This demonstrates that the unified managed-identity approach for the SQL database user works even when Terraform itself is authenticating with a client secret, so there is a single code path rather than one per authentication mode.
Scope note
Live validation of
msimode is not included here. It requires a pre-provisioned user-assigned managed identity that has been granted the necessary Azure RBAC and Microsoft Graph group-management permissions in advance, so it will be validated separately.doherty100 commentedon Sep 22, 2026
Test evidence:
msiauthentication modeA second sandbox was deployed and tested using
arm_auth_mode = "msi"to validate the managedidentity path. This complements the
secretmode evidence posted earlier.Environment
arm_auth_mode:msiarm_client_id: the client id of a pre-provisioned user-assigned managed identity (UAMI)attached to the Terraform execution environment
TF_VAR_arm_client_secretwas unset for the entirerun, and
arm_client_secretwas omitted fromterraform.tfvarsvnet_appandmssql(plus the always-onvnet_shared), matching the secretmode baseline exactly
UAMI preparation
Only two grants had to be made in advance, both to the UAMI:
Ownerat subscription scope.Ownerrather thanContributor, because theconfiguration creates role assignments and therefore needs
Microsoft.Authorization/roleAssignments/write.Group.ReadWrite.Allapplication permission (app role62a82d76-70ea-41e2-9197-370581804d09), required byazuread_group.sql_admins. This is anadmin-consent operation and needed an elevated directory role to grant.
Notably, the data-plane storage roles did not need pre-granting.
modules/vnet-app/locals.tfassigns
Storage Blob Data ContributorandStorage File Data Privileged Contributortodata.azurerm_client_config.current.object_id, which resolves to the UAMI in MSI mode, so Terraformgrants them to itself during the apply.
Provider authentication confirmed before apply
The saved plan recorded
module.vnet_shared.azurerm_role_assignment.roles["kv_secrets_officer_spn"]with aprincipal_idequal to the UAMI's object id. That value is derived from
data.azurerm_client_config.current.object_id, so it confirms theazurermprovider authenticatedas the managed identity. The service principal object id appeared nowhere in the plan.
Terraform apply
Identical resource count to the secret mode run, with no errors.
Microsoft Graph write path confirmed
azuread_group.sql_adminswas created successfully, and both intended members were added:user_object_id)jumpwin1managed identity service principalThis exercises the
Group.ReadWrite.Allgrant end to end: the managed identity created a securitygroup and added a service principal to it, with no client secret involved.
Automated tests
This matches the secret mode result exactly, including the
Azure SQL: jumpwin1 -> testdbintegration test that verifies the contained database user created by
Create-AzSqlDbUser.ps1through an IMDS-acquired token.
An SMB read/write check in
vnet-appfailed on the first attempt and passed on re-run. It was ascheduled task timeout unrelated to provider authentication, and is tracked separately.
Identities in use
In MSI mode the service principal is not used at all. Three separate identities are involved:
azurerm,azapi, andazureadprovidersjumpwin1system-assigned identityConclusion
Both authentication modes are now validated against the same module set, with identical results:
192 resources added and 41 of 41 checks passing.
secretremains the default and is unchanged,and
msienables a fully secretless deployment.doherty100 commentedon Sep 22, 2026
Completed in #743, merged to
vnexton 2026-09-22.Both authentication modes were validated end to end against the same module set, with identical results: 192 resources added and 41 of 41 tests passing in each. Evidence for both runs is recorded in the comments above.
secretremains the unchanged default;msienables a fully secretless deployment. OIDC was intentionally excluded, per the architecture correction on #173.