Skip to content

Commit 7eae2aa

Browse files
vjeffreyclaude
andauthored
feat(azure): Workload Identity Federation for mondoo_integration_azure (#429)
* feat(azure): add use_wif + computed wif_subject/wif_issuer_url to schema; send useWif on create/update, read computed WIF fields - Add UseWif/WifSubject/WifIssuerUrl fields to integrationAzureResourceModel - Make Credential a pointer (nil in WIF mode) - Add use_wif (optional bool), wif_subject/wif_issuer_url (computed string with UseStateForUnknown) to schema - Make credentials Optional (was Required) - Implement resource-level ExactlyOneOf validator for use_wif vs credentials - Build azureOpts conditionally: UseWif=true or Certificate=pem - Read populates wif_subject/wif_issuer_url from provider's own GQL query (WifSubject/WifIssuerUrl added to AzureConfigurationOptions in gql.go) - Fix pre-existing mondoo-go schema-bump compatibility: SpaceMrn *String, ExceptionsDeleteInput.SpaceMrn *String, GcsBucketConfigurationOptionsInput. ServiceAccount *String, BigqueryConfigurationOptionsInput.ServiceAccount *String - Add WebhookConfigurationOptions to ClientIntegrationConfigurationOptions (was missing from gql_generated.go template) - go.mod: replace go.mondoo.com/mondoo-go => local azure-wif-fields branch (pins to tagged release once PR A merges) * docs(azure): WIF example + regenerated docs - Add examples/resources/mondoo_integration_azure/resource_wif.tf showing the WIF DAG: azuread_application -> mondoo_integration_azure(use_wif=true) -> azuread_application_federated_identity_credential(wif_subject/wif_issuer_url) - Regenerate docs/resources/integration_azure.md with use_wif, wif_subject, wif_issuer_url attributes and WIF example - Apply terraform fmt to testdata/test-wif-tf.tf (indentation normalization) * fix(azure): guard nil credentials and avoid dropping state on transient Read errors - Create/Update: with use_wif=false and no credentials block, ExactlyOneOf validation passes (null attr is treated as set), reaching the else branch and dereferencing a nil *integrationAzureCredentialModel. Guard with an explicit data.Credential != nil check and a clear diagnostic instead of panicking. - Read: previously RemoveResource was called on ANY GetClientIntegration error, silently dropping the resource from state on transient (network/auth/server) failures. Only remove on a genuine not-found via isNotFoundError; otherwise surface a diagnostic and keep state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): pin merged mondoo-go, drop local replace directive * fix(lint): use reflect.Pointer instead of deprecated reflect.Ptr golangci-lint v2.12.2 (latest) flags reflect.Ptr via the govet inline check. reflect.Pointer is the modern, identical alias (Go 1.18+). * fix(azure): surface WIF read-back failures + correct import auth mode - Create: warn (in WIF mode) when post-create fetch of wif_subject/ wif_issuer_url fails, instead of silently leaving them empty. - ImportState: detect WIF vs certificate mode from the API response so the imported state satisfies the ExactlyOneOf(use_wif, credentials) validator; certificate PEM is write-only so left null for re-supply. - Drop the stale INTERIM comment on the read-back struct. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c685879 commit 7eae2aa

8 files changed

Lines changed: 665 additions & 34 deletions

File tree

docs/resources/integration_azure.md

Lines changed: 263 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,26 +273,288 @@ resource "mondoo_integration_azure" "azure_integration" {
273273
}
274274
```
275275

276+
```terraform
277+
# Workload Identity Federation (keyless) example
278+
# -----------------------------------------------
279+
#
280+
# This example wires up a Mondoo Azure integration using Workload Identity
281+
# Federation (WIF). No certificate or client secret is stored — Mondoo
282+
# authenticates as a federated workload.
283+
#
284+
# DAG: azuread_application → mondoo_integration_azure(use_wif=true) →
285+
# azuread_application_federated_identity_credential
286+
#
287+
# Usage:
288+
# terraform init
289+
# terraform apply -var tenant_id=<your-tenant-id> \
290+
# -var primary_subscription=<your-subscription-id>
291+
292+
# Variables
293+
# ----------------------------------------------
294+
295+
variable "tenant_id" {
296+
description = "The Azure Active Directory Tenant ID"
297+
type = string
298+
default = "ffffffff-ffff-ffff-ffff-ffffffffffff"
299+
}
300+
301+
variable "primary_subscription" {
302+
description = "The primary Azure Subscription ID"
303+
type = string
304+
default = "ffffffff-ffff-ffff-ffff-ffffffffffff"
305+
}
306+
307+
locals {
308+
mondoo_security_integration_name = "Mondoo Security Integration (WIF)"
309+
}
310+
311+
# Azure AD Application
312+
# ----------------------------------------------
313+
314+
provider "azuread" {
315+
tenant_id = var.tenant_id
316+
}
317+
318+
data "azuread_client_config" "current" {}
319+
320+
# Create the Azure AD application for Mondoo
321+
resource "azuread_application" "mondoo_security" {
322+
display_name = local.mondoo_security_integration_name
323+
324+
required_resource_access {
325+
resource_app_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph
326+
327+
resource_access {
328+
id = "246dd0d5-5bd0-4def-940b-0421030a5b68"
329+
type = "Role"
330+
}
331+
332+
resource_access {
333+
id = "e321f0bb-e7f7-481e-bb28-e3b0b32d4bd0"
334+
type = "Role"
335+
}
336+
337+
resource_access {
338+
id = "5e0edab9-c148-49d0-b423-ac253e121825"
339+
type = "Role"
340+
}
341+
342+
resource_access {
343+
id = "bf394140-e372-4bf9-a898-299cfc7564e5"
344+
type = "Role"
345+
}
346+
347+
resource_access {
348+
id = "6e472fd1-ad78-48da-a0f0-97ab2c6b769e"
349+
type = "Role"
350+
}
351+
352+
resource_access {
353+
id = "dc5007c0-2d7d-4c42-879c-2dab87571379"
354+
type = "Role"
355+
}
356+
357+
resource_access {
358+
id = "b0afded3-3588-46d8-8b3d-9842eff778da"
359+
type = "Role"
360+
}
361+
362+
resource_access {
363+
id = "7ab1d382-f21e-4acd-a863-ba3e13f7da61"
364+
type = "Role"
365+
}
366+
367+
resource_access {
368+
id = "197ee4e9-b993-4066-898f-d6aecc55125b"
369+
type = "Role"
370+
}
371+
372+
resource_access {
373+
id = "9a5d68dd-52b0-4cc2-bd40-abcf44ac3a30"
374+
type = "Role"
375+
}
376+
377+
resource_access {
378+
id = "f8f035bb-2cce-47fb-8bf5-7baf3ecbee48"
379+
type = "Role"
380+
}
381+
382+
resource_access {
383+
id = "dbb9058a-0e50-45d7-ae91-66909b5d4664"
384+
type = "Role"
385+
}
386+
387+
resource_access {
388+
id = "9e640839-a198-48fb-8b9a-013fd6f6cbcd"
389+
type = "Role"
390+
}
391+
392+
resource_access {
393+
id = "37730810-e9ba-4e46-b07e-8ca78d182097"
394+
type = "Role"
395+
}
396+
397+
resource_access {
398+
id = "c7fbd983-d9aa-4fa7-84b8-17382c103bc4"
399+
type = "Role"
400+
}
401+
}
402+
}
403+
404+
# Create a service principal for the application
405+
resource "azuread_service_principal" "mondoo_security" {
406+
client_id = azuread_application.mondoo_security.client_id
407+
app_role_assignment_required = false
408+
owners = [data.azuread_client_config.current.object_id]
409+
}
410+
411+
# Azure Permissions
412+
# ----------------------------------------------
413+
414+
provider "azurerm" {
415+
tenant_id = var.tenant_id
416+
features {}
417+
}
418+
419+
data "azurerm_subscription" "primary" {
420+
subscription_id = var.primary_subscription
421+
}
422+
423+
data "azurerm_subscriptions" "available" {}
424+
425+
# Custom role with the permissions Mondoo needs
426+
resource "azurerm_role_definition" "mondoo_security_role" {
427+
name = "tf-mondoo-security-role-wif"
428+
description = "Permissions for Mondoo Security (WIF mode)"
429+
scope = data.azurerm_subscription.primary.id
430+
431+
permissions {
432+
actions = [
433+
"Microsoft.Authorization/*/read",
434+
"Microsoft.ResourceHealth/availabilityStatuses/read",
435+
"Microsoft.Insights/alertRules/*",
436+
"Microsoft.Resources/deployments/*",
437+
"Microsoft.Resources/subscriptions/resourceGroups/read",
438+
"Microsoft.Support/*",
439+
"Microsoft.Web/listSitesAssignedToHostName/read",
440+
"Microsoft.Web/serverFarms/read",
441+
"Microsoft.Web/sites/config/read",
442+
"Microsoft.Web/sites/config/web/appsettings/read",
443+
"Microsoft.Web/sites/config/web/connectionstrings/read",
444+
"Microsoft.Web/sites/config/appsettings/read",
445+
"Microsoft.web/sites/config/snapshots/read",
446+
"Microsoft.Web/sites/config/list/action",
447+
"Microsoft.Web/sites/read",
448+
"Microsoft.KeyVault/checkNameAvailability/read",
449+
"Microsoft.KeyVault/deletedVaults/read",
450+
"Microsoft.KeyVault/locations/*/read",
451+
"Microsoft.KeyVault/vaults/*/read",
452+
"Microsoft.KeyVault/operations/read",
453+
"Microsoft.Compute/virtualMachines/runCommands/read",
454+
"Microsoft.Compute/virtualMachines/runCommands/write",
455+
"Microsoft.Compute/virtualMachines/runCommands/delete"
456+
]
457+
not_actions = []
458+
data_actions = [
459+
"Microsoft.KeyVault/vaults/*/read",
460+
"Microsoft.KeyVault/vaults/secrets/readMetadata/action"
461+
]
462+
not_data_actions = []
463+
}
464+
465+
assignable_scopes = data.azurerm_subscriptions.available.subscriptions[*].id
466+
}
467+
468+
resource "azurerm_role_assignment" "mondoo_security" {
469+
count = length(data.azurerm_subscriptions.available.subscriptions)
470+
scope = data.azurerm_subscriptions.available.subscriptions[count.index].id
471+
role_definition_id = azurerm_role_definition.mondoo_security_role.role_definition_resource_id
472+
principal_id = azuread_service_principal.mondoo_security.object_id
473+
}
474+
475+
resource "azurerm_role_assignment" "reader" {
476+
count = length(data.azurerm_subscriptions.available.subscriptions)
477+
scope = data.azurerm_subscriptions.available.subscriptions[count.index].id
478+
role_definition_name = "Reader"
479+
principal_id = azuread_service_principal.mondoo_security.object_id
480+
}
481+
482+
# Mondoo Integration (keyless / WIF)
483+
# ----------------------------------------------
484+
485+
provider "mondoo" {
486+
space = "hungry-poet-123456"
487+
}
488+
489+
# Step 1: Create the Mondoo integration with use_wif=true.
490+
# Mondoo returns wif_subject and wif_issuer_url, which are used in step 2.
491+
resource "mondoo_integration_azure" "this" {
492+
name = "Azure ${local.mondoo_security_integration_name}"
493+
tenant_id = var.tenant_id
494+
client_id = azuread_application.mondoo_security.client_id
495+
scan_vms = true
496+
use_wif = true
497+
498+
depends_on = [
499+
azuread_application.mondoo_security,
500+
azuread_service_principal.mondoo_security,
501+
azurerm_role_assignment.mondoo_security,
502+
azurerm_role_assignment.reader,
503+
]
504+
}
505+
506+
# Step 2: Wire up the federated identity credential using the subject/issuer
507+
# that Mondoo emits. This allows Mondoo to authenticate without a certificate.
508+
resource "azuread_application_federated_identity_credential" "mondoo" {
509+
application_id = azuread_application.mondoo_security.id
510+
display_name = "mondoo"
511+
issuer = mondoo_integration_azure.this.wif_issuer_url
512+
subject = mondoo_integration_azure.this.wif_subject
513+
audiences = ["api://AzureADTokenExchange"]
514+
}
515+
516+
# Outputs
517+
# ----------------------------------------------
518+
519+
output "integration_mrn" {
520+
value = mondoo_integration_azure.this.mrn
521+
description = "MRN of the Mondoo Azure integration"
522+
}
523+
524+
output "wif_subject" {
525+
value = mondoo_integration_azure.this.wif_subject
526+
description = "WIF subject configured on the federated identity credential"
527+
}
528+
529+
output "wif_issuer_url" {
530+
value = mondoo_integration_azure.this.wif_issuer_url
531+
description = "WIF issuer URL configured on the federated identity credential"
532+
}
533+
```
534+
276535
<!-- schema generated by tfplugindocs -->
277536
## Schema
278537

279538
### Required
280539

281540
- `client_id` (String) Azure client ID.
282-
- `credentials` (Attributes) (see [below for nested schema](#nestedatt--credentials))
283541
- `name` (String) Name of the integration.
284542
- `tenant_id` (String) Azure tenant ID.
285543

286544
### Optional
287545

546+
- `credentials` (Attributes) Certificate credentials for Azure integration. Mutually exclusive with `use_wif`. (see [below for nested schema](#nestedatt--credentials))
288547
- `scan_vms` (Boolean) Scan VMs.
289548
- `space_id` (String) Mondoo space identifier. If there is no space ID, the provider space is used.
290549
- `subscription_allow_list` (List of String) List of Azure subscriptions to scan.
291550
- `subscription_deny_list` (List of String) List of Azure subscriptions to exclude from scanning.
551+
- `use_wif` (Boolean) Use Workload Identity Federation (keyless) instead of a certificate. Mutually exclusive with `credentials`.
292552

293553
### Read-Only
294554

295555
- `mrn` (String) Integration identifier
556+
- `wif_issuer_url` (String) The WIF issuer URL (populated by Mondoo after creation). Use as the `issuer` of the `azuread_application_federated_identity_credential` resource.
557+
- `wif_subject` (String) The WIF subject (populated by Mondoo after creation). Use as the `subject` of the `azuread_application_federated_identity_credential` resource.
296558

297559
<a id="nestedatt--credentials"></a>
298560
### Nested Schema for `credentials`

0 commit comments

Comments
 (0)