This check flags Azure storage accounts holding your activity log archive that allow public network access. Activity logs record who did what across your subscription, so exposing them hands attackers a free recon map. Lock the storage account down to private networking or trusted services with a single network ruleset change.
Azure Monitor activity logs are the audit trail for your control plane. Every resource created, every role assigned, every NSG rule changed shows up there. When you set up a log profile to archive that history, it lands in a storage account. If that storage account accepts traffic from the public internet, you have effectively published your subscription's operational history to anyone who finds the right endpoint and credential.
This Lensix check, monitor_storagepublic in the monitor_checks module, looks at the storage account backing each activity log profile and fails when its network access is set to allow public access.
What this check detects
An Azure Monitor log profile (or its newer diagnostic settings equivalent) can route activity log data to a storage account for long term retention. The check inspects the networkRuleSet of that storage account. Specifically, it fails when:
- The
publicNetworkAccessproperty is set toEnabled, and - The default network action is
Allowrather thanDeny, meaning any source IP can reach the account subject only to authentication.
Note: "Publicly accessible" here does not mean anonymous reads are allowed. Azure storage still requires a valid key, SAS token, or Azure AD credential. The risk is that the account is reachable over the public internet, which widens the attack surface and removes a strong network boundary that should protect sensitive audit data.
The distinction matters because a storage account with the default firewall set to Allow is one leaked SAS token or compromised key away from full exposure. A privately networked account requires the attacker to first be inside your network.
Why it matters
Activity logs are a high value target for two opposite reasons. Attackers want to read them, and attackers want to delete them.
Reconnaissance for attackers
The activity log is a detailed map of your environment. It reveals resource names, resource group structure, service principals, role assignments, and the timing of administrative actions. An attacker who reads this archive learns which accounts have privileged access, which automation runs on a schedule, and where your sensitive workloads live. That turns a blind probe into a targeted operation.
Covering tracks
If an attacker gains write access to the archive storage account, they can tamper with or delete the historical record of their own activity. That undermines incident response and forensic investigations exactly when you need the data most.
Warning: Many compliance frameworks, including CIS Azure Foundations and ISO 27001, expect audit log storage to be protected with restricted network access and immutability. A publicly reachable activity log store is a common audit finding that can hold up certification.
Real world chain
The typical failure chain looks like this: a developer generates a long lived SAS token to debug something, the token ends up in a config file pushed to a repo, and because the storage account accepts public traffic, anyone with the token can pull the entire activity log archive from anywhere. Network restrictions would have broken that chain even with the leaked token.
How to fix it
The fix is to restrict the storage account network access. You can do this with the Azure CLI, the portal, or infrastructure as code.
Step 1: Identify the storage account
Find the storage account tied to your activity log profile.
az monitor log-profiles list \
--query "[].{name:name, storageAccount:storageAccountId}" -o table
If you use diagnostic settings at the subscription level instead of the legacy log profile, list those:
az monitor diagnostic-settings subscription list \
--query "value[].{name:name, storageAccount:storageAccountId}" -o table
Step 2: Set the default network action to Deny
This is the core remediation. It switches the storage firewall from allow-by-default to deny-by-default.
Danger: Setting the default action to Deny immediately blocks any client not on your allow list. If your log archiving pipeline, SIEM connector, or backup job reaches the account over public IPs, it will lose access. Add those sources to the allow list before applying the deny rule, or you risk breaking log ingestion.
# Replace with your values
RG="my-resource-group"
SA="myactivitylogsa"
az storage account update \
--resource-group "$RG" \
--name "$SA" \
--default-action Deny \
--public-network-access Disabled
Setting --public-network-access Disabled turns off the public endpoint entirely. If you still need limited public access from known IPs, leave it Enabled but keep the default action as Deny and add explicit rules.
Step 3: Allow trusted Azure services
Azure Monitor needs a path to write logs. Allow trusted Microsoft services to bypass the firewall so log delivery keeps working.
az storage account update \
--resource-group "$RG" \
--name "$SA" \
--bypass AzureServices
Step 4: Add specific allow rules if needed
If a SIEM or analyst workstation needs access over public IPs, add narrow rules rather than reopening the account.
# Allow a specific IP or CIDR range
az storage account network-rule add \
--resource-group "$RG" \
--account-name "$SA" \
--ip-address 203.0.113.25
# Allow a specific virtual network subnet
az storage account network-rule add \
--resource-group "$RG" \
--account-name "$SA" \
--vnet-name "my-vnet" \
--subnet "monitoring-subnet"
Preferred approach: private endpoints
The strongest configuration is a private endpoint, which gives the storage account a private IP inside your VNet and removes the public endpoint from the equation.
az network private-endpoint create \
--resource-group "$RG" \
--name "pe-activitylog-sa" \
--vnet-name "my-vnet" \
--subnet "private-endpoints" \
--private-connection-resource-id $(az storage account show -g "$RG" -n "$SA" --query id -o tsv) \
--group-id blob \
--connection-name "activitylog-blob-conn"
Tip: Pair the private endpoint with a private DNS zone (privatelink.blob.core.windows.net) so that name resolution inside your VNet points clients at the private IP automatically. Without the DNS zone, clients will keep resolving the public name.
Fixing it with infrastructure as code
If you manage storage accounts with Terraform, set the network rules explicitly so the secure state is the declared state.
resource "azurerm_storage_account" "activity_logs" {
name = "myactivitylogsa"
resource_group_name = azurerm_resource_group.monitoring.name
location = azurerm_resource_group.monitoring.location
account_tier = "Standard"
account_replication_type = "GRS"
public_network_access_enabled = false
network_rules {
default_action = "Deny"
bypass = ["AzureServices"]
ip_rules = ["203.0.113.25"]
}
}
With Bicep:
resource activityLogSa 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'myactivitylogsa'
location: location
sku: { name: 'Standard_GRS' }
kind: 'StorageV2'
properties: {
publicNetworkAccess: 'Disabled'
networkAcls: {
defaultAction: 'Deny'
bypass: 'AzureServices'
}
}
}
How to prevent it from happening again
Manual fixes drift. Bake the requirement into policy and pipelines so a public log store cannot be created in the first place.
Azure Policy
Assign the built in policy that denies storage accounts with unrestricted network access. The relevant definition is "Storage accounts should restrict network access." Use the Deny effect to block noncompliant creates rather than just auditing them.
az policy assignment create \
--name "deny-public-storage" \
--display-name "Deny storage accounts with public network access" \
--policy "34c877ad-507e-4c82-993e-3452a6e0ad3c" \
--scope "/subscriptions/" \
--params '{"effect":{"value":"Deny"}}'
CI/CD gates
Catch misconfigurations before they deploy. Run a scanner like Checkov or tfsec against your Terraform in the pipeline and fail the build on the relevant rule.
# In your pipeline step
checkov -d ./infra --check CKV_AZURE_35 # ensure storage default action is Deny
Tip: Combine a deny policy with continuous monitoring from Lensix. Policy stops bad creates, and the monitor_storagepublic check catches accounts that were created before the policy existed or modified out of band by someone with sufficient rights.
Best practices
- Isolate audit storage. Keep activity log archives in a dedicated storage account separate from application data, so its access rules stay strict without affecting workloads.
- Use private endpoints over IP allow lists. IP rules drift as networks change. A private endpoint is a durable boundary.
- Enable immutable blob storage. Apply a time based retention policy so logs cannot be deleted or altered, even by someone with write access. This protects against the track-covering scenario.
- Prefer Azure AD authentication. Disable shared key access where possible and rely on role based access with managed identities, which removes long lived keys and SAS tokens from the equation entirely.
- Turn on diagnostic logging for the storage account itself. You want to know who reads the audit archive, not just what is in it.
- Centralize in a Log Analytics workspace too. Storage archiving is for long retention, but query and alerting are far easier against a workspace. Route activity logs to both.
The activity log is the record you reach for after something goes wrong. Treat its storage with the same care you would treat a vault, restrict the network path, lock down credentials, and make the data immutable. A few minutes of network rule configuration buys you a much stronger position when an incident hits.

