Compliance with Azure Bastion: Session recording

Photo of author

Dan Rios

4 min read

Introduction

On May 30th, 2024, Microsoft announced Azure Bastion Premium, featuring the highly anticipated session recording capability. This feature, long requested since Bastion’s general availability in November 2019, marks a significant enhancement.

In my view, session recording is an essential addition to Bastion, as it provides superior compliance and security. It enables the direct recording of Bastion sessions into a Storage Account, bolstering its compliance and security capabilities.

For the uninitiated, Azure Bastion serves as a ‘jumpbox’ for virtual machines hosted on Azure. Its key advantage is eliminating the need to expose virtual machines via a public IP, thus reducing potential attack vectors and fortifying VM security. Azure Bastion is a fully managed, native service that supports both RDP and SSH connections through port 443.

In this blog I will go over how to set this up, and offer a demo/lab with a Bicep template so you can quick-deploy and test the session recording out yourself.

Bastion Premium is currently in Private Preview. It is typically advised not to use it in production environments until it reaches General Availability (GA), as the product may undergo feedback-driven changes and does not have a SLA (usually).

Setup & configuration

Let’s get started with what Azure components we’ll be deploying:

  • Azure Bastion: Premium with Public IP
  • Virtual Network with AzureBastionSubnet /26 & vmSubnet /24
  • Two Virtual Machines (Windows for RDP and Linux for SSH)
  • Storage Account for sessions to save into

The current Microsoft documentation for this is great, so I won’t be repeating what they have already set out: Configure Bastion session recording

Bicep Deployment

Next, you’ll need to login to Azure CLI and set your subscription context to deploy into, followed by creating the Resource Group:

az login
az account set -s "subGuid"
az group create --name rg-bastion-demo --location uksouth
az deployment group create -g 'rg-bastion-demo' -f '.\bastion.bicep' -p '.\bastion.bicepparam' -p parAdminPassword=Some-Password-Here
Bash

Using the below Bicep template & parameters:

using './azbastion-recording.bicep'
// Bicep params change as necessary – Password overriden at CLI runtime so can be ignored
param parvNetName = 'vnet-bastion-demo'
param parBastionName = 'bastion-demo'
param parLocation = 'uksouth'
param parWinVmName = 'win-vm-demo'
param parAdminUsername = 'riosengineer'
param parContainerName = 'recordings'
param parPipName = 'pip-bastion-demo'
param parAdminPassword = 'leaveme'
// az login
// az account set -s "subGuid"
// az group create –name rg-bastion-demo –location uksouth
// az deployment group create -g 'rg-bastion-demo' -f '.\bastion.bicep' -p '.\bastion.bicepparam' -p parAdminPassword=Some-Password-Here
targetScope = 'resourceGroup'
metadata name = 'Quickstart Template Azure Bastion Session Recording'
metadata description = 'Azure Bastion Premium SKU with Session Recording demo with Windows and Linux VMs.'
@description('Virtual Network Name')
param parvNetName string
@description('Bastion Host Name')
param parBastionName string
@description('Location')
param parLocation string
@description('Public IP Name')
param parPipName string
@description('Windows VM Name')
param parWinVmName string
@description('Admin Username')
param parAdminUsername string
@description('Container Name')
param parContainerName string
@secure()
@description('Admin Password')
param parAdminPassword string
// Virtual Network
module vNet 'br/public:avm/res/network/virtual-network:0.1.6' = {
name: '${uniqueString(deployment().name)}-vNet'
params: {
addressPrefixes: [
'10.0.0.0/16'
]
subnets: [
{
name: 'AzureBastionSubnet'
addressPrefix: '10.0.0.0/26'
}
{
name: 'vmSubnet'
addressPrefix: '10.0.1.0/24'
}
]
name: parvNetName
}
}
module PublicIpBastion 'br/public:avm/res/network/public-ip-address:0.4.1' = {
name: '${uniqueString(deployment().name)}-publicIpBastion'
params: {
name: parPipName
}
}
resource bastion 'Microsoft.Network/bastionHosts@2023-11-01' = {
name: parBastionName
location: parLocation
sku: {
name: 'Premium'
}
properties: {
ipConfigurations: [
{
name: 'IpConf'
properties: {
subnet: {
id: vNet.outputs.subnetResourceIds[0]
}
publicIPAddress: {
id: PublicIpBastion.outputs.resourceId
}
}
}
]
}
}
// Storage Account, Container & CORS
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'st${uniqueString(resourceGroup().id)}'
location: parLocation
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
publicNetworkAccess: 'Enabled'
allowBlobPublicAccess: true
accessTier: 'Hot'
}
dependsOn: [
bastion
]
}
resource storageAccountCors 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: storageAccount
name: 'default'
properties: {
cors: {
corsRules: [
{
allowedOrigins: [
'https://${bastion.properties.dnsName}'
]
allowedMethods: [
'GET'
]
maxAgeInSeconds: 86400
exposedHeaders: [
''
]
allowedHeaders: [
''
]
}
]
}
}
}
resource storageAccountContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-04-01' = {
parent: storageAccountCors
name: parContainerName
properties: {
publicAccess: 'Container'
metadata: {}
}
}
// Virtual Machines
module winVm 'br/public:avm/res/compute/virtual-machine:0.4.2' = {
name: '${uniqueString(deployment().name)}-winVm'
params: {
adminUsername: parAdminUsername
adminPassword: parAdminPassword
location: parLocation
encryptionAtHost: false
imageReference: {
offer: 'WindowsServer'
publisher: 'MicrosoftWindowsServer'
sku: '2022-datacenter-azure-edition'
version: 'latest'
}
name: parWinVmName
nicConfigurations: [
{
ipConfigurations: [
{
name: 'ipconfig01'
subnetResourceId: vNet.outputs.subnetResourceIds[1]
}
]
nicSuffix: '-nic-01'
}
]
osDisk: {
caching: 'ReadWrite'
diskSizeGB: 128
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
osType: 'Windows'
vmSize: 'Standard_B2s_v2'
zone: 0
}
}
module linuxVm 'br/public:avm/res/compute/virtual-machine:0.4.2' = {
name: '${uniqueString(deployment().name)}-linuxVm'
params: {
adminUsername: parAdminUsername
adminPassword: parAdminPassword
location: parLocation
encryptionAtHost: false
imageReference: {
offer: 'UbuntuServer'
publisher: 'Canonical'
sku: '18.04-LTS'
version: 'latest'
}
name: 'linuxVm'
nicConfigurations: [
{
ipConfigurations: [
{
name: 'ipconfig01'
subnetResourceId: vNet.outputs.subnetResourceIds[1]
}
]
nicSuffix: '-nic-01'
}
]
osDisk: {
caching: 'ReadWrite'
diskSizeGB: 128
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
osType: 'Linux'
vmSize: 'Standard_B2s_v2'
zone: 0
}
}

Bastion & Storage Setup

As mentioned before, the Microsoft documentation covers setup nicely. However, for our deployment we still need to perform some manual GUI tasks: Enabling session recording on the Bastion instance & generating a SAS URI for use.

Currently, the latest API Microsoft.Network/bastionHosts@2023-11-01 does not have a way (that I can see) to enable session recording at ARM level yet. In addition to this, generating a SAS URL via Bicep is not recommended as it’s exposing secrets via outputs.

Storage

Once the Bicep deployment has finished, locate the Storage Account resource > Data Storage > Containers > Click the three meatballs > Generate SAS:

  • read, create, write, list
  • set an expiry
  • Generate and collect the URL produced
Storage Account SAS Generation
Storage Account – Container Shared Access Signature generation

Lastly, notice under Settings > Resource sharing (CORS), the Bicep template has added the Bastion FQDN URL to the blob service:

Resource sharing
CORS Allowed origins

Bastion Session Recording

Next, you need to enable the session recording feature within the Bastion instance config in the Portal, which can be done by:

  1. Navigate to the Bastion resource in the Azure porta
  2. Go to Settings > Configuration
  3. Tick session recording (Preview)
  4. Wait for the instance to finish deploying the changes before proceeding

Finally, to complete the setup, configure the Azure Bastion session recordings to save into the container. Use the SAS URL you’ve just generated by following these steps:

  1. Navigate to the Bastion resource in the Azure portal.
  2. Go to Settings > Session recordings.
  3. Click on ‘Add or Update SAS URL’.
  4. Paste the generated SAS URL into the field.
  5. Select ‘Upload’ to finalize the configuration
Azure Bastion Session recording

Recordings

Now that everything is set up, simply log in to the Virtual Machines using Azure Bastion, and the session recording will automatically start, saving directly to the Storage Account. In my example below, you will find two recordings for each deployed VM: one for RDP and one for SSH:

Bastion session recording history
Bastion session recording history

These uploads are very quick to appear, once the Bastion connection finishes. It’ll show the audit history with the name, which is the virtual machine, a link to view the recording, session length & date created.

Here’s a GIF on what clicking on the ‘View recording’ hyperlink looks like:

Bastion SSH Session recording
Ubuntu Session recording playback

While exploring deployment options, I encountered a limitation regarding native client support and session recording as of the time of writing:

Bastion session recording limitation

Conclusion

To conclude, I think this is a really welcomed and long anticipated addition to Bastion, under the new Premium SKU. With recordings now uploading to a blob container, organisations can implement lifecycle management and archive recordings as needed, aligning with their specific requirements.

Bastion session recording will help to bring Bastion to the forefront as a solution for organisations who have stricter compliance, regulatory and sensitive workloads as the go-to native tooling.

Moreover, the Premium SKU’s introduction of private-only Bastion access via a private IP further solidifies its security benefits.

I’m curious to hear everyone’s thoughts on the session recording feature. What do you think?

Leave a comment


Skip to content