PROJECTIntermediate

Greenbone OpenVAS: Self-Hosted Network Vulnerability Scanning with Scheduled Scans

Deploy Greenbone Community Edition with Docker Compose, script targets, schedules and tasks with python-gvm, and export weekly vulnerability reports.

Dylan H.

Projects

November 4, 2026
7 min read
3-5 hours
Greenbone OpenVAS: Self-Hosted Network Vulnerability Scanning with Scheduled Scans

Tools & Technologies

Greenbone Community Editiongvm-toolspython-gvmDocker Compose

Overview

Greenbone Community Edition (GCE) is the open-source stack behind OpenVAS. It authenticates against hosts, probes services and matches findings against a large feed of network vulnerability tests (NVTs). Unlike a template-driven scanner such as Nuclei, it performs full host-level scanning: open ports, service versions, missing patches and weak configurations.

In this project you will:

  • Run GCE with the official Greenbone community containers.
  • Create scan targets, a recurring schedule and a task with python-gvm, so the setup lives in version control rather than in the web UI.
  • Export a PDF or CSV report after each scan.

Only scan networks and hosts you own or have written authorisation to test. Full-and-fast scans are noisy and can stress fragile devices such as printers and IoT gear.

Architecture

GCE is a set of cooperating containers:

ComponentRole
gvmdManagement daemon. It holds targets, tasks and results, and speaks GMP.
pg-gvmPostgreSQL database for gvmd.
ospd-openvas + openvasThe scanner and its OSP wrapper.
redis-serverScratch store for scanner state.
gsad + gsaWeb interface on port 9392.
gvm-toolsCLI container for scripting GMP.
Data containersVulnerability tests, SCAP and CERT data, and notus data (feed content).

gvmd and the scanner communicate over Unix sockets in shared volumes. The scripts below attach to the same socket.

 python-gvm script ──GMP/socket──> gvmd ──> pg-gvm
                                    │
                                    └─OSP/socket──> ospd-openvas ──> targets
 browser ──:9392──> gsad ──> gvmd

Prerequisites

  • A Linux host with Docker Engine and the Compose plugin. Plan for at least 4 vCPU, 8 GB RAM and 30 GB of disk. The first feed sync is large.
  • Outbound access to the Greenbone community feed.
  • A scan network reachable from the host. For VLANs, use a host interface or macvlan on that VLAN.

Step 1: Deploy the stack

Greenbone publishes a reference compose file in its community containers documentation. Always start from the current version of that file, because image tags and service names change.

mkdir -p ~/greenbone && cd ~/greenbone
curl -fsSL https://greenbone.github.io/docs/latest/_static/compose.yaml -o compose.yaml
docker compose -f compose.yaml pull
docker compose -f compose.yaml up -d

By default gsad binds to 127.0.0.1:9392. Keep it that way and put it behind a reverse proxy with TLS (see the Traefik project) rather than exposing it directly.

The first start downloads the feeds, which can take a long time. Watch progress:

docker compose -f compose.yaml logs -f gvmd vulnerability-tests

Do not run scans until the feed import has finished. Early scans silently use incomplete NVT data.

Step 2: Set the admin password

docker compose -f compose.yaml exec -u gvmd gvmd \
  gvmd --user=admin --new-password='<long-random-password>'

Store the password in your password manager. Then sign in at http://127.0.0.1:9392 and confirm that the NVT count under Administration > Feed Status is non-zero.

Step 3: Create targets, schedule and task with python-gvm

Doing this in code makes the scan configuration reviewable and repeatable. The script finds the built-in scan config, scanner and port list by name, because their UUIDs differ between versions.

Create provision.py:

import os
from gvm.connections import UnixSocketConnection
from gvm.protocols.gmp import Gmp
from gvm.transforms import EtreeCheckCommandTransform
 
SOCKET = os.environ.get("GVM_SOCKET", "/run/gvmd/gvmd.sock")
USER = os.environ["GVM_USER"]
PASSWORD = os.environ["GVM_PASSWORD"]
HOSTS = os.environ.get("SCAN_HOSTS", "192.168.10.0/24").split(",")
 
 
def find_id(response, xpath_name, wanted):
    for el in response.xpath(xpath_name):
        if el.findtext("name") == wanted:
            return el.get("id")
    raise SystemExit(f"Not found: {wanted}")
 
 
with Gmp(UnixSocketConnection(path=SOCKET), transform=EtreeCheckCommandTransform()) as gmp:
    gmp.authenticate(USER, PASSWORD)
 
    config_id = find_id(gmp.get_scan_configs(), "scan_config", "Full and fast")
    scanner_id = find_id(gmp.get_scanners(), "scanner", "OpenVAS Default")
    ports_id = find_id(gmp.get_port_lists(), "port_list", "All IANA assigned TCP")
 
    target = gmp.create_target(
        name="lab-lan",
        hosts=HOSTS,
        port_list_id=ports_id,
    )
    target_id = target.get("id")
 
    # Sundays at 02:00 local time
    ical = (
        "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//cosmicbytez//EN\r\n"
        "BEGIN:VEVENT\r\nUID:weekly-lab-scan\r\nDTSTAMP:20261101T000000Z\r\n"
        "DTSTART:20261108T020000\r\nDURATION:PT6H\r\n"
        "RRULE:FREQ=WEEKLY;BYDAY=SU\r\nEND:VEVENT\r\nEND:VCALENDAR"
    )
    schedule = gmp.create_schedule(
        name="weekly-sunday-0200",
        icalendar=ical,
        timezone="America/Edmonton",
    )
 
    task = gmp.create_task(
        name="Weekly lab LAN scan",
        config_id=config_id,
        target_id=target_id,
        scanner_id=scanner_id,
        schedule_id=schedule.get("id"),
    )
    print("Created task", task.get("id"))

Run it from the gvm-tools container, or any container that mounts the gvmd socket volume. Check the exact volume and service names in your compose.yaml:

docker compose -f compose.yaml run --rm \
  -e GVM_USER=admin -e GVM_PASSWORD='<password>' \
  -v "$PWD/provision.py:/provision.py:ro" \
  --entrypoint python3 gvm-tools /provision.py

If the gvm-tools image does not include python-gvm on the default interpreter path, use gvm-script socket instead. It supplies the authenticated gmp object for you.

Step 4: Add credentialed scanning

Unauthenticated scans only see the network surface. Credentialed scans find missing patches and local misconfigurations, and they usually give far better results.

  1. Create a dedicated, low-privilege scan account on each host (SSH key for Linux, a local non-admin account with the required remote-registry and WMI rights for Windows).
  2. In GSA, go to Configuration > Credentials and add the credential.
  3. Edit the target and attach the SSH or SMB credential.

Never reuse a domain admin account for scanning.

Step 5: Export reports automatically

Add a second script that finds the newest finished report for the task and saves it:

import base64, os, sys
from gvm.connections import UnixSocketConnection
from gvm.protocols.gmp import Gmp
from gvm.transforms import EtreeCheckCommandTransform
 
with Gmp(UnixSocketConnection(path="/run/gvmd/gvmd.sock"),
         transform=EtreeCheckCommandTransform()) as gmp:
    gmp.authenticate(os.environ["GVM_USER"], os.environ["GVM_PASSWORD"])
    tasks = gmp.get_tasks(filter_string='name="Weekly lab LAN scan"')
    last = tasks.xpath("task/last_report/report")[0].get("id")
 
    fmts = gmp.get_report_formats()
    csv_id = next(f.get("id") for f in fmts.xpath("report_format")
                  if f.findtext("name") == "CSV Results")
 
    rep = gmp.get_report(last, report_format_id=csv_id, ignore_pagination=True,
                         details=True)
    blob = rep.xpath("report/text()")[0]
    with open(sys.argv[1], "wb") as fh:
        fh.write(base64.b64decode(blob))

Feed the CSV into DefectDojo, or diff it week over week to highlight new findings only.

Testing

  1. Feed health: Administration > Feed Status shows all feeds as current.
  2. Smoke test: create a one-host target pointing at a lab VM you control and start the task manually from GSA. A test VM with an outdated SSH daemon or an exposed test service is ideal.
  3. Schedule test: temporarily set DTSTART a few minutes ahead and confirm the task starts on its own.
  4. Detection check: deploy a deliberately vulnerable VM (Metasploitable or similar) and confirm it produces high-severity results. If it shows nothing, the feed sync probably did not finish.
  5. Report check: run the export script and open the CSV.

Deployment and hardening

  • Keep gsad on loopback and publish it through a TLS reverse proxy with SSO.
  • Pin images by digest after you have validated a release, and update on a schedule: docker compose pull && docker compose up -d.
  • Back up the PostgreSQL volume and gvmd data volume (Backrest/restic works well).
  • Run scans in an off-hours window and limit concurrency for slow or fragile networks.
  • Alert on failed tasks by polling get_tasks and checking status for Interrupted or Stopped.
  • Restrict who can reach the scanner host: it holds credentials for every credentialed scan target.

Troubleshooting

SymptomLikely cause
Scan finishes instantly with no resultsFeed sync incomplete, or the target is unreachable from the scanner container
Task stuck at "Requested"ospd-openvas not connected to the socket or Redis
Credentialed scan shows "login failed"Wrong credential type or insufficient rights on the target
Very slow scansToo many parallel hosts or NVTs for the allotted CPU and RAM

Extensions

  • Use the Greenbone Ansible collection to manage targets, credentials and tasks as code.
  • Import weekly results into DefectDojo for deduplication and SLA tracking.
  • Send high-severity findings to Discord or TheHive via an n8n workflow.
  • Add separate tasks for each VLAN with tuned port lists and schedules.
  • Combine with Nuclei for application-layer checks, so OpenVAS covers hosts and services while Nuclei covers web exposure.

Sources