Skip to content

Support secretless managed-identity Terraform provider auth in addition to SPN client secret #496

Closed

Description

Summary

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:

  1. Secret (default) — preserve arm_client_id plus TF_VAR_arm_client_secret unchanged.
  2. 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.

Activity

  1. doherty100 commented on Sep 17, 2026

    @doherty100
    ContributorAuthor

    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)

  2. doherty100 commented on Sep 21, 2026

    @doherty100
    ContributorAuthor

    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. 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.

  3. changed the title [-]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[/+] on Sep 21, 2026
  4. doherty100 commented on Sep 22, 2026

    @doherty100
    ContributorAuthor

    Scope refinement: system-assigned MSI only

    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.

  5. doherty100 commented on Sep 22, 2026

    @doherty100
    ContributorAuthor

    Test evidence: default secret authentication mode

    A full sandbox was deployed and tested to confirm the proposed changes preserve the existing service-principal client-secret behavior without regression.

    Environment

    • Branch: issue-496-secretless-terraform-provider-auth
    • arm_auth_mode: secret (the default, i.e. the variable was not set explicitly)
    • Credentials: service principal arm_client_id plus TF_VAR_arm_client_secret environment variable, exactly as documented today
    • Modules enabled: vnet_app and mssql (plus the always-on vnet_shared)
    • Terraform execution environment: a Linux VM using an azurerm remote state backend

    Terraform apply

    Apply complete! Resources: 192 added, 0 changed, 0 destroyed.
    

    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.

    [PASS] vnet-shared: Passed=9 Failed=0
    [PASS] vnet-app: Passed=20 Failed=0
    [PASS] mssql: Passed=6 Failed=0
    [PASS] vnet-shared: Passed=2 Failed=0
    [PASS] integration: Azure SQL: jumpwin1 -> testdb: Passed=4 Failed=0
    Overall: Passed=41 Failed=0 Total=41
    RESULT: PASS
    

    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_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.

  6. doherty100 commented on Sep 22, 2026

    @doherty100
    ContributorAuthor

    Test evidence: msi authentication mode

    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:

    1. Azure RBAC Owner at subscription scope. Owner rather than Contributor, because the
      configuration creates role assignments and therefore needs
      Microsoft.Authorization/roleAssignments/write.
    2. 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.

    Terraform apply

    Apply complete! Resources: 192 added, 0 changed, 0 destroyed.
    

    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 jumpwin1 managed 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.

    Automated tests

    [PASS] vnet-shared: Passed=9 Failed=0
    [PASS] vnet-app: Passed=20 Failed=0
    [PASS] mssql: Passed=6 Failed=0
    [PASS] vnet-shared: Passed=2 Failed=0
    [PASS] integration: Azure SQL: jumpwin1 -> testdb: Passed=4 Failed=0
    Overall: Passed=41 Failed=0 Total=41
    RESULT: PASS
    

    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.

  7. doherty100 commented on Sep 22, 2026

    @doherty100
    ContributorAuthor

    Completed in #743, merged to vnext on 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.

    secret remains the unchanged default; msi enables a fully secretless deployment. OIDC was intentionally excluded, per the architecture correction on #173.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions