Skip to content

WORM Configuration Guide

Manual setup for Write Once Read Many (WORM) immutable storage policies on Azure Blob Storage (StorageV2) for SEC 17a-4 compliance.


WARNING: IRREVERSIBLE ACTION

WORM policies are PERMANENT once locked. This action CANNOT be undone.

  • Locked WORM policies CANNOT be unlocked
  • Data protected by locked WORM CANNOT be deleted until retention period expires
  • Storage accounts with locked WORM containers CANNOT be deleted
  • Test in a non-production storage account first

This is why WORM configuration is excluded from the provision.py automation script. Manual configuration with explicit confirmation prevents accidental production lockdown.


Overview

WORM (Write Once Read Many) policies enable immutable storage for Azure Blob Storage, meeting SEC 17a-4(f) requirements for non-erasable, non-rewritable storage of broker-dealer communications and records.

Cohasset Validation: Microsoft Azure Blob Storage immutable storage has been validated by Cohasset Associates for compliance with: - SEC Rule 17a-4(f) - FINRA Rule 4511(c) - CFTC Rule 1.31(c)-(d)


Prerequisites

Before configuring WORM policy:

  1. Storage account exists: Provisioned by provision.py (StorageV2, hierarchical namespace disabled)
  2. Export containers exist: Azure Diagnostic Settings auto-creates one blob container per enabled log category on first telemetry export (NOT created by provision.py). The templates/diagnostic-settings.json template in this solution enables four categories, producing four containers (see Which containers must be protected below)
  3. Diagnostic Settings configured: Data is flowing to the containers
  4. Verify test environment: Confirm you are NOT in production (first time)

Which containers must be protected

Azure Monitor diagnostic-settings export to a storage account writes blobs under the container naming convention insights-logs-{log category name} (lowercased), one container per enabled log category. Immutability (WORM) policies in Azure Blob Storage are scoped per container, so a policy applied to one container does not protect the others.

The templates/diagnostic-settings.json template enables four Application Insights log categories. After telemetry begins flowing, expect these containers:

Log category Container Holds
AppEvents insights-logs-appevents Primary audit-of-record. Copilot Studio custom events (BotMessageSend, BotMessageReceived, GenerativeAnswers, topic/action events) that the KQL query library and compliance evidence depend on.
AppTraces insights-logs-apptraces Trace/diagnostic messages
AppRequests insights-logs-apprequests Request telemetry
AppExceptions insights-logs-appexceptions Exception telemetry

Required for SEC 17a-4(f) coverage: Apply the WORM time-based retention policy (with allowProtectedAppendWrites enabled) to each container that holds audit-of-record data. At minimum, insights-logs-appevents must be protected, because it — not insights-logs-apptraces — holds the Copilot Studio interaction events used as books-and-records evidence. Protecting only insights-logs-apptraces leaves the primary audit container unprotected. Repeat Steps 2–8 below for each container, and run the verification script once per container.

The portal steps and CLI examples below use insights-logs-apptraces as a worked example. Substitute the container name for each container you protect, starting with insights-logs-appevents.

Container naming convention reference: Azure Monitor — send diagnostic data to Azure Storage.


Step-by-Step Portal Instructions

Step 1: Navigate to Storage Account

  1. Open Azure Portal
  2. Navigate to Storage accounts
  3. Select your telemetry storage account (e.g., sagentobservability)

Step 2: Open Container Access Policy

  1. In the left menu, click Containers
  2. Click on the insights-logs-apptraces container
  3. In the container blade, click Access policy in the left menu

Step 3: Add Time-Based Retention Policy

  1. Under Immutable blob storage, click + Add policy
  2. Select Time-based retention policy
  3. Configure policy parameters:
  4. Policy state: Unlocked (initially)
  5. Retention period: Enter retention in days
  6. Allow protected append writes: ENABLED (REQUIRED for diagnostic export pipelines)

CRITICAL: Azure Monitor diagnostic settings export telemetry by appending to existing blobs. A locked time-based policy without allowProtectedAppendWrites will block these appends and silently halt telemetry export — the storage account remains immutable but no longer receives data, creating an audit gap. Always enable protected append writes for any container that receives Azure Monitor diagnostic export.

Retention Period Reference: | Regulatory Requirement | Minimum Days | Recommended | |------------------------|--------------|-------------| | SEC 17a-4(b)(4) - Communications | 1095 (3 years) | 1095 | | SEC 17a-4(a) - Financial records | 2190 (6 years) | 2555 (7 years) | | FINRA 4511 - Books and records | 2190 (6 years) | 2555 (7 years) |

  1. Click OK to save the policy

Note: The policy is now in Unlocked state. Data is protected by the retention period, but the policy can still be modified or deleted.

Step 4: Review Policy State

After saving, verify the policy shows: - Policy state: Unlocked - Retention period: Your configured days - Immutability scope: Container

Step 5: Test in Unlocked State

Before locking the policy, verify it works as expected:

  1. Upload a test blob:

    echo "test content" > test-worm.txt
    az storage blob upload \
        --account-name <storage-account> \
        --container-name insights-logs-apptraces \
        --name test-worm.txt \
        --file test-worm.txt \
        --auth-mode login
    

  2. Attempt to delete the test blob (should SUCCEED in Unlocked state):

    az storage blob delete \
        --account-name <storage-account> \
        --container-name insights-logs-apptraces \
        --name test-worm.txt \
        --auth-mode login
    

  3. Expected result: Delete succeeds (Unlocked allows deletion)

Step 6: Verify Compliance Requirements Before Locking

Before locking, confirm:

  • Storage account is the correct production account
  • Retention period matches regulatory requirements
  • You understand this action is IRREVERSIBLE
  • Compliance/Legal team has approved the configuration
  • You have documented this configuration decision

Step 7: Lock the Policy (IRREVERSIBLE)

THIS ACTION CANNOT BE UNDONE

  1. Return to Container > Access policy
  2. Click on your time-based retention policy
  3. Click Lock policy
  4. Read the confirmation warning carefully
  5. Type the confirmation text if required
  6. Click OK to lock

The policy state will change to Locked.

Step 8: Test Locked Policy

Verify immutability is enforced:

  1. Upload a new test blob:

    echo "locked test" > test-locked.txt
    az storage blob upload \
        --account-name <storage-account> \
        --container-name insights-logs-apptraces \
        --name test-locked.txt \
        --file test-locked.txt \
        --auth-mode login
    

  2. Attempt to delete the test blob (should FAIL):

    az storage blob delete \
        --account-name <storage-account> \
        --container-name insights-logs-apptraces \
        --name test-locked.txt \
        --auth-mode login
    

  3. Expected result: Delete fails with error:

    This operation is not permitted as the blob is immutable due to a policy.
    


Verification

Run the verification script to confirm WORM compliance status. The script verifies one container per run, so run it once for each protected container, starting with the primary audit-of-record container insights-logs-appevents:

# Primary audit-of-record container (Copilot Studio interaction events)
python scripts/verify_worm.py --storage-account <storage-account> --container-name insights-logs-appevents

# Repeat for each remaining protected container
python scripts/verify_worm.py --storage-account <storage-account> --container-name insights-logs-apptraces
python scripts/verify_worm.py --storage-account <storage-account> --container-name insights-logs-apprequests
python scripts/verify_worm.py --storage-account <storage-account> --container-name insights-logs-appexceptions

A run exits non-zero if the named container's policy is missing, unlocked, has insufficient retention, or has protected append writes disabled. Treat the WORM posture as adequate only when every audit-of-record container passes.

The script performs read-only verification without modifying any policies.


Compliance States

State SEC 17a-4 Compliant Description
No policy No No immutability protection; data can be deleted
Unlocked No Policy exists but can be modified or deleted
Locked, retention < 6y No Policy is permanent but retention does not meet SEC 17a-4(a) (recommended ≥ 2555 days)
Locked, append writes disabled Partial Existing data is immutable but new diagnostic-export appends will be blocked, creating an audit gap
Locked, retention ≥ 6y, append writes enabled Yes Data is immutable for retention period and Azure Monitor export continues to land

The shipped verify_worm.py checks all four conditions (state, retention, protected append writes, container present) and exits with code 2 if any of them fails.


Policy Management After Locking

Extending Retention Period

You CAN extend the retention period on a locked policy:

  1. Navigate to Container > Access policy
  2. Click on the locked policy
  3. Increase the retention period value
  4. Click OK

Note: You can only EXTEND retention, never reduce it.

In addition to time-based retention, you can apply legal hold for litigation or investigation:

  1. Navigate to Container > Access policy
  2. Under Legal hold, click + Add
  3. Add a legal hold tag (e.g., "Investigation-2026-001")
  4. Click OK

Legal holds: - Can be added/removed without affecting WORM policy - Prevent deletion regardless of retention period - Require explicit removal when legal matter concludes


Troubleshooting

"Cannot delete storage account"

Cause: WORM-locked containers prevent storage account deletion.

Resolution: - Wait for all retention periods to expire - OR contact Azure Support for exceptional circumstances - Prevention: Use separate storage accounts for test and production

"Policy shows Unlocked"

Cause: Policy was created but not explicitly locked.

Resolution: Follow Step 7 to lock the policy. Unlocked policies do NOT meet SEC 17a-4 requirements.

"Delete operation succeeded on locked container"

Possible Causes: - Blob was uploaded BEFORE policy was locked - Blob retention period has expired - Legal hold was removed

Verification: Check blob immutability status in portal or via Azure CLI.

"Cannot modify retention period"

Cause: Attempting to reduce retention on locked policy.

Resolution: Retention can only be extended on locked policies. Plan retention periods carefully before locking.


Cost Implications

Locked WORM storage has cost implications:

Consideration Impact
Storage duration Cannot delete data before retention expires; costs accumulate
Storage account deletion Cannot delete account; must wait for all containers to expire
Testing mistakes Accidentally locked test data remains until expiration

Mitigation: - Use short retention periods for testing (e.g., 1 day) - Maintain separate test and production storage accounts - Document all WORM configurations with expiration dates



WORM Configuration Guide version: 1.2.0 Last updated: February 2026