Deployment guide

MWDB Core

Overview

MWDB Core is the open-source malware repository developed by CERT Polska and the engine behind the mwdb.cert.pl service. It stores files, extracted static configurations and text blobs as content-addressed objects, links them with parent and child relations, and lets analysts enrich every object with tags, attributes and comments.

A Lucene-style query language searches across hashes, names, tags, attributes and configuration fields. A capability-based permission model with users, groups and sharing rules controls who can see and do what. Everything is available through the web interface and a documented REST API, so automated pipelines and analysts work against the same database.

This guide explains how to deploy and configure MWDB Core on Ubuntu 24.04 on Azure with the SecureAnyCloud image from the Azure Marketplace. The image ships the free and open-source, AGPL-3.0-licensed MWDB Core 2.19.0 release, installed the officially supported way from the project's PyPI package (pinned by SHA-256, with the pre-built web application included). It runs under gunicorn with on-box PostgreSQL 16 and Redis 7.

The API, PostgreSQL and Redis bind to the loopback interface only, and nginx terminates TLS as the single public listener. MWDB creates its built-in administrator only on a database with no users, so the image ships an empty database and an empty store. First boot generates everything that must be unique per VM: the PostgreSQL password, the application secret key that signs every session and API key, the Redis password, a self-signed TLS certificate, and the administrator account with a random password. Self-registration is disabled. SecureAnyCloud provides 24/7 support for the image.

MWDB and CERT Polska are names of their respective owners. SecureAnyCloud produces this image, which is not affiliated with, endorsed by, or sponsored by CERT Polska or NASK. It ships the free and open-source AGPL-3.0-licensed software unmodified, and the corresponding source is available at https://github.com/CERT-Polska/mwdb-core.

What you get:

  • MWDB Core 2.19.0: the official PyPI release, pinned by SHA-256 and run unmodified under gunicorn as an unprivileged service account
  • The MWDB web application: the pre-built bundle from the same release, served by nginx
  • PostgreSQL 16: the object database, bound to loopback with no host port published
  • Redis 7: the rate-limiting backend, bound to loopback and password-protected
  • nginx: the single public listener, which terminates TLS and is the only route to the API
  • First-boot secret generation: database password, secret key, Redis password, TLS certificate and administrator account, all unique to each VM
  • A bundled self-test: signs in as the per-instance administrator, uploads a benign text blob, reads it back, finds it by query and deletes it, which proves that the repository works end to end

Prerequisites

  • An Azure subscription with permission to create virtual machines
  • An SSH key pair for administrative access
  • A network security group allowing inbound TCP 22 (SSH) and TCP 443 (HTTPS) from your own address ranges
  • Standard_B2s (2 vCPU, 4 GB RAM) or larger. Choose more memory and a larger disk once you store real sample volumes
1

Deploy from the Azure Portal

  • Open the Azure Marketplace and search for MWDB Core on Ubuntu 24.04 LTS by SecureAnyCloud.
  • Select Create, then choose your subscription, resource group and region.
  • Pick a VM size of Standard_B2s or larger.
  • Under Administrator account, select SSH public key and paste your public key.
  • Under Inbound port rules, allow SSH (22) and HTTPS (443).
  • Select Review + create, then Create.
2

Deploy from the Azure CLI

az group create --name mwdb-rg --location eastus
az vm image terms accept --urn sac1647283583153:mwdb-core-ubuntu-24-04:default:latest
az vm create \
  --resource-group mwdb-rg \
  --name mwdb-vm \
  --image sac1647283583153:mwdb-core-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard
az vm open-port --resource-group mwdb-rg --name mwdb-vm --port 443 --priority 1010
3

Connect to your VM

ssh azureuser@<vm-ip>
4

Confirm the services are running

MWDB Core runs as a systemd service behind nginx, with PostgreSQL and Redis on the same box. Two one-shot units run on boot. mwdb-core-tls creates the per-VM TLS certificate before nginx starts. mwdb-core-firstboot (first boot only) generates the secrets, migrates the database schema and creates the administrator before the API is allowed to start.

systemctl is-active postgresql redis-server nginx mwdb-core

Expected output:

active active active active

The one-shot units report active (exited) after a successful run. The first-boot unit then disables itself so that it can never run twice:

systemctl --no-pager --plain list-units 'mwdb-core*'

Expected output:

UNIT LOAD ACTIVE SUB DESCRIPTION mwdb-core-firstboot.service loaded active exited MWDB Core first-boot setup (per-VM secrets, schema, admin account) (sac) mwdb-core-tls.service loaded active exited Mint the per-VM self-signed TLS certificate for the MWDB Core front door (sac) mwdb-core.service loaded active running MWDB Core malware repository API (gunicorn, loopback :8080) (sac)

The API listens only on the loopback interface, and nginx is the single public listener on ports 80 (which redirects to HTTPS) and 443. Instead of searching for the ports that you expect, list every socket that is bound to something other than loopback, so that anything unexpected shows up:

sudo ss -ltn | awk 'NR>1{print $4}' | grep -vE '^(127\.|\[::1\])' | sort -u

Expected output:

0.0.0.0:22 0.0.0.0:443 0.0.0.0:80 [::]:22 [::]:443 [::]:80

The only entries are 22 (SSH) and 80 and 443 (nginx), on both IPv4 and IPv6. Everything else that the image runs is bound to loopback and is not reachable from off the box. That includes the API on 127.0.0.1:8080, PostgreSQL on 127.0.0.1:5432 and Redis on 127.0.0.1:6379. Because this command lists the full set rather than a filtered subset, run it again after you install anything else on the VM.

The API itself answers over HTTPS and confirms which release you are running:

curl -sk https://127.0.0.1/api/server | jq '{server_version, instance_name, is_registration_enabled, is_password_auth_enabled}'

Expected output:

{ "server_version": "2.19.0", "instance_name": "mwdb", "is_registration_enabled": false, "is_password_auth_enabled": true }
5

Retrieve your per-instance credentials

Every VM generates its own secrets and administrator on first boot. They are written to a root-only file:

sudo cat /root/mwdb-core-credentials.txt

The file contains:

  • MWDB_ADMIN_USER / MWDB_ADMIN_PASSWORD, the per-instance administrator account
  • MWDB_URL, the HTTPS URL of your instance

The PostgreSQL password, the application secret key and the Redis password live in /etc/mwdb-core/mwdb.ini, which is readable only by root and the service account. Keep both files safe, and change the administrator password from the profile page after you sign in.

6

Understand the security model

MWDB's built-in admin account is created by its configure step, and only on a database with no users. The image therefore ships with an empty database, so no administrator, no session secret and no API key exists until your VM boots. First boot generates everything security-sensitive uniquely for each VM, before the API is allowed to start:

  • The PostgreSQL password for the mwdb role
  • The application secret key, which signs every session cookie and every API key
  • The Redis password for the rate limiter
  • A per-instance self-signed TLS certificate
  • The administrator account and its 24-character password

The image is also configured the way upstream recommends for a production deployment. The API sits behind nginx with X-Forwarded-For handling enabled, self-registration is disabled, password login is on, and rate limiting is on with Redis as its store.

You can verify the whole model end to end with the bundled self-test. It proves that the front door works over TLS, that registration is refused, that a wrong password is rejected, and that the per-instance administrator can sign in. It also uploads an object through the real API, reads it back, finds it by query and deletes it again, leaving the store empty:

sudo /usr/local/bin/mwdb-core-selftest

Expected output:

OK mwdb-core self-test passed: healthz 200, :80 301, SPA + text/css served, /api/ping ok, registration disabled, rate limit on, register 403, wrong password 403, admin login + profile OK, benign blob upload/read/search/delete round-trip OK, store empty, exposed ports exactly {22,80,443}, gunicorn/PostgreSQL/Redis loopback-only, Redis requires auth

Registration is closed. An attempt to self-register is refused with 403:

curl -sk -o /dev/null -w '%{http_code}\n' -H 'Content-Type: application/json' \
  -d '{"login":"someone","email":"someone@example.invalid","additional_info":"x","recaptcha":""}' \
  https://127.0.0.1/api/auth/register

Expected output:

403
7

Sign in to the web interface

Open your instance in a browser. The URL is the MWDB_URL value from your credentials file:

https://<vm-ip>/

The TLS certificate is self-signed and unique to each VM, so your browser shows a warning once. Accept it to continue, or install a CA-issued certificate as described in Step 11 to remove the warning. Sign in as MWDB_ADMIN_USER with MWDB_ADMIN_PASSWORD from your credentials file.

After you sign in, you land on the recent objects view. It is empty, because this image ships no samples, no rule sets and no signature databases of any kind. Everything stored in the repository is yours.

8

Upload, tag and find an object

MWDB stores three kinds of object: files (samples), configs (extracted static configuration as JSON) and blobs (text, such as decoded strings or a note). The quickest way to see the workflow is to upload a text blob. Open Upload, choose Upload blob, paste some content, give it a name and a type, and submit.

You land on the object page. The Details tab shows the name, size, type and timestamps. Preview renders the content, and Relations shows parents and children. Add a tag from the Tags panel on the right, attributes from the Attributes panel, and comments at the bottom. Sharing and inherited shares are listed on the same page.

Every field you see is searchable. The search bar accepts Lucene-style queries such as tag:sac, blob.name:*.txt, file.size:>1000000 or config.family:emotet. The quick query buttons store the queries that you use most.

The same round trip is available from the command line. This example signs in as the per-instance administrator, uploads a small benign blob, finds it by name, reads it back and removes it again, leaving the repository empty:

ADMIN_PW=$(sudo awk -F= '/^MWDB_ADMIN_PASSWORD=/{print $2}' /root/mwdb-core-credentials.txt)
TOKEN=$(curl -sk -H 'Content-Type: application/json' \
  -d "{\"login\":\"admin\",\"password\":\"${ADMIN_PW}\"}" https://127.0.0.1/api/auth/login | jq -r .token)
ID=$(curl -sk -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"blob_name":"guide-example.txt","blob_type":"note","content":"hello from the deployment guide","tags":[{"tag":"guide"}]}' \
  https://127.0.0.1/api/blob | jq -r .id)
curl -sk -G -H "Authorization: Bearer ${TOKEN}" --data-urlencode 'query=blob.name:"guide-example.txt"' https://127.0.0.1/api/blob | jq -r '.blobs[].blob_name'
curl -sk -H "Authorization: Bearer ${TOKEN}" "https://127.0.0.1/api/blob/${ID}" | jq -r .content
curl -sk -o /dev/null -w 'delete -> %{http_code}\n' -X DELETE -H "Authorization: Bearer ${TOKEN}" "https://127.0.0.1/api/object/${ID}"

Expected output:

guide-example.txt hello from the deployment guide delete -> 200

From your workstation, use https://<vm-ip> in place of https://127.0.0.1 (the MWDB_URL value from your credentials file), and keep -k until you install a CA-issued certificate. Files are uploaded with a multipart POST /api/file. After you sign in, the full API is documented on your own instance at https://<vm-ip>/docs. The mwdblib Python client and its mwdb command-line tool use the same API.

9

Manage users, groups and API keys

Self-registration is closed by default on this image. Create analysts from Settings > Users > Register user, and put them in groups from Settings > Groups. Control what each group may do with capabilities such as adding files, adding tags, sharing objects or removing objects.

A user can see an object only through a group that has been given access, so groups are how you segment a repository between teams or partner organisations.

Each user can issue API keys from their profile page (API keys). An API key is a bearer token for scripts and pipelines, and you can revoke it without changing the password. The administrator can read the security-relevant server flags from the API:

ADMIN_PW=$(sudo awk -F= '/^MWDB_ADMIN_PASSWORD=/{print $2}' /root/mwdb-core-credentials.txt)
TOKEN=$(curl -sk -H 'Content-Type: application/json' \
  -d "{\"login\":\"admin\",\"password\":\"${ADMIN_PW}\"}" https://127.0.0.1/api/auth/login | jq -r .token)
curl -sk -H "Authorization: Bearer ${TOKEN}" https://127.0.0.1/api/server/admin | jq '{rate_limit_enabled, plugins_enabled, active_plugins}'

Expected output:

{ "rate_limit_enabled": true, "plugins_enabled": true, "active_plugins": {} }

The plugin mechanism is enabled, but no plugin is installed. To add one, install it in the /opt/mwdb-core/venv virtual environment, declare it under plugins = in /etc/mwdb-core/mwdb.ini, and restart the API.

10

Configuration and key paths

The live configuration is /etc/mwdb-core/mwdb.ini, generated on first boot. You can add any key from the upstream configuration reference to its [mwdb] section. Common changes:

  • max_upload_size sets the upload ceiling in bytes (256 MiB as shipped). Also raise client_max_body_size in the nginx site.
  • enable_registration = 1 plus the mail_* keys opens self-registration with email confirmation.
  • storage_provider = s3 plus the s3_storage_* keys keeps samples in Azure Blob Storage through its S3-compatible gateway, or in any S3-compatible store, instead of the local uploads folder.
  • enable_karton = 1 hooks a Karton analysis pipeline to every upload.

After you edit the file, restart the API:

$
sudo systemctl restart mwdb-core

These are the key paths on the VM:

  • /etc/mwdb-core/mwdb.ini: MWDB configuration (database, secret key, uploads folder, feature flags)
  • /etc/mwdb-core/gunicorn.conf.py: gunicorn settings (bind address, workers, timeouts)
  • /root/mwdb-core-credentials.txt: per-VM administrator credential and URL, root-only
  • /var/lib/mwdb-core/uploads: uploaded samples (empty as shipped)
  • /opt/mwdb-core/venv: the Python virtual environment that holds the pinned release
  • /opt/mwdb-core/web: the pre-built web application served by nginx
  • /etc/nginx/sites-available/sac-mwdb-core: the nginx TLS front end
  • /etc/nginx/tls/mwdb-core.crt and .key: the per-VM TLS certificate and key
  • /usr/local/bin/mwdb-core-selftest: the end-to-end appliance self-test
  • /var/log/sac/sac-firstboot.log: the first-boot log

Confirm the installed version at any time:

/opt/mwdb-core/venv/bin/pip show mwdb-core | grep -E '^(Name|Version):'

Expected output:

Name: mwdb-core Version: 2.19.0
11

Replace the certificate for production

The per-VM certificate is self-signed. For a real deployment, point a DNS name at the VM and obtain a CA-issued certificate for it. Install the certificate and key over /etc/nginx/tls/mwdb-core.crt and /etc/nginx/tls/mwdb-core.key, set base_url = https://<your-domain> in /etc/mwdb-core/mwdb.ini, and restart the services:

$
sudo systemctl restart nginx mwdb-core

The mwdb-core-tls unit creates a certificate only when none exists, so a certificate that you install is never overwritten.

Backups

All state lives in PostgreSQL and in the uploads folder. Back up both:

$
sudo -u postgres pg_dump mwdb | gzip > mwdb-$(date +%F).sql.gz $
sudo tar czf mwdb-uploads-$(date +%F).tar.gz -C /var/lib/mwdb-core uploads

Store both backups off the VM, together with a copy of /etc/mwdb-core/mwdb.ini. The secret key inside that file keeps existing sessions and API keys valid after a restore.

Security notes

Nothing is reachable without a credential. The API, PostgreSQL and Redis bind to loopback. nginx on 443 is the only network-facing surface, and it serves MWDB's own login. Self-registration is off, and login is rate-limited.

Every secret is per VM. The administrator password, PostgreSQL password, secret key, Redis password and TLS certificate are all generated on first boot. Nothing usable is baked into the image, and the API cannot start until first boot has produced them.

The image ships empty. No sample, plugin, rule set or signature database of any kind is present. What you upload is malware by definition, so keep the VM's disk encrypted at rest (Azure managed disks are) and restrict who may download from the repository with capabilities. MWDB serves downloads in password-protected archives when you use the zipped download option.

Restrict SSH. Allow port 22 only from your management network, and keep key-based authentication.

The base image is patched at build time and ships with unattended security upgrades enabled. MWDB itself is installed from PyPI and is never upgraded automatically. Take a newer release deliberately with sudo /opt/mwdb-core/venv/bin/pip install -U mwdb-core, then run mwdb-core configure as the mwdb user to apply database migrations, as upstream documents.

Support

SecureAnyCloud provides 24/7 support with a guaranteed 24-hour response SLA for this image. MWDB Core itself is open source under the GNU Affero General Public License v3 with the upstream plugin exception, and it is shipped unmodified. SecureAnyCloud is not affiliated with CERT Polska or NASK. For questions about the image, its configuration or its security posture, contact SecureAnyCloud support.

Deploy on Azure

Launch MWDB Core on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure