OWASP ZAP on Ubuntu 24.04 on Azure User Guide
Overview
OWASP ZAP (Zed Attack Proxy) is the world's most widely used open-source web application security scanner. It performs Dynamic Application Security Testing (DAST): it spiders a target web application, proxies the traffic, and runs passive and active scan rules to find vulnerabilities such as missing security headers, injection points and information leaks. The cloudimg image runs ZAP 2.17.0 in headless daemon mode as an appliance: a long-running ZAP daemon exposing its REST API and local scanning proxy, driven by the API or the ZAP Automation Framework. It runs as a dedicated zap system user, stores its sessions and scan results on a dedicated Azure data disk, and rotates a unique API key into the image on first boot. Backed by 24/7 cloudimg support.
What is included:
- OWASP ZAP 2.17.0 (the official Linux distribution from the ZAP project) in
-daemonmode - A Temurin 17 JRE (the Java runtime ZAP 2.17 requires)
- A unique per-VM API key generated on first boot (no shared default credential)
- ZAP home, sessions, scan results and add-ons on a dedicated 20 GiB Azure data disk at
/var/lib/zap - A loopback-only ZAP API and proxy on
127.0.0.1:8090, fronted by nginx on:80 - A static, unauthenticated
/healthendpoint for load balancers and probes - 24/7 cloudimg support
This is a scanner appliance. The ZAP REST API and scanning proxy listen on 127.0.0.1:8090 only and are gated by the per-VM API key. Port 8090 is not opened to the internet - call the API locally on the VM or over an SSH tunnel.
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2ms (2 vCPU / 8 GiB RAM) is a good starting point - ZAP is a Java application and benefits from the extra memory. NSG inbound: allow 22/tcp from your management network. No inbound application ports are required because the ZAP API is reached over the SSH tunnel.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for OWASP ZAP by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) only. Then Review + create then Create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name owasp-zap \
--image <marketplace-image-urn> \
--size Standard_B2ms \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
Step 3 - Connect to your VM
ssh azureuser@<vm-public-ip>
Step 4 - Confirm ZAP is installed and running
The ZAP daemon (zap.service) and the nginx front end (nginx.service) start automatically on boot. Check the version through the API and confirm both services are active:
sudo systemctl is-active zap.service nginx.service
Both should report active. The screenshot below shows the ZAP daemon reporting version 2.17.0 over its REST API and both services active:

Step 5 - Retrieve your per-VM API key
On first boot, the image generates a unique ZAP API key for this VM and writes it (with the API URL and a security note) to /root/owasp-zap-credentials.txt (mode 0600, root only):
sudo cat /root/owasp-zap-credentials.txt
The API key on the zap.api.key= line is the credential that gates every ZAP API call. Store it somewhere safe. For the commands below, export it into your shell:
ZAP_API_KEY=$(sudo grep '^zap.api.key=' /root/owasp-zap-credentials.txt | cut -d= -f2-)
Step 6 - Call the ZAP API with your API key
The ZAP REST API requires the API key, passed either as the apikey query parameter or the X-ZAP-API-Key header. A correct key returns the data; a wrong or missing key is rejected. This is what gates your scanner:
curl -s "http://127.0.0.1:8090/JSON/core/view/version/?apikey=<ZAP_API_KEY>"
You should see JSON containing "version":"2.17.0". The screenshot below shows the correct key returning HTTP 200 plus the version, and a wrong key being rejected:

You can also use the header form:
curl -s -H "X-ZAP-API-Key: <ZAP_API_KEY>" "http://127.0.0.1:8090/JSON/core/view/version/"
Step 7 - Run an automated scan via the API
ZAP scans a target web application in two stages: a spider crawls and discovers URLs, and the passive scanner analyses every request and response the spider made. The example below stands up a tiny local test page and scans it end to end so you can see the workflow; in practice you point url= at your own application.
Start a local test target, then spider it through ZAP:
TARGET="http://127.0.0.1:8099"
D=$(mktemp -d); printf '%s' '<!doctype html><html><head><title>scan target</title></head><body><h1>target</h1><a href="/page2.html">page 2</a></body></html>' > "$D/index.html"; printf '%s' '<!doctype html><html><body><h1>page two</h1></body></html>' > "$D/page2.html"
setsid bash -c "cd '$D' && exec python3 -m http.server 8099 --bind 127.0.0.1 >/tmp/zap-target.log 2>&1" </dev/null >/dev/null 2>&1 & disown 2>/dev/null || true
sleep 2
curl -s "http://127.0.0.1:8090/JSON/spider/action/scan/?apikey=<ZAP_API_KEY>&url=${TARGET}/&recurse=true"
The spider returns a scan id ({"scan":"0"}). Wait for it to reach 100%, then read the URLs it discovered:
SID=$(curl -s "http://127.0.0.1:8090/JSON/spider/view/scans/?apikey=<ZAP_API_KEY>" | jq -r '.scans[-1].id')
for i in $(seq 1 40); do P=$(curl -s "http://127.0.0.1:8090/JSON/spider/view/status/?apikey=<ZAP_API_KEY>&scanId=${SID}" | jq -r '.status'); [ "$P" = 100 ] && break; sleep 2; done
curl -s "http://127.0.0.1:8090/JSON/spider/view/results/?apikey=<ZAP_API_KEY>&scanId=${SID}" | jq '.results'
The screenshot below shows the spider results (the URLs discovered), the site in ZAP's scan tree, and the number of HTTP messages recorded:

Step 8 - Read the scan findings (alerts)
The passive scanner raises alerts for the issues it finds. Wait for the passive scan queue to drain, then read the alert summary and the individual findings for your target:
for i in $(seq 1 30); do Q=$(curl -s "http://127.0.0.1:8090/JSON/pscan/view/recordsToScan/?apikey=<ZAP_API_KEY>" | jq -r '.recordsToScan'); [ "$Q" = 0 ] && break; sleep 2; done
curl -s "http://127.0.0.1:8090/JSON/alert/view/alertsSummary/?apikey=<ZAP_API_KEY>&baseurl=http://127.0.0.1:8099" | jq .
curl -s "http://127.0.0.1:8090/JSON/alert/view/alerts/?apikey=<ZAP_API_KEY>&baseurl=http://127.0.0.1:8099&start=0&count=6" | jq '[.alerts[] | {alert:.alert, risk:.risk, url:.url}]'
The screenshot below shows the alert summary by risk and a sample of the findings (for example a missing Content Security Policy header and a server version leak):

When you are done, stop the local test target:
pkill -f 'http.server 8099' 2>/dev/null || true
Step 9 - Reach the API from your workstation (SSH tunnel)
The ZAP API and proxy listen on loopback only and are not exposed publicly. To drive ZAP from your workstation - or to point a browser at the ZAP API UI at http://127.0.0.1:8090/ - open an SSH tunnel and call the API locally:
ssh -L 8090:127.0.0.1:8090 azureuser@<vm-public-ip>
# then on your workstation:
curl -s "http://127.0.0.1:8090/JSON/core/view/version/?apikey=<your-api-key>"
To use ZAP as an HTTP proxy for a browser or a CI job, tunnel the same port and set your client's HTTP proxy to 127.0.0.1:8090. If you choose to expose 8090 (or the nginx :80) to other hosts, put your own authentication and TLS in front of it - the ZAP API key alone is not a substitute for transport security.
Step 10 - Health check
A static, unauthenticated health endpoint is served by nginx on port 80 and does not depend on the ZAP daemon being fully warmed up - use it for Azure Load Balancer or Application Gateway probes:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/health
It returns 200.
Data persistence
ZAP's home directory (-dir /var/lib/zap) holds its session databases, scan results, contexts and add-ons. It lives on a dedicated 20 GiB Azure data disk mounted at /var/lib/zap, captured into the image and re-provisioned on every VM, so your sessions and results survive restarts and the volume can be resized independently. The cloudimg image ships with a clean ZAP home and a placeholder key; first boot generates this VM's unique API key and an empty home.
Security model
- The ZAP REST API and scanning proxy bind to
127.0.0.1:8090only and are never exposed on the Azure NSG. - Every API call is gated by the per-VM API key in
/root/owasp-zap-credentials.txt. - nginx on
:80reverse-proxies the API and serves the static/health. - Reach the API remotely over an SSH tunnel; add your own auth and TLS before exposing any port publicly.
- The OS receives unattended security updates.
Support
This image is maintained by cloudimg with 24/7 support. OWASP, ZAP and Zed Attack Proxy are trademarks of the OWASP Foundation; this image repackages the upstream Apache-2.0 software and is not affiliated with or endorsed by the OWASP Foundation.