Creating a Custom and Secure Azure Portal Offering
Published Dec 14 2021 10:09 AM 4,279 Views
Microsoft

Introduction

A customer of mine wanted to provide certain internal users the ability to create specific Azure resources, in this case Windows-based virtual machines (VMs), in a very controlled manner. Some of their requirements included:

  • A completely custom user interface served up via the Azure Portal.
  • Integration with Azure Key Vault for certain sensitive values (e.g., passwords) so that they are not displayed, and the user cannot enter.
  • Enforcement and validation of naming conventions on various fields, with appropriate interactive guidance and feedback. For example, all VMs must start with the letters “VM_.”
  • Limitations on certain fields (e.g., regions allowed size/family of VM, etc.).
  • Forced usage of existing VNets and resource groups.
  • All scripts should be contained in a private GitHub repository.
  • Ability to provide data via custom parameters on the portal interface that gets passed into a PowerShell script, which gets automatically invoked upon VM creation. An example would be allowing the user creating the VM to specify via a “Yes/No” parameter whether Microsoft Internet Information Services (IIS) should be installed upon VM creation.

Piecing all of these requirements together entailed stitching together a number of new (to me) technologies such as nested ARM deployments, Azure Managed Applications, etc. This article will take you though these one by one and explain some of my “School of Hard Knocks” lessons learned along the way.

 

Custom User Interface

One of the first was the custom user interface definition specification that is available as part of Azure Managed Applications.

 

Capabilities

Creating a “uiFormDefinition.json” file is a simple yet powerful way to provide custom user interfaces for your end users. Some of the capabilities include:

  • Separation of your user interface into separate “steps” for each logical group, which manifest themselves as unique tabbed sections in the UI. For example, one step could contain all the networking configuration options available to the end user, while another the disk options.
  • Ability to filter the available locations to only those locations that support the resource types to deploy, provide an array of the resource types. If you provide more than one resource type, only those locations that support all of the resource types are returned.
  • Deployment of services via Managed Identities (in preview as of December 14th, 2021).
  • Custom built-in user interface elements. They are grouped into several distinct units:
    • Common contains typical elements such as checkboxes, dropdowns, textboxes, etc. It also has some more specific ones like ones for passwords, file uploads, tags, etc.
    • Compute has elements specific to VM creation, such as a field geared toward picking the VM’s size.
    • KeyVault has one to help select a certificate.
    • ManagedIdentity has one to select an identity.
    • Network provides elements to assist in creation or selection of public IPs and virtual networks.
    • Solutions is aimed at capabilities specific to Managed Applications, such as the ability call get results from an Azure Resource Manager API call.
    • Storage makes it possible for the user to select and create storage accounts and select blobs more easily.
  • Custom built-in user interface functions. There are numerous functions available for operations such as:
    • String manipulation and logic.
    • Data manipulation and logic.
    • Dates and times manipulation and logic.
    • Collection manipulation and logic.
    • Math operations.
  • Element-specific validations and constraints. This includes simple things, like forcing a naming convention, but also validations specific to the given control, such as having the File Upload element only allow certain file types/extensions (e.g., “.doc,.docx,.xml,application/msword”).

 

Example Screen

Here is an example I created based on one at this GitHub site. Note the error message for “Virtual Machine name” indicating that the entry does not match validation requirements.

gamullen_0-1639504467726.png

 

 

Example Code

My entire “uiFormDefinition.json” example can be found here. I’ll point out a few areas of interest in this section.

 

Validations

I placed several constraints on the name of the virtual machine.

  • The name must be between 4 and 15 characters long.
  • It can only contain letters, numbers, and hyphens.
  • It must start with the letters “gary.”

Note that I was able to specify informative error messages.

 

 

 

 

{
    "name": "vmName",
    "type": "Microsoft.Common.TextBox",
    "label": "Virtual Machine name",
    "toolTip": "The name of the Virtual Machine.",
    "defaultValue": "gary-vm",
    "constraints": {
        "required": true,
        "validations": [
            {
                "regex": "^[a-z0-9A-Z-]{4,15}$",
                "message": "The VM name must start with 'gary,' be between 4 and 15 characters long and contain letters, numbers and hyphens only."
            },
            {
                "isValid": "[startsWith(steps('basics').vmName,'gary')]",
                "message": "Must start with 'gary'."
            }
        ]
    }
}

 

 

 

 

 

Tool Tips

I was able to create custom “tool tips” to provide additional information to the user. For example, hovering over the “ⓘ” symbol shows the configured tool tip:

gamullen_1-1639504467730.png

 

Here is the code that specifies the tool tip:

 

 

 

 

"toolTip": {
    "virtualNetwork": "Creating a new VNet is not allowed, attempts to do so will fail",
    "subnets": "Must select an existing subnet"
}

 

 

 

 

 

Azure Key Vault Access

Because we don’t have a parameter file, we’ll need to used linked or nested templates as described in this section. I used a nested template just to keep all the code together. Note that nested templates add complexity to debugging as there are multiple deployments to explore.

 

My deployment ARM template is here. In it you can see how I pulled both the administrator password and the Storage Account access key from an Azure Key Vault.

 

Make sure you allow the user creating the VM to have access to the Key Vault for ARM deployments only by following the instructions here.

 

Resource Authorization

The goal is to lock down what the user is able to do on the Azure Portal as much as possible. For example, we don’t want her to be able to just go to the standard Virtual Machine creation user interface and spin up a VM from there. Be aware that you may have to add additional roles depending on what kind of virtual machine you create – for example, if you have boot diagnostics enabled you’ll need to add a “Storage Contributor” role to the user’s resource group. Other VM features might drag along similar requirements.

 

Azure Role-Based Access Control (RBAC)

In order to create a Virtual Machine, even from our custom UI, the end user must have “Virtual Machine Contributor” permission. Here’s how I scoped down what that user can do.

 

Create Unique Resource Group

I created a resource group just for the given user called “gary-vm-rg,” and designated the “Virtual Machine Contributor” access at that level rather than at the broader subscription level. That is the only role assigned to the user for the resource group.

 

In doing so, the only resource group the user can choose is “gary-vm-rg.” Trying to create a new resource group results in the following error message. Unfortunately, it’s not possible to configure the user interface to not show a “Create new” link below the resource group.

 

gamullen_2-1639504467736.png

 

Virtual Network Access

Because the user only has “Virtual Machine Contributor” access at the resource group level, he is not able to either create a new or select an existing virtual network. To enable this, I created a new resource group called “VNets” containing the target virtual networks, and assigned the user two roles at that level.

  • Reader. This allows the user to view the virtual networks within the “VNets” resource group.
  • A custom role that I called “Subnet Joiner.” This is necessary to allow the network interface created to join the virtual network’s subnet. The only action permitted for this custom role is “Microsoft.Network/virtualNetworks/subnets/join/action.”

Note that I could have also done this at the virtual network level itself to further tighten access.

 

As with resource groups, it currently is not possible to hide the “Create new” link next to the virtual network field. Unlike with resource groups, which gives an error immediately if the user does not have permission to create one, attempts to create a new virtual network will pass validation and fail during deployment.

 

Private GitHub Access

Creating a “Deploy to Azure” for custom UI and ARM scripts is easy, and described well here. However, it relies on the GitHub repository being public, not private. There is a note in that section that references an external article describing how to do so using a private repository using a custom Azure Function, but the article involves passing the GitHub personal access token in as an HTTP Get parameter, which largely defeats the purpose of having a private repository.

 

I created a new Azure Function that pulls the GitHub token out of Azure Key Vault, making the solution much more secure.

 

GitHub Proxy Function Application

My revamped GitHub Proxy Function can be found here. It is pretty simple – it just takes in a URL to a file in the repository as an HTTP Get parameter called “gitHubURL” whose value is a URL to the raw GitHub version of a file. For example:

You can test out your usage of the function from a browser; for example:

To use the new proxy from the “Deploy to Azure” button I had to redo the URL for the link. You can see the new URL from README.md file. The new URL is:

 

Azure Key Vault

As mentioned above, I reworked the original Function App to pull the GitHub access token from Azure Key Vault. Doing so is quite simple:

 

 

 

 

string keyVaultURL = "https://keyvaultgary.vault.azure.net/";
var kvClient = new SecretClient(new Uri(keyVaultURL), new DefaultAzureCredential());
KeyVaultSecret secret = kvClient.GetSecret(secretName);

 

 

 

 

Note that in order to give the Function App access to my Azure Key Vault I had to create a managed identity for the Function App and configure access from within the Key Vault so that identity could see secrets.

 

You can generate a personal access token for the GitHub Proxy Function App by navigating to “Settings / Developer settings / Personal access tokens” in GitHub. Here is a screenshot of the token I created:

gamullen_3-1639504467739.png

 

 

CORS

Even though you may be able to successfully retrieve your UI and ARM files from a private GitHub repository via a browser, it will fail when using the “Deploy to Azure” button. That’s because the code behind that employs JavaScript, and in doing so mimics a cross-origin resource sharing (CORS) attack. You will get an error message similar to the one shown below:

gamullen_4-1639504467741.png

 

To get around this we must add the URLs for the Azure Portal to the list of allowed origins for our Function App as shown below:

gamullen_5-1639504467745.png

 

After doing so the “Deploy to Azure” button should work fine.

 

Portal Parameter Passing to PowerShell Script

This was among the easiest of the requirements to solve.

 

I used the CustomScriptExtension to execute my PowerShell script. The script itself get pulled from a Storage Account container. I pull the Storage Account key from Azure Key Vault to keep it secure.

 

The parameter “scriptParameter” matches a Microsoft.Common.TextBox element from the custom UI. You could also make it a Microsoft.Common.CheckBox for a true/false value. As you can see below, I simply append it to the script to be called. If I had multiple parameters I could just keep adding more, separated by spaces. The PowerShell script just accesses them via the “args[]” array.

 

 

 

 

{
    "type": "Microsoft.Compute/virtualMachines/extensions",
    "name": "[concat(parameters('vmName'),'/CustomScriptExtension')]",
    "apiVersion": "2015-05-01-preview",
    "location": "[resourceGroup().location]",
    "dependsOn": [
    "[concat('Microsoft.Compute/virtualMachines/', parameters('vmName'))]"
    ],
    "properties": {
        "publisher": "Microsoft.Compute",
        "type": "CustomScriptExtension",
        "protectedSettings": {
            "fileUris": [ "[parameters('scriptFile')]"],
            "storageAccountName": "customscriptsgary",
            "storageAccountKey": "[parameters('storageAccountKey')]"
        },
        "typeHandlerVersion": "1.7",
        "autoUpgradeMinorVersion": true,
        "settings": {
            "commandToExecute": "[concat('powershell -ExecutionPolicy Unrestricted -file ', parameters('scriptName'), ' ', parameters('scriptParameter'))]"
        }
    }
}

 

 

 

 

 

Conclusion

This article describes a powerful, easy to use, and secure set of technologies that make it possible to create a custom portal experience for users. Sensitive values are stored in Azure Key Vault to protect them.

Co-Authors
Version history
Last update:
‎Dec 15 2021 05:27 AM
Updated by: