Applies to: Azure Public Cloud
Workloads: Azure Virtual Machines | Azure Blob Storage | Azure Data Lake Storage | Azure Files | Azure SQL family
This article details how to manually onboard Azure subscriptions to Druva for cloud-native data protection via the Azure portal.
Whom is this for? Manual onboarding is designed for organizations with strict governance policies that prohibit running third-party automated scripts within Microsoft Entra ID or Azure environments.
Workflow at a glance
To complete setup, perform these steps in order:
Tenant Provisioning (Entra ID): Create the application registration, service principal, and client secret.
Subscription Provisioning (Azure RBAC): Create the resource group, user-assigned managed identity, custom RBAC roles, and Key Vaults.
Platform Registration: Submit the collected Azure resource parameters into the Druva console to complete onboarding.
Provision Tenant Resources (Entra ID)
Executed by: Microsoft Entra ID Administrator
Prerequisites
Microsoft Entra ID Role: Cloud Application Administrator.
Naming Convention Suffix: Establish a unique suffix (for example, your Druva Organization ID, such as 16567) to append to all resource names.
Register the Backup Application
Procedure
Sign in to the Azure Portal as a Cloud Application Administrator.
Search for and select Microsoft Entra ID.
In the left navigation menu, select App registrations > + New registration.
Configure the following settings:
Name:
druva-app-<suffix>(for example, druva-app-16567).Supported account types: Select Accounts in this organizational directory only (Single tenant).
Redirect URI: Leave blank.
Click Register.
On the Overview page, copy and record the following values:
Directory (tenant) ID (recorded as tenantID).
Application (client) ID (recorded as appID).
Locate the Enterprise Application (Service Principal)
Procedure
Creating the App Registration automatically provisions its Service Principal inside your tenant. You must record its specific Object ID for subscription-level role assignments.
In the left navigation menu of Microsoft Entra ID, select Enterprise applications.
Search for
druva-app-<suffix>and select your newly created application.On the Overview page, locate the Properties section.
Copy and record the Object ID (recorded as servicePrincipalID).
❗️Important
Securely record the Service Principal Object ID. Do not confuse this with the App Registration Object ID.
Generate the Application Client Secret
Procedure
Return to Microsoft Entra ID > App registrations and select your
druva-app-<suffix>application.In the left menu, select Certificates & secrets.
Under the Client secrets tab, click + New client secret.
Configure the secret:
Description:
druva-app-<suffix>-secret.Expires: Select 90 days (or per your organization's security policy).
Click Add.
Immediately copy and record the generated values:
Value: Copy the clear-text secret string (recorded as
clientSecretValue).Secret ID: Copy the GUID string (recorded as
secretKeyID).
Calculate or convert the secret's expiration date into a Unix Epoch Timestamp in seconds (recorded as
secretExpiryTS, e.g., 1893456000). See epochconverter.
⚠ Warning
Copy the client secret Value immediately. Azure obscures this value permanently once you navigate away from the page.
Hand-off Checklist
Save these values and securely share them with the Druva Cloud Administrator and Microsoft Azure Administrator for subscription and platform registration:
[ ] Tenant Suffix (
suffix): The unique naming identifier (e.g., Druva Organization ID 16567) used across all Entra ID and Azure resources.[ ] Tenant ID (
tenantID): Microsoft Entra ID Tenant GUID[ ] Application ID (
appID): App Registration Client GUID[ ] Service Principal ID (
servicePrincipalID): Enterprise Application Object ID[ ] Secret Key ID (
secretKeyID): Secret Identifier GUID[ ] Secret Expiration (
secretExpiryTS): Expiration date in Unix epoch seconds[ ] Secret Value: Plaintext application password
Provision Subscription Resources (Azure RBAC)
Executed by: Azure Administrator
Scope: Performed for each individual Azure subscription you want to protect with Druva.
Prerequisites
Resource Suffix (suffix): The unique naming suffix (e.g., 16567) established in Step 1 and provided by the Microsoft Entra ID Administrator.
Primary and Secondary Regions: Target primary and secondary Azure regions selected to host your Key Vaults (e.g., eastus and westus).
Azure Custom Role: A custom role assigned at subscription-level scope containing the baseline permissions defined in Druva-Subscription-Provisioning-Role. Create this role using the following JSON definition:
{
"properties": {
"roleName": "Druva-Subscription-Provisioning-Role",
"description": "Custom role containing baseline permissions required to provision Druva infrastructure components.",
"assignableScopes": [
"/subscriptions/00000000-0000-0000-0000-000000000000"
],
"permissions": [
{
"actions": [ "Microsoft.Resources/subscriptions/resourceGroups/write",
"Microsoft.Resources/subscriptions/resourceGroups/read", "Microsoft.ManagedIdentity/userAssignedIdentities/write",
"Microsoft.ManagedIdentity/userAssignedIdentities/read",
"Microsoft.Authorization/roleDefinitions/write",
"Microsoft.Authorization/roleDefinitions/read",
"Microsoft.Authorization/roleAssignments/write",
"Microsoft.Authorization/roleAssignments/read",
"Microsoft.KeyVault/vaults/read",
"Microsoft.KeyVault/vaults/write"
],
"notActions": [],
"dataActions": [],
"notDataActions": []
}
]
}
}
Create the Dedicated Resource Group
Create a dedicated resource group in your primary Azure region to house Druva infrastructure components.
Create a resource group
Sign in to the Azure Portal.
Search for and select Resource groups.
Select + Create.
On the Basics tab, configure the following settings:
Subscription: Select your target Azure subscription.
Resource group: Enter
druva-rg-<suffix>(e.g., druva-rg-16567).Region: Select your designated Primary Region.
Select Review + create, and then select Create.
Record the Resource Group Name (resourceGroupName).
📘 Additional Reading: For details on managing Azure resource groups, see Manage Azure Resource Manager resource groups by using the Azure portal in the Microsoft Learn documentation.
Create the User-Assigned Managed Identity
Create a user-assigned managed identity within the dedicated resource group to enable secure, passwordless access strictly to Druva-specific infrastructure secrets stored in Key Vault.
Create a user-assigned managed identity
Sign in to the Azure Portal.
In the search box at the top of the portal, enter Managed Identities.
Under Services, select Managed Identities.
Select + Create (or + Add).
In the Create User Assigned Managed Identity pane, on the Basics tab, configure the following settings:
Subscription: Select your target Azure subscription.
Resource group: Select
druva-rg-<suffix>.Name: Enter
druva-mi-<suffix>(e.g., druva-mi-16567).Region: Select your designated Primary Region (e.g., East US).
Restrictions: Leave as None.
Resource (Preview): Leave as default.
Select Review + create to review the changes, and then select Create.
Once deployment finishes, select Go to resource.
On the Overview page, record the following values:
Managed Identity Name (managedIdentityName)
Object (principal) ID (recorded for Key Vault access policies)
📘 Additional Reading: For official Microsoft guidance on creating and managing user-assigned managed identities, see Manage user-assigned managed identities using the Azure portal in the Microsoft Learn documentation.
Create Custom RBAC Roles
Create the mandatory Key Vault Permissions Role and the required Workload-Specific Custom Roles based on the Azure services you plan to protect.
Create the Key Vault Permissions Role (Mandatory)
Sign in to the Azure Portal.
Search for and select Subscriptions, and then select your target subscription.
Select Access Control (IAM) from the left menu.
Select + Add, and then select Add custom role.
On the Basics tab, set the Custom role name to
druva-role-kv-<suffix>(e.g., druva-role-kv-16567).On the JSON tab, select Edit, paste the following role definition, and select Save:
{
"properties": {
"roleName": "druva-role-kv-00000000-0000-0000-0000-000000000000-<suffix>",
"description": "Druva manual onboarding Key Vault permissions role",
"assignableScopes": [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/<Druva-Resource-Group-Id>"
],
"permissions": [
{
"actions": [
"Microsoft.KeyVault/vaults/read",
"Microsoft.KeyVault/vaults/write",
"Microsoft.KeyVault/vaults/secrets/read",
"Microsoft.KeyVault/vaults/secrets/write"
],
"notActions": [],
"dataActions": [],
"notDataActions": []
}
]
}
}Select Review + create, and then select Create.
Record the exact Role Name (azureRoleName for Key Vault).
📘 Additional Reading: For official Microsoft guidance on creating and managing custom roles in Azure, see Azure custom roles in the Microsoft Learn documentation.
Create the Azure Virtual Machines Workload Role
On your subscription page, select Access Control (IAM) > + Add > Add custom role.
On the Basics tab, set the Custom role name to
druva-role-vm-<suffix>(e.g., druva-role-vm-16567).On the JSON tab, select Edit, paste the following role definition, and select Save:
{
"properties": {
"roleName": "druva-role-vm-<suffix>",
"description": "Druva manual onboarding Azure Virtual Machines permissions role",
"assignableScopes": [
"/subscriptions/00000000-0000-0000-0000-000000000000"
],
"permissions": [
{
"actions": [
"Microsoft.Authorization/roleAssignments/read",
"Microsoft.Compute/diskEncryptionSets/read",
"Microsoft.Compute/diskEncryptionSets/write",
"Microsoft.Compute/disks/beginGetAccess/action",
"Microsoft.Compute/disks/delete",
"Microsoft.Compute/disks/endGetAccess/action",
"Microsoft.Compute/disks/read",
"Microsoft.Compute/disks/write",
"Microsoft.Compute/images/read",
"Microsoft.Compute/locations/operations/read",
"Microsoft.Compute/locations/vmSizes/read",
"Microsoft.Compute/skus/read",
"Microsoft.Compute/snapshots/beginGetAccess/action",
"Microsoft.Compute/snapshots/delete",
"Microsoft.Compute/snapshots/endGetAccess/action",
"Microsoft.Compute/snapshots/read",
"Microsoft.Compute/snapshots/write",
"Microsoft.Compute/virtualMachines/deallocate/action",
"Microsoft.Compute/virtualMachines/delete",
"Microsoft.Compute/virtualMachines/instanceView/read",
"Microsoft.Compute/virtualMachines/powerOff/action",
"Microsoft.Compute/virtualMachines/read",
"Microsoft.Compute/virtualMachines/vmSizes/read",
"Microsoft.Compute/virtualMachines/write",
"Microsoft.ManagedIdentity/identities/read",
"Microsoft.ManagedIdentity/userAssignedIdentities/assign/action",
"Microsoft.ManagedIdentity/userAssignedIdentities/read",
"Microsoft.Network/networkInterfaces/delete",
"Microsoft.Network/networkInterfaces/join/action",
"Microsoft.Network/networkInterfaces/read",
"Microsoft.Network/networkInterfaces/write",
"Microsoft.Network/networkSecurityGroups/join/action",
"Microsoft.Network/networkSecurityGroups/read",
"Microsoft.Network/publicIPAddresses/join/action",
"Microsoft.Network/publicIPAddresses/read",
"Microsoft.Network/virtualNetworks/CheckIPAddressAvailability/read",
"Microsoft.Network/virtualNetworks/read",
"Microsoft.Network/virtualNetworks/subnets/join/action",
"Microsoft.Network/virtualNetworks/subnets/read",
"Microsoft.Resources/subscriptions/resourceGroups/read",
"Microsoft.Storage/storageAccounts/read",
"Microsoft.Storage/storageAccounts/blobServices/containers/read"
],
"notActions": [],
"dataActions": [
"Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write"
],
"notDataActions": []
}
]
}
}Select Review + create, and then select Create.
Record the exact Role Name (azureRoleName for Azure Virtual Machines).
📘 Additional Reading: For official Microsoft guidance on creating and managing custom roles in Azure, see Azure custom roles in the Microsoft Learn documentation.
Create the Azure SQL Family Workload Role
Covers: Azure SQL Database | Azure SQL Managed Instance | SQL Server on Azure Virtual Machines
On your subscription page, select Access Control (IAM) > + Add > Add custom role.
On the Basics tab, set the Custom role name to
druva-role-sql-<suffix>(e.g., druva-role-sql-16567).On the JSON tab, select Edit, paste the following role definition, and select Save:
{
"properties": {
"roleName": "druva-role-vm-<suffix>",
"description": "Druva manual onboarding Azure Virtual Machines permissions role",
"assignableScopes": [
"/subscriptions/00000000-0000-0000-0000-000000000000"
],
"permissions": [
{
"actions": [
"Microsoft.Authorization/roleAssignments/read",
"Microsoft.Compute/diskEncryptionSets/read",
"Microsoft.Compute/diskEncryptionSets/write",
"Microsoft.Compute/disks/beginGetAccess/action",
"Microsoft.Compute/disks/delete",
"Microsoft.Compute/disks/endGetAccess/action",
"Microsoft.Compute/disks/read",
"Microsoft.Compute/disks/write",
"Microsoft.Compute/images/read",
"Microsoft.Compute/locations/operations/read",
"Microsoft.Compute/locations/vmSizes/read",
"Microsoft.Compute/skus/read",
"Microsoft.Compute/snapshots/beginGetAccess/action",
"Microsoft.Compute/snapshots/delete",
"Microsoft.Compute/snapshots/endGetAccess/action",
"Microsoft.Compute/snapshots/read",
"Microsoft.Compute/snapshots/write",
"Microsoft.Compute/virtualMachines/deallocate/action",
"Microsoft.Compute/virtualMachines/delete",
"Microsoft.Compute/virtualMachines/instanceView/read",
"Microsoft.Compute/virtualMachines/powerOff/action",
"Microsoft.Compute/virtualMachines/read",
"Microsoft.Compute/virtualMachines/vmSizes/read",
"Microsoft.Compute/virtualMachines/write",
"Microsoft.ManagedIdentity/identities/read",
"Microsoft.ManagedIdentity/userAssignedIdentities/assign/action",
"Microsoft.ManagedIdentity/userAssignedIdentities/read",
"Microsoft.Network/networkInterfaces/delete",
"Microsoft.Network/networkInterfaces/join/action",
"Microsoft.Network/networkInterfaces/read",
"Microsoft.Network/networkInterfaces/write",
"Microsoft.Network/networkSecurityGroups/join/action",
"Microsoft.Network/networkSecurityGroups/read",
"Microsoft.Network/publicIPAddresses/join/action",
"Microsoft.Network/publicIPAddresses/read",
"Microsoft.Network/virtualNetworks/CheckIPAddressAvailability/read",
"Microsoft.Network/virtualNetworks/read",
"Microsoft.Network/virtualNetworks/subnets/join/action",
"Microsoft.Network/virtualNetworks/subnets/read",
"Microsoft.Resources/subscriptions/resourceGroups/read",
"Microsoft.Storage/storageAccounts/read",
"Microsoft.Storage/storageAccounts/blobServices/containers/read"
],
"notActions": [],
"dataActions": [
"Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write"
],
"notDataActions": []
}
]
}
}Select Review + create, and then select Create.
Record the exact Role Name (azureRoleName for Azure SQL).
📘 Additional Reading: For official Microsoft guidance on creating and managing custom roles in Azure, see Azure custom roles in the Microsoft Learn documentation.
Create the Azure Blob Storage & Data Lake Storage Workload Role
On your subscription page, select Access Control (IAM) > + Add > Add custom role.
On the Basics tab, set the Custom role name to
druva-role-blob-<suffix>(e.g., druva-role-blob-16567).On the JSON tab, select Edit, paste the following role definition, and select Save:
{
"properties": {
"roleName": "druva-role-blob-<suffix>",
"description": "Druva manual onboarding Azure Blob Storage and Data Lake Storage permissions role",
"assignableScopes": [
"/subscriptions/00000000-0000-0000-0000-000000000000"
],
"permissions": [
{
"actions": [
"Microsoft.Authorization/roleAssignments/read",
"Microsoft.Compute/disks/delete",
"Microsoft.Compute/disks/read",
"Microsoft.Compute/disks/write",
"Microsoft.Compute/locations/operations/read",
"Microsoft.Compute/virtualMachines/deallocate/action",
"Microsoft.Compute/virtualMachines/delete",
"Microsoft.Compute/virtualMachines/instanceView/read",
"Microsoft.Compute/virtualMachines/powerOff/action",
"Microsoft.Compute/virtualMachines/read",
"Microsoft.Compute/virtualMachines/write",
"Microsoft.ManagedIdentity/identities/read",
"Microsoft.ManagedIdentity/userAssignedIdentities/assign/action",
"Microsoft.ManagedIdentity/userAssignedIdentities/read",
"Microsoft.Network/azureFirewalls/read",
"Microsoft.Network/firewallPolicies/read",
"Microsoft.Network/firewallPolicies/ruleCollectionGroups/read",
"Microsoft.Network/networkInterfaces/delete",
"Microsoft.Network/networkInterfaces/join/action",
"Microsoft.Network/networkInterfaces/read",
"Microsoft.Network/networkInterfaces/write",
"Microsoft.Network/networkSecurityGroups/join/action",
"Microsoft.Network/networkSecurityGroups/read",
"Microsoft.Network/networkSecurityPerimeters/profiles/accessRules/read",
"Microsoft.Network/networkSecurityPerimeters/profiles/read",
"Microsoft.Network/networkSecurityPerimeters/read",
"Microsoft.Network/networkSecurityPerimeters/resourceAssociations/read",
"Microsoft.Network/privateDnsZones/read",
"Microsoft.Network/privateDnsZones/virtualNetworkLinks/read",
"Microsoft.Network/privateEndpoints/read",
"Microsoft.Network/virtualNetworks/read",
"Microsoft.Network/virtualNetworks/subnets/join/action",
"Microsoft.Network/virtualNetworks/subnets/joinViaServiceEndpoint/action",
"Microsoft.Network/virtualNetworks/subnets/read",
"Microsoft.Resources/subscriptions/resourceGroups/read",
"Microsoft.Storage/storageAccounts/blobServices/containers/getAcl/action",
"Microsoft.Storage/storageAccounts/blobServices/containers/read",
"Microsoft.Storage/storageAccounts/blobServices/containers/setAcl/action",
"Microsoft.Storage/storageAccounts/blobServices/containers/write",
"Microsoft.Storage/storageAccounts/blobServices/read",
"Microsoft.Storage/storageAccounts/read"
],
"notActions": [],
"dataActions": [
"Microsoft.Storage/storageAccounts/blobServices/containers/blobs/filter/action",
"Microsoft.Storage/storageAccounts/blobServices/containers/blobs/manageOwnership/action",
"Microsoft.Storage/storageAccounts/blobServices/containers/blobs/modifyPermissions/action",
"Microsoft.Storage/storageAccounts/blobServices/containers/blobs/move/action",
"Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read",
"Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/read",
"Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/write",
"Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write"
],
"notDataActions": []
}
]
}
}Select Review + create, and then select Create.
Record the exact Role Name (azureRoleName for Azure Blob Storage).
📘 Additional Reading: For official Microsoft guidance on creating and managing custom roles in Azure, see Azure custom roles in the Microsoft Learn documentation.
Create the Azure Files Workload Role
On your subscription page, select Access Control (IAM) > + Add > Add custom role.
On the Basics tab, set the Custom role name to
druva-role-fs-<suffix>(e.g.,druva-role-fs-16567).On the JSON tab, select Edit, paste the following role definition, and select Save:
{
"properties": {
"roleName": "druva-role-fs-<suffix>",
"description": "Druva manual onboarding Azure Files permissions role",
"assignableScopes": [
"/subscriptions/00000000-0000-0000-0000-000000000000"
],
"permissions": [
{
"actions": [
"Microsoft.Authorization/roleAssignments/read",
"Microsoft.Compute/disks/delete",
"Microsoft.Compute/disks/read",
"Microsoft.Compute/disks/write",
"Microsoft.Compute/locations/operations/read",
"Microsoft.Compute/virtualMachines/deallocate/action",
"Microsoft.Compute/virtualMachines/delete",
"Microsoft.Compute/virtualMachines/instanceView/read",
"Microsoft.Compute/virtualMachines/powerOff/action",
"Microsoft.Compute/virtualMachines/read",
"Microsoft.Compute/virtualMachines/write",
"Microsoft.ManagedIdentity/identities/read",
"Microsoft.ManagedIdentity/userAssignedIdentities/assign/action",
"Microsoft.ManagedIdentity/userAssignedIdentities/read",
"Microsoft.Network/azureFirewalls/read",
"Microsoft.Network/firewallPolicies/read",
"Microsoft.Network/firewallPolicies/ruleCollectionGroups/read",
"Microsoft.Network/networkInterfaces/delete",
"Microsoft.Network/networkInterfaces/join/action",
"Microsoft.Network/networkInterfaces/read",
"Microsoft.Network/networkInterfaces/write",
"Microsoft.Network/networkSecurityGroups/join/action",
"Microsoft.Network/networkSecurityGroups/read",
"Microsoft.Network/networkSecurityPerimeters/profiles/accessRules/read",
"Microsoft.Network/networkSecurityPerimeters/profiles/read",
"Microsoft.Network/networkSecurityPerimeters/read",
"Microsoft.Network/networkSecurityPerimeters/resourceAssociations/read",
"Microsoft.Network/privateDnsZones/read",
"Microsoft.Network/privateDnsZones/virtualNetworkLinks/read",
"Microsoft.Network/privateEndpoints/read",
"Microsoft.Network/virtualNetworks/read",
"Microsoft.Network/virtualNetworks/subnets/join/action",
"Microsoft.Network/virtualNetworks/subnets/joinViaServiceEndpoint/action",
"Microsoft.Network/virtualNetworks/subnets/read",
"Microsoft.Resources/subscriptions/resourceGroups/read",
"Microsoft.Storage/storageAccounts/fileServices/read",
"Microsoft.Storage/storageAccounts/fileServices/shares/read",
"Microsoft.Storage/storageAccounts/read"
],
"notActions": [],
"dataActions": [
"Microsoft.Storage/storageAccounts/fileServices/fileshares/files/modifypermissions/action",
"Microsoft.Storage/storageAccounts/fileServices/fileshares/files/read",
"Microsoft.Storage/storageAccounts/fileServices/fileshares/files/write",
"Microsoft.Storage/storageAccounts/fileServices/readFileBackupSemantics/action",
"Microsoft.Storage/storageAccounts/fileServices/writeFileBackupSemantics/action"
],
"notDataActions": []
}
]
}
}
Select Review + create, and then select Create.
Record the exact Role Name (
azureRoleNamefor Azure Files).
📘 Additional Reading: For official Microsoft guidance on creating and managing custom roles in Azure, see Azure custom roles in the Microsoft Learn documentation.
Assign Custom RBAC Roles to the Service Principal
Bind your newly created custom roles to the Druva Service Principal created in Step 1 to authorize backup and recovery operations at the appropriate resource scopes.
Assign Key Vault Permissions Role (Resource Group Scope)
Sign in to the Azure Portal.
Search for and select Resource groups, and then select
druva-rg-<suffix>.Select Access control (IAM) from the left menu.
Select + Add, and then select Add role assignment.
On the Role tab, search for and select
druva-role-kv-<suffix>. Select Next.On the Members tab, under Assign access to, select User, group, or service principal.
Select + Select members, search for and select the Druva Service Principal
(druva-app-<suffix>), and then select Select.Select Review + assign, and then select Review + assign to confirm the role assignment.
Record the 36-character Role Assignment GUID (
azureRoleAssignNamefor Key Vault) by downloading the roleAssignments (with children) and search on the role name..
Assign Workload-Specific Custom Roles (Subscription Scope)
Search for and select Subscriptions, and then select your target subscription.
Select Access control (IAM) from the left menu.
Select + Add, and then select Add role assignment.
On the Role tab, search for and select your custom workload role (e.g.,
druva-role-vm-<suffix>). Select Next.On the Members tab, under Assign access to, select User, group, or service principal.
Select + Select members, search for and select the Druva Service Principal (
druva-app-<suffix>), and then select Select.Select Review + assign, and then select Review + assign to confirm the role assignment.
Record the 36-character Role Assignment GUID (azureRoleAssignName for the workload).
📘 Additional Reading: For official Microsoft guidance on the role assignment process, security principals, and scope levels, see Steps to assign an Azure role and Assign Azure roles using the Azure portal in the Microsoft Learn documentation.
Create Primary and Secondary Key Vaults
Deploy two Azure Key Vaults with Vault Access Policies enabled to store Druva-specific data protection delegation secrets (eKey). These vaults serve as dedicated security infrastructure isolated strictly for Druva backup and recovery operations.
Create the Primary Key Vault
Sign in to the Azure Portal.
In the search box at the top of the portal, enter Key vaults.
Under Services, select Key vaults.
Select + Create (or + Add).
On the Basics tab, configure the following settings:
Subscription: Select your target Azure subscription.
Resource group: Select
druva-rg-<suffix>.Key vault name: Enter
druva-pkv-<random-string>-<suffix>(must be globally unique across Azure, 3–24 alphanumeric characters and hyphens).Region: Select your designated Primary Region (e.g., East US).
Pricing tier: Select Standard.
Ensure Soft-delete is enabled (enabled by default).
Set Days to retain deleted vaults: 90 (enabled by default).
Select Enable purge protection (recommended security practice).
On the Access configuration tab:
Under Permission model, select Vault access policy.
On the Networking tab:
Ensure Enable public access is enabled (enabled by default).
Under Public Access, Allow access from All networks (enabled by default).
Select Review + create, and then select Create.
Configure Access Policies on the Primary Key Vault
Open the newly created Primary Key Vault (
druva-pkv-<random-string>-<suffix>).In the left navigation menu, under Settings, select Access policies.
Select + Create.
Grant Permissions to the Druva Service Principal:
On the Permissions tab, under Secret permissions, select Get, List, Set, and Delete. Select Next.
On the Principal tab, search for and select the Druva Service Principal (
druva-app-<suffix>). Select Next.Select Review + create, and then select Create.
Grant Permissions to the User-Assigned Managed Identity:
On the Access policies page, select + Create again.
On the Permissions tab, under Secret permissions, select Get and List. Select Next.
On the Principal tab, search for and select the User-Assigned Managed Identity (
druva-mi-<suffix>). Select Next.Select Review + create, and then select Create.
Create and Configure the Secondary Key Vault
Repeat the steps under Create the Primary Key Vault to deploy
druva-skv-<random-string>-<suffix>in your designated Secondary Region (e.g., West US) using the Standard pricing tier.Repeat the steps under Configure Access Policies on the Primary Key Vault to grant the exact same secret permissions to both
druva-app-<suffix>anddruva-mi-<suffix>on the Secondary Key Vault.Record the Primary Key Vault Name (
primaryKVName) and Secondary Key Vault Name (secondaryKVName).
📘 Additional Reading: For official Microsoft guidance on creating key vaults and managing access policies, see Quickstart: Create a key vault using the Azure portal and Assign an Azure Key Vault access policy using the Azure portal in the Microsoft Learn documentation.
Hand-off Checklist
Save these values and securely share them with the Druva Cloud Administrator:
[ ] Subscription ID (
subscriptionID): Target Azure Subscription GUID[ ] Subscription Name (
subscriptionName): Azure Subscription Display Name[ ] Resource Group Name (
resourceGroupName): druva-rg-<suffix>[ ] Managed Identity Name (
managedIdentityName): druva-mi-<suffix>[ ] Primary Key Vault Name (
primaryKVName): druva-pkv-<random>-<suffix>[ ] Secondary Key Vault Name (
secondaryKVName): druva-skv-<random>-<suffix>[ ] Key Vault Role Metadata: Role Name (
azureRoleName) & Assignment GUID (azureRoleAssignName)[ ] Workload Role Metadata: Role Name (
azureRoleName) & Assignment GUID (azureRoleAssignName) for each configured workload
Register Azure with Druva
Executed by: Druva Cloud Administrator
Scope: Performed once per tenant and batch of subscriptions to complete the cloud registration.
Prerequisites
Before executing the onboarding script, gather the required Druva Cloud parameters and launch your execution environment.
Druva Management Console Requirements
Organization ID (
organizationID): The numerical ID of your targeted Druva Organization.Admin Group ID (
adminGroupID): The numerical ID of the Druva Administrative Group assigned to manage the Azure workload protection.Druva API Credentials: An API Client ID (
clientID) and Client Secret (DRUVA_CLIENT_SECRET) created within the Druva Management Console with permissions to access the Connection Service.
Workstation and Environment Setup (Azure Cloud Shell)
For the simplest, pre-configured execution environment, run the onboarding script directly within Azure Cloud Shell:
Open Azure Cloud Shell by navigating to https://shell.azure.com or by selecting the Cloud Shell icon ($\_>$) in the top navigation bar of the Azure Portal.
Select PowerShell from the environment drop-down menu in the top-left corner of the terminal window.
Verify that the pre-installed Az PowerShell module is active (Azure Cloud Shell maintains and updates Az modules automatically—no manual module installation is required).
Prepare the Resource Manifest (druva_resources.json)
Create a local configuration file named druva_resources.json using the metadata collected during Step 1 (Entra ID App Registration) and Step 2 (Subscription Provisioning).
{
"backupApp": {
"tenantID": "00000000-0000-0000-0000-000000000000",
"appID": "00000000-0000-0000-0000-000000000000",
"servicePrincipalID": "00000000-0000-0000-0000-000000000000",
"secretKeyID": "00000000-0000-0000-0000-000000000000",
"secretExpiryTS": 1767225600
},
"druva": {
"organizationID": 100001,
"adminGroupID": 5001,
"clientID": "your-druva-api-client-id"
},
"subscriptions": [
{
"subscriptionID": "00000000-0000-0000-0000-000000000000",
"subscriptionName": "Production-Azure-Subscription",
"resourceGroupName": "druva-rg-16567",
"managedIdentityName": "druva-mi-16567",
"primaryKVName": "druva-pkv-a1b2-16567",
"secondaryKVName": "druva-skv-c3d4-16567",
"azureDruvaKeyVaultPermissionsRole": {
"azureRoleName": "druva-role-kv-16567",
"azureRoleAssignName": "00000000-0000-0000-0000-000000000000"
},
"azureVMRole": {
"azureRoleName": "druva-role-vm-16567",
"azureRoleAssignName": "00000000-0000-0000-0000-000000000000"
}
}
]
}
Execution Script: Start-DruvaAzureManualOnboarding.ps1
Save the following code block as Start-DruvaAzureManualOnboarding.ps1 in the same directory as your druva_resources.json file.
<#
.SYNOPSIS
Azure Manual Onboarding Script (PowerShell Edition)
.DESCRIPTION
End-to-end automation for registering an Azure tenant and its subscriptions with
Druva's cloud connection service ("manual onboarding").
Reads a druva_resources.json manifest, validates runtime and configuration,
discovers required Azure resources via Az PowerShell modules, obtains Druva OAuth tokens,
and registers connection and subscription manifests.
.PARAMETER ResourcesJson
Path to the resources manifest JSON file. Defaults to 'druva_resources.json' in current dir.
.PARAMETER CredentialFile
Optional path to a JSON file containing DRUVA_BKP_APP_SECRET and DRUVA_CLIENT_SECRET.
.PARAMETER AzureRetryCount
Number of retries for Azure discovery calls. Defaults to 0 (no retries).
.PARAMETER DruvaRetryCount
Number of retries for Druva REST API calls. Defaults to 0 (no retries).
.PARAMETER VerboseLogging
Enables detailed DEBUG-level output and logging.
.EXAMPLE
.\Start-AzureManualOnboarding.ps1
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $false, Position = 0)]
[Alias("ResourceJson")]
[string]$ResourcesJson = "druva_resources.json",
[Parameter(Mandatory = $false)]
[string]$CredentialFile,
[Parameter(Mandatory = $false)]
[int]$AzureRetryCount = 0,
[Parameter(Mandatory = $false)]
[int]$DruvaRetryCount = 0,
[Parameter(Mandatory = $false)]
[switch]$VerboseLogging
)
# --- GLOBAL WARNING SUPPRESSION ---
$global:WarningPreference = 'SilentlyContinue'
$env:AZURE_CLIENTS_SHOW_BREAKING_CHANGE_WARNINGS = "false"
# --- CONFIGURATION & CONSTANTS ---
$DRUVA_BASE_URL = "https://apis-us0.druva.com"
$DRUVA_OAUTH_TOKEN_URL = "https://apis-us0.druva.com/token"
$LOG_FILE = "druva_onboarding.log"
$ENV_BKP_APP_SECRET = "DRUVA_BKP_APP_SECRET"
$ENV_CLIENT_SECRET = "DRUVA_CLIENT_SECRET"
$MANDATORY_WORKLOAD_ROLE_KEY = "azureDruvaKeyVaultPermissionsRole"
$WORKLOAD_SPECIFIC_ROLE_KEYS = @("azureVMRole", "azureSQLRole", "azureBlobRole", "azureFSRole")
$WORKLOAD_ROLE_SPECS = @(
@{ Key = "azureVMRole"; Label = "VM" },
@{ Key = "azureSQLRole"; Label = "SQL" },
@{ Key = "azureBlobRole"; Label = "Blob" },
@{ Key = "azureFSRole"; Label = "FS" },
@{ Key = "azureDruvaKeyVaultPermissionsRole"; Label = "Druva Key Vault Permissions" }
)
# --- LOGGING HELPER ---
function Write-Log {
param (
[string]$Message,
[ValidateSet("INFO", "WARNING", "ERROR", "DEBUG", "RAW")]
[string]$Level = "INFO"
)
if ($Level -eq "RAW") {
Add-Content -Path $LOG_FILE -Value $Message -ErrorAction SilentlyContinue
return
}
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logLine = "[$timestamp] [$Level] $Message"
Add-Content -Path $LOG_FILE -Value $logLine -ErrorAction SilentlyContinue
switch ($Level) {
"INFO" { Write-Host $Message -ForegroundColor Green }
"WARNING" { Write-Host $Message -ForegroundColor Yellow }
"ERROR" { Write-Host $Message -ForegroundColor Red }
"DEBUG" { if ($VerboseLogging) { Write-Host "[DEBUG] $Message" -ForegroundColor DarkGray } }
}
}
# --- RETRY WRAPPER ---
function Invoke-WithRetry {
param (
[scriptblock]$ScriptBlock,
[int]$MaxRetries = 0,
[int]$InitialDelaySec = 5,
[string]$OperationName = "Operation"
)
$attempt = 0
while ($true) {
try {
return &$ScriptBlock
}
catch {
$statusCode = 0
if ($_.Exception.Response) {
if ($_.Exception.Response.StatusCode -is [int]) {
$statusCode = $_.Exception.Response.StatusCode
} elseif ($_.Exception.Response.StatusCode.value__) {
$statusCode = $_.Exception.Response.StatusCode.value__
}
}
if ($statusCode -eq 429) {
Write-Log "$OperationName encountered HTTP 429 throttling. Halting retries." "ERROR"
throw $_
}
if ($attempt -ge $MaxRetries) {
if ($MaxRetries -gt 0) {
Write-Log "$OperationName failed after $attempt retries. Exiting." "ERROR"
}
throw $_
}
$attempt++
$delay = $InitialDelaySec * [math]::Pow(2, $attempt - 1)
Write-Log " [Retry] $OperationName failed ($($_.Exception.Message)). Retrying in ${delay}s (Attempt $attempt/$MaxRetries)..." "WARNING"
Start-Sleep -Seconds $delay
}
}
}
# --- PRE-FLIGHT VALIDATION ---
function Test-RuntimeEnvironment {
param ([string]$ConfigPath)
if (-not (Test-Path -Path $ConfigPath -PathType Leaf)) {
throw "Config file not found: '$ConfigPath'. Ensure 'druva_resources.json' exists in current folder or specify -ResourcesJson."
}
$requiredModules = @('Az.Accounts', 'Az.Resources', 'Az.KeyVault', 'Az.ManagedServiceIdentity')
foreach ($module in $requiredModules) {
if (-not (Get-Module -ListAvailable -Name $module)) {
throw "Missing required PowerShell module: $module. Please run: Install-Module -Name Az -Scope CurrentUser"
}
}
}
# --- SECRET PROMPTING ---
function Read-MaskedSecret {
param ([string]$Prompt)
$secure = Read-Host -Prompt $Prompt -AsSecureString
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
try {
return [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
}
finally {
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
}
function Get-OnboardingCredentials {
param ([string]$CredFile)
$bkpSecret = ""
$clientSecret = ""
if ($CredFile -and (Test-Path -Path $CredFile)) {
$json = Get-Content -Path $CredFile -Raw | ConvertFrom-Json
$bkpSecret = $json.DRUVA_BKP_APP_SECRET
$clientSecret = $json.DRUVA_CLIENT_SECRET
Write-Log "Loaded onboarding credentials from file: $CredFile"
return @{ BkpSecret = $bkpSecret; ClientSecret = $clientSecret }
}
$envBkp = [Environment]::GetEnvironmentVariable($ENV_BKP_APP_SECRET)
if ([string]::IsNullOrWhiteSpace($envBkp)) { $envBkp = $env:DRUVA_BKP_APP_SECRET }
$envCli = [Environment]::GetEnvironmentVariable($ENV_CLIENT_SECRET)
if ([string]::IsNullOrWhiteSpace($envCli)) { $envCli = $env:DRUVA_CLIENT_SECRET }
if (-not [string]::IsNullOrWhiteSpace($envBkp)) {
$bkpSecret = $envBkp
Write-Log "Loaded Azure Backup App secret from environment variable '$ENV_BKP_APP_SECRET'." "INFO"
}
if (-not [string]::IsNullOrWhiteSpace($envCli)) {
$clientSecret = $envCli
Write-Log "Loaded Druva Client secret from environment variable '$ENV_CLIENT_SECRET'." "INFO"
}
if ([string]::IsNullOrWhiteSpace($bkpSecret) -or [string]::IsNullOrWhiteSpace($clientSecret)) {
Write-Host "`n=================================================="
Write-Host " SECURITY CREDENTIAL PROMPTS"
Write-Host "=================================================="
if ([string]::IsNullOrWhiteSpace($bkpSecret)) {
$bkpSecret = Read-MaskedSecret -Prompt "1. Enter Azure Backup App Secret"
}
if ([string]::IsNullOrWhiteSpace($clientSecret)) {
$clientSecret = Read-MaskedSecret -Prompt "2. Enter Druva API Client Secret"
}
Write-Host "==================================================`n"
}
if ([string]::IsNullOrWhiteSpace($bkpSecret) -or [string]::IsNullOrWhiteSpace($clientSecret)) {
throw "Both Backup App Secret and Druva Client Secret are required."
}
return @{ BkpSecret = $bkpSecret; ClientSecret = $clientSecret }
}
# --- CONFIG VALIDATION ---
function Assert-ValidConfig {
param ($Config)
if (-not $Config.backupApp -or -not $Config.druva -or -not $Config.subscriptions) {
throw "resources.json missing required root keys (backupApp, druva, subscriptions)."
}
$bApp = $Config.backupApp
if ($bApp.secretExpiryTS -le [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()) {
throw "backupApp.secretExpiryTS is expired or in the past."
}
foreach ($sub in $Config.subscriptions) {
if ($sub.primaryKVName.Trim().ToLower() -eq $sub.secondaryKVName.Trim().ToLower()) {
throw "primaryKVName and secondaryKVName must be different for subscription '$($sub.subscriptionID)'."
}
if (-not $sub.$MANDATORY_WORKLOAD_ROLE_KEY) {
throw "Subscription '$($sub.subscriptionID)' missing mandatory role block: $MANDATORY_WORKLOAD_ROLE_KEY"
}
$hasWorkload = $false
foreach ($wkKey in $WORKLOAD_SPECIFIC_ROLE_KEYS) {
if ($sub.$wkKey -and $sub.$wkKey.azureRoleName) { $hasWorkload = $true; break }
}
if (-not $hasWorkload) {
throw "Subscription '$($sub.subscriptionID)' must specify at least one workload role ($($WORKLOAD_SPECIFIC_ROLE_KEYS -join ', '))."
}
}
Write-Log "Configuration schema validation successful."
}
# --- AZURE DISCOVERY ---
function Discover-AzureResources {
param (
$Config,
[string]$BkpAppSecret
)
Write-Log "Verifying Azure Backup App credentials..."
$tenantId = [string]$Config.backupApp.tenantID
$appId = [string]$Config.backupApp.appID
$spId = [string]$Config.backupApp.servicePrincipalID
$secSecret = ConvertTo-SecureString $BkpAppSecret -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($appId, $secSecret)
try { Set-AzConfig -DisplayBreakingChangeWarning $false -Scope Process -ErrorAction SilentlyContinue | Out-Null } catch {}
Invoke-WithRetry -OperationName "Azure Authentication" -MaxRetries $AzureRetryCount -ScriptBlock {
Connect-AzAccount -ServicePrincipal -Credential $credential -Tenant $tenantId -WarningAction SilentlyContinue -ErrorAction Stop | Out-Null
}
Write-Log "Azure authentication successful using Backup App credentials."
foreach ($sub in $Config.subscriptions) {
$subId = [string]$sub.subscriptionID
$rgName = [string]$sub.resourceGroupName
Write-Log "--- Validating Subscription: $subId ---"
Select-AzSubscription -SubscriptionId $subId -WarningAction SilentlyContinue | Out-Null
# 1. Resource Group
$rg = Invoke-WithRetry -OperationName "Get Resource Group" -MaxRetries $AzureRetryCount -ScriptBlock {
Get-AzResourceGroup -Name $rgName -WarningAction SilentlyContinue -ErrorAction Stop
}
Write-Log " Found Resource Group in $($rg.Location)"
# 2. Key Vaults
$pKv = Invoke-WithRetry -OperationName "Get Primary Key Vault" -MaxRetries $AzureRetryCount -ScriptBlock {
Get-AzKeyVault -VaultName $sub.primaryKVName -ResourceGroupName $rgName -WarningAction SilentlyContinue -ErrorAction Stop
}
$sub | Add-Member -NotePropertyName "primaryKVURI" -NotePropertyValue ([string]$pKv.VaultUri) -Force
$sub | Add-Member -NotePropertyName "primaryKVLocation" -NotePropertyValue ([string]$pKv.Location) -Force
$sKv = Invoke-WithRetry -OperationName "Get Secondary Key Vault" -MaxRetries $AzureRetryCount -ScriptBlock {
Get-AzKeyVault -VaultName $sub.secondaryKVName -ResourceGroupName $rgName -WarningAction SilentlyContinue -ErrorAction Stop
}
$sub | Add-Member -NotePropertyName "secondaryKVURI" -NotePropertyValue ([string]$sKv.VaultUri) -Force
$sub | Add-Member -NotePropertyName "secondaryKVLocation" -NotePropertyValue ([string]$sKv.Location) -Force
# 3. Managed Identity
$mi = Invoke-WithRetry -OperationName "Get Managed Identity" -MaxRetries $AzureRetryCount -ScriptBlock {
Get-AzUserAssignedIdentity -ResourceGroupName $rgName -Name $sub.managedIdentityName -WarningAction SilentlyContinue -ErrorAction Stop
}
$sub | Add-Member -NotePropertyName "managedIdentityID" -NotePropertyValue ([string]$mi.Id) -Force
$sub | Add-Member -NotePropertyName "managedIdentityPrincipalID" -NotePropertyValue ([string]$mi.PrincipalId) -Force
$sub | Add-Member -NotePropertyName "managedIdentityLocation" -NotePropertyValue ([string]$mi.Location) -Force
# 4. Workload Roles Resolution
foreach ($spec in $WORKLOAD_ROLE_SPECS) {
$rKey = $spec.Key
$rLabel = $spec.Label
$roleObj = $sub.$rKey
if ($null -eq $roleObj -or [string]::IsNullOrWhiteSpace($roleObj.azureRoleName)) { continue }
$queryScope = if ($rKey -eq $MANDATORY_WORKLOAD_ROLE_KEY) {
"/subscriptions/$subId/resourceGroups/$rgName"
} else {
"/subscriptions/$subId"
}
# Discover Role Definition
$roleDef = Invoke-WithRetry -OperationName "Get Role Definition ($rLabel)" -MaxRetries $AzureRetryCount -ScriptBlock {
Get-AzRoleDefinition -Name $roleObj.azureRoleName -Scope $queryScope -WarningAction SilentlyContinue -ErrorAction Stop
}
$rawRoleGuid = $roleDef.Id.Split('/')[-1]
# Standard ARM Role Definition ID path (subscription-scoped for all roles)
$fullRoleArmId = "/subscriptions/$subId/providers/Microsoft.Authorization/roleDefinitions/$rawRoleGuid"
$roleObj | Add-Member -NotePropertyName "azureRoleDefinitionID" -NotePropertyValue ([string]$fullRoleArmId) -Force
# Resolve Role Assignment if missing
if ([string]::IsNullOrWhiteSpace($roleObj.azureRoleAssignName)) {
$roleAssign = Invoke-WithRetry -OperationName "Find Role Assignment ($rLabel)" -MaxRetries $AzureRetryCount -ScriptBlock {
Get-AzRoleAssignment -ObjectId $spId -RoleDefinitionName $roleObj.azureRoleName -Scope $queryScope -WarningAction SilentlyContinue -ErrorAction Stop
}
if ($null -eq $roleAssign) {
throw "No $rLabel role assignment found for Backup App Principal '$spId' at scope '$queryScope'."
}
$roleObj | Add-Member -NotePropertyName "azureRoleAssignName" -NotePropertyValue ([string]$roleAssign.RoleAssignmentId.Split('/')[-1]) -Force
}
}
}
}
# --- DRUVA API INTEGRATION ---
function Get-DruvaOAuthToken {
param ($Config, [string]$DruvaClientSecret)
Write-Log "Fetching OAuth access token for adminAccessToken..."
$clientId = $Config.druva.clientID
$pair = "${clientId}:${DruvaClientSecret}"
$bytes = [System.Text.Encoding]::ASCII.GetBytes($pair)
$base64 = [Convert]::ToBase64String($bytes)
$headers = @{
"Authorization" = "Basic $base64"
"Content-Type" = "application/x-www-form-urlencoded"
"Accept" = "application/json"
}
$body = "grant_type=client_credentials&scope=read"
Write-Log "DRUVA API REQUEST [POST $DRUVA_OAUTH_TOKEN_URL] Body: $body" "INFO"
$response = Invoke-WithRetry -OperationName "Druva OAuth Token" -MaxRetries $DruvaRetryCount -ScriptBlock {
Invoke-RestMethod -Uri $DRUVA_OAUTH_TOKEN_URL -Method Post -Headers $headers -Body $body -ErrorAction Stop
}
$resJson = $response | ConvertTo-Json -Compress
Write-Log "DRUVA API RESPONSE [POST $DRUVA_OAUTH_TOKEN_URL]: $resJson" "INFO"
if (-not $response.access_token) {
throw "OAuth response missing 'access_token'."
}
return $response.access_token
}
function Register-DruvaConnection {
param ($Config, [string]$AdminAccessToken, [string]$BkpAppSecret)
Write-Log "POSTing manual connection manifest to Druva..."
$url = "$DRUVA_BASE_URL/unity/connectionsvc/v1/azure/manual/connections"
$headers = @{
"Authorization" = "Bearer $AdminAccessToken"
"Content-Type" = "application/json"
"Accept" = "application/json"
"authEntities" = "pType=org;pID=$($Config.druva.organizationID);sType=group;sID=$($Config.druva.adminGroupID)"
}
$bodyObj = @{
microsoftTenantID = [string]$Config.backupApp.tenantID
backupAppID = [string]$Config.backupApp.appID
backupAppSecret = "[MASKED_SECRET]"
backupAppSecretKeyID = [string]$Config.backupApp.secretKeyID
backupAppSecretExpiryTS = [int64]$Config.backupApp.secretExpiryTS
backupAppServicePrincipalID = [string]$Config.backupApp.servicePrincipalID
}
$bodyLogJson = $bodyObj | ConvertTo-Json -Compress
Write-Log "DRUVA API REQUEST [POST $url] Payload: $bodyLogJson" "INFO"
$bodyObj.backupAppSecret = [string]$BkpAppSecret
$bodyJson = $bodyObj | ConvertTo-Json -Compress
$response = Invoke-WithRetry -OperationName "Register Druva Connection" -MaxRetries $DruvaRetryCount -ScriptBlock {
Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $bodyJson -ErrorAction Stop
}
$resJson = $response | ConvertTo-Json -Compress
Write-Log "DRUVA API RESPONSE [POST $url]: $resJson" "INFO"
if (-not $response.connectionID) {
throw "Connection registration response did not contain a valid 'connectionID'."
}
Write-Log "Druva Connection Created! ID: $($response.connectionID)"
return $response.connectionID
}
function Register-DruvaSubscriptions {
param ($Config, [string]$AdminAccessToken, [int]$ConnectionID)
$tenantId = [string]$Config.backupApp.tenantID
$url = "$DRUVA_BASE_URL/unity/connectionsvc/v1/azure/manual/$tenantId/subscriptions"
$headers = @{
"Authorization" = "Bearer $AdminAccessToken"
"Content-Type" = "application/json"
"Accept" = "application/json"
"authEntities" = "pType=org;pID=$($Config.druva.organizationID);sType=group;sID=$($Config.druva.adminGroupID)"
}
foreach ($sub in $Config.subscriptions) {
Write-Log "Registering Subscription $($sub.subscriptionID) with Druva..."
$subId = [string]$sub.subscriptionID
$pLocation = if ($sub.primaryKVLocation) { [string]$sub.primaryKVLocation } else { "" }
$sLocation = if ($sub.secondaryKVLocation) { [string]$sub.secondaryKVLocation } else { "" }
$miID = if ($sub.managedIdentityID) { [string]$sub.managedIdentityID } else { "" }
$miPrinID = if ($sub.managedIdentityPrincipalID) { [string]$sub.managedIdentityPrincipalID } else { "" }
$miLoc = if ($sub.managedIdentityLocation) { [string]$sub.managedIdentityLocation } else { "" }
$payloadDict = [ordered]@{
connectionID = [int]$ConnectionID
adminGroupID = [int]$Config.druva.adminGroupID
adminAccessToken = [string]$AdminAccessToken
microsoftSubscriptionID = [string]$sub.subscriptionID
microsoftSubscriptionName = [string]$sub.subscriptionName
azureResourceGroupName = [string]$sub.resourceGroupName
azureManagedIdentityName = [string]$sub.managedIdentityName
azureManagedIdentityID = [string]$miID
azureManagedIdentityPrincipalID = [string]$miPrinID
azureManagedIdentityLocation = [string]$miLoc
azurePrimaryKeyVaultURI = [string]$sub.primaryKVName
azurePrimaryKeyVaultLocation = [string]$pLocation
azureSecondaryKeyVaultURI = [string]$sub.secondaryKVName
azureSecondaryKeyVaultLocation = [string]$sLocation
}
foreach ($spec in $WORKLOAD_ROLE_SPECS) {
$rKey = $spec.Key
$rLabel = $spec.Label
$roleObj = $sub.PSObject.Properties[$rKey]?.Value
if ($null -ne $roleObj -and -not [string]::IsNullOrWhiteSpace($roleObj.azureRoleName)) {
# 1. Extract raw GUID suffix
$rawVal = if ($roleObj.PSObject.Properties['azureRoleDefinitionID']?.Value) {
[string]$roleObj.azureRoleDefinitionID
} else {
[string]$roleObj.azureRoleName
}
$roleGuid = $rawVal.Split('/')[-1]
# 2. Standard ARM role definition path format expected by Azure & Druva
$roleDefId = "/subscriptions/$subId/providers/Microsoft.Authorization/roleDefinitions/$roleGuid"
$assignName = [string]$roleObj.azureRoleAssignName
if ([string]::IsNullOrWhiteSpace($assignName)) {
throw "Workload role '$rLabel' assignment ID (azureRoleAssignName) is missing for subscription '$subId'."
}
$payloadDict[$rKey] = [ordered]@{
azureRoleName = $roleDefId
azureRoleAssignName = $assignName
}
}
}
$jsonBody = $payloadDict | ConvertTo-Json -Depth 10 -Compress
Write-Log "DRUVA API REQUEST [POST $url] Payload: $jsonBody" "INFO"
$response = Invoke-WithRetry -OperationName "Register Subscription $($sub.subscriptionID)" -MaxRetries $DruvaRetryCount -ScriptBlock {
try {
Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $jsonBody -ErrorAction Stop
}
catch {
if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
Write-Log "DRUVA API REJECTION RESPONSE [POST $url]: $($_.ErrorDetails.Message)" "ERROR"
} elseif ($_.Exception.Message) {
Write-Log "DRUVA API EXCEPTION [POST $url]: $($_.Exception.Message)" "ERROR"
}
throw $_
}
}
$resJson = $response | ConvertTo-Json -Compress
Write-Log "DRUVA API RESPONSE [POST $url]: $resJson" "INFO"
Write-Log " SUCCESS: Subscription $($sub.subscriptionID) is now onboarded and authorized."
}
}
# --- MAIN RUNTIME ---
function Main {
Write-Log "`r`n`r`n====================================================================================================" "RAW"
Write-Log "================================ NEW SCRIPT EXECUTION STARTED ====================================" "RAW"
Write-Log "====================================================================================================`r`n" "RAW"
try {
Write-Log "=== STARTING AZURE MANUAL ONBOARDING VALIDATION ==="
Test-RuntimeEnvironment -ConfigPath $ResourcesJson
$configData = Get-Content -Path $ResourcesJson -Raw | ConvertFrom-Json
Assert-ValidConfig -Config $configData
$creds = Get-OnboardingCredentials -CredFile $CredentialFile
# Phase A: Discovery
Discover-AzureResources -Config $configData -BkpAppSecret $creds.BkpSecret
# Phase B: Druva API Onboarding
$adminToken = Get-DruvaOAuthToken -Config $configData -DruvaClientSecret $creds.ClientSecret
$connId = Register-DruvaConnection -Config $configData -AdminAccessToken $adminToken -BkpAppSecret $creds.BkpSecret
Register-DruvaSubscriptions -Config $configData -AdminAccessToken $adminToken -ConnectionID $connId
Write-Host "`n=================================================="
Write-Log "=== ALL OPERATIONS COMPLETED SUCCESSFULLY ===" "INFO"
Write-Host "==================================================`n"
Write-Log "`r`n================================ EXECUTION COMPLETED SUCCESSFULLY ================================`r`n`r`n" "RAW"
}
catch {
Write-Log "!!! CRITICAL FAILURE !!!" "ERROR"
Write-Log "Details: $($_.Exception.Message)" "ERROR"
Write-Log "Please check $LOG_FILE for full debug information." "INFO"
Write-Log "`r`n================================ EXECUTION FAILED WITH ERRORS ====================================`r`n`r`n" "RAW"
exit 1
}
}
# Entrypoint execution
Main
Execute the Onboarding Process
Run the PowerShell Script in Azure Cloud Shell
In Azure Cloud Shell (PowerShell mode), ensure
Start-DruvaAzureManualOnboarding.ps1anddruva_resources.jsonare present in your current working directory.Execute the script:
.\Start-DruvaAzureManualOnboarding.ps1 -ResourcesJson .\druva_resources.json
When prompted in the console, enter your secrets securely:
Azure Backup App Secret: Paste the client secret generated for the
druva-app-<suffix>App Registration in Step 1.Druva API Client Secret: Paste the API secret generated in the Druva Management Console.
Verification and Storage Rule Setup
Once the script outputs === ALL OPERATIONS COMPLETED SUCCESSFULLY ===, complete these quick steps in the Druva Console:
Log in to the Druva Management Console.
Go to Home > Enterprise Workloads > Protect > Azure > All Subscriptions.
Verify your newly onboarded subscription status is Connected and Authorized.
Select the subscription, go to the Storage Rules tab, and configure a Default Storage Rule to map your Druva storage location.
