Create a hardened Ubuntu Pro 18.04 LTS shared image with Azure Image Builder

Key Value
Summary Create a hardened Ubuntu Pro 18.04 LTS shared image with Azure Image Builder
Categories Azure, Cloud, CIS, Security
Difficulty 4
Author Aaron Whitehouse aaron.whitehouse@canonical.com

Overview

Duration: 1:00

In this tutorial, we will use Azure Image Builder to create a hardened Ubuntu Pro 18.04 LTS “golden” image in an Azure Shared Image Gallery.

The resulting images will have CIS hardening applied to them, which helps meet security best practice, CIS-specific requirements and also improves compliance with the Azure Linux Security Baseline policy.

What you’ll learn

  • How to set up your Azure environment with a Shared Image Gallery and Azure resources you need to distribute the image within your environment
  • How to create an image definition for Ubuntu Pro 18.04 LTS and customise the image build JSON to apply the CIS hardening and add any other applications we may want in every Ubuntu VM
  • How to create the image version in this Azure Image Builder service
  • How to create a VM from the image in the Shared Image Gallery

We will be using Ubuntu Pro as the starting point for our Ubuntu images. These are normally the best choice for production workloads on Azure and they include access to CIS hardening scripts.

What you’ll need

Credits

This tutorial is based on the article Preview: Create a Linux image and distribute it to a Shared Image Gallery by using Azure CLI in the Microsoft documentation and an earlier version of this tutorial created by David Coronel here.

Setup your Shared Image Gallery

Duration: 5:00

We will be using some pieces of information repeatedly, so we will create some variables to store that information. Then we will create the identity, image definition and gallery that we will need for the image.

Set variables for use throughout the tutorial

We will create the resource group below, so the name should be one you are not already using. We will delete the resource groups that we have created at the end of the tutorial.

# Resource group name - we are using ibUbuntuProGalleryRG in this example
sigResourceGroup=ibUbuntuProGalleryRG
# Datacenter location - we are using North Europe in this example
location=northeurope
# Additional region to replicate the image to - we are using West Europe in this example
additionalregion=westeurope

Now we will set variables for the Gallery Name and Image Definition Name. The image will be displayed in the Azure Portal as sigName/imageDefName.

# name of the shared image gallery - in this example we are using myGallery
sigName=myUbuntuProGallery
# name of the image definition to be created
imageDefName=UbuntuPro1804CIS
# image distribution metadata reference name
runOutputName=aibLinuxSIG

Create a variable for your subscription ID. If you only have one, this will be the output of:
az account show -o json | grep id
If you have more than one, you can use
az account list -o table
and take the value from the SubscriptionId column for the subscription you would like to use for this tutorial.

Then set this to the variable as follows:

subscriptionID=<Subscription ID>

Now we are going to set variables for the Ubuntu Pro plan we are going to use in the tutorial. If you have an Ubuntu Pro private offer with Canonical, for example including 24x7 Technical Support with SLAs, you will have a custom Offer and Sku we can enter these here instead. If not, we will use the plan name and product for the public Ubuntu Pro from the Azure Marketplace.

# ProPlanPublisher the 'Publisher' field for the Marketplace VM Offer we want to start from
ProPlanPublisher=canonical
# ProPlanOffer the 'Offer' field for the Marketplace VM Offer we want to start from
ProPlanOffer=0001-com-ubuntu-pro-bionic
# ProPlanSku the 'Sku' field for the Marketplace VM Offer we want to start from
ProPlanSku=pro-18_04-lts

Create required resources, identities and permissions

Create the resource group:

az group create -n $sigResourceGroup -l $location --subscription $subscriptionID

Image Builder will use the user-identity provided to inject the image into the Azure Shared Image Gallery (SIG). In this example, you will create an Azure role definition that has the granular actions to perform distributing the image to the SIG. The role definition will then be assigned to the user-identity.

# create user assigned identity for image builder to access the storage account where the script is located
identityName=aibBuiUserId$(date +'%s')
az identity create -g $sigResourceGroup -n $identityName --subscription $subscriptionID

# get identity id
imgBuilderCliId=$(az identity show -g $sigResourceGroup -n $identityName --subscription $subscriptionID -o json | grep "clientId" | cut -c16- | tr -d '",')

# get the user identity URI, needed for the template
imgBuilderId=/subscriptions/$subscriptionID/resourcegroups/$sigResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/$identityName

# this command will download an Azure role definition template, and update the template with the parameters specified earlier.
curl https://raw.githubusercontent.com/Azure/azvmimagebuilder/master/solutions/12_Creating_AIB_Security_Roles/aibRoleImageCreation.json -o aibRoleImageCreation.json

imageRoleDefName="Azure Image Builder Image Def"$(date +'%s')

# update the definition
sed -i -e "s/<subscriptionID>/$subscriptionID/g" aibRoleImageCreation.json
sed -i -e "s/<rgName>/$sigResourceGroup/g" aibRoleImageCreation.json
sed -i -e "s/Azure Image Builder Service Image Creation Role/$imageRoleDefName/g" aibRoleImageCreation.json

# create role definitions
az role definition create --role-definition ./aibRoleImageCreation.json

# need to wait a bit here
sleep 45

# grant role definition to the user assigned identity
# If this gives an error, wait a bit longer and try again
az role assignment create \
    --assignee $imgBuilderCliId \
    --role "$imageRoleDefName" \
    --scope /subscriptions/$subscriptionID/resourceGroups/$sigResourceGroup

Create an image definition and gallery

To use Image Builder with a shared image gallery, you need to have an existing image gallery and image definition. Image Builder will not create the image gallery and image definition for you.

We will start by creating a gallery and image definition. First, we will create the Gallery:

az sig create \
    -g $sigResourceGroup \
    --gallery-name $sigName \
    --subscription $subscriptionID

Then, create an image definition. This uses the variables we set earlier.

az sig image-definition create \
   -g $sigResourceGroup \
   --gallery-name $sigName \
   --gallery-image-definition $imageDefName \
   --publisher $ProPlanPublisher \
   --offer $ProPlanOffer \
   --sku $ProPlanSku \
   --os-type Linux \
   --plan-name $ProPlanSku \
   --plan-product $ProPlanOffer \
   --plan-publisher $ProPlanPublisher \
   --subscription $subscriptionID

Customise a template for our deployment

Duration: 2:00

Now we are going to create a template that contains the build instructions for the standard, “golden” image we want to create.

We can download a starting point for this from here

wget https://gist.githubusercontent.com/Hooloovoo/3e544681a12121a36b5dbda684465b8d/raw/85a7c4461e704b722b901ffc981e23db9a14ce82/BasicCISUbuntuPro1804SIGTemplate.json

Then we can customise it to use the values we have set above. The sed commands below simply replace the <variable> placeholders in BasicCISUbuntuPro1804SIGTemplate.json with the values for the parameters that we set earlier:

sed -i -e "s/<subscriptionID>/$subscriptionID/g" BasicCISUbuntuPro1804SIGTemplate.json
sed -i -e "s/<rgName>/$sigResourceGroup/g" BasicCISUbuntuPro1804SIGTemplate.json
sed -i -e "s/<imageDefName>/$imageDefName/g" BasicCISUbuntuPro1804SIGTemplate.json
sed -i -e "s/<sharedImageGalName>/$sigName/g" BasicCISUbuntuPro1804SIGTemplate.json
sed -i -e "s/<region1>/$location/g" BasicCISUbuntuPro1804SIGTemplate.json
sed -i -e "s/<region2>/$additionalregion/g" BasicCISUbuntuPro1804SIGTemplate.json
sed -i -e "s/<runOutputName>/$runOutputName/g" BasicCISUbuntuPro1804SIGTemplate.json
sed -i -e "s%<imgBuilderId>%$imgBuilderId%g" BasicCISUbuntuPro1804SIGTemplate.json
sed -i -e "s/<ProPlanPublisher>/$ProPlanPublisher/g" BasicCISUbuntuPro1804SIGTemplate.json
sed -i -e "s/<ProPlanOffer>/$ProPlanOffer/g" BasicCISUbuntuPro1804SIGTemplate.json
sed -i -e "s/<ProPlanSku>/$ProPlanSku/g" BasicCISUbuntuPro1804SIGTemplate.json

Review the contents of the template file

Duration: 2:00

Let’s review some of the sections of the BasicCISUbuntuPro1804SIGTemplate.json we have just updated. It is worth reading over all of this (short) file, but below we will look at particular sections in more detail. We should not need to change any of these values.

Note the below section:

        "source": {
            "type": "PlatformImage",
                "publisher": "canonical",
                "offer": "0001-com-ubuntu-pro-bionic",
                "sku": "pro-18_04-lts",
                "version": "latest",
        "planInfo": {
                    "planName": "pro-18_04-lts",
                    "planProduct": "0001-com-ubuntu-pro-bionic",
                    "planPublisher": "canonical"
                }
        },

This will show the plan details for the Marketplace or Private Offer VM image you are using as a starting point for your golden image.

The customize section allows us to run commands as part of the image building process. The following waits until Ubuntu’s ua client has attached to its subscription and then enables access to the CIS hardening scripts:

        "customize": [
            {
            "type": "Shell",
            "name": "WaitForUAtokenAutoAttach",
            "inline": [
                "sudo ua status --wait"
            ]
        },

        {
            "type": "Shell",
            "name": "EnableCISfeature",
            "inline": [
            	"sudo ua enable cis"
            ]
        },

In a real deployment, we would at this point in the customisation script modify /usr/share/ubuntu-scap-security-guides/cis-hardening/ruleset-params.conf to set values appropriate for our environment, before running the CIS hardening script. See this documentation for more information. See also the rest of the CIS documentation here. To keep things simple, however, we will skip that step here.

The following runs the CIS hardening script and then removes one of these rules, as it conflicts with how Azure provides provisioning information to VMs:

        {
            "type": "Shell",
            "name": "RunCIShardening - see https://ubuntu.com/security/certifications/docs/cis-compliance",
            "inline": [
                "sudo /usr/share/ubuntu-scap-security-guides/cis-hardening/Canonical_Ubuntu_18.04_CIS-harden.sh lvl1_server"
            ]
        },

        {
            "type": "Shell",
            "name": "UDFworkaroundForAzureVMbooting - UDF is required for Azure image provisioning",
            "inline": [
                "sudo rm -f /etc/modprobe.d/Canonical_Ubuntu_CIS_rule-1.1.1.7.conf"
            ]
        },

The following is a placeholder for any custom commands we want to run. We will come back to this in the next step.

        {
            "type": "Shell",
            "name": "Placeholder for custom commands required in each Ubuntu VM",
            "inline": [
                "echo 'Replace me!'"
            ]
        },

The below runs some commands that deregisters the golden image from Ubuntu Pro and removes the machine-id. This ensures that VMs generated from the golden image will generate their own unique IDs.

	    {
            "type": "Shell",
            "name": "DetachUA -- images created from this will auto attach themselves with new credentials",
            "inline": [
                "sudo ua detach --assume-yes && sudo rm -rf /var/log/ubuntu-advantage.log"
            ]
     	},

        {
            "type": "Shell",
            "name": "Replace /etc/machine-id with empty file to ensure UA client does not see clones as duplicates",
            "inline": [
                "sudo rm -f /etc/machine-id && sudo touch /etc/machine-id"
            ]
        }

Custom customisation: Make the VMs auto-update

As an example of how we can customise an image building script for our requirements, we will now change one of these customize steps.

Find the following section in BasicCISUbuntuPro1804SIGTemplate.json:

        {
            "type": "Shell",
            "name": "Placeholder for custom commands required in each Ubuntu VM",
            "inline": [
                "echo 'Replace me!'"
            ]
        },

Change this to the below, which enables unattended upgrades on the machine:

        {
            "type": "Shell",
            "name": "Install upgrades automatically",
            "inline": [
                "sudo apt install unattended-upgrades"
            ]
        },

Build/Create the image version

Duration: 30:00

We will now create the image version in the gallery.

First, we submit the image configuration to the Azure Image Builder service:

az resource create \
    --resource-group $sigResourceGroup \
    --subscription $subscriptionID \
    --properties @BasicCISUbuntuPro1804SIGTemplate.json \
    --is-full-object \
    --resource-type Microsoft.VirtualMachineImages/imageTemplates \
    -n BasicCISUbuntuPro1804SIG01

In the next step we will start the image build. This step can take many minutes (25-30 mins on my testing), as Azure will actually launch a VM and run the steps we have defined. We need to wait for this to complete before we can create a VM.

az resource invoke-action \
     --resource-group $sigResourceGroup \
     --subscription $subscriptionID \
     --resource-type  Microsoft.VirtualMachineImages/imageTemplates \
     -n BasicCISUbuntuPro1804SIG01 \
     --action Run

If this fails, see the section below
If this fails, see the “If required: Accept Marketplace terms and retry” section below

While we are waiting, however, we can see the logs of the AIB build process by going to the storage account inside the resource group created by AIB (ie. Azure Portal > Resource groups > [IT_ibUbuntuProGalleryRG_BasicCISUbuntuPro18_randomID > Random ID of the storage account > Containers > packerlogs > Random ID of the container > customization.log > Download.

You should be able to see traces of the CIS hardening like this:

[...]
[4c6d22ac-7c0f-466f-a1c2-b30672274e89] PACKER OUT     azure-arm: Execute rule 1.1.1.2
[4c6d22ac-7c0f-466f-a1c2-b30672274e89] PACKER OUT     azure-arm:
[4c6d22ac-7c0f-466f-a1c2-b30672274e89] PACKER OUT     azure-arm: Ensure mounting of freevxfs filesytems is disabled
[4c6d22ac-7c0f-466f-a1c2-b30672274e89] PACKER OUT     azure-arm: Execute rule 1.1.1.3
[4c6d22ac-7c0f-466f-a1c2-b30672274e89] PACKER OUT     azure-arm:
[4c6d22ac-7c0f-466f-a1c2-b30672274e89] PACKER OUT     azure-arm: Ensure mounting of jffs2 filesytems is disabled
[4c6d22ac-7c0f-466f-a1c2-b30672274e89] PACKER OUT     azure-arm: Execute rule 1.1.1.4
[4c6d22ac-7c0f-466f-a1c2-b30672274e89] PACKER OUT     azure-arm:
[4c6d22ac-7c0f-466f-a1c2-b30672274e89] PACKER OUT     azure-arm: Ensure mounting of hfs filesytems is disabled
[...]

If required: Accept Marketplace terms and retry

If you have never launched an Ubuntu Pro 18.04 LTS image before, this command may fail with something like:

Deployment failed. Correlation ID: eba17586-86f5-495a-85f9-3f9191065447. During the image build a failure has occurred. Please review the build log to identify which build/customization step failed. For more troubleshooting steps go to https://aka.ms/azvmimagebuilderts. Image build log location: https://armecifcu32rwz7jlos8zurq.blob.core.windows.net/packerlogs/bf091959-ba35-4db2-8888-c2b3179f119e/customization.log. OperationId: 1e853ba4-7d46-493f-890e-7e039fa44f9f. Use this operationId to search packer logs.

and if you look at the logs it mentions, you can see something like:

{\"code\":\"MarketplacePurchaseEligibilityFailed\",\"message\":\"Marketplace purchase eligibilty check returned errors. See inner errors for details. \",\"details\":[{\"code\":\"BadRequest\",\"message\":\"Offer with PublisherId: 'canonical', OfferId: '0001-com-ubuntu-pro-bionic' cannot be purchased due to validation errors. For more information see details. [...] 
You have not accepted the legal terms on this subscription: '[subscription ID]' for this plan. Before the subscription can be used, you need to accept the legal terms of the image. To read and accept legal terms, use the Azure CLI commands described at https://go.microsoft.com/fwlink/?[REDACTED] or the PowerShell commands available at https://go.microsoft.com/fwlink/?[REDACTED] Alternatively, deploying via the Azure portal provides a UI experience for reading and accepting the legal terms. Offer details: publisher='canonical' offer = '0001-com-ubuntu-pro-bionic', sku = 'pro-18_04-lts', Correlation Id: '7c302144-1c69-4642-be15-10e75049d0f7'.

In this case, we will need to accept the terms and retry the above command. You can see the terms on the Ubuntu Pro 18.04 Pro LTS marketplace listing here or for your specific Private Offer.

To accept, you can either launch a single VM through the Portal for a UI experience or you can accept from the commandline with:

az vm image terms accept --plan $ProPlanSku --offer $ProPlanOffer --publisher $ProPlanPublisher --subscription $subscriptionID

Then we need to re-run:

az resource invoke-action \
     --resource-group $sigResourceGroup \
     --subscription $subscriptionID \
     --resource-type  Microsoft.VirtualMachineImages/imageTemplates \
     -n BasicCISUbuntuPro1804SIG01 \
     --action Run

(If it does not work immediately, wait a few minutes and try again.)

Completing the build process

Once it has completed, it will change from “Running” to show something like the following:

{
  "endTime": "2021-11-03T17:42:21.7582048Z",
  "name": "67B28A17-AD58-4BDD-8CFB-1777906F8626",
  "startTime": "2021-11-03T17:11:38.9933333Z",
  "status": "Succeeded"
}

Create a VM from the shared image gallery from the Portal

Duration: 5:00

Create a VM from the Azure Portal

We are now going to create a VM from the Azure Portal.

Once you log in, click “Virtual machines”:
image

Click “Create” > “Virtual machine”
image

In the “Create a virtual machine” screen, you will need to click “See all images” below the “Image” drop down:

Select “Shared Images” on the left-hand side:
image

You should see our image in the “My Items”:
image

You can click this and it will select that as the image type.

Complete the remaining fields as you wish and click “Review + Create” to create a VM (or simply proceed to the next step if you prefer, as we will not be using the VM from this step):

Create and test a VM from the shared image gallery from the AZ CLI

Duration: 5:00

Create a VM from the commandline

We are now going to create a VM from the Shared Image Gallery. As we created the image with plan information, we also need to specify that information when launching the instance.

If you already have an SSH key that you want to use, then use the following to launch the VM:

SSHPublicKeyPath=<path to your id_rsa.pub>
az vm create \
  --resource-group $sigResourceGroup \
  --subscription $subscriptionID \
  --name myAibGalleryVM \
  --admin-username aibuser \
  --location $location \
  --image "/subscriptions/$subscriptionID/resourceGroups/$sigResourceGroup/providers/Microsoft.Compute/galleries/$sigName/images/$imageDefName/versions/latest" \
  --ssh-key-values $SSHPublicKeyPath \
  --plan-name $ProPlanSku \
  --plan-product $ProPlanOffer \
  --public-ip-sku Standard \
  --plan-publisher $ProPlanPublisher

Alternatively, if you do not yet have SSH keys, you can use the following and replace the --ssh-key-values $SSHPublicKeyPath with --generate-ssh-keys (note that this may overwrite the ssh keypair “id_rsa” and “id_rsa.pub” under .ssh in your home directory):

az vm create \
  --resource-group $sigResourceGroup \
  --subscription $subscriptionID \
  --name myAibGalleryVM \
  --admin-username aibuser \
  --location $location \
  --image "/subscriptions/$subscriptionID/resourceGroups/$sigResourceGroup/providers/Microsoft.Compute/galleries/$sigName/images/$imageDefName/versions/latest" \
  --generate-ssh-keys \
  --plan-name $ProPlanSku \
  --plan-product $ProPlanOffer \
  --public-ip-sku Standard \
  --plan-publisher $ProPlanPublisher

When this completes, you should see something like the following:

{
  "fqdns": "",
  "id": "/subscriptions/[your subscription]/resourceGroups/ibUbuntuProGalleryRG/providers/Microsoft.Compute/virtualMachines/myAibGalleryVM",
  "location": "northeurope",
  "macAddress": "00-0D-3A-B9-32-27",
  "powerState": "VM running",
  "privateIpAddress": "10.0.0.4",
  "publicIpAddress": "20.67.164.125",
  "resourceGroup": "ibUbuntuProGalleryRG",
  "zones": ""
}

Note the publicIpAddress, which in my case is 20.67.164.125. This is what you will use to ssh into the machine in the next step.

Logging into the new VM and testing

ssh aibuser@[the IP address from above]

In my case, this is:

ssh aibuser@20.67.164.125

It will likely say:

The authenticity of host '20.67.164.125 (20.67.164.125)' can't be established.
ECDSA key fingerprint is SHA256:hvDCR6zYnEYDhnQSdOhKZzrFQ017nH5FqPL2hty1WE0.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

To which you can respond yes.

Once you are in the VM, you can type sudo ua status --wait and check the output:

$ sudo ua status --wait
SERVICE       ENTITLED  STATUS    DESCRIPTION
cis           yes       disabled  Center for Internet Security Audit Tools
esm-apps      yes       enabled   UA Apps: Extended Security Maintenance (ESM)
esm-infra     yes       enabled   UA Infra: Extended Security Maintenance (ESM)
fips          yes       disabled  NIST-certified core packages
fips-updates  yes       disabled  NIST-certified core packages with priority security updates
livepatch     yes       n/a       Canonical Livepatch service

Enable services with: ua enable <service>

                Account: <redacted>
           Subscription: <redacted>
            Valid until: 9999-12-31 00:00:00+00:00
Technical support level: essential

We can see that this VM is attached to an Ubuntu Pro subscription and that we have great features like esm-apps and esm-infra enabled.

(If you are using a Private Offer that includes support, your Technical support level will read advanced instead of essential.)

And we can confirm that the CIS hardening is in place:

$ cat /etc/issue.net
Authorized uses only. All activity may be monitored and reported.

We can even run a CIS audit:

$ sudo ua enable cis
One moment, checking your subscription first
CIS Audit is already enabled.
See: sudo ua status
$ sudo cis-audit level1_server
Title   Ensure mounting of cramfs filesystems is disabled
Rule    xccdf_com.ubuntu.bionic.cis_rule_CIS-1.1.1.1
Result  pass

Title   Ensure mounting of freevxfs filesystems is disabled
Rule    xccdf_com.ubuntu.bionic.cis_rule_CIS-1.1.1.2
Result  pass

Title   Ensure mounting of jffs2 filesystems is disabled
Rule    xccdf_com.ubuntu.bionic.cis_rule_CIS-1.1.1.3
Result  pass

Title   Ensure mounting of hfs filesystems is disabled
Rule    xccdf_com.ubuntu.bionic.cis_rule_CIS-1.1.1.4
Result  pass
[...]

CIS audit scan completed. The scan results are available in /usr/share/ubuntu-scap-security-guides/cis-18.04-report.html report.

You can bring this onto your local machine by first changing the permissions within your ssh session:

sudo chown aibuser /usr/share/ubuntu-scap-security-guides/cis-18.04-report.html

and then using a separate terminal window to download it with scp:

$ scp aibuser@20.67.164.125:/usr/share/ubuntu-scap-security-guides/cis-18.04-report.html .
Authorized uses only. All activity may be monitored and reported.
cis-18.04-report.html                         100%  579KB   3.1MB/s   00:00

For comparison, on an unhardened Ubuntu 18.04 VM, there are 80 CIS failures:

In our image, even without filing in the ruleset-params.conf or taking the manual CIS hardening steps set out in the documentation, the CIS audit should only show 13 failed rules:

We could add further customisations to our deployment template to improve this even further.

Cleanup

Duration: 2:00

You should be able to see the Resource Groups that have been created as part of this tutorial by typing:

az group list --query [].name --output table --subscription $subscriptionID | grep $sigResourceGroup

In my case this returns:

ibUbuntuProGalleryRG
IT_ibUbuntuProGalleryRG_BasicCISUbuntuPro18_e52db49b-9cc0-4d52-936e-ecf570746def

Check you are happy for these to be deleted. If so, type:

az group delete --name [the name from above] --subscription $subscriptionID

for each of these Resource Groups, for example:

$ az group delete --name ibUbuntuProGalleryRG --subscription $subscriptionID
Are you sure you want to perform this operation? (y/n): y
$ az group delete --name IT_ibUbuntuProGalleryRG_BasicCISUbuntuPro18_e52db49b-9cc0-4d52-936e-ecf570746def --subscription $subscriptionID
Are you sure you want to perform this operation? (y/n): y

(You may find that deleting the first automatically deletes the second.)

That’s all folks!

Duration: 1:00

Congratulations! We have created a Shared Image Gallery with a hardened Ubuntu Pro 18.04 LTS image inside, and launched and tested virtual machines created from this.

I hope that you have found this a helpful introduction to using a Shared Image Gallery with your Ubuntu Pro entitlements. If you have any questions, comments or suggestions, please do click the “Suggest changes” link and comment on the Discourse article for this tutorial.