HASTE — Secure Configuration Guidance#
Field |
Value |
|---|---|
Repository |
microsoft/haste |
Audience |
Customers / consumers / operators deploying HASTE |
Last updated |
2026-10-02 |
1. Overview#
HASTE (High-speed Assessment and Satellite Tracking for Emergencies) is an AI-driven framework for rapid disaster assessment using satellite imagery. The repository is published as a library + reference deployment: Microsoft operates the public reference deployment, while external consumers integrate the hastegeo Python library and may optionally redeploy the full stack (UI + Function Apps + Azure Batch) into their own subscription.
This guide tells you how to configure HASTE securely in your environment. It covers:
Authentication, authorization, and role assignment
Secrets and key management
Network and transport configuration (CORS, CSP, TLS)
Container and infrastructure hardening
Logging, monitoring, and incident response
Known security limitations and backward-compatibility caveats
How to report a vulnerability
It is not a substitute for your own threat model. Treat HASTE as one component within a larger Azure workload and apply the Microsoft Cloud Security Benchmark (MCSB) controls that apply to your overall workload.
2. Deployment models and their security posture#
HASTE supports three deployment modes. Each has a distinct threat model.
Mode |
Intended use |
Security posture |
|---|---|---|
Local development (Docker Compose) |
Single-developer evaluation only |
Disabled auth, wildcard CORS, hardcoded dev keys (Azurite). Never expose to a network beyond |
Self-hosted Azure deployment |
Customer running HASTE in their own subscription |
Production-grade if configured per this guide. Customer is responsible for SWA auth, Key Vault, RBAC, and infra hardening. |
Library-only integration |
Consuming the |
You inherit only the library’s process-level threat model; deployment-time controls are yours. |
Required disclosure: The local-development Docker Compose stack (docker/docker-compose.yml) mounts /var/run/docker.sock into containers to enable a local execution mode. This grants root-equivalent host access and is acceptable only on developer workstations. The file header documents this; this guide repeats it because it is the single most consequential local-vs-production gap.
4. Secrets and key management#
4.1 Use managed identity wherever possible#
HASTE’s Function Apps support system-assigned managed identity for:
Azure Maps — Use
VITE_AZURE_MAPS_CLIENT_ID(managed-identity client ID) instead of a subscription key. The UI fetches an AAD token from a dedicatedGetAzureMapsTokenendpoint exposed by the Function App. Do not bake subscription keys into the client bundle.Azure Blob Storage — Configure
BLOB_ACCOUNT_URLand grant the Function App’s identity theStorage Blob Data ContributorRBAC role on the storage account. Prefer this overBLOB_CONNECTION_STRINGin production.Azure Cosmos DB — Use AAD authentication via
DefaultAzureCredential; assignCosmos DB Built-in Data Contributor.
4.3 Function master keys#
The CI deploy script (.github/scripts/deploy_apps.sh) writes the Function App master key to .env during deployment. Treat this file as sensitive and remove it from any machine that is not your local workstation. (The azd workflow instead injects the Function host key into the APIM backends via the postdeploy hook and does not persist it to disk.) Rotate this key periodically in production via:
az functionapp keys set --resource-group <rg> --name <function-app> --key-type masterKey --key-name default
4.4 Do not pass secrets as positional CLI arguments#
If you author your own deploy scripts based on the HASTE templates, use the file form az functionapp config appsettings set --settings @<file>.json rather than --settings KEY=VALUE on the command line. Positional secrets leak via process tables and CI debug logs.
4.5 Disable basic-auth publishing after deployment#
The deploy scripts set properties.allow=true for SCM/FTP publishing during deployment. Disable basic-auth publishing on the Function App after deployment:
az resource update --resource-group <rg> --name scm --namespace Microsoft.Web \
--resource-type basicPublishingCredentialsPolicies \
--parent sites/<function-app> --set properties.allow=false
az resource update --resource-group <rg> --name ftp --namespace Microsoft.Web \
--resource-type basicPublishingCredentialsPolicies \
--parent sites/<function-app> --set properties.allow=false
5. Network and transport security#
5.1 CORS#
Local development: Use explicit origins (
http://localhost:5173,http://localhost:4280). Seelocal.settings.example.jsonc.Production: Set CORS to your UI’s domain only. Never use
*in production.
az functionapp cors add --resource-group <rg> --name <function-app> \
--allowed-origins https://your-haste-ui.azurewebsites.net
5.2 HTTP security headers#
HASTE ships a staticwebapp.config.json (under ui/public/) with the following headers. This is the policy the reference UI runs under — verified against the live Azure Maps and labeling pages with a clean console:
{
"globalHeaders": {
"Content-Security-Policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; script-src 'self' https://atlas.microsoft.com; style-src 'self' 'unsafe-inline' https://atlas.microsoft.com; font-src 'self' data: https://atlas.microsoft.com https://c.s-microsoft.com https://*.cdn.office.net; img-src 'self' data: blob: https://atlas.microsoft.com https://*.atlas.microsoft.com https://*.tile.openstreetmap.org https://vantor-opendata.s3.amazonaws.com https://data.source.coop; connect-src 'self' https://atlas.microsoft.com https://*.atlas.microsoft.com https://*.tile.openstreetmap.org https://vantor-opendata.s3.amazonaws.com https://data.source.coop; worker-src 'self' blob:; child-src blob:",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "strict-origin-when-cross-origin"
}
}
Notes on why each non-'self' source is present — tighten if your build removes the dependency:
atlas.microsoft.com/*.atlas.microsoft.com(script, style, font, img, connect) — the Azure Maps Web SDK is loaded from the CDN inindex.htmland fetches map styles, vector tiles, and glyph fonts at runtime.'unsafe-inline'instyle-srcis required by Fluent UI’s runtime style injection.c.s-microsoft.comand*.cdn.office.net(font) — Fluent UI loads Segoe UI and its icon/language fonts from these Microsoft CDNs at runtime. To drop them, self-host the Fluent fonts and register them locally.blob:(img, worker, child) — the Azure Maps control renders tiles and spawns web workers from blob URLs.*.tile.openstreetmap.org(img, connect) — only needed if you configure an OSM basemap; remove otherwise.vantor-opendata.s3.amazonaws.comanddata.source.coop(img, connect) — the Open Data Catalog explorer fetches the Vantor and Planet STAC catalogs (and scene thumbnails) directly from the browser. Remove if the Open Data Catalog is not enabled, or move discovery behind a same-origin backend proxy so only'self'is needed.
Per-deployment adjustment: if your VITE_API_URL points at a separate origin (a direct *.azurewebsites.net Function App rather than the same-origin SWA /api linked-backend route), add that origin to connect-src. The default policy assumes the recommended same-origin linked-backend topology (§3.1).
5.3 TLS#
Static Web Apps and Function Apps enforce HTTPS by default. Do not disable.
For custom domains, use Azure-managed certificates and verify auto-renewal.
5.4 CSRF#
The UI’s API client does not send explicit CSRF tokens; HASTE relies on the SWA Easy Auth session cookie having SameSite=Lax and on Origin/Referer enforcement at the SWA edge. If you front HASTE with a different reverse proxy or auth layer, validate that equivalent CSRF protection is in place — see §8.2.
5.5 Function App inbound access#
The browser reaches the TiTiler tile server only through the Static Web App and APIM (/api/titiler/*); no HASTE backend calls it directly. The reference deployment therefore applies this baseline to the TiTiler Function App:
Site |
Default action |
Allowed sources |
|---|---|---|
Main ( |
Deny |
The APIM VNet integration subnet only (needs the |
SCM / Kudu ( |
Deny |
None. The SCM site does not inherit the main-site rules. |
The app also requires HTTPS, TLS 1.2 or later on both sites, and has FTP/FTPS disabled.
The baseline is enforced in three places so redeployment cannot widen it:
Bicep —
infra/modules/functionApp.bicep(allowedInboundSubnetIds,restrictMainSite,restrictScmSite) with the TiTiler wiring ininfra/modules/functions.bicep.deploy_apps.sh— before every TiTiler deploy it re-applies the rule and default actions, then verifies the result. An allow rule it does not expect stops the deployment instead of being deleted, so an operator decides what the rule was for.Publishing — a temporary
haste-ci-*/32rule lets the deploying machine reach the locked sites while it publishes (see Network access during deployment).
Operational notes:
Direct requests to
<app>.azurewebsites.netand<app>.scm.azurewebsites.netreturn403 Forbidden. Test TiTiler through the UI or the APIM gateway (/api/titiler/healthz).The Kudu console and log stream are unreachable from the internet. Use Application Insights or Log Analytics, or add a short-lived
/32rule and remove it afterwards.The API and queues Function Apps are not covered by this baseline yet. For them, §3.1 and §8.1 still apply.
# Inspect the effective rules for an app
az functionapp config access-restriction show --resource-group <rg> --name <function-app>
5.6 TiTiler input, APIM surface and identity#
TiTiler opens whatever dataset URL the caller passes, so it is treated as untrusted input on a service with as little reach as possible.
Dataset URLs (api/titilerfuncapi/app/dataset_url.py):
Only plain
https://URLs on an exact host list are accepted: the two public open-data hosts the catalog explorer previews, plus the hosts inTITILER_ALLOWED_HOSTS. Bicep anddeploy_apps.shset it to the environment’s blob host. Wildcard domains are not supported.Everything else gets a generic
400before GDAL is involved. That covers other schemes, local and GDAL virtual-file-system paths, inline documents, archive paths, traversal, credentials, explicit ports, IP literals, control and non-ASCII characters, and fragments. The log records the reason, never the URL, since it can carry a SAS token.The first use of a URL sends one
HEADwith redirects disabled. Redirects, non-2xx responses, and hosts resolving to loopback, link-local, metadata or (for public hosts) private addresses are refused. Passing URLs are cached for 10 minutes.Datasets open with the
GTiffdriver only. GDAL settings that disable plugin drivers, sibling-file probing, PAM, and Python or raw-file VRT bands are forced at startup and per request.Only
/cog/tiles/WebMercatorQuad/{z}/{x}/{y}(with its format and scale variants) and/healthzare served.
APIM (infra/modules/apimApis.bicep): get-tiles (GET) is TiTiler’s only operation. The API policy refuses requests with a body (413) and URLs over titilerMaxUrlLength (414), and rate limits tile requests per user (titilerCallsPerClientPerMinute, default 1,200) and in total (titilerCallsPerMinute, default 20,000).
The per-user key is the user’s address without its port. The Static Web App appends the user as ip:port to any X-Forwarded-For the browser sent, and APIM’s front end appends the Static Web App the same way. So on the Static Web App’s product, which accepts only requests the Static Web App signed, the policy uses the second-to-last entry and ignores anything before it. Other callers are keyed on the address APIM sees.
APIM v2 tiers count these limits approximately. An over-limit key is slowed rather than cut off, and throttling can start some way past the configured number, so size them as throttling, not as a hard cap.
Identity and storage (infra/modules/functionApp.bicep, infra/modules/storage.bicep):
TiTiler runs from its own host storage account. That account accepts identity auth only, has public blob access off, and is reachable only from the functions subnet.
TiTiler does not get the shared user-assigned identity, the account-key
/datamount, the Blob Delegator role, or any role on the shared storage account.Bicep never deletes what it stops declaring, so the azd
postprovisionhookdeploy/reconcile-titiler-baseline.ps1deletes TiTiler role assignments outside its own account and any TiTiler APIM operation other thanget-tiles. Provisioning fails if either remains.
Verifying an environment:
pwsh deploy/verify-titiler-baseline.ps1 -EnvName <env>
The script prints PASS or FAIL per check and exits non-zero on any failure. Add -ReportPath <file> for a JSON report. Add -SwaCookie <value> to also test the per-user rate limit through the Static Web App. That test sends up to 3,000 requests and can throttle the signed-in user for a minute, so run it on dev environments only. Both options are described in the script header.
6. Containers and infrastructure#
6.1 Pin Docker base images to digests#
For reproducible and supply-chain-safe builds, pin Docker base images to immutable digests rather than floating tags:
FROM mcr.microsoft.com/azure-functions/python:4-python3.11@sha256:<digest>
Use Dependabot for Docker to keep digests current.
6.2 Run containers as a non-root user#
For production deployments to Azure Batch, AKS, or Container Apps, add a non-root USER directive to each Dockerfile after install steps:
RUN useradd -m -u 10001 hasteuser
USER hasteuser
Verify file permissions on mounted volumes match the non-root UID.
6.3 Use ACR scanning and signed images#
Enable Microsoft Defender for Cloud — Container scanning on your Azure Container Registry.
Tag images with the commit SHA (not
:latest) when pushing for production use. Thebuild_and_push_images.shscript accepts-t <tag>; pass a SHA.Consider content trust / image signing for high-assurance deployments.
6.4 Docker Compose hardening#
The shipped docker/docker-compose.yml is local-development only. If you adapt it for any networked environment, add:
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true # where the workload allows it
And bind ports to 127.0.0.1 explicitly rather than 0.0.0.0.
7. Logging, monitoring, and incident response#
7.1 Enable Application Insights#
az monitor app-insights component create --app haste-insights --location <region> --resource-group <rg>
az functionapp config appsettings set --name <function-app> --resource-group <rg> \
--settings APPLICATIONINSIGHTS_CONNECTION_STRING="<conn-string>"
Recommended log retention: 90 days minimum for security events; longer if regulatory requirements apply.
7.2 What HASTE logs (and what it does not)#
Logs: HTTP method + route, principal
userId, job state transitions, errors with stack traces.Does not log: Request bodies, imagery contents, or values of admin settings — by design. If you require auditing of admin-config changes, layer Azure Activity Log or a custom audit trail on top.
7.3 Alerts to configure#
At minimum, alert on:
5xx error rate > 1% for 5 minutes (API availability)
Repeated 401/403 from a single principal (credential stuffing / role probing)
Spikes in user-management calls (anomalous admin activity)
Function App restart loops (potential exploit / crash)
7.4 Incident response — reporting vulnerabilities#
If you discover a vulnerability in HASTE, do not file a public GitHub issue. Report it via:
Microsoft Security Response Center: https://aka.ms/SECURITY.md and https://msrc.microsoft.com
Encrypted email to
secure@microsoft.com(PGP key on the MSRC site)
See SECURITY.md in the repository.
8. Known security limitations and backward-compatibility notes#
This section documents current limitations that customers must understand to configure HASTE securely. Each entry describes the limitation and the operational mitigation you should apply.
8.1 The API trusts the SWA-injected principal header without independent verification#
HASTE’s API trusts the x-ms-client-principal header from Azure Static Web Apps without performing independent signature verification. This is safe only when the Function App is unreachable except through SWA’s linked-backend route. If you deploy the Function App with a publicly reachable endpoint, you must either:
Front it with an API gateway that strips and re-injects identity, or
Add network-level access restrictions so the Function App accepts traffic only from SWA’s outbound IP ranges, or
Use VNet integration with a private endpoint.
See §3.1 for the recommended SWA linked-backend topology.
8.3 CI runners — applies if you fork the repo#
CI runs on GitHub Actions with GitHub-hosted runners (CodeQL and Gitleaks secret scanning); the legacy azure-pipelines.yml, which used a self-hosted pool, has been removed. If you fork HASTE and add self-hosted runners, note that fork pull-request builds can execute arbitrary code on a self-hosted runner. Keep untrusted-fork PRs on GitHub-hosted runners, or restrict workflow triggers to internal branches before exposing the fork.
8.4 Path traversal hardening in chunked uploads#
The chunked file-upload endpoint accepts a file_id parameter and does not yet enforce a strict character allow-list against path-traversal sequences. The endpoint requires authentication and the parameter is constrained by the UI, but operators should not expose the chunked-upload route to untrusted authenticated clients. A character-class allow-list and realpath prefix check are recommended additions if you extend the API.
8.5 Imagery URL allowlist and outbound egress controls#
HASTE enforces an application-layer allowlist on user-supplied imagery URLs. The PutLayer API rejects (HTTP 400) any submission whose preEventImageryUrls or postEventImageryUrls contains a host outside *.blob.core.windows.net (Azure Blob Storage) or *.amazonaws.com (AWS S3, including s3.amazonaws.com and regional subdomains). The UI mirrors this check client-side so users get inline feedback when adding a URL rather than after submitting a layer. The imagery downloader (ImageryDownloader._validate_url) applies the same allowlist as defense-in-depth at fetch time.
This closes the application-layer SSRF risk that previously existed in the imagery-download path: the Function App and Batch workers cannot be coerced into fetching arbitrary user-controlled URLs (which could otherwise reach the Instance Metadata Service at 169.254.169.254, internal VNet addresses, localhost-bound services on the host, or attacker-controlled HTTP endpoints from the server’s network position).
Defense-in-depth recommendation: even with the application-layer allowlist, restrict outbound egress from the Function App and Batch node pool (NSG, Azure Firewall, or Function App outbound IP restrictions) to the specific imagery providers you actually use. This protects against:
Future code paths that bypass the allowlist
Compromise of an allowlisted hostname (e.g., a hostile Azure Blob container in another tenant)
DNS rebinding — the application-layer check validates the hostname string, but the actual TCP connection performs a separate DNS resolution at fetch time. Pinning egress at the network layer to known imagery-provider IP ranges removes that window.
If your imagery sources are a small static set (customer-owned storage accounts, specific public S3 buckets), use that exact set for the egress allowlist as well rather than the broad *.blob.core.windows.net / *.amazonaws.com patterns.
8.6 Open redirect on logout#
The UI’s logout flow validates the redirectPath parameter via sanitizeRedirectPath() (ui/src/util/validation.js) before building the post_logout_redirect_uri: the value is collapsed to a same-origin relative path, so absolute URLs, protocol-relative (//host), backslash-normalized (/\host), and any scheme-bearing input fall back to /. This closes the previously-documented open-redirect; no operator action is required.
8.7 Backward-compatibility notes for upgrades#
User-management endpoint scope (post-v1.4.1): The
PutUserendpoint was originally gated entirely on theadministratorsrole. A subsequent release narrowed the gate to allow self-service updates only when the caller’s email matches the target’s email, and rejects role-change attempts from non-admins. Customers upgrading from v1.4.0 or earlier should re-test any external integrations that callPutUseron behalf of non-admin users.VITE_AZURE_MAPS_KEYremoved (post-v1.4.1): The UI no longer readsVITE_AZURE_MAPS_KEY. SetVITE_AZURE_MAPS_CLIENT_IDto your Function App’s managed-identity client ID. If you ever setVITE_AZURE_MAPS_KEY, rotate the corresponding Azure Maps subscription key, because it was previously baked into the client bundle and may have been distributed.azuriteremoved frompackage.json: Developers must now install Azurite globally (npm install -g azurite). No production impact.Imagery URL allowlist enforced (post-v1.4.1):
PutLayernow rejects requests whosepreEventImageryUrlsorpostEventImageryUrlscontain a host outside*.blob.core.windows.netor*.amazonaws.com. The pre-existing batch-download code silently skipped such URLs, so this change makes the rejection visible at submission time rather than producing failed jobs with empty outputs. Customers with API integrations that submit imagery from other sources must move that imagery into an allowlisted source before upgrading, or the affected layer-creation calls will fail.
8.8 GDAL driver allowlist and ingestion size/type limits#
The shared-runtime build pins GDAL 3.13.3 and builds the GDAL-linked Python bindings against the same native libraries. Existing deployments continue using their currently deployed packages until each consumer receives the verified runtime and passes its startup check; the legacy runtime exception remains open until rollout is complete (see known-vulnerabilities.md Root Cause C and ADR-0004).
Shared runtime and driver allowlist. The immutable runtime bundle contains one
libgdalandlibproj, with GDAL3.13.3bindings forosgeo, Rasterio, Fiona, Pyogrio, and PyProj. At process startuphastegeo.core.utils.gdal_security.harden_gdal()verifies those package versions and restricts GDAL/OGR to the drivers HASTE uses (rasterGTiff, COG, VRT, JPEG, PNG, MEM; vectorGPKG, GeoJSON, Memory) by deregistering every other driver. The check fails closed whenHASTE_GDAL_RUNTIME_REQUIRED=trueand the runtime is missing or inconsistent.VRT and plugin configuration. Bicep,
deploy_apps.sh, Dockerfiles, and local Compose setGDAL_VRT_ENABLE_RAWRASTERBAND=NO,GDAL_VRT_ENABLE_PYTHON=NO,GDAL_DRIVER_PATH=disable,GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR, andCPL_VSIL_CURL_HEADER_FILE_KVP_ENABLED=NO. TiTiler’s shared-runtime policy additionally deregisters VRT; internal mosaic workflows retain it.Ingestion size/type limits. Chunked uploads are capped (
HASTE_MAX_UPLOAD_BYTES, default 5 GiB) and the assembled file’s magic bytes must match its declared format before GDAL parses it. Remote imagery fetches are capped (HASTE_MAX_IMAGERY_DOWNLOAD_BYTES, default 8 GiB) and refuse cross-host redirects (SSRF guard). Tune the same-named GitHub Environment configuration variables, then run Update Size Limits. Both deployment and lightweight updates apply these desired values. Bicep declares the caps explicitly; localazdenvironments must supply matching byte counts before provisioning. The workflow verifies stored settings and samples HTTP worker responses, not queue workers or Batch enforcement. New Batch tasks capture the queues worker’s download cap; existing tasks retain their submitted values. See the operator procedure for reset, rollback, resource budgets, and verification.
Operator action: keep the GDAL hardening settings in any custom GDAL-bearing image and do not add file formats to the pipeline without extending the allowlist in gdal_security.py. If you legitimately need a format outside the allowlist, add its driver explicitly rather than disabling the control.
Each HASTE release documents security-relevant changes in CHANGELOG.md. The rolling list of acknowledged dependency vulnerabilities — fixed, dismissed with rationale, or pending — is maintained at docs/known-vulnerabilities.md.
9. Secure configuration checklist (pre-production)#
Before exposing HASTE to anyone other than its developers, verify:
[ ] Function App is reachable only via SWA linked backend (or equivalent access restriction)
[ ] The TiTiler Function App’s main site allows only the APIM subnet, and its SCM site denies all traffic (§5.5)
[ ]
deploy/verify-titiler-baseline.ps1passes for the environment, andTITILER_ALLOWED_HOSTSlists only hosts you serve imagery from (§5.6)[ ] AAD (Entra ID) is configured as the SWA identity provider; mock auth is disabled
[ ]
DEVELOPMENT_MODEis not set in any Application Setting[ ] All secrets are in Key Vault or supplied via managed identity (no inline connection strings in source control)
[ ]
VITE_AZURE_MAPS_KEYis not set;VITE_AZURE_MAPS_CLIENT_IDis set instead[ ] CORS is restricted to your UI’s exact origin (no
*)[ ]
staticwebapp.config.jsondefines CSP, HSTS, X-Content-Type-Options, X-Frame-Options[ ] Basic-auth publishing is disabled on the Function App post-deployment
[ ] Docker images are pinned to digests; ACR scanning is enabled
[ ] Application Insights is enabled with ≥90-day retention
[ ] Alerts are configured for 5xx rate, auth failures, and admin-action spikes
[ ] Outbound network egress from the Function App is restricted (NSG or Azure Firewall) — see §8.5
[ ] All administrators have been reviewed and assigned via least-privilege
[ ] The known-limitations section (§8) has been read and the listed operational mitigations applied
10. References#
Microsoft Cloud Security Benchmark — https://learn.microsoft.com/security/benchmark/azure/
Azure Static Web Apps — Authentication and Authorization — https://learn.microsoft.com/azure/static-web-apps/authentication-authorization
Azure Functions — Securing Functions — https://learn.microsoft.com/azure/azure-functions/security-concepts
Azure Key Vault references in App Service — https://learn.microsoft.com/azure/app-service/app-service-key-vault-references
Azure RBAC for storage — https://learn.microsoft.com/azure/storage/blobs/assign-azure-role-data-access
Microsoft Defender for Cloud — Containers — https://learn.microsoft.com/azure/defender-for-cloud/defender-for-containers-introduction
MSRC vulnerability reporting — https://msrc.microsoft.com and https://aka.ms/SECURITY.md
HASTE Security Policy — https://github.com/microsoft/haste/blob/main/SECURITY.md
HASTE CHANGELOG — https://github.com/microsoft/haste/blob/main/CHANGELOG.md
HASTE Known Vulnerabilities (rolling) — https://github.com/microsoft/haste/blob/main/docs/known-vulnerabilities.md