SQL Server Audit and Vulnerability Scans with Bicep
Update 3/31/22: Realized this assumed Defender was already turned on the server so adding the Bicep to do that.
Update 4/14/23: Active example haws been moved to: https://github.com/JFolberth/Azure_IaC_Flavors/tree/main/code/sqlServerAAD/bicep
Introduction
This one is tricky. If you are well verse in Azure and some of the security best practices then you are aware that Microsoft Defender for Cloud recommends that SQL Server have Vulnerability Assessments and Auditing turned on. So how do you enable SQL Server Audit and Vulnerability Scans with Bicep?
These settings can be defined via Infrastructure as Code (IaC); however, it’s not very straightforward. First the vulnerability assessments should go to an Azure Storage Account. Audit Logs can go to a variety of places; however, Log Analytic Workspaces tend to be the most preferred so we will do that. On top of these individual components, we need to create them then we have to wire it all to talk to each other.
I’ll end by giving you the .bicep module for SQL Server that will configure all this. You just need to pass in the appropriate parameters.
Assumptions
- There already is an existing Log Analytic Workspace (You usually don’t have a 1:1 relationship on these)
What are Vulnerability Assessments and Audit Logs?
TL:DR These are important security features which Microsoft recommends you turn on.
Vulnerability assessment is a scanning service built into Azure SQL Database. The service employs a knowledge base of rules that flag security vulnerabilities. It highlights deviations from best practices, such as misconfigurations, excessive permissions, and unprotected sensitive data.
azure-sql/database/sql-vulnerability-assessment
The simple version is Microsoft’s way of scanning SQL to ensure best practice is being adhered to and flagging anything that may seem out of the ordinary.
So what about Audit Logs? This is a pretty standard term as it’s your SQL Audit logs. Per Microsoft
Azure SQL Database auditing tracks database events and writes them to an audit log in your Azure storage account, or sends them to Event Hub or Log Analytics for downstream processing and analysis
azure-sql/database/audit-log-format
Auditing
This is the trickier of the two. Why is that? There is a fairly under documented notion that for SQL Server auditing to be turned on the destination is actually the master
database. I discovered this by going over the schema for the microsoft.sql/servers/auditsettings:
When using REST API to configure auditing, Diagnostic Settings with ‘SQLSecurityAuditEvents’ diagnostic logs category on the database should be also created.
Note that for server level audit you should use the ‘master’ database as {databaseName}.
This seems simple enough. There’s just one problem that exists both in ARM and Bicep; however, I could argue it is more obfuscated in Bicep. The relative parent and naming segments. The provider endpoint is for server
yet the above makes you realize this needs to be a database
segment. so….what do to do.
The diagnosticSettings
will still be at a server and will look like Microsoft.Sql/servers/databases/providers/diagnosticSettings
. To get this setting at the database we need to update the name of the individual resource. This would be accomplished by something like '${sqlServer.name}/master/microsoft.insights/LogAnalytics'
The LogAnalytics
piece can be substituted for the name of the diagnosticSettings
you’d prefer.
The second half of this the actual tuning of the auditSettings
to be collected was taken from microsoft.sql/servers/auditsettings and I just used the recommend subset. In this case that is
'BATCH_COMPLETED_GROUP'
'SUCCESSFUL_DATABASE_AUTHENTICATION_GROUP'
'FAILED_DATABASE_AUTHENTICATION_GROUP'
However feel free to check any aditional auditng you require.
Vulnerability Assessments
The Vulnerability Assessments aren’t as hard to configure. It’s just more resources which again could make the case of ARM vs Bicep here since we need to create a Storage Account, Blob, and Container just in Azure Resources. Bicep this is easier to create individual resources vs ARM where these resources would be nested. This leads to something that looks a little off like:
resource storageAccountVulnerability 'Microsoft.Storage/storageAccounts@2021-08-01' = {
name: vulnerabilitySAName
kind: 'StorageV2'
location: location
sku: {
name: 'Standard_LRS'
}
}
resource storageAccountBlobVulnerability 'Microsoft.Storage/storageAccounts/blobServices@2021-08-01' = {
name: 'default'
parent: storageAccountVulnerability
}
resource storageAccountContainerVulnerability 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-08-01' = {
name: 'vulnerabilityscans'
parent: storageAccountBlobVulnerability
}
Next up the vulnerabilityAssessments
needs to configured the storageContainerPath
. This requires a bit of string interpolation. I have opened a discussion on the Bicep repo page as feels this could be referenced as part of the storageAccountContainerVulnerability
properties.
So, the end result is something like:
storageContainerPath: '${storageAccountVulnerability.properties.primaryEndpoints.blob}${storageAccountContainerVulnerability.name}'
Again, this looks a little goofy but does accomplish what we are setting out for.
Now the last part is how to grant the SQL Server access to write to the Storage Account. From what I have seen all the documentation instructs the usage of Access Keys. If you follow me, you know my preference on anything involving this type of Access is to use Role Based Access Control (RBAC). For additional reading on the topic check out “The Perilous Dilemma of RBAC in an Automated World” or “Nested Loops In Azure Bicep with an RBAC Example“
To leverage RBAC Controls, we need to set the SQL Server Resource’s identity property:
identity:{
type: 'SystemAssigned'
}
And then we will need a block for the Role Assignment:
resource sqlStorageAccountRBAC 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = {
name: guid(sqlServer.name, blobContributorRoleId,storageAccountVulnerability.name )
scope: storageAccountVulnerability
properties:{
roleDefinitionId: blobContributorRoleId
principalId: sqlServer.identity.principalId
principalType: 'ServicePrincipal'
}
}
Also set a variable to store the Blob Contributor Role ID:
var blobContributorRoleId = 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'
This should then give the SQL Server contributor access over the Blob, thus making it inherently safer than passing keys, even if they are being securely passed.
Enable Defender
Azure Defender for Cloud may not be automatically enabled so we need to turn that one with the following Bicep code:
resource sqlServerAdvancedSecurityAssessment 'Microsoft.Sql/servers/securityAlertPolicies@2021-08-01-preview' = {
name: '${sqlServer.name}/advancedSecurityAssessment'
properties:{
state: 'Enabled'
}
}
Conclusion
Automating SQL Server Audit and Vulnerability Scans with Bicep was not as easy as I had hoped. It took some time and manipulation to get it working and something where I could argue which was easier Bicep or ARM. Don’t get me wrong Bicep is still the way to go…..just don’t start with this one. This is one of those that has always been a pain to automate and it’s a set it and forget, for the most part, configuration.
This automation though is required to be consistent across environments and even further enabled by the Microsoft Defender for Cloud recommendations.
Also as promised here is the full SQL Server Module which you should be able to plug and play into your existing SQL Server Bicep Deployments. Which to my early point with Bicep should be 100% for you to consume. Feel free to provide any feedback!
sql-server.module.bicep
param serverName string
param tags object
param sqlAdminGroupObjectId string
param sqlAdminGroupName string
param location string = resourceGroup().location
param vulnerabilityScanEmails string = 'SQLAdmin@TEST.COM'
param vulnerabilitySAName string
param logAnalyticsWorkspaceID string
var blobContributorRoleId = 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'
resource sqlServerAdvancedSecurityAssessment 'Microsoft.Sql/servers/securityAlertPolicies@2021-08-01-preview' = {
name: '${sqlServer.name}/advancedSecurityAssessment'
properties:{
state: 'Enabled'
}
}
resource sqlServer 'Microsoft.Sql/servers@2021-05-01-preview' = {
name: serverName
location: location
tags: tags
properties: {
version: '12.0'
minimalTlsVersion: '1.2'
publicNetworkAccess: 'Enabled'
restrictOutboundNetworkAccess: 'Disabled'
administrators: {
administratorType: 'ActiveDirectory'
tenantId: subscription().tenantId
principalType: 'Group'
azureADOnlyAuthentication: true
login: sqlAdminGroupName
sid: sqlAdminGroupObjectId
}
}
identity:{
type: 'SystemAssigned'
}
}
resource storageAccountVulnerability 'Microsoft.Storage/storageAccounts@2021-08-01' = {
name: vulnerabilitySAName
kind: 'StorageV2'
location: location
sku: {
name: 'Standard_LRS'
}
}
resource storageAccountBlobVulnerability 'Microsoft.Storage/storageAccounts/blobServices@2021-08-01' = {
name: 'default'
parent: storageAccountVulnerability
}
resource storageAccountContainerVulnerability 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-08-01' = {
name: 'vulnerabilityscans'
parent: storageAccountBlobVulnerability
}
resource sqlVulnerability 'Microsoft.Sql/servers/vulnerabilityAssessments@2021-08-01-preview' = {
name: 'default'
parent: sqlServer
properties: {
recurringScans:{
emails: [
vulnerabilityScanEmails
]
isEnabled: true
}
storageContainerPath: '${storageAccountVulnerability.properties.primaryEndpoints.blob}${storageAccountContainerVulnerability.name}'
}
dependsOn: [
sqlStorageAccountRBAC
sqlServerAdvancedSecurityAssessment
]
}
resource sqlStorageAccountRBAC 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = {
name: guid(sqlServer.name, blobContributorRoleId,storageAccountVulnerability.name )
scope: storageAccountVulnerability
properties:{
roleDefinitionId: blobContributorRoleId
principalId: sqlServer.identity.principalId
principalType: 'ServicePrincipal'
}
}
resource SqlDbDiagnosticSettings 'Microsoft.Sql/servers/databases/providers/diagnosticSettings@2021-05-01-preview' ={
name: '${sqlServer.name}/master/microsoft.insights/LogAnalytics'
properties: {
workspaceId: logAnalyticsWorkspaceID
logs:[
{
category: 'SQLSecurityAuditEvents'
enabled: true
}
]
}
}
resource sqlAudit 'Microsoft.Sql/servers/auditingSettings@2021-08-01-preview'={
name: 'default'
parent: sqlServer
properties:{
auditActionsAndGroups:[
'BATCH_COMPLETED_GROUP'
'SUCCESSFUL_DATABASE_AUTHENTICATION_GROUP'
'FAILED_DATABASE_AUTHENTICATION_GROUP'
]
isAzureMonitorTargetEnabled: true
state:'Enabled'
}
}
Thank you for the script! Is it possible to direct Azure SQL Server audit logs to storage account as well? If yes, how to do that using Bicep?
Correct. So
SqlDbDiagnosticSettings
will need to be updated with the name pointing to the storage account endpoint. Also within propertiesstorageAccountId
will need to have the storage account ID populated. Here is a link to provide more details on the schema.Thanks John! Much appreciated!
Hi John,
Thanks for sharing this script, I stumbled across this article while trying to resolve issues for converting our ARM templates to Bicep. However, the SqlDbDiagnosticSettings resource contains an unknown template reference? I did find the following link though: https://github.com/Azure/azure-quickstart-templates/blob/master/quickstarts/microsoft.sql/sql-auditing-server-policy-to-oms/main.bicep
Regards,
AJ
If you have an error log, I am more than happy to help take a look. If I remember the main difference between the quickstart and what I have is the use of modules. My knee jerk guess is the issue is with the scope property needing to reference the master DB on the server….but don’t have enough information to confirm.
Hi John,
Thanks in advanced for all these bicep post, are really helpful, I appreciate it. I’m trying to enable the Log Analytics for the SQL Server but not success following your code. I’m using an ‘existing’ Log Analytics. It worked when the SQL Server is already deployed and I use your code to enable the Log Analytics, but if I’m trying to build a new sql server and set the Log Analytics Audit using an existing Log Analytics in the same deployment I got an error:
New-AzResourceGroupDeployment: 8:15:16 PM – The deployment ’01_SqlServer_Standard’ failed with error(s). Showing 2 out of 2 error(s).
Status Message: The specified resource ‘subscriptions/a123bc4d-e5fg-678h-90i1-jkl234m56n78/resourcegroups/contoso-rg/providers/Microsoft.Sql/servers/contoso-sql-server/providers/microsoft.Sql/servers/contoso-sql-server/databases/master/providers/microsoft.insights/diagnosticSettings/LogAnalytics’ was not found. (Code:ResourceNotFound)
Status Message: At least one resource deployment operation failed. Please list deployment operations for details. Please see https://aka.ms/DeployOperations for usage details. (Code: DeploymentFailed)
– {
“error”: {
“code”: “ResourceNotFound”,
“message”: “The specified resource ‘subscriptions/a123bc4d-e5fg-678h-90i1-jkl234m56n78/resourcegroups/contoso-rg/providers/Microsoft.Sql/servers/contoso-sql-server/providers/microsoft.Sql/servers/contoso-sql-server/databases/master/providers/microsoft.insights/diagnosticSettings/LogAnalytics’ was not found.”
}
} (Code:NotFound)
Do I need and depends On or something similar?
Thank you!
Hi John thanks for you post in advance.
I’m trying to enable this SQL Audit Logs using an existing Log Analytics Workspace. But I got an error:
New-AzResourceGroupDeployment: 8:15:16 PM – The deployment ’01_SqlServer_Standard’ failed with error(s). Showing 2 out of 2 error(s).
Status Message: The specified resource ‘subscriptions/a123bc4d-e5fg-678h-90i1-jkl234m56n78/resourcegroups/contoso-rg/providers/Microsoft.Sql/servers/contoso-sql-server/providers/microsoft.Sql/servers/control-sql-server/databases/master/providers/microsoft.insights/diagnosticSettings/LogAnalytics’ was not found. (Code:ResourceNotFound)
Status Message: At least one resource deployment operation failed. Please list deployment operations for details. Please see https://aka.ms/DeployOperations for usage details. (Code: DeploymentFailed)
– {
“error”: {
“code”: “ResourceNotFound”,
“message”: “The specified resource ‘subscriptions/a123bc4d-e5fg-678h-90i1-jkl234m56n78/resourcegroups/contoso-rg/providers/Microsoft.Sql/servers/contoso-sql-server/providers/microsoft.Sql/servers/control-sql-server/databases/master/providers/microsoft.insights/diagnosticSettings/LogAnalytics’ was not found.”
}
Do I need to set up a depends on or something similar?
It worked when I try to enable it after the sql server is deployed. But creating and enable it in the same module didn’t work.
Hey Michel,
I appreciate the feedback! Looking at your error message I’d have to see the updated Bicep. That error messages, to me, doesn’t indicate an issue with finding the Log Analytics Workspace.
I think it’s a resource path issues. In the code above the module is looking for:
/master/microsoft.insights/LogAnalytics’
vs
master/providers/microsoft.insights/diagnosticSettings/LogAnalytics
I could be incorrect; however, if you want to provide more of your Bicep code, happy to take a look.
Thanks for your quick response. This is the module that I’m working:
param sqlServerName string
param sqlServerlocation string
param sqlServerAdminLogin string
@secure()
param sqlServerAdminPassword string
param sqlServerAdminGroupName string
param sqlServerAdminGroupObjectId string
param sqlServerAdminGroupTenantId string = subscription().tenantId
param storageAccountName string
param logAnalyticsWorkspaceName string
param subscriptionId string
var sqlServerVars = json(loadTextContent(‘main.variables.json’))
resource sqlServer ‘Microsoft.Sql/servers@2022-02-01-preview’ = {
name: sqlServerName
location: sqlServerlocation
tags: sqlServerVars.tagsValues
identity: {
type: sqlServerVars.sqlServerIdentityType
}
properties: {
administratorLogin: sqlServerAdminLogin
administratorLoginPassword: sqlServerAdminPassword
administrators: {
administratorType: sqlServerVars.sqlServerAdminType
azureADOnlyAuthentication: false
login: sqlServerAdminGroupName
principalType: sqlServerVars.sqlServerAdminPrincipalType
sid: sqlServerAdminGroupObjectId
tenantId: sqlServerAdminGroupTenantId
}
minimalTlsVersion: sqlServerVars.minimalTlsVersion
publicNetworkAccess: sqlServerVars.publicNetworkAccess
}
}
resource storageAccount ‘Microsoft.Storage/storageAccounts@2021-09-01’ existing = {
name: storageAccountName
}
resource sqlStorageAccountRBAC ‘Microsoft.Authorization/roleAssignments@2022-04-01’ = {
name: guid(sqlServer.name, sqlServerVars.blobContributorRoleId, resourceGroup().id)
scope: storageAccount
properties:{
roleDefinitionId: resourceId(‘Microsoft.Authorization/roleDefinitions’, sqlServerVars.blobContributorRoleId)
principalId: sqlServer.identity.principalId
principalType: ‘ServicePrincipal’
}
dependsOn: [
storageAccount
]
}
resource logAnalyticsWorkspace ‘Microsoft.OperationalInsights/workspaces@2021-12-01-preview’ existing = {
name: logAnalyticsWorkspaceName
}
resource sqlDiagnosticSettings ‘microsoft.Sql/servers/databases/providers/diagnosticSettings@2021-05-01-preview’ = {
name: ‘${sqlServer.name}/master/microsoft.insights/LogAnalytics’
scope: sqlServer
properties: {
workspaceId: logAnalyticsWorkspace.id
}
}
resource sqlAuditingSettings ‘Microsoft.Sql/servers/auditingSettings@2022-02-01-preview’ = {
name: ‘default’
parent: sqlServer
properties: {
auditActionsAndGroups: sqlServerVars.auditActionsAndGroups
isAzureMonitorTargetEnabled: true
isManagedIdentityInUse: true
retentionDays: sqlServerVars.retentionDays
state: ‘Enabled’
storageEndpoint: storageAccount.properties.primaryEndpoints.blob
storageAccountSubscriptionId: subscriptionId
}
dependsOn: [
sqlDiagnosticSettings
]
}
resource sqlServerConnectionPolicies ‘Microsoft.Sql/servers/connectionPolicies@2022-02-01-preview’ = {
parent: sqlServer
name: ‘default’
properties: {
connectionType: sqlServerVars.sqlServerConnectionType
}
}
output sqlServerIdentity string = sqlServer.identity.principalId
Let me know your thoughts.
Thank you!
Hi John,
This is solved. I’ve to add the master db resource to my module and it worked:
resource masterDb ‘Microsoft.Sql/servers/databases@2021-11-01-preview’ = {
parent: sqlServer
location: sqlServerlocation
name: ‘master’
properties: {}
}
resource sqlDiagnosticSettings ‘Microsoft.Insights/diagnosticSettings@2021-05-01-preview’ = {
scope: masterDb
name: sqlServerVars.diagnosticSettingsName
properties: {
workspaceId: logAnalyticsWorkspace.id
logs: [
{
category: ‘SQLSecurityAuditEvents’
enabled: true
retentionPolicy: {
days: 0
enabled: true
}
}
]
}
}
Thanks for quickly jump into this. Excellent blog and posts!
Cheers.
Wanted to say thanks for providing the missing information. I actually ran into this when implementing again and your post helped me. I have placed a working example out in : https://github.com/JFolberth/Azure_IaC_Flavors/tree/main/code/sqlServerAAD/bicep