Skip to content

feat: support WIF for MCA SPs #54

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [v0.12.0]

### Added

- Optional administrative unit support
- Support workload identity federation for MCA service principals

## [v0.11.0]

### Added

- Billing scope to outputs
- Admin consent for delegated permission in SSO module

### Changed

- Upgraded minimum terraform provider versions

## [v0.10.0]
Expand Down Expand Up @@ -82,7 +98,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Initial Release

[unreleased]: https://github.com/meshcloud/terraform-azure-meshplatform/compare/v0.10.0...HEAD
[unreleased]: https://github.com/meshcloud/terraform-azure-meshplatform/compare/v0.12.0...HEAD
[v0.12.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.12.0
[v0.11.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.11.0
[v0.1.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.1.0
[v0.2.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.2.0
[v0.3.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.3.0
Expand Down
43 changes: 39 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ To run this module, you need the following:

- [Terraform installed](https://learn.hashicorp.com/tutorials/terraform/install-cli) (already installed in Azure Portal)
- [Azure CLI installed](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) (already installed in Azure Portal)
- Permissions on AAD level. If using Microsoft Customer Agreement, AAD level permissions must be set in the Tenant Directory that will create the subscriptions (*Source Tenant*) as well as the Tenant Directory that will receive the subscriptions (*Destination Tenant*). An Azure account with one of the following roles:
- Permissions on Entra ID level. If using Microsoft Customer Agreement, Entra ID level permissions must be set in the Tenant Directory that will create the subscriptions (*Source Tenant*) as well as the Tenant Directory that will receive the subscriptions (*Destination Tenant*). An Azure account with one of the following roles:
1. Global Administrator
2. Privileged Role Administrator AND (Cloud) Application Administrator
- Permissions on Azure Resource Level: User Access Administrator on the Management Group that should be managed by meshStack
Expand Down Expand Up @@ -83,11 +83,11 @@ To run this module, you need the following:

**Prerequisites**:

- Ensure you have permissions in the source AAD Tenant for granting access to the billing account used for subscription creation using the `Account Administrator` role
- Ensure you have permissions in the source Entra ID Tenant for granting access to the billing account used for subscription creation using the `Account Administrator` role

**Create MCA service principals**:

> With this module, you can create multiple MCA service principals by passing a list of `mca.service_principal_names`. This is useful for environments with restricted acceses to the AAD tenant holding the MCA license.
> With this module, you can create multiple MCA service principals by passing a list of `mca.service_principal_names`. This is useful for environments with restricted access to the Entra ID tenant holding the MCA license. A sample case would be to have one "source" Entra ID tenant in your organization in which you can create Azure subscriptions, and multiple "destination" tenants.

Add an `mca` block when calling this module.

Expand All @@ -107,6 +107,41 @@ module "meshplatform" {
}
```

## Workload Identity Federation for Multiple Environments

When using multiple MCA service principals with Workload Identity Federation (WIF), you can configure per-service-principal subjects to support different Kubernetes namespaces or environments.

### Single Subject (All MCA Service Principals)

Use `mca_subject` when all MCA service principals should use the same Kubernetes service account:

```hcl
workload_identity_federation = {
issuer = "..."
replicator_subject = "system:serviceaccount:meshcloud:replicator"
kraken_subject = "system:serviceaccount:meshcloud:kraken"
mca_subject = "system:serviceaccount:meshcloud:replicator" # with this, all MCA SPs will have this subject in its federated credentials
}
```

### Per-Service-Principal Subjects (Recommended for Multi-Environment)

Use `mca_subjects` to configure different subjects for each MCA service principal

```hcl
workload_identity_federation = {
issuer = "..."
replicator_subject = "system:serviceaccount:meshcloud-dev:replicator"
kraken_subject = "system:serviceaccount:meshcloud-dev:kraken"
mca_subjects = {
"meshcloud-dev" = "system:serviceaccount:meshcloud-dev:replicator"
"meshcloud-prod" = "system:serviceaccount:meshcloud-prod:replicator"
}
}
```

This approach allows each service principal to have its own custom subject when configuring WIF.

### Using Pre-provisioned Subscriptions

meshStack will need to be able to read subscriptions at the source location
Expand Down Expand Up @@ -218,7 +253,7 @@ Before opening a Pull Request, please do the following:
| <a name="input_sso_identity_provider_alias"></a> [sso\_identity\_provider\_alias](#input\_sso\_identity\_provider\_alias) | Identity provider alias. This value needs to be passed to meshcloud to configure the identity provider. | `string` | `"oidc"` | no |
| <a name="input_sso_meshstack_idp_domain"></a> [sso\_meshstack\_idp\_domain](#input\_sso\_meshstack\_idp\_domain) | meshStack identity provider domain that was provided by meshcloud. It is individual per meshStack. In most cases it is sso.<portal-domain> | `string` | `"replaceme"` | no |
| <a name="input_sso_service_principal_name"></a> [sso\_service\_principal\_name](#input\_sso\_service\_principal\_name) | Service principal for Entra ID SSO. Name must be unique per Entra ID. | `string` | `"meshcloud SSO"` | no |
| <a name="input_workload_identity_federation"></a> [workload\_identity\_federation](#input\_workload\_identity\_federation) | Enable workload identity federation by creating federated credentials for enterprise applications. Usually you'd receive the required settings when attempting to configure a platform with workload identity federation in meshStack. | `object({ issuer = string, replicator_subject = string, kraken_subject = string })` | `null` | no |
| <a name="input_workload_identity_federation"></a> [workload\_identity\_federation](#input\_workload\_identity\_federation) | Enable workload identity federation by creating federated credentials for enterprise applications. Usually you'd receive the required settings when attempting to configure a platform with workload identity federation in meshStack. | <pre>object({<br> issuer = string<br> replicator_subject = string<br> kraken_subject = string<br> # For MCA service principals: can be either a single subject for all SPs or a map of SP name to subject<br> mca_subject = optional(string)<br> mca_subjects = optional(map(string))<br> })</pre> | `null` | no |

## Outputs

Expand Down
8 changes: 8 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ module "mca_service_principal" {
billing_profile_name = var.mca.billing_profile_name
invoice_section_name = var.mca.invoice_section_name

create_password = var.create_passwords
workload_identity_federation = var.workload_identity_federation == null ? null : {
issuer = var.workload_identity_federation.issuer
# Use per-service-principal subjects if provided, otherwise fall back to single subject
subject = var.workload_identity_federation.mca_subjects == null ? var.workload_identity_federation.mca_subject : null
subjects = var.workload_identity_federation.mca_subjects
}

application_owners = var.application_owners
}

Expand Down
35 changes: 33 additions & 2 deletions modules/meshcloud-mca-service-principal/module.tf
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,45 @@ resource "azapi_resource_action" "remove_role_assignment_subscription_creator" {
// Create new client secret and associate it with the application
//---------------------------------------------------------------------------
resource "time_rotating" "mca_secret_rotation" {
count = var.create_password ? 1 : 0

rotation_days = 365
}

resource "azuread_application_password" "mca" {
for_each = toset(var.service_principal_names)
for_each = var.create_password ? toset(var.service_principal_names) : toset([])

application_id = azuread_application.mca[each.key].id
rotate_when_changed = {
rotation = time_rotating.mca_secret_rotation.id
rotation = time_rotating.mca_secret_rotation[0].id
}
}

//---------------------------------------------------------------------------
// Create federated identity credentials
//---------------------------------------------------------------------------
locals {
# Determine the subject for each service principal
wif_subjects = var.workload_identity_federation == null ? {} : (
var.workload_identity_federation.subjects != null
? var.workload_identity_federation.subjects
: var.workload_identity_federation.subject != null
? { for name in var.service_principal_names : name => var.workload_identity_federation.subject }
: {}
)
}

resource "azuread_application_federated_identity_credential" "mca" {
for_each = length(local.wif_subjects) > 0 ? toset(keys(local.wif_subjects)) : toset([])

application_id = azuread_application.mca[each.key].id
display_name = each.key
audiences = ["api://AzureADTokenExchange"]
issuer = var.workload_identity_federation.issuer
subject = local.wif_subjects[each.key]
}

moved {
from = time_rotating.mca_secret_rotation
to = time_rotating.mca_secret_rotation[0]
}
5 changes: 2 additions & 3 deletions modules/meshcloud-mca-service-principal/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ output "credentials" {
for name in var.service_principal_names : name => {
Enterprise_Application_Object_ID = azuread_service_principal.mca[name].object_id
Application_Client_ID = azuread_application.mca[name].client_id
Client_Secret = "Execute `terraform output mca_service_principal_password` to see the password"
Client_Secret = var.create_password ? "Execute `terraform output mca_service_principal_password` to see the password" : "No password was created"
}
}
}

output "application_client_secret" {
description = "Client Secret Of the Application."
value = { for name in var.service_principal_names : name => azuread_application_password.mca[name].value }
value = var.create_password ? { for name in var.service_principal_names : name => azuread_application_password.mca[name].value } : {}
sensitive = true
}

28 changes: 28 additions & 0 deletions modules/meshcloud-mca-service-principal/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,31 @@ variable "application_owners" {
description = "List of user principals that should be added as owners to the mca service principal."
default = []
}

variable "create_password" {
type = bool
description = "Create a password for the enterprise application."
default = true
}

variable "workload_identity_federation" {
default = null
description = "Enable workload identity federation instead of using a password by providing these additional settings. Can be either a single configuration for all service principals, or a map with per-service-principal configuration."
type = object({
issuer = string
# subject can be either a single string (applied to all SPs) or a map of SP name to subject
subject = optional(string)
subjects = optional(map(string))
})

validation {
condition = var.workload_identity_federation == null || (
var.workload_identity_federation.subject != null && var.workload_identity_federation.subjects == null
) || (
var.workload_identity_federation.subject == null && var.workload_identity_federation.subjects != null
) || (
var.workload_identity_federation.subject == null && var.workload_identity_federation.subjects == null
)
error_message = "If using workload_identity_federation for MCA, either 'subject' (for all service principals) or 'subjects' (per service principal) can be provided, but not both. Both can be null if MCA WIF is not needed."
}
}
20 changes: 19 additions & 1 deletion variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,25 @@ variable "create_passwords" {
variable "workload_identity_federation" {
default = null
description = "Enable workload identity federation by creating federated credentials for enterprise applications. Usually you'd receive the required settings when attempting to configure a platform with workload identity federation in meshStack."
type = object({ issuer = string, replicator_subject = string, kraken_subject = string })
type = object({
issuer = string
replicator_subject = string
kraken_subject = string
# For MCA service principals: can be either a single subject for all SPs or a map of SP name to subject
mca_subject = optional(string)
mca_subjects = optional(map(string))
})

validation {
condition = var.workload_identity_federation == null || (
var.workload_identity_federation.mca_subject == null && var.workload_identity_federation.mca_subjects == null
) || (
var.workload_identity_federation.mca_subject != null && var.workload_identity_federation.mca_subjects == null
) || (
var.workload_identity_federation.mca_subject == null && var.workload_identity_federation.mca_subjects != null
)
error_message = "For MCA configuration, either 'mca_subject' (for all service principals) or 'mca_subjects' (per service principal) can be provided, but not both."
}
}

variable "mca" {
Expand Down
Loading