Manual penetration testing doesn't scale with modern release cadences, and most teams shipping weekly (or daily) builds can't wait for a quarterly pentest to catch a reflected XSS or a misconfigured CORS header. That's the gap dynamic application security testing (DAST) is meant to fill — and the OWASP ZAP scanner (Zed Attack Proxy) remains the most widely deployed open-source option for it. The problem most teams run into isn't whether to use ZAP, it's how to configure OWASP ZAP for automated scanning so it runs unattended in CI/CD, produces consistent results, and doesn't flood engineers with false positives on every pull request.
This guide walks through a practical setup: running ZAP in Docker, executing a baseline scan, configuring an API scan against an OpenAPI spec, tuning scan policies, and wiring the whole thing into a CI/CD pipeline with pass/fail thresholds. By the end, you'll have a repeatable pipeline stage instead of a one-off scan you run manually before releases.
Why Configure OWASP ZAP for Automated Scanning
Running ZAP interactively through its desktop GUI is fine for ad-hoc testing, but it doesn't fit a pipeline. Automated scanning means ZAP needs to start headless, target a known URL or spec, run a defined ruleset, exit with a meaningful status code, and hand back a report your team can act on. The steps below build that up incrementally, starting with the runtime environment.
Step 1: Set Up OWASP ZAP with Docker
The fastest path to a repeatable ZAP environment is the official Docker images rather than a local install, since it guarantees the same ZAP version and Java runtime across developer machines and CI runners. This is the foundation of any owasp zap docker setup you'll use going forward.
Pull the stable image:
docker pull ghcr.io/zaproxy/zaproxy:stable
Confirm it runs and check the version:
docker run --rm ghcr.io/zaproxy/zaproxy:stable zap.sh -version
For CI use, mount a working directory so reports persist outside the container:
mkdir -p zap-work && chmod 777 zap-work
docker run --rm -v $(pwd)/zap-work:/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
-t https://staging.example.com -r baseline-report.html
The chmod 777 is a common friction point — the ZAP container runs as a non-root user internally, and without world-writable permissions on the mounted volume, report generation silently fails or the container errors on write. Keep this scoped to ephemeral CI workspaces, not production hosts.
Step 2: Run a Baseline Scan
The baseline scan is ZAP's passive, time-boxed mode — it spiders the target briefly and checks responses against passive rules without sending attack payloads, making it safe to run against staging or even production. This zap baseline scan tutorial pattern is usually the first automated check teams add because it's low-risk and fast.
docker run --rm -v $(pwd)/zap-work:/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
-t https://staging.example.com \
-m 5 \
-r baseline-report.html \
-J baseline-report.json
Key flags:
-t— target URL-m— spider max time in minutes (keep this short for CI; 5 minutes is a reasonable default)-r/-J— HTML and JSON report output-a— include alpha-quality passive rules if you want broader coverage at the cost of more noise
By default, zap-baseline.py exits with code 1 if warnings are found, which will fail your pipeline unless you explicitly handle it — more on that in the troubleshooting section below.
Step 3: Configure ZAP API Scan Against an OpenAPI Spec
Baseline scans crawl HTML applications well but miss most of a JSON API's surface, since there's nothing for the spider to click through. For API-first services, zap api scan configuration against an OpenAPI/Swagger or GraphQL definition gives ZAP a map of every endpoint to actually exercise with active attack rules.
docker run --rm -v $(pwd)/zap-work:/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable zap-api-scan.py \
-t https://api.staging.example.com/openapi.json \
-f openapi \
-r api-scan-report.html \
-z "-config api.disablekey=true"
-f accepts openapi, soap, or graphql depending on your spec format. If your API sits behind authentication, generate a session token beforehand and pass it via a context file or the -z flag to inject an Authorization header, otherwise every request will hit 401s and the scan will report nothing meaningful — a false sense of security that's worse than no scan at all.
Step 4: Tune Scan Policies to Reduce Noise
Out of the box, ZAP applies its default policy, which includes rules that generate high false-positive rates on specific frameworks (for example, CSRF token warnings on apps using double-submit cookies correctly). Create a custom policy file and reference it explicitly rather than tuning per-run flags every time:
docker run --rm -v $(pwd)/zap-work:/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
-t https://staging.example.com \
-c custom-policy.conf \
-r baseline-report.html
A minimal custom-policy.conf disables specific rule IDs your team has triaged and accepted:
10015 IGNORE # Incomplete Cache-Control
10096 IGNORE # Timestamp Disclosure
Rule IDs come straight from ZAP's alert reference, so check the report output first, decide what's a real finding versus environmental noise, and codify that decision in the config file rather than in someone's memory.
Step 5: Wire ZAP into Your CI/CD Pipeline
With the scan commands working locally, add them as a pipeline stage that runs on every deploy to staging. A GitHub Actions example:
jobs:
zap-scan:
runs-on: ubuntu-latest
steps:
- name: Run ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: 'https://staging.example.com'
rules_file_name: 'custom-policy.conf'
cmd_options: '-a'
- name: Upload ZAP report
uses: actions/upload-artifact@v4
with:
name: zap-report
path: report_html.html
Run this against staging, not production, and gate it after deployment completes but before the change is promoted further. Store historical reports as build artifacts so you can diff findings across releases instead of only ever looking at the latest run.
Step 6: Set Exit Codes and Alert Thresholds
A scan that always passes is worthless, and one that always fails on cosmetic findings gets ignored within a sprint. Use ZAP's -l flag to set the minimum alert level that triggers a non-zero exit code:
zap-baseline.py -t https://staging.example.com -l WARN
Combine this with the custom policy file from Step 4 so that only findings your team has agreed are actionable actually break the build. Route the JSON report output to whatever system tracks your vulnerability backlog so new findings get ticketed automatically rather than living only in a CI log that scrolls away.
Troubleshooting and Verification
- Container exits immediately with a permissions error: the mounted volume isn't writable by the ZAP user inside the container. Re-check the
chmodon your working directory, or run with-u zap:zapmatching the container's expected UID. - API scan reports zero findings on an authenticated API: verify your token or session cookie is actually being injected — check the raw request log in the HTML report to confirm the
Authorizationheader is present on outbound calls, not just configured in your command. - Baseline scan finds almost nothing: the spider likely timed out before reaching most of the app, or the app requires JavaScript rendering ZAP's basic spider doesn't execute. Increase
-m, or switch to the Ajax spider (-j) for single-page applications. - Pipeline fails on every run regardless of severity: check whether you're catching the exit code correctly.
zap-baseline.pyreturns 2 for both errors and warnings by default; use-lto set a real threshold instead of treating any non-zero exit as a hard failure. - Verify your setup end-to-end: run the exact CI command locally against a deliberately vulnerable test app (like OWASP Juice Shop) before trusting it against real staging traffic — you should see known findings (like reflected XSS) reported reliably before you rely on the pipeline for anything else.
How Safeguard Helps
The OWASP ZAP scanner is a strong DAST engine, but running it well in production pipelines means managing scan configs, rule exceptions, and result triage across every service your org ships — and that overhead grows fast once you're past a handful of repos. Safeguard's software supply chain security platform ingests ZAP scan results alongside your SAST, SCA, and container scanning output, correlating dynamic findings with the actual code paths and dependencies that introduced them. Instead of a report your team has to manually triage per repo, Safeguard normalizes ZAP alerts against your existing vulnerability backlog, deduplicates repeat findings across scan runs, and flags which issues sit on your actual deployment path versus dead code. That means the ZAP pipeline you just built feeds directly into a single risk view your security and engineering teams already trust, instead of becoming yet another dashboard nobody checks after the second sprint.