Automated access block¶
When clients keep sending suspicious requests (the same rules as SuspiciousRequestBlockMiddleware), Django returns 404 as today but also enqueues the event. A shared Azure Function on the stack rate-counts in Table Storage and, above a threshold, adds an App Service IP Deny access restriction. Further requests from that IP are rejected at the App Service edge (403) without reaching Gunicorn.
No Redis is required. Django never reads or writes block state — only fire-and-forget queue messages.
How it works¶
sequenceDiagram
participant Client
participant AppService as AppService_edge
participant Django as Django_Gunicorn
participant Queue as StorageQueue
participant Fn as AccessBlockFunction
participant Table as TableStorage
participant ARM as Azure_ARM
Client->>AppService: suspicious request
AppService->>Django: forward
Django->>Queue: enqueue (fire-and-forget)
Django-->>Client: 404
Queue->>Fn: trigger
Fn->>Table: increment counter
Note over Fn,Table: threshold reached
Fn->>Table: block history + active block
Fn->>ARM: PUT ipSecurityRestrictions Deny
Client->>AppService: next request
AppService-->>Client: 403 Forbidden
Pulumi¶
Shared access-block infrastructure (Function, queue, tables) is provisioned automatically when the first site has access block enabled (default on). Further sites reuse the same resources.
- Storage queue
access-block-requests - Table Storage tables
SuspiciousRequestCounters,AccessBlockActive,AccessBlockHistory - Function App
fn-access-block-{stack}on the existing App Service plan
Stack exports (after the first enabled site): access_block_function_name, access_block_queue_url (secret).
Access block is on by default for each Django website. Pulumi sets ACCESS_BLOCK_ENABLED=true and the queue SAS URL on that Web App. Pass access_block=False to disable enqueueing on a specific site.
The queue SAS uses fixed start/expiry timestamps so ACCESS_BLOCK_QUEUE_URL is stable across deploys (a changing app setting would recycle the site). The signature still changes if the storage account key rotates.
Escalating block duration¶
Managed by the Function (not Django):
| Block # | Duration |
|---|---|
| 1 | 24 hours |
| 2 | 48 hours |
| 3 | 4 days |
| … | doubles each time |
| cap | 1 year |
Repeat history is stored in AccessBlockHistory after a Deny rule expires, so the next block for the same client IP is longer.
Limits and safety¶
- 500 auto-managed Deny rules per Web App (
accessblock-*prefix). Azure’s hard limit is 512; ~12 slots are reserved for manual Portal rules. - When the 500 cap is hit, the oldest active block is removed (FIFO). Block history is kept for escalation.
- The Function only modifies rules whose name starts with
accessblock-. Manual access restrictions are never touched. - Idempotent cleanup: if an
accessblock-*rule was already removed in the Portal, the purge timer and FIFO eviction skip the missing rule and still clear Table Storage state (no crash). - Unmatched traffic stays allowed: each block write sets
ipSecurityRestrictionsDefaultActiontoAllow. Without that, App Service’s implicit deny all (once any restriction exists) would take the whole site offline. This feature is a denylist and is incompatible with allowlist-style access restrictions (default Deny + specific Allow rules). - Shared NAT risk: only suspicious-path and empty-
User-Agenthits count. TuneACCESS_BLOCK_THRESHOLDandACCESS_BLOCK_WINDOW_SECONDSon the Function if needed.
Function app settings¶
| Setting | Default |
|---|---|
ACCESS_BLOCK_THRESHOLD |
15 |
ACCESS_BLOCK_WINDOW_SECONDS |
300 |
ACCESS_BLOCK_BASE_TTL_HOURS |
24 |
ACCESS_BLOCK_MAX_TTL_HOURS |
8760 |
ACCESS_BLOCK_MAX_ACTIVE_RULES |
500 |
Django app settings¶
| Setting | Purpose |
|---|---|
ACCESS_BLOCK_ENABLED |
Per-site master switch (Pulumi sets true by default; false when access_block=False) |
ACCESS_BLOCK_QUEUE_URL |
SAS URL for enqueue (set by Pulumi; stable across deploys) |
ACCESS_BLOCK_RESOURCE_GROUP |
Resource group name for the Web App |
ACCESS_BLOCK_SUBSCRIPTION_ID |
Azure subscription ID |
Middleware: SuspiciousRequestAccessBlockMiddleware (registered by patch_django_settings_for_azure when enabled and queue URL is set).
Manual smoke test¶
- Deploy a site (access block is enabled by default).
- From a test IP, request suspicious paths (e.g.
/.env) until threshold (default 15 in 5 minutes). - In Azure Portal → Web App → Networking → Access restrictions, confirm an
accessblock-*Deny rule. - Further requests from that IP should return 403 at the edge.
- After TTL expiry (or timer purge every 20 minutes), the rule should be removed; history remains for escalation.