6 Easy Ways to Manage and Harden VM Images in Azure
In this article, take a look at six ways to manage and harden VM images in Azure.
Join the DZone community and get the full member experience.
Join For FreeManaging VM Images may be a nightmare. Here are 6 simple approaches on how to seamlessly build, share, test, and copy images in Azure. And as a bonus, you also get how to build image notifications based on Event-Driven-Architecture.
Problem
Whether you are managing 100 Virtual Machines or 1000+ that build and harden VM images, do you manually manage your images? If you do, you know that it is quite expensive and leads to hard-to-detect errors and potential security vulnerabilities.
Here is how I approach this issue for my customers. My procedure of creating image management for Azure.
1. Building an Image Using the Azure Image Builder
The Azure Image Builder is a service that allows you to create custom images with Azure CLI. The image creation based on the JSON template, example below.
{
"type": "Microsoft.VirtualMachineImages/imageTemplates",
"apiVersion": "2019-05-01-preview",
"location": "<region>",
"dependsOn": [],
"tags": {
"imagebuilderTemplate": "ubuntu1804",
"userIdentity": "enabled"
},
"identity": {
"type": "UserAssigned",
"userAssignedIdentities": {
"<imgBuilderId>": {}
}
},
"properties": {
"buildTimeoutInMinutes" : 80,
"vmProfile":
{
"vmSize": "Standard_D1_v2",
"osDiskSizeGB": 30
},
"source": {
"type": "PlatformImage",
"publisher": "Canonical",
"offer": "UbuntuServer",
"sku": "18.04-LTS",
"version": "latest"
},
"customize": [
{
"type": "Shell",
"name": "RunScriptFromSource",
"scriptUri": "https://raw.githubusercontent.com/danielsollondon/azvmimagebuilder/master/quickquickstarts/customizeScript.sh"
}
],
"distribute":
[
]
}
}
I have to cut the original template as it is quite long. Here you can find full example.
The ARM template is quite simple it contains the following properties:
- Identity section is required, you have to create Managed Identity for image builder to have an access to create and edit images
- VmProfile is to set up VM configuration plan
- Source allows you to specify base image parameters. I use the latest Ubuntu Server 18.04 LTS from Canonical
The customization section allows you to specify VM hardening scripts. - The customization section allows you to specify VM hardening scripts.
Here you can see the full list of the Image Builder options.
Below you can find CLI commands that build the image based on the mentioned JSON template.
xxxxxxxxxx
.
# submit the image configuration to the VM Image Builder Service
az resource create \
--resource-group $imageResourceGroup \
--properties @ubuntuImageTemplate.json \
--is-full-object \
--resource-type Microsoft.VirtualMachineImages/imageTemplates \
-n helloImageTemplateLinux01
# Run the image build
az resource invoke-action \
--resource-group $imageResourceGroup \
--resource-type Microsoft.VirtualMachineImages/imageTemplates \
-n helloImageTemplateLinux01 \
--action Run
Before you run this template you should enable the Azure Image Builder, set permissions, create resource groups, and create the managed identity. Here you can find a step-by-step process.
Advantages
Templates and the process itself is easy to understand and it can be easily integrated with Azure DevOps.
Disadvantages
The image builder is still in the review therefore it is not recommended using it in the production. Set up can be a bit difficult in comparison to Hashicorp Packer. (explain how to work with the Packer in the next section).
2. Building an Image Using the Hashicorp Packer
Hashicorp Packer is a multi-platform solution that allows building custom images based on JSON templates. The JSON templates are well-structured and based on an easy-to-understand object model. The JSON templates have just three root objects: Communicators, Builders, Provisioners, and Post-Processors. Below you can find the JSON template.
xxxxxxxxxx
{
"builders": [{
"type": "azure-arm",
"client_id": "client_id",
"client_secret": "client_secret",
"tenant_id": "tenant_id",
"subscription_id": "subscription_id",
"managed_image_resource_group_name": "HBI",
"managed_image_name": "test-image-sig",
"os_type": "Linux",
"image_publisher": "Canonical",
"image_offer": "UbuntuServer",
"image_sku": "16.04-LTS",
"azure_tags": {
"dept": "Test",
"task": "Image test"
},
"storage_account": "packerimgestest",
"capture_container_name": "image-vhd",
"capture_name_prefix": "image-pref",
"resource_group_name": "imageholder-sig-rg",
"location": "West Europe",
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'",
"inline": [
"apt-get update",
"apt-get upgrade -y",
"apt-get -y install nginx",
"/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync"
],
"inline_shebang": "/bin/sh -x",
"type": "shell"
}]
}
JSON template creates the VHD image of Ubuntu with a preinstalled NGINX web server and other updates. Here you can find a lot of other templates.
To set up packer you can use Chocolatie package manager:
xxxxxxxxxx
choco install packer -y
You can run the JSON template using the following command:
xxxxxxxxxx
packer build <path/your/template.json>
Before your run the template you need to create Management Identity or Service Principal with proper permissions, For example:
xxxxxxxxxx
az ad sp create-for-rbac -n "ImageContributor"
--role contributor `
--scopes /subscriptions/<subscription_id> `
--sdk-auth > az-principal.auth
This command automatically creates JSON file with clientId
, clientSecret
, tenantId
, subscriptionId
and other fields.
Here you can find other details of setting up and run the Packer templates.
Advantages
The Packer and JSON templates simple to understand. They allow you to quickly set up the environment and start building images. There is also a strong community. Plus, the Packer supports multiple cloud providers.
Disadvantages
I have not found any serious disadvantages. However, while building an image, the packer always removes the disk that is required in some operations with images. For example, for copying an image.
3. Sharing Images
To share images in Azure, you may use the Shared Image Gallery. It can:
- Create image definition
- Keep versions of an image
- Share an image
For example, you can share an image across your Azure subscriptions, resource groups, and tenants.
In the current scenario, you can create a user group or a single user/service principal, assign contributor rights only for this shared image gallery.
xxxxxxxxxx
az ad sp create-for-rbac -n "ImageContributor"
--role contributor `
--scopes /subscriptions/<subscr-id>/resourceGroups/sig-we-rg/providers/Microsoft.Compute/galleries/testsig
As a result, users from the group can create the Virtual Machine based on the Ubuntu image from this Shared Image Gallery in the different subscriptions.
xxxxxxxxxx
az vm create `
--resource-group myResourceGroup `
--name UbuntuVM`
--image "/subscriptions/<subscr-id>/resourceGroups/sig-we-rg/providers/Microsoft.Compute/galleries/testsig/images/ubuntu-server-image-def/versions/1.0.0" \
--admin-username azureuser \
--generate-ssh-keys
Advantages
The Azure Shared Image gallery easily allows you to build, share, manage, and customize images within your organization. SIG has Azure CLI, so you can easily automate image distribution.
Disadvantages
The image remains in a shared access gallery. Thus, it physically stays there. So, when you share it across subscriptions, you cannot change or remove it independently for each subscription.
4. Copying Images
By copying images, you can deliver images from one subscription to another or from one resource group to another. All copied images are independent of each other. You can copy images using Azure Image Copy extension, or implement manual copying using Go. Let us have a look at the examples below.
Copying Image With Az Copy Extension
To copy images between subscriptions, I use Az Image Copy extension. It creates a new image (from the source image) in Resource Group A.
Install copy extension:
xxxxxxxxxx
az extension add --name image-copy-extension
Copy command:
xxxxxxxxxx
az image copy --source-resource-group Azure-Resource-Group-B `
--source-object-name image-version-1.0 `
--target-resource-group Azure-Resource-Group-A `
--target-location westeurope `
--target-subscription 111111-2222-2222-0000-0000000 ` # Subscription A
--cleanup
Important. Packer removes the disk automatically after managed image is created.
Advantages
Az Image Copy extension is simple to use and automate.
Disadvantages
An image must contain a managed disc, otherwise the copy process fails with ‘Resource not found’ error message.
Copy Image Manually
If Az Copy Image Extension does not work for you, you can create a VHD image in the storage account and copy it to another destination storage account.
The process workflow:
- Create 2 Storage accounts, Source and Destination
- Create the VHD image in the Source storage account. You can use the Packer script from the first section.
- Generate Shared Access Signature for the VHD Source image. Here you can find an example of how to do it with Azure CLI.
4. And copy the image. To demonstrate I use the following Go script. You can also use the AzCopy tool, here the example of how to do it.
xxxxxxxxxx
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/url"
"os"
"time"
"github.com/Azure/azure-storage-blob-go/azblob"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/auth"
)
var (
ctx = context.Background()
clientData clientInfo
authorizer autorest.Authorizer
)
type clientInfo struct {
SubscriptionID string
}
const (
accountKey = "ACCOUNT_KEY"
accountName = "ACCOUNT_NAME"
azureAuthLocation = "AZURE_AUTH_LOCATION"
)
func init() {
var err error
authorizer, err = auth.NewAuthorizerFromFile(azure.PublicCloud.ResourceManagerEndpoint)
if err != nil {
log.Fatalf("Failed to get config: %v", err)
}
authInfo, err := readJSON(os.Getenv(azureAuthLocation))
if err != nil {
log.Fatalf("JSON Reading issues: %+v", err)
}
clientData.SubscriptionID = (*authInfo)["subscriptionId"].(string)
}
func main() {
accountName, accountKey := accountInfo()
// Set up Destination image file
targetBlobURL, _ := url.Parse(fmt.Sprintf("https://%s.blob.core.windows.net/destination-image.vhd", accountName))
// Set up and get image source, using Shared Acess Signature
src, _ := url.Parse("https://<SOURCE-STORAGE-ACCOUNT-NAME>.blob.core.windows.net/source-image.vhd?sp=r&st=2020-05-12T12:52:55Z&se=2021-05-12T20:52:55Z&spr=https&sv=2019-10-10&sr=b&sig=sdfsdfsdfs323423443df")
// Create and get SP credentials
credential, err := azblob.NewSharedKeyCredential(accountName, accountKey)
if err != nil {
log.Fatal(err)
}
blobURL := azblob.NewBlobURL(*targetBlobURL, azblob.NewPipeline(credential, azblob.PipelineOptions{}))
// Start copy
startCopy, err := blobURL.StartCopyFromURL(ctx, *src, nil, azblob.ModifiedAccessConditions{}, azblob.BlobAccessConditions{})
if err != nil {
log.Fatal(err)
}
copyID := startCopy.CopyID()
copyStatus := startCopy.CopyStatus()
for copyStatus == azblob.CopyStatusPending {
time.Sleep(time.Second * 2)
getMetadata, err := blobURL.GetProperties(ctx, azblob.BlobAccessConditions{})
if err != nil {
log.Fatal(err)
}
copyStatus = getMetadata.CopyStatus()
}
fmt.Printf("Copy Status %s to %s: ID=%s, Status=%s\n", src.String(), blobURL, copyID, copyStatus)
}
func accountInfo() (string, string) {
return return os.Getenv(accountName), os.Getenv(accountKey)
}
func readJSON(path string) (*map[string]interface{}, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
log.Fatalf("failed to read file: %v", err)
}
contents := make(map[string]interface{})
_ = json.Unmarshal(data, &contents)
return &contents, nil
}
Advantages
The current image coping workflow is fully custom. Therefore, it can be changed at any time. Also, it can be useful when the Az Image Copy extension does not work you. For example, when it removes the managed disk while creating an image.
Disadvantages
You have to implement all workflow steps including:
- creating VHD
- generating and managing SAS token
- copying an image
- cleaning up
- converting an image
5. Image and Disk Converting
The operations to convert a VHD image to a Managed Disk, or a Managed Disk to a VHD image. This is useful when you need to distribute images across your organization with Azure Shared Image Gallery in the image copy process and when you need you to spin up a new VM.
Also, it is useful when you have some legacy VHD and you need to support it, install updates, set up automatic backups, use availability, and availability zones.
Here is a list of advantages of the Managed Image.
Converting VHD Image to the Managed Disk
xxxxxxxxxx
# Provide the subscription Id where Managed Disks will be created
$subscriptionId = '00000-00000-0000-0000-0000000'
# Provide the name of your resource group where Managed Disks will be created.
$resourceGroupName ='HBI'
# Provide the name of the Managed Disk
$diskName = 'image-test-disk'
# The Disk It should be greater than the VHD file size.
$diskSize = '130'
# Storage LRS. Options: Premium_LRS, Standard_LRS, etc
$storageType = 'Premium_LRS'
# Set Azure region (e.g. westus, westeurope), Ensure that location of the VHD image (alongside with Storage Account),
# and future Managed Disk is the same.
$location = 'westeurope'
# Set URI of the VHD file (page blob) in a storage account.
$sourceVHDURI = 'https://imagesstorage.blob.core.windows.net/test-image-disk.vhd'
# Set the Resource Id of the Source storage account where VHD file is stored. You can avoid it if VHD in the same subscription
$storageAccountId = '/subscriptions/subscription-id/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/imagesstorage'
#Set the context to the subscription Id where Managed Disk will be created
Select-AzSubscription -SubscriptionId $SubscriptionId
$diskConfig = New-AzDiskConfig -AccountType $storageType -Location $location -CreateOption Import -StorageAccountId $storageAccountId -SourceUri $sourceVHDURI
New-AzDisk -Disk $diskConfig -ResourceGroupName $resourceGroupName -DiskName $diskName
Convert Managed Disk to Managed Image
xxxxxxxxxx
$rgName = "HBI"
$location = "westeurope"
$imageName = "boriszn-test-imagefromdisk"
# Get disk
$disk = Get-AzDisk -ResourceGroupName 'HBI' -DiskName 'image-test-disk'
$diskId = $disk.Id
# Create Managed Image Config with Managed Disk Info
# OS Types (Linux, Windows)
$imageConfig = New-AzImageConfig -Location $location
$imageConfig = Set-AzImageOsDisk -Image $imageConfig -OsState Generalized -OsType Linux -ManagedDiskId $diskId
# Create the image.
$image = New-AzImage -ImageName $imageName -ResourceGroupName $rgName -Image $imageConfig
Advantages
With both cmdlets, you can deliver an image to your Azure Shared Image across different subscriptions and resource groups.
Disadvantages
The conversion process can be complicated because some images can be outdated, and the conversion process may fail.
6. Testing Images
To test images, you can simply spin-up the new VM from the image gallery and use an image definition.
xxxxxxxxxx
az vm create `
--resource-group container-image-rg `
--name vm-test `
--image "/subscriptions/<subscription-id>/resourceGroups/container-image-rg/providers/Microsoft.Compute/galleries/test-gallery/images/image-test-def" `
--generate-ssh-keys
Advantages
The command and process itself quite simple.
Disadvantages
It does not check whether specific software and services are installed correctly in the JSON configuration. So this logic has to be implemented separately.
The Images Pub/Sub Subsystem Concept (Bonus)
Managing images across several subscriptions or resource groups can be difficult, especially when you constantly producing new versions of images, or you have some automated process to spin-up new virtual machines.
In this case, you need to build a notification system for notifying different components when a new image was created, a new version or image definition appears in the Azure Shared Image Gallery. You can easily create it using Azure Event Grid with filters.
Below you can see how it can be done with the event grid. You can use Queue, Webhook, and even Azure Service Bus to deliver messages to the target component.
The following example demonstrates how you can create an event grid with image filters.
xxxxxxxxxx
az eventgrid event-subscription create `
--source-resource-id $topicid `
--name $eventSubscriptionName `
--endpoint-type "storagequeue" `
--endpoint $queueid `
--included-event-types Microsoft.Resources.ResourceWriteSuccess `
--advanced-filter data.operationName StringContains "Microsoft.Compute/virtualMachines/write" `
--advanced-filter data.operationName StringContains "Microsoft.Compute/galleries/images/write" `
--advanced-filter data.operationName StringContains "Microsoft.Compute/galleries/images/versions/write"
Here you can find the complete Azure DevOps pipeline with the steps on how to deploy this Event Grid and link with other required resources.
Conclusion
That’s it. Based on these ways you can easily set up images Hardening and management pipeline in the Azure DevOps.
Opinions expressed by DZone contributors are their own.
Comments