Skip to main content

Masscan as a Service

CI

It happened to everyone -- forgotten rule in iptables caused open access to docker control port. Next thing you know, malicious containers running on your infrastructure, sending spam emails at best. Woudn't be great if you get notified, that something on a previously unseen TCP or UDP port started accepting connections on your server?

This project leverages an excellent masscan which can scan whole IPv4 range under 6 minutes (if you really want to push your network). We will also add support for nmap as masscan doesn't play nice with IPv6.

WARNING: commence port scanning from and to systems you operate, and you are allowed to send large volume of packets (e.g. TCP ACK) towards. Also make sure, that you have cleared permissions with your hosting providers. This will certainly trip various intrusion detection / anomaly detection systems.

Installation

Whole project is distributed as a Python package (PEP 621 / pyproject.toml) to make it simple to include in your tooling. It is not yet on https://pypi.org, so install it straight from git:

uv tool install git+https://github.com/bobek/masscan_as_a_service.git
# or, with pip
pip install git+https://github.com/bobek/masscan_as_a_service.git

Or declare it as a dependency of your project in pyproject.toml:

[project]
dependencies = [
    "masscan-as-a-service @ git+https://github.com/bobek/masscan_as_a_service.git",
]

This will give you masscan_as_a_service binary in your bin directory. As usual, a virtual environment (python -m venv, uv, ...) is recommended.

For development we use uv. The Python and uv versions are pinned in mise.toml, so with mise installed you get both with:

mise install

Without mise, install uv any other way - it will fetch the Python version from .python-version on its own.

Then clone the repository and let uv create the virtual environment, install the pinned dependencies from uv.lock and the project itself in editable mode:

make install   # uv sync
make lint      # uv run ruff check .
make test      # uv run pytest
make build     # uv build
make README.md # regenerate this file after changing the CLI

All of the above run on every push and pull request, see .github/workflows/ci.yml. The tests are offline - they use a fake in place of the Hetzner Cloud client, so no API token is needed and nothing gets provisioned.

Theory of Operation

  1. use some system to schedule regular executions. For example gitlab CD or github actions.
  2. run masscan_as_a_service masscan, you will need
    • a list of targets (path passed as --targets). One target per line. It is up to you how you are going to generate it. For example, in our deployments, we fetch list servers to test from inventory management system (aka Machine DataBase).
    • access details based on your cloud provider (check below). Also prepare appropriate configuration file.
    • ssh key (private and public parts) to get access to worker.
    • clone repository which you use to store results and point scanner to it (--output_dir)
  3. scanner will update per host files in the output directory. You should now commit them to your repository and trigger audit in case anything changed.

Please make sure, that the provisioned VM is in the network which makes sense for your test. For example, we provision completely separete VM, which is not connected to our internal systems in any way (except control ssh connection). Thus is represents a random Internet user as close as possible as we are interested in ports being opened to the wild.

Scan Result Storage

We have mentioned use of a repository (we use git) for storing the output files. Motivation behind it is that you will keep a history of changes within the version control. So you will know how the state looked in the past.

We also use version control as a way of detecting changes (e.g. new port being open). git will not let you make an empty commit. On the other side, if you successfully push to remote repository with a new commit, you can trigger an action. We use Phabricator's Audits for this. Phabricator is watching repository with results and when a new commit appears, it will start a new audit. Responsible members of the team will either describe why is the new port being open as it is an intended behavior. Or they will start investigating the root cause of it being open. In any case, you have one central place for storing information about why is some port opened and accessible.

VM Providers

Hetzner Cloud

Create a project at Hetzner cloud console. Open the project and go to Access section. Create a new API key under API Tokens tab.

Token is then read from the environment variable, which is set in the configuration file (api_token_env). Example configuration is at hcloud_example.yml.

Usage masscan_as_a_service

When installed, you will get a masscan_as_a_service application. It can perform various commands based on the first positional argument.

Global options

Some arguments can be defined only on the global level. For example, you turn debugging on for cleanup command with masscan_as_a_service -d cleanup.

usage: masscan_as_a_service [-h] [-d] -e ENV_CONFIG [-R]
                            {masscan,cleanup,cleanup-expired} ...

Masscan in a box

positional arguments:
  {masscan,cleanup,cleanup-expired}

options:
  -h, --help            show this help message and exit
  -d, --debug           Enable debugging
  -e, --environment-config ENV_CONFIG
                        YAML file describing execution environment
  -R, --no-resolve      Do not resolve IP address to FQDN

Command masscan

usage: masscan_as_a_service masscan [-h] (-t TARGETS | -a API_KEYS)
                                    [-L [LABEL ...]] -o DESTINATION_DIR
                                    --ssh-public-key SSH_PUBLIC_KEY
                                    --ssh-private-key SSH_PRIVATE_KEY

options:
  -h, --help            show this help message and exit
  -t, --targets TARGETS
                        File with targets (IP address) to scan. One per line.
  -a, --api_keys API_KEYS
                        File with API keys of projects to scan. YAML array.
  -L, --label [LABEL ...]
                        Label to be added to the VM (key=value)
  -o, --output_dir DESTINATION_DIR
                        Directory to write results to
  --ssh-public-key SSH_PUBLIC_KEY
                        File with the public SSH key to be given access to
                        created VM
  --ssh-private-key SSH_PRIVATE_KEY
                        File with the private SSH key corresponding to the
                        ssh-public-key

Command cleanup

usage: masscan_as_a_service cleanup [-h] -t THRESHOLD

options:
  -h, --help            show this help message and exit
  -t, --threshold THRESHOLD
                        All VMs older then THRESHOLD seconds will be deleted.

Command cleanup_expired

usage: masscan_as_a_service cleanup-expired [-h] -L LABEL

options:
  -h, --help         show this help message and exit
  -L, --label LABEL  All expired (expired delete_after label) VMs matching
                     {label} will be deleted

Example

I have provisioned 2 VM on Hetzner Cloud for this demo. Their IP addresses are 94.130.26.161 and 95.217.232.216.

Setup

You would normally automate following steps in your execution system. I will perform preparation steps manually.

First, we will need some ephemeral ssh key to access worker node. We will generate a new key stored in vm_key file with the following command:

ssh-keygen -t ed25519 -f /tmp/vm_key -q -N ""

We also need a list of targets to perform port scanning against. Let's store in the file named targets.list.

cat  <<EOF > /tmp/targets.list
94.130.26.161
95.217.232.216
EOF

Optionally you can specify list of api keys with name. masscan_as_a_service loads targets dynamically from Hetzner.

cat <<EOF > /tmp/api_tokens.yaml
- name: jbarton # project name, description, ...
  token: XfT5q...
EOF

Checkout a git repo to place results into:

git clone some_remote_repo_with_audits_enabled /tmp/out

Initial scan

HCLOUD_TOKEN=VERY_LONG_STRING_WITH_API_TOKEN masscan_as_a_service -d -e examples/hcloud.yml masscan --ssh-public-key /tmp/vm_key.pub --ssh-private-key /tmp/vm_key -t /tmp/targets.list -o /tmp/out
# OR
HCLOUD_TOKEN=VERY_LONG_STRING_WITH_API_TOKEN masscan_as_a_service -d -e examples/hcloud.yml masscan --ssh-public-key /tmp/vm_key.pub --ssh-private-key /tmp/vm_key -a /tmp/api_tokens.yaml -o /tmp/out

After the successful scan we will get two new files, one per target. They have the same content as on ssh is currently open on these newly provisioned VMs.

{
  "22/tcp": {
    "reason": "syn-ack",
    "status": "open"
  }
}

We can happily commit and push our scan results. Depending on your setup, this will trigger a source code audit as we have made changes to the repository. We will just resolved it with "provisioned 2 new VMs for demoing the masscan" and move on with our DevOps work.

Opening some ports

Scan was executed multiple times as we are running it regularly from our scheduler. But one day this happens:

diff --git i/94.130.26.161.json w/94.130.26.161.json
index bceef26..fe5c4ee 100644
--- i/94.130.26.161.json
+++ w/94.130.26.161.json
@@ -1,6 +1,26 @@
 {
+  "10250/tcp": {
+    "reason": "syn-ack",
+    "status": "open"
+  },
+  "10251/tcp": {
+    "reason": "syn-ack",
+    "status": "open"
+  },
+  "10252/tcp": {
+    "reason": "syn-ack",
+    "status": "open"
+  },
+  "10256/tcp": {
+    "reason": "syn-ack",
+    "status": "open"
+  },
   "22/tcp": {
     "reason": "syn-ack",
     "status": "open"
+  },
+  "6443/tcp": {
+    "reason": "syn-ack",
+    "status": "open"
   }
diff --git i/95.217.232.216.json w/95.217.232.216.json
index bceef26..45249f2 100644
--- i/95.217.232.216.json
+++ w/95.217.232.216.json
@@ -1,6 +1,14 @@
 {
+  "10250/tcp": {
+    "reason": "syn-ack",
+    "status": "open"
+  },
+  "10256/tcp": {
+    "reason": "syn-ack",
+    "status": "open"
+  },
   "22/tcp": {
     "reason": "syn-ack",
     "status": "open"
   }

As usual, we automatically commit and push the changed files to the repository. Source-code audit is going to be triggered as we have a new commit. We see that new ports are now open to the wild Internet. Brief look suggests that somebody has installed a new Kubernetes cluster on these VMs, but had totally forgotten to take care of filtering access to its services from the outside world.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

masscan_as_a_service-1.0.0.tar.gz (11.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

masscan_as_a_service-1.0.0-py3-none-any.whl (12.8 kB view details)

Uploaded Python 3

File details

Details for the file masscan_as_a_service-1.0.0.tar.gz.

File metadata

  • Download URL: masscan_as_a_service-1.0.0.tar.gz
  • Upload date:
  • Size: 11.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":null,"id":"forky","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for masscan_as_a_service-1.0.0.tar.gz
Algorithm Hash digest
SHA256 987af932df3eb1ae698aef9c82f3976bea43d184f185c14bd0fa435d198e1dba
MD5 18f961e41d10a77024abd4e28c055997
BLAKE2b-256 ea304ba4d182971f01469624d5a06b256c2fbf70953c6d14c4a04a7e8599c56c

See more details on using hashes here.

File details

Details for the file masscan_as_a_service-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: masscan_as_a_service-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 12.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":null,"id":"forky","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for masscan_as_a_service-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0184e0ccbfe2c31deb0e81da1388c00fd89b4567266890642e5f859454a81215
MD5 684c8f28aba39b5eb52465fdc9f64546
BLAKE2b-256 f697c7d244a45a65b9f47eb09c21a51d53eae743d15df76666179b558b93d2d4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page