Security Azure

OWASP ZAP on Ubuntu 24.04 on Azure User Guide

| Product: OWASP ZAP on Ubuntu 24.04 LTS on Azure

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 -daemon mode
  • 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 /health endpoint 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:

ZAP version and service status

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:

ZAP API version call with the per-VM API key

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:

ZAP spider and passive scan via the API

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):

ZAP scan alerts and report output

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:8090 only 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 :80 reverse-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.