Dockerized, OWASP-ZAP security scanning, in Jenkins, part one

For a very long time, I’ve been interested in automated security testing. Alan Parkinson’sAutomated Security Testing” presentation at Selenium Conference 2012 really highlighted the possibilities, for me. Since then, I’ve wanted to get a simple, yet powerful and effective, automated security-scanning and reporting tool integrated into our development, testing, and release process (which I’ll also refer to as “pipeline.”)

Separately, I’ve also been very interested in exploring and learning Docker. Last quarter, I was happy to learn that there is a Dockerized OWASP ZAP container, but I didn’t then have the time set aside to learn both Docker and ZAP. By using Docker to containerize/Dockerize our OWASP-ZAP instance, we could get it running in our Jenkins continuous-integration environment, and essentially take the Docker image and run it in other (developers’, operations’, etc.) instances.

This quarter, the two are coming together, now that I’ve set aside some focused time and my personal deliverable to “Get a Dockererized OWASP ZAP (CLI) instance up and running against a staged instance of one of our key sites: either AMO,, or MDN, in Web QA’s Jenkins instance, on either/both a cronjob or on-demand.”

When I was first pointed to I saw a dizzying array of options: did I want the CLI? ZAPR? Headless? Headless, with Xvfb? GUI VNC?

Because our stack is primarily Python-based, and I’ll want to stand this up in a Jenkins instance, along with, hopefully, our WebDriver tests (which use pytest-selenium), I chose the ZAP CLI, which is a Python wrapper. It’ll also be lighter weight than the GUI VNC, but should integrate well with our setup.

Let’s start!

So, I downloaded and installed Docker for Mac OS X, from

Next, I launched the Docker Quickstart Terminal

Then I did:

$ docker pull owasp/zap2docker-stable

This command pulls the image “zap2docker-stable” from Docker’s Hub (specifically,

The 1st thing I tried was just a sample command from the first set of docs, to see if it worked:

$ docker run -i owasp/zap2docker-stable zap-cli quick-scan --self-contained --start-options '-config api.disablekey=true'

[INFO] Starting ZAP daemon
[INFO] Running a quick scan for
[INFO] Issues found: 0
[INFO] Shutting down ZAP daemon

Voila! No issues found – it started, scanned, and shut down, all very quickly. Good.

Looking to go further with the scan, I glanced at just to get up and running, then tried substituting in ZAP’s active-scan command for quick-scan, which didn’t work (try it yourself, if you’d like) and after whittling away the given options one by one: --self-contained, --start-options, and -config, I ended up with this, which I tried to run:

$ docker run -i owasp/zap2docker-stable zap-cli active-scan

I got this:

IOError: [Errno socket error] [Errno 111] Connection refused

It was then that I was helpfully pointed back to the documentation (, which does point out that zap-cli *only* starts ZAP automatically when using the ‘quick-scan’ option. So, I went back to and re-read the headless section, where it shows you how to start ZAP first, in its own container, which, I did, like so:

$ docker run -p 8090:8090 -i owasp/zap2docker-stable -daemon -port 8090 -host

That gave me the following output, which told me that ZAP was indeed running:

4370 [ZAP-daemon] INFO org.zaproxy.zap.DaemonBootstrap - ZAP is now listening on

So, ZAP is running, but it’s running in a terminal/console window, and if we CTRL+C, we’ll lose the ZAP process. As a quick exercise, issue the docker run command above, then open a new Docker terminal window (Quickstart, if you’re on OS X), and do a docker ps. You should see something like so:

$ docker ps
f6e9a636f8c2 owasp/zap2docker-stable " -daemon -port" About a minute ago Up About a minute>8090/tcp determined_pike

It’s rather obvious now, but in my initial excitement at having ZAP still running in the above container, I failed to understand how to correctly address/reach the already-running ZAP, and so when I ran this:

$ docker run owasp/zap2docker-stable zap-cli open-url

It naturally gave me the same error I got earlier:

IOError: [Errno socket error] [Errno 111] Connection refused

So, we know ZAP is already running, but the zap-cli isn’t accessing it. What can we do? Docker’s docs on run specify that the command is for running an image, which we’ve already done. That’s the ‘owasp/zap2docker-stable’ in all of our above commands.

So, what we want, instead of running another new image or even a new container, is to run our zap-cli command *against* (i.e. to use) the already-running ZAP container, which is /from/ the owasp/zap2docker image.

For that, Docker’s doc says to use exec, which “runs a new command in a running container.” Perfecto! I found this StackOverflow article on “exec vs. attach” helpful.

You can find and address Docker containers by either their name or their unique ID, both of which Docker will create for you. See Docker’s docs on run, here, for more help. For our purposes here, it’s much easier to reference by name, rather than a really long ID like “0801f5872da47202beabb4aa122a2a5beeed6cf39759e312a0a67d225025b049”

To do that, it’s a matter of finding the container name, which you can do by:

$ docker ps
f6e9a636f8c2 owasp/zap2docker-stable " -daemon -port" About a minute ago Up About a minute>8090/tcp determined_pike

Here, we can see that CONTAINER ID is f6e9a636f8c2 and NAMES gives us determined_pike

So we can be sure ZAP is accessible outside of the Docker container, we follow the advice and commands from which tells us to pass in -host

For now, we also don’t have nor need an API key, so following the last section of, we set -config.api-disablekey=true

$ docker run -u zap -p 8090:8090 -d owasp/zap2docker-stable -daemon -port 8090 -host -config api.disablekey=true

Here, we’re running as the “zap” user, rather than Docker’s default user, which is the root.

After issuing this command, you should see a long dynamically-generated container ID, like so:

Finally, we can reference the Docker ZAP container by either its name or its container ID, and here I’ll choose to use its Docker-generated name, which is determined_pike

$ docker exec determined_pike zap-cli open-url ''
[INFO] Accessing URL

$ docker ps
ac68d9afcfad owasp/zap2docker-stable " -daemon -port" 29 seconds ago Up 28 seconds>8090/tcp determined_pike

$ docker exec determined_pike zap-cli active-scan ''
[INFO] Running an active scan...

$ docker logs determined_pike
Found Java version 1.7.0_91
Available memory: 2002 MB
Setting jvm heap size: -Xmx512m
275 [main] INFO org.zaproxy.zap.DaemonBootstrap - OWASP ZAP 2.4.3 started.
701 [main] INFO hsqldb.db.HSQLDB379AF3DEBD.ENGINE - dataFileCache open start
707 [main] INFO hsqldb.db.HSQLDB379AF3DEBD.ENGINE - dataFileCache open end
1108 [main] INFO org.parosproxy.paros.common.AbstractParam - Setting config api.disablekey = true was null
1111 [main] INFO - Reading supported SSL/TLS protocols...
1112 [main] INFO - Using a SSLEngine...
1360 [main] INFO - Done reading supported SSL/TLS protocols: [SSLv2Hello, SSLv3, TLSv1, TLSv1.1, TLSv1.2]
1367 [main] INFO org.parosproxy.paros.extension.option.OptionsParamCertificate - Unsafe SSL renegotiation disabled.
1376 [ZAP-daemon] INFO org.zaproxy.zap.control.ExtensionFactory - Loading extensions
2332 [ZAP-daemon] INFO org.zaproxy.zap.control.ExtensionFactory - Extensions loaded

Snipped many lines, here…

141243 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - start host | TestPersistentXSSSpider strength MEDIUM threshold MEDIUM
141262 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - completed host/plugin | TestPersistentXSSSpider in 0.019s
141263 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - start host | TestPersistentXSSAttack strength MEDIUM threshold MEDIUM
141264 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - completed host/plugin | TestPersistentXSSAttack in 0.002s
141265 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - start host | ScriptsActiveScanner strength MEDIUM threshold MEDIUM
141266 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - completed host/plugin | ScriptsActiveScanner in 0.002s
141266 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - completed host in 1.184s
141267 [Thread-8] INFO org.parosproxy.paros.core.scanner.Scanner - scanner completed in 1.216s

While it’s complete, having *just* the log output doesn’t tell us actionable items – at least, not in an easy-to-read way.

So, that’s next on our list of many things to do! See the follow-up blog posts, coming within the next couple of weeks, where I aim to address this, and the rest of the issues I’ve raised, and will be raising in my GitHub repo:

Until then!

2 comments on “Dockerized, OWASP-ZAP security scanning, in Jenkins, part one”

Post a comment

  1. Roman wrote on

    Very useful guide. But now I’m stuck with the same problem where you left off – creating a list of actionable items.

    What I’m really looking for is what the owasp UI outputs as alerts. Did you ever write a follow-up blog that covers this?


  2. Stephen Donner wrote on

    Hi Roman –

    Although I’ve now moved on to using the Python API directly (via the newly introduced, which recently landed in the main zaproxy repo), the following follow-up posts might be of more use to you, hopefully?


    – Stephen


Leave a Reply

Your email address will not be published. Required fields are marked *